递归更新的实现React15的递归更新逻辑是先把需要更新的组件放入脏组件队列中(这个在上一篇已经介绍过,如果没看过,可以先看《React 架构的演变 - 从同步到异步》),然后把组件取出来进行一次递归,一直往下找子节点,看看是否需要更新。这里有一段代码简单描述一下这个过程:updateComponent(prevElement,nextElement){if(//如果组件的type和key没有变化,则更新prevElement.type===nextElement.type&&prevElement.key===nextElement.key){//文本节点更新if(prevElement.type==='text'){if(prevElement.value!==nextElement.value){this.replaceText(nextElement.value)}}//DOM节点updateelse{//先更新DOM属性this.updateProps(prevElement,nextElement)//再更新childrenthis.updateChildren(prevElement,nextElement)}}//如果组件的type和key发生变化,直接重新渲染组件else{//触发卸载生命周期ReactReconciler.unmountComponent(prevElement)//渲染一个新组件this._instantiateReactComponent(nextElement)}},updateChildren(prevElement,nextElement){varprevChildren=prevElement.childrenvarnextChildren=nextElement.children//省略由关键进程重新排序的差异if(prevChildren===null){}//渲染新的子节点if(nextChildren===null){}//clearallchildnodes//比较子节点prevChildren.forEach((prevChild,index)=>{constnextChild=nextChildren[index]//递归过程this.updateComponent(prevChild,nextChild)})}为了更清楚的看到这个过程,我们还是写一个简单的Demo来构造一个3*3的Table组件。Table//https://codesandbox.io/embed/react-sync-demo-nlijfclassColextendsReact.Component{render(){//渲染前暂停8ms,对渲染造成一点压力conststart=performance.now()while(性能.now()-start<8)return
{this.props.children} | }}exportdefaultclassDemoextendsReact.Component{state={val:0}render(){const{val}=this.stateconstarray=Array(3).fill()//构造一个3*3的表constrows=array.map((_,row)=>
{array.map((_,col)=>{val}?/Col>)})return({rows})}}然后每隔一个表里面的值更新一次,这样val每次都会+1,从0到9不断循环。{tick=()=>{setTimeout(()=>{this.setState({val:next<10?next:0})this.tick()},1000)}componentDidMount(){this.tick()}}完整代码在线地址:https://codesandbox.io/embed/react-同步演示nlijf。Demo组件每次调用setState时,React都会先判断组件的类型是否被修改。如果是这样,整个组件将被重新渲染。如果不是,则更新状态,然后向下判断table组件,table组件继续向下判断tr组件,tr组件向下判断td组件,最终发现td组件下的文本节点被修改,通过DOMAPI更新。Update通过Performance的函数调用栈也可以清楚的看到这个过程。updateComponent之后的updateChildren会继续调用子组件的updateComponent,直到所有组件都递归完毕,表示更新完成。调用堆栈递归的缺点是显而易见的。它不能暂停更新。一旦开始,就必须从头到尾。这显然与React16拆分时间片,给浏览器喘息的理念不符。因此,React必须切换架构并将虚拟DOM从树中移除。shape结构改为链表结构。CircularFiber这里说的链表结构就是Fiber。链表结构最大的优点就是可以循环遍历。只要记住当前的遍历位置,即使中断也可以快速恢复,重新开始遍历。我们先看一个Fiber节点的数据结构:functionFiberNode(tag,key){//节点key,主要用于链表优化diffthis.key=key//节点类型;FunctionComponent:0,ClassComponent:1,HostRoot:3...this.tag=tag//子节点this.child=null//父节点this.return=null//兄弟节点this.sibling=null//更新队列用于暂存setState的值this.updateQueue=null//节点更新过期时间,用于时间分片//react17改为:lanes,childLanesthis.expirationTime=NoLanesthis.childExpirationTime=NoLanes//对应真实DOM节点页面的this.stateNode=null//Fiber节点的Copy,可以理解为备胎,主要用来提高更新的性能this.alternate=null}下面是一个例子,这里我们有一段普通的HTML文本:1 | 1 |
1 |
在之前的React版本中,jsx会被转换成createElement方法来创建一个具有shape结构的树形VirtualDOM。constVDOMRoot={type:'table',props:{className:'table'},children:[{type:'tr',props:{},children:[{type:'td',props:{},children:[{type:'text',value:'1'}]},{type:'td',props:{},children:[{type:'text',value:'1'}]}]},{type:'tr',props:{},children:[{type:'td',props:{},children:[{type:'text',value:'1'}]}]}]}Fiber架构下,结构如下://经过简化,与React真正的Fiber结构不一致constFiberRoot={type:'table',return:null,sibling:null,child:{type:'tr',return:FiberNode,//表的FiberNodesibling:{type:'tr',return:FiberNode,//表的FiberNodesibling:null,child:{type:'td',return:FiberNode,//tr的FiberNodesibling:{type:'td',return:FiberNode,//fiberNodesiblingoftr:null,child:null,text:'1'//子节点只有文本节点},child:null,text:'1'//子节点只有textnodes}},child:{type:'td',return:FiberNode,//FiberNodesiblingoftr:null,child:null,text:'1'//子节点只有文本节点}}}Fiber的实现循环更新那么,在setState的时候,React是如何进行Fiber遍历的呢?letworkInProgress=FiberRoot//遍历Fiber节点,时间片时间用完则停止遍历functionworkLoopConcurrent(){while(workInProgress!==null&&!shouldYield()//用于判断当前时间片是否过期){performUnitOfWork(workInProgress)}}functionperformUnitOfWork(){constnext=beginWork(workInProgress)//返回当前Fiber的childif(next){//child存在//repeatSetworkInProgresstochildworkInProgress=next}else{//child不存在//向上回溯节点letcompletedWork=workInProgresswhile(completedWork!==null){//收集副作用,主要用于标记节点是否需要操作DOMcompleteWork(completedWork)//获取Fiber.siblingletsiblingFiber=workInProgress.siblingif(siblingFiber){//sibling存在,跳出完整流程,继续beginWorkworkInProgress=siblingFiberreturn;}completedWork=completedWork.returnworkInProgress=completedWork}}}functionbeginWork(workInProgress){//调用render方法,创建Sub-Fiber,进行diff//操作完成后,返回当前Fiber的childreturnworkInProgress.child}functioncompleteWork(workInProgress){//收集节点sideeffects}纤维遍历本质上是一个循环。全局有一个workInProgress变量,用来存放当前进度对于diff的节点,首先使用beginWork方法对当前节点进行一次diff操作(diff之前会调用render,会重新计算state和prop),并且当前节点的第一个子节点(fiber.child)将作为新的工作节点返回。直到没有子节点然后调用当前节点的completedWork方法来存储beginWork过程中产生的副作用。如果当前节点有兄弟节点(fiber.sibling),修改工作节点为兄弟节点,重新进入beginWork流程。直到completedWork返回到根节点,执行commitRoot将所有副作用反映到真实的DOM中。在Fiber工作循环的一次遍历中,每个节点都会经过beginWork和completeWork,直到回到根节点,最后通过commitRoot提交所有的更新。关于这部分的更多信息,请参见:《React 技术揭秘》。时间分片的秘密前面说过,遍历Fiber结构是支持中断恢复的。为了观察这个过程,我们将之前的3*3Table组件改为Concurrent模式,在线地址:https://codesandbox.io/embed/react-async-demo-h1lbz。由于每次调用Col组件的render部分需要8ms,会超过一个时间片,所以每个td部分都会暂停一次。classColextendsReact.Component{render(){//渲染前暂停8ms,对渲染造成一点压力conststart=performance.now();while(performance.now()-start<8);return{this.props.children | }}在这个3*3的组件中,一共有9个Col组件,所以会有9个耗时任务,分散在9个时间片中,可以通过callstackofPerformance情况:在异步模式调用栈的非Concurrent模式下,Fiber节点的遍历是一次性进行的,不会分成多个时间片。不同的是遍历时会调用workLoopSync方法,不会判断时间片是否用完。//遍历Fiber节点的调用栈已经用完了,这也是决定React是同步渲染还是异步渲染的关键。如果去掉任务优先级的概念,shouldYield方法可以说非常简单,就是判断当前时间是否超过了预设的deadline。functiongetCurrentTime(){returnperformance.now()}functionshouldYield(){//获取当前时间varcurrentTime=getCurrentTime()returncurrentTime>=deadline}如何获取deadline?可以回顾上篇文章(《React 架构的演变 - 从同步到异步》)提到的ChannelMessage,当更新开始时,会通过requestHostCallback发送一个异步消息(即:port2.send),并在performWorkUntilDeadline(即:port1.onmessage)中接收消息.performWorkUntilDeadline每收到一条消息,就代表进入了下一个任务队列,此时会更新deadline。异步调用栈varchannel=newMessageChannel()varport=channel.port2channel.port1.onmessage=functionperformWorkUntilDeadline(){if(scheduledHostCallback!==null){varcurrentTime=getCurrentTime()//重置超时deadline=currentTime+yieldIntervalvarhasTimeRemaining=truevarhasMoreWork=scheduledHostCallback()if(!hasMoreWork){//没有任务了,修改状态isMessageLoopRunning=false;scheduledHostCallback=null;}else{//还有任务,放入下一个任务队列执行,交给浏览器一个呼吸口的机会queuecallcallbackport.postMessage(null)}}超时设置是在当前时间的基础上增加一个yieldInterval。这个yieldInterval的默认值为5ms。deadline=currentTime+yieldInterval同时,React也提供了修改yieldInterval的手段,通过手动指定fps,来确定一帧的具体时间(单位:ms),fps越高,一帧的时间越短时间片,对设备的性能要求越高。forceFrameRate=function(fps){if(fps<0||fps>125){//帧率只支持0~125return}if(fps>0){//一般60fps设备//一个时间片的时间总结forMath.floor(1000/60)=16yieldInterval=Math.floor(1000/fps)}else{//resettheframerateyieldInterval=5}}接下来我们串联异步逻辑、循环更新、时间分片。让我们回顾一下上一篇文章。在Concurrent模式下,setState之后的调用顺序:Component.setState()=>enqueueSetState()=>scheduleUpdate()=>scheduleCallback(performConcurrentWorkOnRoot)=>requestHostCallback()=>postMessage()=>performWorkUntilDeadline()scheduleCallback方法会将传入的回调(performConcurrentWorkOnRoot)组装成任务放入taskQueue,然后调用requestHostCallback发送消息,进入异步任务。performWorkUntilDeadline收到异步消息,从taskQueue中取出任务开始执行。这里的任务就是之前传递的performConcurrentWorkOnRoot方法。该方法最终会调用workLoopConcurrent(workLoopConcurrent之前已经介绍过,这里不再赘述)。如果workLoopConcurrent被超时中断,hasMoreWork返回true,通过postMessage发送消息,将操作推迟到下一个任务队列。流程图的全过程就到这里了。希望你看完文章后有所收获。下一篇文章将介绍Fiber架构下Hook的实现。本文转载自微信公众号《更神奇的前端》,可通过以下二维码关注。转载本文请联系更牛逼的前端公众号。