React开发依赖
- 开发React必须依赖三个库
- react:包含react所必须的核心代码
- react-dom:react渲染在不同平台所需要的核心代码
- babel:将jsx转换成React代码的工具
- 第一次接触React会被它繁琐的依赖搞蒙,对于 Vue 来说我们只依赖一个Vue.js文件,但是react居然要依赖三个库
- cdn引入
- 本地引入
- npm包管理
snippet-generator.app //代码片段生成器
认识JSX
const element = <h2>hello world</h2>
ReactDOM.render(<App />, document.getElementById('app'))
- 这段
element
变量的声明右侧赋值的标签语法是什么呢?- 它不是一段字符串(因为没有使用引号包裹)
- 其实是不可以的,如果我们将
type="text/babel"
去掉,那么就会出现语法错误 - 它到底是什么
- JSX是什么?
jsx
是一种javascript
的语法扩展eXtension
也在很多地方称之为JavaScript XML
, 因为看起来就是一段XM
L语法- 它用于描述我们的UI界面,并且其完成可以和
javascript
融合在一起使用 - 它不同于Vue中的模板语法,你不需要专门学习模板语法中的一些指令(v-bind,v-show,v-for)
为什么选中JSX
- react 认为渲染逻辑本质与其他UI逻辑存在内在的耦合
- 比如UI需要绑定事件(button , a原生等等)
- 比如UI中需要展示数据状态,在某些状态发生变化时,又需要改变UI
- 他们之间密不可分,所以react 没有将标记分离不同的文件中,而将他们组合到一起,这个地方就是组件(Component)
- 在这里我们只需要知道JSX是嵌入到Javascript中的一种结构语法
- JSX的书写规范
- JSX的顶层 只能有一个根元素,所以我们很多时候会在最外层包裹一个原生div标签
- 为了方便阅读,我们通常在jsx的外层包裹一个小括号 这样可以方便阅读,并且jsx可以换行书写
- jsx可以是单标签,也可以是双标签(注意:如果是单标签,必须以 /> 结尾)
JSX的使用
- JSX嵌入变量
- 情况1:变量是Number, String, Array类型的时候,可以正常显示
- 情况2:当变量是null,undefined, Boolean 那么需要转成字符串
- 如果希望可以显示null undefined Boolean 那么需要转为字符串
- 转换的方式有很多,比如toString方法,和空字符串拼接 String(变量)等方式
- 情况3:对象类型不能作为子元素(not valid as a React child)
JSX属性的绑定
render() {
const { imageUrl, warn, link, isActive } = this.state
return (
<div>
{/*1.绑定普通属性*/}
<h3 title={warn}>1.绑定普通属性</h3>
<img src={getImgSize(imageUrl, 140)} alt={warn} />
<a href={link} target="_blank">百度一下</a>
{/*2.绑定class 由于jsx融合了js 因此避免js中一些关键字冲突,个别属性名会改变*/}
<h4 className='title' >2.绑定class</h4>
<h5 className={isActive ? 'active' : ''} onClick={this.changeColor.bind(this)}>Hello World(点击变色)</h5>
<label htmlFor=""></label>
{/*3.绑定style 注意此处两层大括号*/}
<h4 style={{ color: 'red', fontSize: '50px' }} >3.绑定style</h4>
</div>
)
}
JSX事件绑定
class App extends React.Component {
constructor() {
super()
this.state = {
counter: 100
}
// 还可以在此处对函数方法的this进行重定向
// this.increment = this.increment.bind(this)
}
render() {
return (
<div>
<h2>{this.state.counter}</h2>
{/*1.bind绑定this*/}
<button onClick={this.increment.bind(this)}>+1</button>
{/*2.定义函数的时候使用箭头函数*/}
<button onClick={this.decrement}>-1</button>
{/*3.(推荐)直接传入箭头函数,在箭头函数中调用需要执行的函数*/}
<button onClick={() => {
this.changeCounter('hello react')
}}>推荐方式</button>
</div>
)
}
changeCounter(str) {
this.setState({
counter: this.state.counter + str
})
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
decrement = () => {
this.setState({
counter: this.state.counter - 1
})
}
}
JSX事件参数
render() {
return (
<div>
<button onClick={this.btnClick.bind()}>按钮一</button>
<ul>
{
this.state.movies.map((item, index) => {
return (
<li onClick={e => { this.liClick(item, index, e) }}>
{item}
</li>
)
})
}
</ul>
</div>
)
}
btnClick(e) {
console.log(e);
}
liClick(item, index, e) {
console.log(item, index, e);
}
}
React条件渲染
- 某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容:
- 在Vue中,我们会通过指令控制
- 在React中,所有的条件判断都和普通的Javascript代码一致
- 常见的条件渲染的方式有哪些呢?
- 方式一:条件判断语句
- 适合逻辑较多的情况
- 方式二:三目运算
- 适合真假都有值,且逻辑较少的情况
- 方式三:逻辑运算符
- 适合真假一边有值,且逻辑较少的情况
- v-show 的效果
- 主要是控制display属性是否为none
React列表渲染
class App extends React.Component {
constructor() {
super()
this.state = {
names: ['abc', 'acb', 'bac', 'bca', 'cba', 'cab'],
numbers: [11, 22, 33, 222, 1, 33, 4523, 142, 22, 3, 132, 34, 12, 35]
}
}
render() {
return (
<div>
<h2>名字</h2>
<ul>
{
this.state.names.map(item => {
return (<li>{item}</li>)
})
}
</ul>
<h2>数字</h2>
<ul>
{
this.state.numbers.filter(item => item > 30).map(data => <li>{data}</li>)
}
</ul>
</div>
)
}
}
JSX 的本质
- 实际上,jsx仅仅只是React.createElement(component,props,…children)函数的语法糖
- 所有的jsx最终都会被转换成React.createElement的函数调用
- React.createElement在源码的react文件中
- createElement需要传递三个参数:
- 参数一:type
- 当前ReactElement的类型
- 如果是标签元素,那么就使用字符串表示"div"
- 如果是组件元素,那么就直接使用组件的名称
- 参数二:config
- 所有jsx中的属性都在config中以对象的属性和值的形式存储
- 参数三:children
- 存放在标签中的内容,以children数组的方式进行存储
- 当然如果是多个元素呢?React内部有对他们进行处理
- 该参数往后若多个参数,源码中是如何的实现的
- 1.首先判断aguments -2 的长度,即除去前两个参数后还有几个参数
- 2.如果只有一个参数,则将该参数赋给children
- 3.不止一个参数则遍历方式将第三个参数起的参数放到一个数组中,然后与chilren参数关联
虚拟DOM的创建过程
- 我们通过React.createElement最终创建出来一个ReactElement对象:
- 这个ReactElement对象组成了一个JavaScript的对象树
- javascript的对象树就是大名鼎鼎的虚拟DOM(Virtual DOM)
- 如何查看ReactElement的树结构呢?
- 我们可以将之前的jsx返回结果进行打印
- 注意下面代码中我对jsx的打印
- 而ReactElement最终形成的树结构就是Virtual DOM
为什么使用虚拟DOM
- 为什么要采用虚拟DOM,而不是直接修改真实的DOM呢
- 1.很难跟踪状态发生的变化:原有的开发模式,我们很难跟踪到状态发生的变化,不方便针对我们的应用程序进行调试
- 2.操作真实的DOM性能很低:传统的开发模式会进行频繁的操作DOM
- DOM操作性能非常低
- 首先 document.createElement本身创建出来的就是一个非常复杂的对象
- 其次DOM操作会引起浏览器的回流和重绘,所以开发中应该避免频繁操作DOM
虚拟DOM帮助我们呢从命令式编程转到了声明式编程的模式
- reacr官方的说法:virtual DOM是一种编程理念
- 在这个理念中,UI以一种理想化的方式保存在内存中,并且他是一个相对简单的javascript对象
- 我们可以通过ReactDOM.render 让虚拟DOM和真实DOM同步起来,这个过程叫做协调(Reconciliation)
- 这种编程的方式赋予了React声明式的API
- 你只需要告诉React希望让UI是什么状态
- React来确保DOM和这些状态是匹配的
- 你不需要直接进行DOM操作,就可以从手动更改DOM。属性操作,事件处理中解放出来
React的组件化
- 组件化思想应用
- 有了组件化的思想,我们在之后的开发中就要充分的利用他
- 尽可能的将页面拆成一个个小的,可复用的组件
- 这样让我们呢的代码更加方便组织和管理,并且扩展性更强
- React的组件相对于Vue更加灵活和多样,按照不通过的方式可以分成很多类组件
- 根据组件的定义方式可以分为 函数组件 和 类组件
- 根据组件内部是否有状态需要维护,可以分成:无状态组件和有状态组件
- 根据组件的不同职责,可以分为:展示形组件和容器形组件
- 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离
- 函数组件,无状态组件,展示形组件主要关注UI展示
- 类组件,有状态组件,容器形组件主要关注数据逻辑
- 当然还有很多其他组件的概念:比如异步组件,高阶组件等
类组件
- 类组件的定义有如下要求:
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自React.Component
- 类组件必须实现render函数
- 在ES6之前,可已通过 create-react-class模块来定义类组件,但是目前官网建议我们使用ES6的class类定义
- 使用class定义一个组件:
- constructor是可选的,我们通常在constructor中初始化一些数据
- this.state中维护的就是我们组件内部的数据
- render()方法是class组件中唯一必须实现的方法
render函数的返回值
- 当render被调用时,他会检查this.props和this.state的变化并返回一下类型之一
- react元素
- 通常通过JSX创建
- 例如,
会被react渲染成为DOM节点, 会被react渲染成为自定义组件
- 以上均为react组件
- 数组或fragments:使得渲染子节点到不同的DOM子树中
- Portals:可以渲染子节点到不同的DOM子树中
- 字符串或数值类型:他们在DOM中会被渲染为文本节点
- 布尔类型或null:什么都不渲染
函数组件
- 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件render函数一样的内容
- 函数组件有自己的特点(当然,我们后面会将hooks,就不一样了)
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- 没有this (组件实例)
- 没有内部状态 (state)
认识生命周期
- 很多食物都有创建到销毁的整个过程,这个过程我们称之为生命周期
- react组件也有自己的生命周期,了解组件的生命周期让我们在最合适的地方完成自己想要的功能
- 生命周期和证明周期函数的关系
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
- 比如装载节点,组件第一次在DOM树中被渲染的过程
- …类似Vue
componentDidMount
componentDidUpdate
componentWillUnmount
生命周期函数
Constructor
- 如果不初始化state或不进行方法绑定,则不需要为React组件实现构造函数
- constructor中通常只做两件事情
- 通过给this.state 赋值对象来初始化内部的state
- 为事件绑定实例this
- componentDidMount
- componentDidMount中通常进行哪里操作呢
- 依赖于DOM的操作可以在这里进行
- 在此处发送网络请求就是最好的地方
- componentDidUpdate()会在更新后会被立即调用,首次渲染不会执行此方法
- 当组件更新后,可以在此处对DOM进行操作
- 如果你对更新前后的props进行了比较,也可以选择在此处进行网络请求(例如,当props未发生变化时,则不会执行网络请求)
- componentWillUnmount
- componentWillUnmount()会在组件卸载及销毁之前直接调用
- 在此方法中执行必要的清理请求或清除
- 例如 timer 取消网络请求或清除
- 在componentDidMount()中创建的订阅等
父子组件传值(父 -> 子)
-
父组件通过属性挂载的方式将数据挂载在元素标签的属性上
-
子组件通过 this.props拿到挂载的这个组件上的所有属性的对象
-
当然传递属性可以对其类型进行限制,控制,利用propTypes这个库
-
class ReactCpn1 extends Component { static propTypes = { name: propTypes.string, age: propTypes.number, height: propTypes.number } render () { const { name, age, height } = this.props return ( <div> <h3>{`name:${name} === age:${age} === height:${height}`}</h3> </div> ) } } // 如果是function组件的话,直接将 propTypes属性加到构造函数的属性上即可 function ReactCpn (props) { const { name, age, height } = props return ( <div> <h3>{`name:${name} === age:${age} === height:${height}`}</h3> </div> ) } ReactCpn.propTypes = { name: propTypes.string, age: propTypes.number, height: propTypes.number }
- defaultProps 同上可以限制父组件不给子组件传值的时候的默认值是什么
- 视频7有props的源码分析
- 为什么constructor中拿props为 undefined 在 componentDidMount中却能拿到
- render()中也能拿到
子传父
- 思路一样是父组件中使用子组件,给子组件绑定属性,属性对应方法的调用
- 子组件拿到该属性对象的方法
- 将该方法绑定到特定事件上或者其他方式进行执行即可
- 数据以参数形式进行传递
react实现slot插槽
- 两种方案,一种是个插槽中只有一个元素,另一种适合多个元素
- 一个元素的时候,则无顺序,使用双标签
- 直接在组件双标签中写入插入的内容元素,或组件即可
- 在子组件中通过this.props.children拿到插入元素的数组集合
- 通过索引拿到 插入到子组件中即可
- 多个元素的时候,有顺序,建议单标签
- 通过传值的形式将元素,组件通过props传到子组件
- 子组件通过 this.props 解构拿到插入的元素
- 将这些元素放入子组件中对应的位置即可
跨组件传值
-
两套方案:1.一层层传递 2.利用context
-
一层层传递的时候可以使用 属性展开的方式 {…props}
-
context方式
-
1.创建Context对象
const UserContext = React.createContext({ nickName:'abc', level:-1 })
-
2.通过 UserContext.Provider 的组件包裹的方式,包裹住子孙组件,并给UserContext.Provider组件绑定 value值,绑定的对象就是传递的数据
-
3.通过 目标组件.contextType = UserContext 方式绑定
-
4.之后再目标组件中通过 this.context拿到传递过来的数据
-
-
上面context方式目标组件需要是类组件,当是函数式组件的时候
- 1.return一个UserContext.Consumer包裹的组件
- 2.UserContext.Consumer包裹的jsx通过 value=>{ }函数的形式返回
- 3.value参数就是上层传递下来的数据
-
为什么使用setState
- 开发中我们并不能直接通过修改state的值来让界面发生更新
- 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改并不能让React知道
- React并没有实现类似Vue2中的Object.defineProperty或者Vue3的Proxy方式来监听数据 变化
- 我们必须通过setState来告知React数据已经发生了变化
- 疑惑:在组件中并没有实现setState方法,为什么可以调用
- 原因很简单,setState方法是从Component中继承过来的
setState异步更新
- setState的更新是异步的?
- 最终打印结果是Hello world
- 可见setState是异步的操作,我们并不能在执行完setState后立马拿到最新的state结果
- 为什么setState涉及为异步?
- setState涉及为异步其实在GITHUB上也有很多讨论
- 之后react核心成员 redux的作者进行了回复
- 简单总结该回复
- setState涉及为异步,可以显著提高性能
- 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁的调用,界面重新渲染,效率很低
- 最好的办法应该是获取多个更新,之后进行批量更新
- 如果更新了state,但是render函数还没有执行,那么state和props没有能够保持同步
- state和 props不能保持一致性,会在开发中产生很多的问题
- setState涉及为异步,可以显著提高性能
如何获取异步的结果
- 方式一 setState回调
- setState 接受两个参数:第一个参数是回调函数,这个回调函数会在更新后执行
- 格式如下: setState( partialState, callback )
- 当然也可以在ComponentDidUpdate生命周期中执行,此时state也会是更新后的状态
setState一定是异步的吗
不是
- 如果在setTimeout中的更新,则会同步
- 如果在原生DOM事件监听中执行,也会是同步
- 其实分成了以下两个情况:
- 在组件中生命周期或React合成事件中,setState是异步
- 在setTimeout或者原生dom事件中setState是同步
数据的合并
- 通过setState去修改message 是不会对name产生影响的(用setState对state中某个数据进行更新,不会影响其他数据)
- 因为源码其实是有对原对象和新对象进行合并的,通过
Object.assign({ }, preState,partialState)
方法
- 因为源码其实是有对原对象和新对象进行合并的,通过
多个state的合并
-
如果setState异步方式进行更新操作,并且更新同样的内容多次,则最终并不能导致state中数据被进行了三次同样的操作
- 因为react内部会对这些更新操作先放到一个队列中,最终进行合并更新(个人理解多次操作,在拿到this.state.xx数据的时候,此时多次拿到的这个数据都是一样的,因此,异步操作后,数据依旧是只从初始数据改变了一次)
-
如果想要多次同样的数据操作能够不被合并,原本传入setState中的对象应该改为传入一个函数,返回一个对象
-
this.setState((state,props) => { return { counter:state.counter + 1 } })
-
React更新机制
- JSX —> 虚拟DOM -----> 真实DOM
- react的更新流程
- props/state改变 -----> render函数重新执行 -----> 产生新的DOM树 -----> 新旧DOM树进行diff算法比较 -----> 计算出差异 -----> 更新到真实的DOM
- react的diff算法是 优化过的,或者说是简化国的
- 同层节点之间相互比较,不会跨节点比较
- 不同了类型的节点,产生不同树结构
- 开发中,可以通过KEY来指定哪些节点在不同的渲染下保持稳定
- 不同类型的元素,删除,添加,更改都会对节点进行渲染
- 单纯的该className style等元素不会重新渲染更新
KEY值的优化
- 首先默然情况下,react对子节点的渲染更新是通过递归比较的方式进行的
- 更新后的树节点和之前的树节点会一层层的比较,当有一层不同时,则会生成一个mutation,之后将产生mutation的节点进行重新渲染更新
- 由于上面的机制,当我们在这个树节点中插入一个元素,则会导致该元素开始的之后的所有节点都会生成一个mutation 会导致原本后面没有更改的节点全部重新渲染更新,这样的会造成性能问题
- 因此我们需要通过一个标记来标记这些个节点,这就引出了我们的key
- 当树节点都绑定了key值的话,这个时候插入一个节点,此时diff算法比较的时候,其内部就能知道插入的元素是新插入的元素,该元素后面的节点只需要让出位置给这个新插入的节点即可,在节点的比较的时候,也会根据对应的key来比较节点,这样就不会生成那么多mutation 更新那么多原本不必要更新的节点了
render函数被调用
- 按照之前的代码开发,当我们修改根组件中的state中的数据的时候,会导致根组件,包括子孙组件的render函数被调用,这样大大的降低了性能
- 如何控制render是否被调用,可以通过 shouldComponentUpdate 方法即可(简称SCU)
- 该方法有两个参数:参数1:nextProps 修改之后,最新的props属性,参数2:nextState修改之后,最新的state属性
- 该方法返回值是一个boolean类型
- 返回值true,那么久需要调用render方法
- 返回值false,那么就不需要调用render方法
- 默认是true,也就是state变化,就会调用render
PureComponent
- 如果所有的类,我们都需要手动来实现shouldCompinentUpdate,那么会增加我们很多的工作量
- react已经考虑到了这点,因此我们可以将class继承自PureComponent
- 源码中这个组件首先是默认返回isPureReactComponent 为 true的,其次先判断了 shouldUpdate是否等于undefined, 之后判断 isPureReactComponent 是否为true,是的话然后通过 shallowEqual方法 对新旧 state进行浅层比较 ,如果有变化则更新, 没有则不更新
高阶组件memo
- PureComponent的方式是针对 类组件的 ,那么函数式组件该如果处理
- 我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹
- Footer没有使用memo函数进行包裹;
- 最终的效果是,当counter发生改变时,Header、Banner、ProductList的函数不会重新执行,而Footer的函数会被重新执 行