React逻辑浅析
React的颠覆式创新是:当数据发生变化时,UI可以自动将变化反映出来,其主要实现思路就是在业务状态和UI状态之间建立一个绑定关系 React发展浅析
- v16.0:
- 为了解决之前大型React应用一次更新遍历大量虚拟DOM带来的卡顿问题,React重写了模块Reconcil,启用了Fiber架构
- 为了让节点渲染到指定容器内,更好的实现相关功能(如弹窗),推出了createPortal API
- 为了捕获渲染中的异常,引入componentDidCache钩子,划分了错误边界
- v16.2:
- 推出了Fragment,解决数组元素问题
- v16.3:
- 增加React.createRef() API,可以通过React.createRef()获取到Ref对象
- 增加React.ForwardRef() API,解决高阶组件Ref传值问题
- 推出新版本context API,迎接Provider/Consumer时代;
- 增加getDerivedStateFromProps和getSnapshotBeforeUpdate生命周期
- v16.6:
- 增加React.memo() API,用于控制子组件渲染
- class组件中可以通过React.PureComponent API来实现,底层就是实现了浅比较state
- 增加React.lazy() API,实现代码分割
- 增加contextType让类组件更便捷的使用context
- 增加生命周期 getDerivedStateFromError 代替 componentDidCatch
- 增加React.memo() API,用于控制子组件渲染
- v16.8:
- 全新React-Hooks支持,让函数组件也可以做类组件的一切事情
- v17:
- 事件绑定由document变成container,移除事件池等
React进阶实践指南git
- 事件绑定由document变成container,移除事件池等
Reconciliation协调过程浅析
Reconciliation过程图解:
props或state改变 => render函数返回不同的元素树 => 新旧DOM树比较(diff:也是性能优化时的主要手段减少 Diff 过程
) => 针对差异的地方进行更新 => 渲染为真实的DOM树
- Diff设计思想概述
- 常规的循环递归的方式是对每一个节点进行比较,但是其算法的复杂度可以达到O(n^3),这还没有精细化到类型、属性、值等的节点细节比较
- 但是在React中,其Diff的复杂度可以达到O(n)级别,具体原因/思想如下
- 永远只比较同层级的节点,不会跨层级节点比较节点
- 不同的两个节点产生不同的数;也就是下述「优化Diff过程分解」中的节点类型不一致时将原来的节点包括其后代都干掉,替换成新的
- 通过key值指定哪些元素是相同的
- key选取的原则一般是:
不需要全局唯一,但必须是列表中保持唯一
- 当key值在列表中不唯一时,一旦顺序发生了变化,Diff效率就有可能骤然下降
- key选取的原则一般是:
- 优化Diff过程分解
- class组件中,可以在
shouldComponentUpdate
中控制返回true/false来控制是否发生VDOM树的Diff过程(一般都是进行props和state的浅比较
),典型的应用就是React中推出的PureComponent
这个API,会在props或state改变时对两者数据进行浅比较
;-
PureComponent和memo浅比较步骤原理
- 基础数据类型直接比较出结果
js// 升级版本 => 非浅层对比 // 第一关:保证两者都是基本数据类型。基础数据类型直接比较出结果。 if (objA == null && objB == null) return true; if (typeof objA !== 'object' && typeof objB !== 'object' && is(objA, objB)) { return true; }
- 只要一个不是对象类型(或是null)就返回false
js// 升级版本逻辑一致 // 普通版本 => 浅层对比 // 第二关:只要有一个不是对象数据类型就返回 false if ( typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null ) { return false; }
-
此时表示两个都是对象类型,优先先比较属性数量是不是相同
-
其次比较两者的属性是否相同,值是否相同
js// 普通版本 => 浅层对比 // 比较两者的属性是否相等,值是否相等 for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } }
js// 升级版本 => 非浅层对比 // 但是也是存在浪费 当数据量很大 改动较小时 都是需要遍历的 会有较大的性能浪费 // 第四关:比较两者的属性是否相等,值是否相等 for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call (objB, keysA [i]) || !is (objA [keysA [i]], objB [keysA [i]]) ) { return false; } else { // 对属性值再次进行递归比较 即重新执行上述步骤 if (!deepEqual (objA [keysA [i]], objB [keysA [i]])){ return false; } } }
-
浅比较其缺点也是显而易见的,在进行到最后一层的拦截中时,一旦属性的值为引用类型的时候浅比较就会出问题(浅比较是只会比较对象的引用类型地址是否改变了,当起地址没有改变的情况下,即使值变了,也会认为是没有改变的),因此这种方式只适用于
无状态组件
或者状态数据非常简单的组件
,对于大量应用型组件是无能为力的;
-
- 函数式组件中:React为函数组件提供了一个
memo
的方法进行依赖数据的浅层比较(是高阶组件
,有记忆组件渲染结果的逻辑,从而提高组件的性能表现),其和PureComponent在数据比较上唯一的区别在于只进行了props的浅比较
,其用法是直接将函数传入memo API中导出即可 => 这也就是为什么在函数组件中需要在每个组件导出时都需要加上memo
包裹了;如export default React.memo(APP)
- React在Diff时节点类型不一致时,其简单粗暴的方式是
直接将原 VDOM 树上的该节点以及该节点下所有的后代节点全部删除,然后替换为新VDOM树上的同一位置
- class组件中,可以在
diff优化方案三 => immutable数据结构+SCU(memo)浅层对比
从本质上来说,无论是浅层对比还是深层对比,最终目的都是需要知道组件的props或stats数据有无发生变化,在这种情况下就产生了
immutable
数据就产生了;
immutable简介
immutable 数据是一种利用
结构共享(Structural Sharing)
形成的持久化数据结构
,一旦有部分改动,那么将会返回一个全新的对象,并且原来相同的节点会直接共享
,直接共享
避免了deepCopy将所有节点都复制一遍而带来的JS性能损耗。总结来说就是:immutable对象数据内部采用的是
多叉树
的结构,凡是有节点被改变,那么与之相关的所有上级节点
都会更新(没有变化的上级节点则直接共享)
-
immutable常见的API
- fromJS:将JS对象转换为immutable对象
jsimport {fromJS} from 'immutable'; const immutableState = fromJS ({ count: 0 });
- toJS:将immutable对象转换为JS对象
const jsObj = immutable.toJS()
- get/getIn:用来获取immutable对象的属性
js// JS 对象 let jsObj = {a: {b: 1},c:2}; let res = jsObj.a.b; let res2 = jsObj.c; //immutable 对象 let immutableObj = fromJS (jsObj); let res2 = immutableObj.get ('c');// 传入需要获取的key值 let res = immutableObj.getIn (['a', 'b']);// 注意传入的是一个数组
- set:用来对immutable对象的属性赋值
js// JS 对象 let jsObj = {a: 2}; jsObj.a = 3 //immutable 对象 let immutableObj = fromJS (jsObj); let res2 = immutableObj.set ('a',1);
- merge:新数据与旧数据对比,旧数据中不存在的属性直接添加,旧数据中存在的属性用新数据进行覆盖
js// JS 对象 let jsObj = {a: 2}; let newObj = Object.assign(jsObj, {a:3,b:4}, ...) //immutable 对象 let immutableObj = fromJS (jsObj); let res2 = immutableObj.merge ({ a: 1, b: 2, c: 3 });
-
immutable优缺点浅析
- 优点:
- 降低mutable带来的复杂度
- 节省内存
- 历史追溯性:类似于历史记录的功能,每次的更改操作都会被保留缓存下来,当需要撤销时只需要取出即可,这个特性在redux和flux中特别有用
- 拥抱函数式编程:immutable本身就是函数式编程的概念,纯函数编程的特点就是:只要输入一致,输出必然一致,相对于面相对象,这样的开发组件和调试更加方便
- 缺点:
- 和常规的JS有出入,需要新学习API
- 混用时容易出错 immutable过程动态图解
- 优点:
组件、状态和JSX浅析
使用组件的方式描述UI
在React中,所有的UI都是通过
组件
的方式去描述和组织的,当然也可以理解成React中一切皆组件
类组件在执行构造函数过程中会在实例上绑定props和context,初始化空Ref属性,原型上绑定SetState、ForceUpdate方法,因为props是在构造函数中进行绑定的,因此在子类组件中需要通过super
进行执行类组件的构造函数,此时需要将props参数传递给super,不然会打印出空的props
在React中,组件本质上就是类和函数,因此函数与类上的特性在React组件上同样具有,与常规的类和函数不同的是:组件承载了渲染视图的UI和更新视图的setState、useState等方法
,React在底层逻辑上会像正常实例化类和正常执行函数那样处理组件
-
setstate和forceUpdate
-
setstate:
-
参数
- 参数一:可以是对象数据或函数
- 对象时:则为将要合并的state
this.setState({num: 1}, ()=> {console.log(this.state.num)})
- 函数时:当前组件的state和props将作为参数,返回值用于合并新的state
this.setState((state,props) => {return {num: 1}})
- 参数二:更新后的实时state
- 参数一:可以是对象数据或函数
-
执行后的相关逻辑
- setState会产生当前更新的优先级(老版本用expirationTime、新版本用lane)
- 接着React会从fiber Root根部fiber向下调和子节点,调和阶段将对比发生更新的地方,更新对比expireationTime,找到发生更新的组件,合并state,然后触发render函数,得到新的UI视图层,完成render阶段
- 接下来到commit阶段,commit阶段替换真实DOM,完成此次更新流程
- 此时还是在commit阶段,会执行SetState中的callback函数,执行完毕后就完成了一次SetState的全过程
- 总结:
- 触发setState → 计算expirationTime → 更新调度,调和fiber树 → 合并state,执行render → 替换真实DOM → 执行callback函数
- render阶段render函数执行 → commit阶段真实DOM替换 → setState回调函数执行callback
-
类组件如何限制state更新DOM
- PureComponent:可以对state和props进行浅比较,如果没有发生变化,则组件不更新
- shouldComponentUpdate:生命周期可以通过判断前后state变化来决定组件是否需要更新,需要更新返回true,否则返回false
-
是执行组件更新的主要方式,类组件初始化过程中就绑定了负责更新的
update
对象 -
其会将组件state的更改加入到更新队列里,并通知React需要使用更新后的state重新渲染此组件及其子组件
-
在Class组件中,除了继承React.Component,底层还加入了
update
对象,组件中调用的setState
和forceUpdate
本质上是调用了update
对象上的enqueueSetState
和enqueueForceUpdate
方法 -
其具体内部实现是在react-dom中实现的,具体是通过
enqueueSetState
实现,enqueueSetState
的作用就是在调用SetState时,给Fiber对象创建一个update,然后该update对象添加到updateQueue中,进入任务调度流程jsenqueueSetState(){ /* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */ const update = createUpdate(expirationTime, suspenseConfig); /* callback 可以理解为 setState 回调函数,第二个参数 */ callback && (update.callback = callback) /* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */ enqueueUpdate(fiber, update); /* 开始调度更新 */ scheduleUpdateOnFiber(fiber, expirationTime); }
- 创建新的update对象时,会先计算当前时间和更新的优先级,根据当前时间和更新优先级创建一个新的update对象,然后将传递进来的需要更新的state对象(update.payload)和callback(update.callback)添加到该update对象上;最后将update对象添加到updateQueue中,updateQueue是一个环形链表
queueUpdate(fiber,update,lane)
- lane:是一个优先级变量,其是通过
requestEventTime(fiber)
实现的,也有lane模型(车道模型)的说法
- lane:是一个优先级变量,其是通过
- 创建新的update对象时,会先计算当前时间和更新的优先级,根据当前时间和更新优先级创建一个新的update对象,然后将传递进来的需要更新的state对象(update.payload)和callback(update.callback)添加到该update对象上;最后将update对象添加到updateQueue中,updateQueue是一个环形链表
-
-
forceUpdate
- 是强制让组件更新的方式
forceUpdate
是通过enqueueForceUpdate
来触发render的,enqueueForceUpdate
的作用就是在调用forceUpdate
时,给fiber对象创建一个update,然后将update对象添加到updateQueue中,并进入任务调度流程;enqueueForceUpdate
与enqueueSetState
类似,唯一不同点是修改了 属性tag的值- tag的值类型有:
- export const UpdateState = 0;// 更新(默认)
- export const ReplaceState = 1;// 替换
- export const ForceUpdate = 2;// 强制更新
- export const CaptureUpdate = 3;// 捕获更新
- tag的值类型有:
-
两者更新流程(都相似)
- 获取ClassComponent的this对象上的fiber对象
- 计算当前时间和更新优先级
- 根据当前时间和更新优先级创建update对象
- 将setstate中要更新的State对象和要执行的callback添加到update对象上
- 将当前update对象加入到updateQueue队列中
- 进入任务调度流程
-
两者不同
jsfunction Component(props, context, updater) { this.props = props; //绑定props this.context = context; //绑定context this.refs = emptyObject; //绑定ref this.updater = updater || ReactNoopUpdateQueue; //上面所属的updater 对象 } /* 绑定setState 方法 */ Component.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState'); } /* 绑定forceupdate 方法 */ Component.prototype.forceUpdate = function(callback) { this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); }
- forceUpdate是update对象的tag属性值2(2),从而触发了ForceUpdate
-
-
React批量更新逻辑浅析
-
React采用
事件合成
的机制,每一个事件都是由React事件系统统一调度的,React中state批量更新正是和事件系统息息相关的;在legacy
模式下,所有事件都是经过了dispatchEventForLegacyPluginEventSystem
中的batchedEventUpdates
进行统一处理的jsfunction batchedEventUpdates(fn, a) { /* 开启批量更新 */ isBatchingEventUpdates = true; try { /* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */ return batchedEventUpdatesImpl(fn, a, b); } finally { /* try 里面 return 不会影响 finally 执行 */ /* 完成一次事件,批量更新 */ isBatchingEventUpdates = false; } }
-
在React的批量更新中,如果是常规的事件处理的方式进行state批量更新,此时会进行state的批量更新进而触发state的合并处理逻辑,最终结果就是都不是最新的state
-
当用异步操作
promise
或setTimeout
将事件函数包裹后,批量操作会打破,批量操作此时会变成顺序执行了,类似于每个setState都单独在setTimeout中执行了
-
-
React自定义提升更新优先级
- 通过ReactDOM提供的
flushSync
API实现,flushSync
可以将回调函数中的更新任务,放在一个较高级别的优先级中 flushSync
在同步条件下,会合并之前的setState或useState,可以理解成「如果发现了flushSync,会先执行更新,如果之前有未更新的setState或useState,就会一起合并了」
- 因此React中的更新优先级是:
- flushSync中的setState > 正常执行上下文中setState > setTimeout、promise中的setState
- 通过ReactDOM提供的
-
React对组件的处理流程
- 函数组件
- React源码中对函数组件的处理方式是直接调用的方式,因此不能给函数组件直接绑定属性或对函数组件的prototype绑定属性或方法(不是通过new的方式进行实现的)
jsfunction renderWithHooks( current, // 当前函数组件对应的 `fiber`, 初始化 workInProgress, // 当前正在工作的 fiber 对象 Component, // 我们函数组件 props, // 函数组件第一个参数 props secondArg, // 函数组件其他参数 nextRenderExpirationTime, //下次渲染过期时间 ){ /* 执行我们的函数组件,得到 return 返回的 React.element对象 */ let children = Component(props, secondArg); }
- 类组件
- 在类组件中,组件的调用方式是通过new的方式进行实现的,因此可以在类的原型上进行属性或方法的拓展
jsfunction constructClassInstance( workInProgress, // 当前正在工作的 fiber 对象 ctor, // 我们的类组件 props // props ){ /* 实例化组件,得到组件实例 instance */ const instance = new ctor(props, context) }
- 类组件和函数组件的总结
- 对于类组件来说,底层只需要实例化一次,实例中保存了组件的state等状态,对于每次更新只需要重新执行render方法及对应的生命周期即可;
- 但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明,为了让函数可以保存一些状态,执行一些副作用钩子,React Hooks应运而生,它可以帮助记录React中组件的状态,处理一些额外的副作用;
- 函数组件
Reactfiber
浅析
Fiber是用来解决更新时不够精确带来的缺陷,是用来让原来同步的调用颗粒化的
在
fiber 对象
出现之前,React架构体系只有协调器Reconcil和渲染器render;当有新的update时,React会递归所有的VDOM节点,当DOM节点过多时,会导致其他事件滞后,造成卡顿;React Fiber本质上是将更新过程碎片化,每执行完一段更新过程,就将控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有则继续执行更新过程,如果有则先执行紧急任务,然后再去执行更新操作;在此过程中的
每一个分片单元就是Fiber,一个Fiber就是一个工作单元
- React在Fiber阶段的主要操作
- 为每个任务增加了优先级,优先级高的任务可以中断优先级低的任务。然后再重新执行优先级低的任务
- 增加了异步任务,调用requestIdleCallback API,浏览器空闲的时候会执行
- requestIdleCallback API是在浏览器的空闲时段内调用的函数排队。方法提供deadline,即任务执行限制时间,以切分为任务,避免长时间执行,阻塞UI渲染而掉帧,安排低优先级或非必要的函数在帧结束时的空闲时间被调用
- requestAnimationFrame:安排高优先级的函数在下一个动画帧之前被调用
- DOM Diff树变成了链表,一个DOM对应两个Fiber(一个链表),对应两个队列,这都是为了找到被中断的任务,重新执行
- 从架构方面来说,Fiber是对React核心算法(调和过程)的重写
- 从编码角度来说,Fiber是React内部所定义的一种数据结构,是Fiber树结构的节点单位,即是React16新架构下的虚拟DOM
- Fiber是如何工作的
- ReactDOM.render()和setState的时候开始创建更新
- 在调用这两个API进行组件渲染和更新时,React会经历两个阶段:reconciler和render阶段
- Reconciler(调和阶段):React会自顶向下通过递归,遍历新数据生成新的Virtual DOM,然后通过Diff算法,找到需要变更的元素(patch),放到更新队列里去
- render(渲染阶段):遍历更新队列,通过调用宿主环境的API,实际更新渲染对应的元素;宿主环境指DOM、Native、WebGL等
- React15最大的问题就是:调和阶段产生的虚拟DOM是通过深度优先递归的,并且中途不可间断,在浏览器中JS线程和浏览器GUI线程是互斥的,加入虚拟DOM很深,处理JS的时间过长,会导致浏览器刷新时掉帧,造成卡顿;
- React则实现了异步的可中断的更新
- Fiber使用了
requestAnimationFrame
来处理优先级较高的更新,使用requestIdleCallback
来处理优先级较低的更新;因此在调度工作时,Fiber检查当前更新的优先级和deadline(帧结束后的自由时间)
- Fiber使用了
- Fiber有可能暂停、重用和中止工作的原因:如果优先级高于待处理的工作,或者没有截止日期或截止日期没有到,Fiber可以在一帧之后安排多个工作单元。而下一组工作单元会被带到更多的帧上
- 在调用这两个API进行组件渲染和更新时,React会经历两个阶段:reconciler和render阶段
- 将创建的更新加入任务队列,等待调度
- 在
requestIdleCallback
空闲时执行任务 - 从根节点开始遍历Fiber Node,并且构建WorkeInProgress Tree
- 生成effectList
- 根据EffectList更新DOM 总结
- ReactDOM.render()和setState的时候开始创建更新
React Fiber协调器使之有可能将工作分为多个工作单元。它设置每个工作单元的优先级,并使之暂停、重用和中止工作单元成为可能。在fiber树中,单个节点保持跟踪,这是使得上述事情成为可能得必要条件。每个Fiber都是一个链表的节点,他们通过子、兄弟节点和返回引用连接起来;
vue不需要fiber是因为他使用nextTick来异步决定什么时候执行renderFunction,本质上思路是和React一致,但是响应式原理没有太大关系
- 组件分类
- 内置组件:就是
映射到HTML节点的组件
,例如div、table、span等,作为一种约束,都是小写字母 - 自定义组件:即就是
自己创建的组件
,作为约束,使用时以大写字母开头,如TopCard等 - 备注:在实际开发中,会将不同的UI划分成各种组件,然后将这些组件拼接到一起,当然组件的设计也会有各种相应的规则:如
高内聚、低耦合
等
- 内置组件:就是
- 组件的本质分析
- React组件的变相理解就是
Model
到View
层的映射,Model
就是React中的state
和props
,而View
即表示是展示层 - 因此可以理解成:UI展示可以看成是一个函数执行的过程,其中Model是输入参数,函数的执行结果是DOM树,也就是View层,在React内部需要保证的是每当Model层发生变化时,函数会重新执行,并且生成新的DOM树,然后React再将新的DOM树以最优的方式更新到浏览器中
- React组件的变相理解就是
- 组件的强化方式
-
类组件的继承:
- React类组件有良好的
继承属性
,可以针对一些基础组件,首先实现一部分基础功能,再针对项目的要求进行有方向的改造
、强化
和添加额外功能
等 - 其主要优势是:
- 可以控制父类的render,还可以添加一些其他的渲染内容
- 可以共享父类的方法,还可以添加额外的方法和属性
- 劣势是:
- 父组件的state、生命周期、方法等可能会被继承后的子组件修改,例如componentDidMount继承后子组件会执行,父组件不会执行,父组件中的方法、State也会被子组件影响覆盖掉
ts/* 人类 */ class Person extends React.Component { constructor(props) { super(props) console.log('hello , i am person') } componentDidMount() { console.log(1111) } eat() { /* 吃饭 */ } sleep() { /* 睡觉 */ } ddd() { console.log('打豆豆') /* 打豆豆 */ } render() { return <div> 大家好,我是一个person </div> } } /* 程序员 */ class Programmer extends Person { constructor(props) { super(props) console.log('hello , i am Programmer too') } componentDidMount() { console.log(this) } code() { /* 敲代码 */ } render() { return <div style={{ marginTop: '50px' }} > {super.render()} { /* 让 Person 中的 render 执行 */} 我还是一个程序员! { /* 添加自己的内容 */} </div> } } export default Programmer
- React类组件有良好的
-
函数组件自定义Hooks
-
HOC高阶组件
-
- class作为组件载体的弊端
- class中有最具特色的
继承
特性,而在React组件中是没有所谓的继承的,例如一般不会编写一个组件去继承自另一个组件 - 所有的UI都是由状态驱动的,因此很少在组件外部去调用组件的(类实例)的方法 → 组件内部的方法都是在内部进行调用或者在组件内的生命周期中进行调用
- 结论:通过函数的方式去描述一个组件才是最自然的方式 → React中的函数组件的机制,函数组件更适合去描述
state => View
的映射关系
- class中有最具特色的
- 初期函数组件的弊端
- 没有state即
状态
的概念 - 没有生命周期方法
- 没有state即
状态管理
React中使用
state
和props
来管理状态,以满足其核心机制在数据变化时可以自动重新渲染UI
,state是状态保存机制,props则是为了父子组件传递状态,从而实现组件之间的交互;
props浅析
无论是函数组件还是Class组件,父组件绑定到当前子组件上的属性和方法最终会变成props传递给他们,但有一些特殊的属性,如Ref、key等,React底层会对其进行额外的处理;
js
/* children 组件 */
function ChidrenComponent(){
<div> In this chapter, let's learn about react props ! </div>
}
/* props 接受处理 */
class PropsComponent extends React.Component{
componentDidMount(){
console.log(this,'_this')
}
render(){
const { children , mes , renderName , say ,Component } = this.props
const renderFunction = children[0]
const renderComponent = children[1]
/* 对于子组件,不同的props是怎么被处理 */
<div>
{ renderFunction() }
{ mes }
{ renderName() }
{ renderComponent }
<Component />
<button onClick={ () => say() } > change content </button>
</div>
}
}
/* props 定义绑定 */
class Index extends React.Component{
state={
mes: "hello,React"
}
node = null
say= () => this.setState({ mes:'let us learn React!' })
render(){
<div>
<PropsComponent
mes={this.state.mes} // ① props 作为一个渲染数据源
say={ this.say } // ② props 作为一个回调函数 callback
Component={ ChidrenComponent } // ③ props 作为一个组件
renderName={ ()=><div> my name is alien </div> } // ④ props 作为渲染函数
>
<div>hello,world</div> } { /* ⑤render props */ }
<ChidrenComponent /> { /* ⑥render component */ }
</PropsComponent>
</div>
}
}
- 在React中props可以为什么
- 作为一个子组件渲染的数据源
- 作为一个通知父组件的回调函数
- 作为一个单纯的组件传递
- 作为渲染函数
- 对于标签内的属性和方法会直接绑定在props对象的属性上,对于组件的插槽会被绑定到props的children属性中
- 监听props变化
- Class组件中
- 可以通过
componentWillReceiveProps
作为监听props的生命周期,但是在React后续版本中可能会遗弃这个生命周期,因为其可能会引起多次执行等情况的发生;可以用getDerivedStateFromProps
进行检测props变化
- 可以通过
- 函数组件中
- 函数组件也可以通过
useEffect
来作为props改变后的监听函数(但是useEffect
在初始化的时候会默认执行一次)
- 函数组件也可以通过
- Class组件中
- 操作props技巧
-
抽象props:
- 一般用于跨层级传递props,一般不需要具体指出props中的某个属性,而是将props直接传入或抽离到子组件中
-
混入props:
- 一般用于跨组件props传递,可以将多层级组件的数据进行传递
-
抽离props
-
一般用于将从父组件props中抽离某个属性,然后再传递给子组件
jsfunction Son(props){ console.log(props) <div> hello,world </div> } function Father(props){ const { age,...fatherProps } = props return <Son { ...fatherProps } /> } function Index(){ const indexProps = { name:'alien', age:'28', mes:'let us learn React !' } return <Father { ...indexProps } /> }
-
-
注入props
- 显式注入props:就是可以直观的看见标签中绑定的props
jsfunction Son(props){ console.log(props) // {name: "alien", age: "28"} <div> hello,world </div> } function Father(prop){ return prop.children } function Index(){ <Father> <Son name="alien" age="28" /> </Father> }
- 隐式注入props:一般通过react.cloneElement对props.children克隆再混入新的props
jsfunction Son(props){ console.log(props) // {name: "alien", age: "28", mes: "let us learn React !"} <div> hello,world </div> } function Father(prop){ return React.cloneElement(prop.children,{ mes:'let us learn React !' }) } function Index(){ <Father> <Son name="alien" age="28" /> </Father> }
-
函数组件中的hooks机制浅析
最大特点就是 → 逻辑复用
,从而使得函数式组件的价值最大化,实现了React中的类组件到函数组件的革命性的转变
React中的函数组件是没有
状态state
的机制的,因此需要一个函数之外的空间
来保持这个状态,并且可以监测其变化,从而在变化时可以触发函数组件的重新渲染(可以将一个函数外部的数据绑定到函数执行,当数据变化时,函数可以重新执行);因此,任何会影响UI展示的外部数据,都可以通过这个机制绑定到React的函数组件上 → 上述机制就是Hooks;
Hooks就是将某个目标结果(DOM树)钩
到某个可能会变化的数据源或者事件源上,那么当被钩
到的数据或事件发生变化时,产生这个目标结果的代码就会被重新执行,产生更新后的结果; => hooks执行的最终结果都是导致UI的变化
Hooks的最大好处就是逻辑复用
,hooks中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个Hook执行的结果,从而实现了逻辑复用
高阶组件实现窗口大小监测从而动态替换对应的组件展示
js
const withWindowSize = Component => {
// 产生一个高阶组件 WrappedComponent,只包含监听窗口大小的逻辑
class WrappedComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
size: this.getSize()
};
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
getSize() {
return window.innerWidth > 1000 ? "large" :"small";
}
handleResize = ()=> {
const currentSize = this.getSize();
this.setState({
size: this.getSize()
});
}
render() {
// 将窗口大小传递给真正的业务逻辑组件
return <Component size={this.state.size} />;
}
}
return WrappedComponent;
};
// 自定义组件使用高阶组件逻辑
class MyComponent extends React.Component{
render() {
const { size } = this.props;
if (size === "small") return <SmallComponent />;
else return <LargeComponent />;
}
}
// 使用 withWindowSize 产生高阶组件,用于产生 size 属性传递给真正的业务组件
export default withWindowSize(MyComponent);
- 高阶组件的缺点分析
- 为了传递外部状态,需要定义一个没有UI的外层组件,这个组件只是为了封账一段可重用的逻辑 Hooks实现窗口大小监测从而动态替换对应的组件展示
js
const getSize = () => {
return window.innerWidth > 1000 ? "large" : "small";
}
const useWindowSize = () => {
const [size, setSize] = useState(getSize());
useEffect(() => {
const handler = () => {
setSize(getSize())
};
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
return size;
};
// 自定义组件使用hooks逻辑
const Demo = () => {
const size = useWindowSize();
if (size === "small") return <SmallComponent />;
else return <LargeComponent />;
};
hooks的另一个好处 → 有助于关注分离
有助于关注分离:即hooks可以让针对同一个业务逻辑的代码尽可能聚合在一块,这在class中是很难做到的,class组件中,不得不将同一个业务逻辑代码分散在类组件的不同生命周期的方法中;
在函数组件的hooks中可以将同一个或者类似的业务逻辑代码都整合在一起,不用像class组件中分散在不同的生命周期中,例如在componentDidMound中进行初始化,componentWillUnmount中进行相关卸载和事件解绑
自定义Hook浅析
-
基本思想
- 自定义hook是标准的封装和共享编辑的方式
- 自定义hook是一个函数,其名称以use开头
- 自定义hook其实就是逻辑和内置的hook的组合
- 总结:自定义hook就是对组件间共用逻辑的封装,其名称以use开头
-
基本案例
- 封装input的value和onchange属性(同理像image等的都可以进行hooks封装提取)
jsfunction useUpdateInput(initialValue) { const [value, setValue] = useState(initialValue); return { value, onChange: (e) => setValue(e.target.value), }; } function App() { const usernameInput = useUpdateInput(""); const passwordInput = useUpdateInput(""); const submitForm = (event) => { event.preventDefault(); console.log(usernameInput.value); console.log(passwordInput.value); }; return ( <form onSubmit={submitForm}> <input type="text" name="username" {...usernameInput} /> <input type="password" name="password" {...passwordInput} /> <input type="submit" /> </form> ); }
- 推荐案例地址自定义Hook-组件状态
总结
- hooks完成了
state => View
的函数式映射 - hooks解决了class组件存在的代码冗余、难以逻辑复用的问题
常见hooks浅析
-
函数组件中的state 在React-hooks正式发布后,useState可以使得函数组件像类组件一样拥有state,即函数组件可以通过useState来改变UI视图
- 修改state,保证视图与state进行「双向绑定」
- 基本用法:
[state,dispatch] = useState(initData)
- 参数解析
- state:作为渲染视图的数据源,用于将视图和数据绑定起来
- dispatch:改变state的函数,也可以理解成推动函数组件渲染的渲染函数
- 情况一:非函数:此时将作为新的值赋予给state,作为下一次渲染使用
- 情况二:函数:此时可以获取到上一次返回的最新的state,函数的返回值作为新的state,该情况可以解决dispatch是异步的问题,避免获取到不是最新的值的情况
- initData:两种情况(函数或非函数)
- 非函数:将作为state的初始值
- 函数:函数的返回值作为初始值
- 参数解析
- 注意点:
- 在函数组件中,dispatch的更新效果是和Class组件类似的,但是useState中调用改变state的函数dispatch,在本次函数执行上下文中是获取不到最新的state值的;
- 原因是:函数组件更新就是函数的执行,在函数的一次执行中,函数内部所有变量都进行了重新声明,因此改变的state只有在下次函数组件更新执行时才可以被更新
- useState的dispatchAction处理中,会进行两次浅比较state,发现state相同时就不会开启更新调度任务,尤其是在state为对象类的情况下,只改变对象的某一个属性值有可能会引起页面视图不更新,当然是可以通过dispatchState时进行解构达到浅拷贝对象从而解决上述问题
- 在函数组件中,dispatch的更新效果是和Class组件类似的,但是useState中调用改变state的函数dispatch,在本次函数执行上下文中是获取不到最新的state值的;
- 基本用法:
- 监听state变化
- 在Class组件中可以通过setState的第二个参数callback或者生命周期componentDidUpdate进行检测到state变化或是组件进行了更新
- 在函数组件中,可以通过
useEffect
进行检测state变化,通常可以将指定的state传入到useEffect的第二个参数deps中,但是useEffect初始化时会默认执行一次
- 修改state,保证视图与state进行「双向绑定」
-
useState:让函数组件具有维持状态的能力
- 参数是创建state的初始值,可以是任意类型
- 返回值是一个有着两个元素的数组,第一个元素表示用于读取state的值,第二个用来设置这个state的值
- 注意📢📢:
- state的变量是只读的,只能通过第二个数组元素来设置他的值
- Hooks中的state遵循的原则是:
state中永远不要保存可以通过计算得到的值
,即需要通过计算得到的值或在使用时需要读取的外界的值(href中、localstorage等)要在使用时进行读取或计算,而不是计算后或读取好后放入state中;类似于依赖就近
原则,即在使用时进行获取或计算初始化,因为每次用到这个组件的时候都需要进行重新获取(此过程就包含了计算和读取)即使此次使用或此刻并不会用到这些数据
- useState和setState比较
- 相同点:从原理角度出发,useState和setState更新视图,底层都是调用了
scheduleUpdateOnFiber
方法,而且事件驱动的前提下都会有批量更新的规则 - 不同点:
- 在不是PureComponent组件模式下,setState不会浅比较两次State的值,只要调用了setState,在没有其他优化手段的前提下,就会执行更新逻辑;但是在useState中的dispatchAction会默认比较两次State是否相同,然后决定是否更新组件
- setState有专门监听State变化的回调函数callback,可以获取到最新的State;但在函数组件中,只能通过useEffect来执行State变化引起的副作用
- setState在底层处理逻辑上主要是和老State进行合并处理,而useState更倾向于重新赋值
- 相同点:从原理角度出发,useState和setState更新视图,底层都是调用了
- useState工作流程
- 会先判断组件当前处于什么阶段
- 当时渲染阶段:不会检查State值是否相同
- 不是渲染阶段:会检查State值是否相同
- 值不相同,该组件重新渲染
- 值相同,在某些情况下也会继续执行当前组件的渲染
- 初始化状态:在组件的初始化过程中,useState会根据传入的初始值来初始化对应的状态
- 组件渲染时的数据绑定:在组件渲染时,useState返回的状态值会被读取并渲染到页面上
- 会先判断组件当前处于什么阶段
-
useEffect:执行副作用
- 定义:去执行一段和当前执行结果无关的代码,如修改函数外部的一个变量或发起一个请求等,即
在函数组件的当次执行过程中,useEffect中的代码执行是不会影响到渲染出来的UI的
- 参数是回调、依赖项和返回一个回调函数
- 依赖项:
- 没有依赖项:每次render都会重新执行
- 依赖项为空数组:只在首次执行时触发,对应的是class中的componentDidMount
- 有依赖项:只有在依赖项变化时重新执行
- 返回回调函数:用于在组件销毁时做一些清理工作,对应class中的componentWillUnmount
- 依赖项:
- 定义:去执行一段和当前执行结果无关的代码,如修改函数外部的一个变量或发起一个请求等,即
-
useCallback:缓存回调函数
在函数组件中,每次组件状态发生变化的时候,函数组件实际上是重新执行一遍,在每次重新执行时,实际上都会创建一个新的内部的属性和事件,当内部的事件有更改state,都会进行重新的渲染- 语法:useCallback(fn,deps)
- 当依赖项deps为空数组时,就会在组件初始化时创建一次,后续不会再重新创建了(相当于componentDidmount),当没有第二个参数依赖项时每次都会重新执行创建
- 设计目的:useCallback缓存的是一个函数,与useMemo不同的是,useMemo缓存的是计算结果
- 作用:只有fn依赖的数据变化时,才需要重新定义一个回调函数
- 重新定义回调函数的目的:每次组件状态发生变化的时候,函数组件都会重新执行一遍,在每次执行的过程中,实际上都会创建新的内部事件处理函数,以确保每次都可以得到正确的结果
- 实践结论:
-
前提:当fn回调函数中依赖的数据没有变化,但是组件其他状态数据变化时整个函数组件都会重新执行一遍
-
普通定义的事件处理函数:每次都会产生新的事件处理函数,即使变化的状态不是自己依赖的数据
-
用useCallback包裹的事件处理函数:只有自生依赖的数据变化时才会重新定义,否则不会重新定义事件函数,
jsconst changeNum = () => { setNum(num+1) } // const changeNum = useCallback( // () => setNum(num + 1), // [num] // //只有当 count 发生变化时, 才会重新创建回调函数 // ); console.warn(changeNum === cachefn, 88888888888, cachefn); // 普通的都会返回false useCallback包裹的会返回true(不是num变化的情况下) cachefn = changeNum; console.warn(changeNum === cachefn, 99999999, cachefn);
-
-
useMemo:缓存计算的结果 → 避免
重复计算
与子组件的重复渲染
- 语法:useMemo(fn,deps)
- fn:是产生所需数据的一个计算函数,通常来说,fn会使用deps中声明的一些变量来生成一个结果,用来渲染出最终的UI
- deps需要依赖的数据
- 设计目的:如果某个数据是通过其他数据计算得到的,那么只有当用到的数据(依赖的数据)发生变化时才会重新计算,这个API在性能优化方面会有一定的效果
- 设计需要解决的问题:如果采用普通的事件处理函数进行实现,则在每次数据发生变化时,都会进行一次重新计算(数据发生变化时,整个函数组件都会进行重新执行),造成不必要的计算性能浪费
- useMemo除了解决自身计算的性能问题外,还有就是可以避免接受这个数据的组价过多的重新渲染,以及依赖这个数据的其他hooks多余的计算,因此建议即使简单的计算也是要useMemo
- 语法:useMemo(fn,deps)
-
useMemo和useCallback比较
- 哪些情况一个组件会重新渲染
- 组件自己的state变化了
- 父组件传递过来的props变化了
- 父组件重新渲染了
- 父组件重新渲染防止子组件不必要渲染的手段
- shouldComponentupdate(nextProps,nextState) 函数中进行判断,当props值没有变化时,直接返回false就会实现:
当这个属性没有变化时,父组件的渲染就不会影响到子组件
- shouldComponentupdate(nextProps,nextState) 函数中进行判断,当props值没有变化时,直接返回false就会实现:
- 父组件重新渲染防止子组件不必要渲染的手段
- 父子组件生命周期过程
- 挂载时:
- 父组件:constructor、componentWillMount、render
- 子组件:constructor、componentWillMount、render、componentDidMount
- 父组件:componentDidMount
- 组件更新时:
- 父组件:shouldComponentUpdate、componentWillUpdate、父组件render
- 子组件:componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidMount
- 父组件:componentDidUpdate
- 注意:子组件主动更新时没有
componentWillReceiveProps
- 挂载时:
- 本质上,两个API都做了同一件事:建立了一个绑定某个结果到依赖数据的关系,只有当依赖变了,整个结果才需要被重新得到
- useCallback可以实现的功能是可以通过useMemo来实现
jsconst handleIncrement = useMemo( () => { // 返回一个函数作为缓存结果 return ( ) => { // 事件处理逻辑 setCount(count + 1) } } , [count] ) ;
- 哪些情况一个组件会重新渲染
-
useRef:在多次渲染之间共享数据
- 语法:
const myRef = useRef(initialValue)
- 理解:可以将useRef看做是函数组件之外创建的一个容器空间,在这个容器上,可以通过唯一的current属性值,从而在函数组件的多次渲染之间共享这个值
- 用途:
- 用途一:在多次渲染之间共享数据
- 可以理解为在函数组件之外创建一个容器空间,在这个容器上可以通过唯一的current属性设置一个值,从而在函数组件的多次渲染之间共享这个值
- 这个通过useRef保存的数据一般是和UI的渲染无关的,因此当保存的数据变化时,是
不会触发组件的重新渲染
的
- 用途二:保存某个DOM节点的引用
- 通过保存的Ref对象的
current
属性获得真实DOM节点,进而进行一些操作
- 通过保存的Ref对象的
- 用途一:在多次渲染之间共享数据
- 语法:
-
useContext:定义全局状态
- 常规的父子组件间数据传递,可以通过props进行传递,但是当涉及到多个组件时就需要使用其他方式了,例如
全局状态管理
- 定义:React通过
Context机制
可以使得所有在某个组件开始的组件树上建立一个Context,这样所有的组件树上的组件都可以访问和修改这个Context了 - 语法:
const demoContext = useContext(ContextData)
- 然后通过
demoContext
的Provide
属性标签对其下组件进行提供当前的数据源,其标签的value属性可以实现值传递<demoContext.Provide value={theme.data}> <children /></demoContext.Provide>
- 在跨级组件中,通过
useContext
来获取当前组件树提供的数据源
- 然后通过
- 弊端
- 较难追踪某个Context的变换是如何产生的
- 使得组件复用变得困难,当复用组件时,必须要确保在当前父组件路径上一定提供了Provider
- 常规的父子组件间数据传递,可以通过props进行传递,但是当涉及到多个组件时就需要使用其他方式了,例如
-
hooks依赖项
-
简介:Hooks提供了让你监测某个数据发生变化的能力,这个变化可能触发组件的刷新、可能创建一个副作用或者刷新一个缓存;那么定义要监听哪些数据变化的机制其实就是指定Hooks的依赖项
-
定义依赖项时需要注意的点
-
依赖项中定义的变量一定是会在回调函数中用到的,否则声明依赖项是没有意义的
-
依赖项一般是一个常量数组,而不是一个变量,因为在定义回调时一定会知道该回调需要依赖哪些依赖数据了
-
React会使用浅比较来对比依赖项是否发生变化,特别要注意数组或对象类型,即使每次创建一个相同数据的新对象,即使和之前的值是等价的,也会被认为是依赖项发生了变化
jsfunction Sample() { // 这里在每次组件执行时创建了一个新数组 const todos = [{ text: 'Learn hooks.'}]; useEffect(() => { console.log('Todos changed.'); }, [todos]); } // 依赖项时定义在函数内部,因此在每次执行函数时都会认为是依赖项数据变化了,都会触发回调函数的再次执行
-
-
Hooks使用规则
-
只能在函数组件的顶级作用域中使用
- 是指:Hooks不能在循环、条件判断或嵌套函数内执行,也不能在return之后,而必须是在顶层
- Hooks在组件的多次渲染之间,必须按顺序被执行
- 总结:所有Hooks必须都被执行到,且按顺序执行
-
只能在函数组件或其他Hooks中使用
- 因为Hooks是专门为函数组件设计的机制,其使用情况只有两种
- 在函数组件内
- 在自定义的Hooks内
- 因为Hooks是专门为函数组件设计的机制,其使用情况只有两种
-
已经定义好的Hooks在class组件中的使用方式
- 思想:利用高阶组件的模式,将Hooks封装成高阶组件,从而让类组件使用
- 例如复用之前定义的
useWindowSize
jsimport React from 'react'; import { useWindowSize } from '../hooks/useWindowSize'; export const withWindowSize = (Comp) => { return props => { const windowSize = useWindowSize(); return <Comp windowSize={windowSize} {...props} />; }; }; // 使用 import React from 'react'; import { withWindowSize } from './withWindowSize'; class MyComp { render() { const { windowSize } = this.props; // ... } } // 通过 withWindowSize 高阶组件给 MyComp 添加 windowSize 属性 export default withWindowSize(MyComp);
-
可以通过
eslint-plugin-react-hooks
进行检查Hooks是否被正确使用- 只需要在webpack Eslint配置文件中加入对应的配置信息即可
js{ "plugins": [ // ... "react-hooks" ], "rules": { // ... // 检查 Hooks 的使用规则 "react-hooks/rules-of-hooks": "error", // 检查依赖项的声明 "react-hooks/exhaustive-deps": "warn" } }
-
-
JSX浅析
本质上来说,JSX并不是一个新的模版语法,可以认为是一个
语法糖
,当然JSX也不是必选的,也可以有替代方案进行实现
React.createElement浅析
React.createElement
是用于创建一个组件的实例,该API接受一组参数,第一个表示组件的类型、第二个表示传递给组件的属性也就是props、第三个及后续的所有参数是子组件;通过
React.createElement
可以构建出需要的组件树,而JSX只是让这种描述变得更加直观和高效,所以说JSX
只是一种语法糖;
js
<div>
<TextComponent />
<div>hello,world</div>
let us learn React!
</div>
React.createElement("div", null,
React.createElement(TextComponent, null),
React.createElement("div", null, "hello,world"),
"let us learn React!"
)
React进阶
React常见问题汇总
React防止子组件进行不必要渲染的优化手段
在Class组件中,可以通过将子组件继承自React.PureComponent来实现props没有改变就不需要进行重新render;函数组件中没有PureComponent的概念,可以通过React.memo的高阶组件来实现,当然函数组件中的useCallback和useMemo也可以对性能进行相关的优化;
使用场景下,PureComponent适用于状态不多、不需要处理复杂逻辑的组件;而React.memo则适用于任何情况下,甚至可以代替React.PureComponent
自定义Hooks浅析
函数组件中最主要要考虑的问题是:
这个功能中哪些逻辑是可以单独抽离成独立的Hooks的
,其目的是有助于实现代码模块化和解耦,同时也方便了后续的维护;上述逻辑主要依赖了Hooks的两个核心的优点:一是方便逻辑复用、二是帮助关注分离;
-
自定义hooks的特点
自定义Hooks在形式上非常简单,就是一个名字以use开头的函数,与普通函数的区别就是名字和在自定义Hooks中用到了其他Hook(包括自定义hook和内置hooks)-
名字一定是以use开头的函数,这样React才可以知道这个函数是一个Hook
-
函数内部一定调用了其他的Hooks,可以是内置的Hooks,也可以是其他自定义的Hooks,这样才能让组件刷新,或去产生副作用
-
总结:其实就是将对应的函数逻辑单独抽离出来,内部使用hook进行数据的动态绑定,实现视图与数据的绑定,然后将这些函数逻辑和相关数据暴露出去(普通函数是没有办法直接更改组件的状态的,进而没有办法实现改变对应的数据使得组件重新渲染)
jsimport { useState } from 'react'; const useAsync = (asyncFunction) => { // 设置三个异步逻辑相关的 state const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 定义一个 callback 用于执行异步逻辑 const execute = useCallback(() => { // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态 setLoading(true); setData(null); setError(null); return asyncFunction() .then((response) => { // 请求成功时,将数据写进 state,设置 loading 为 false setData(response); setLoading(false); }) .catch((error) => { // 请求失败时,设置 loading 为 false,并设置错误状态 setError(error); setLoading(false); }); }, [asyncFunction]); return { execute, loading, data, error }; };
-
-
自定义hook的应用场景
- 解耦相关逻辑
- 解耦和拆分逻辑的目的不一定是为了重用,也可以是为了业务逻辑的隔离
- 封装通用逻辑
- 利用hooks可以管理React组件状态的能力,将一个组件中的某一部分状态独立出来,从而实现了通用逻辑的重用
- 监听浏览器相关事件或监听浏览器存储相关数据
- 拆分复杂组件
jsimport React, { useEffect, useCallback, useMemo, useState } from "react"; import { Select, Table } from "antd"; import _ from "lodash"; import useAsync from "./useAsync"; const endpoint = "https://myserver.com/api/"; const useArticles = () => { // 使用上面创建的 useAsync 获取文章列表 const { execute, data, loading, error } = useAsync( useCallback(async () => { const res = await fetch(`${endpoint}/posts`); return await res.json(); }, []), ); // 执行异步调用 useEffect(() => execute(), [execute]); // 返回语义化的数据结构 return { articles: data, articlesLoading: loading, articlesError: error, }; }; const useCategories = () => { // 使用上面创建的 useAsync 获取分类列表 const { execute, data, loading, error } = useAsync( useCallback(async () => { const res = await fetch(`${endpoint}/categories`); return await res.json(); }, []), ); // 执行异步调用 useEffect(() => execute(), [execute]); // 返回语义化的数据结构 return { categories: data, categoriesLoading: loading, categoriesError: error, }; }; const useCombinedArticles = (articles, categories) => { // 将文章数据和分类数据组合到一起 return useMemo(() => { // 如果没有文章或者分类数据则返回 null if (!articles || !categories) return null; return articles.map((article) => { return { ...article, category: categories.find( (c) => String(c.id) === String(article.categoryId), ), }; }); }, [articles, categories]); }; const useFilteredArticles = (articles, selectedCategory) => { // 实现按照分类过滤 return useMemo(() => { if (!articles) return null; if (!selectedCategory) return articles; return articles.filter((article) => { console.log("filter: ", article.categoryId, selectedCategory); return String(article?.category?.name) === String(selectedCategory); }); }, [articles, selectedCategory]); }; const columns = [ { dataIndex: "title", title: "Title" }, { dataIndex: ["category", "name"], title: "Category" }, ]; export default function BlogList() { const [selectedCategory, setSelectedCategory] = useState(null); // 获取文章列表 const { articles, articlesError } = useArticles(); // 获取分类列表 const { categories, categoriesError } = useCategories(); // 组合数据 const combined = useCombinedArticles(articles, categories); // 实现过滤 const result = useFilteredArticles(combined, selectedCategory); // 分类下拉框选项用于过滤 const options = useMemo(() => { const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({ value: c.name, label: c.name, })); arr.unshift({ value: null, label: "All" }); return arr; }, [categories]); // 如果出错,简单返回 Failed if (articlesError || categoriesError) return "Failed"; // 如果没有结果,说明正在加载 if (!result) return "Loading..."; return ( <div> <Select value={selectedCategory} onChange={(value) => setSelectedCategory(value)} options={options} style={{ width: "200px" }} placeholder="Select a category" /> <Table dataSource={result} columns={columns} /> </div> ); }
- 解耦相关逻辑
React常见面试题
生命周期相关
- React_16中的Fiber机制
Fiber是为了解决在复杂组件树中上层Props改变后会导致调用栈很长,调用栈过长加之中间组件的其他操作会导致长时间的阻塞主线程,带来不好的用户体验问题;
Fiber本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。
React异步渲染的阶段
- Reconciliation阶段(是可以打断暂停的)
- componentWillMount
- componentWillReceiveProps
- 在v16版本中采用
getDerivedStateFromProps(nextProps, prevState)
替换componentWillReceiveProps - getDerivedStateFromProps可以通过比较前后数据是否变化进而控制组件是否更新,当返回
null
时,表示组件不需要进行更新
- 在v16版本中采用
- shouldComponentUpdate
- 通过返回布尔值来决定当前组件是否需要更新
- ComponentWillUpdate
- getSnapshotBrforeUpdate可以替换ComponentWillUpdate,该函数会在update后DOM更新前被调用,用于获取最新的DOM数据
- 注意:上述的生命周期除了
shouldComponentUpdate
外,其他的应该尽量少去使用,因为其他的生命周期函数会在Reconciliation阶段的暂停开始操作中多次被调用触发
- commit阶段(不可以打断暂停的,一直到更新界面完成)
- componentDidMount
- componentDidUpdate
- componentWillUpdate