React | 2020-06-08 21:42:42 858次 3次
一、单链表
上一篇介绍了如何合理利用浏览器每一帧时间执行多个任务,本篇介绍任务队单链表实现。单链表是一种链式存取的数据结构,通过 next 指针指向下个数据单元。
把之前的任务通过链表来实现,为了可以更好的控制执行,可中断某个操作,再继续执行,如下图所示:
通过代码实现:
class Update {//payload数据或者 说元素 constructor(payload, nextUpdate) { this.payload = payload; this.nextUpdate = nextUpdate;//指向下一个节点的指针 } } class UpdateQueue { constructor() { this.baseState = null;//原状态 this.firstUpdate = null;//第一个更新 this.lastUpdate = null;//最后一个更新 } enqueueUpdate(update) { if (this.firstUpdate == null) { this.firstUpdate = this.lastUpdate = update; } else { this.lastUpdate.nextUpdate = update;//上一个最后一个节点的nextUpdate指向自己 this.lastUpdate = update;//让最后一个节指向自己 } } //1.获取老状态。然后遍历这个链表,进行更新 得到新状态 forceUpdate() { let currentState = this.baseState || {};//初始状态 let currentUpdate = this.firstUpdate; while (currentUpdate) { let nextState = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload; currentState = { ...currentState, ...nextState };//使用当前更新得到新的状态 currentUpdate = currentUpdate.nextUpdate;// 找下一个节点 } this.firstUpdate = this.lastUpdate = null;//更新完成后要把链表清空 this.baseState = currentState; return currentState; } } //计数器 {number:0} setState({number:1}) setState((state)=>({number:state.number+1})) let queue = new UpdateQueue(); queue.enqueueUpdate(new Update({ })); queue.enqueueUpdate(new Update({ num: 0 })); queue.enqueueUpdate(new Update((state) => ({ num: state.num + 1 }))); queue.enqueueUpdate(new Update((state) => ({ num: state.num + 1 }))); queue.forceUpdate(); console.log(queue.baseState); //{ num: 2 }
二、递归树
在react之前的做法中,每一次更新都会遍历整棵树,找出差异化然后更新。
let root = { key: 'A1', children: [ { key: 'B1', children: [ { key: 'C1', children: [] }, { key: 'C2', children: [] }, ] }, { key: "B2", children: [] } ] } function walk(vdom) { doWork(vdom); vdom.children.forEach((child) => { walk(child); }); } function doWork(vdom) { console.log(vdom.key); } walk(root);
但是这样做有一个缺点就是组件数量庞大的时候,会严重占用浏览器主线程渲染,也就是一帧内必须要做完渲染,不然就会卡顿。当React决定要加载或者更新组件树时,会调用各个组件的生命周期函数,计算和比对 Virtual DOM,最后更新 DOM 树,如果这个之间用户进行了 input 输入操作或者点击动作,则界面不会有反应,交互体验就会变得很差,所以需要一种可中断的数据结构--链表来重新设计 react 的核心逻辑,结合上一篇介绍的浏览器渲染帧原理来进行空闲时间渲染,这样可以进一步改善交互体验(注意这样做并不是为了所谓的提升速度)。
三、Fiber介绍
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
React Fiber 把更新过程碎片化,执行过程如下图所示,每执行完一段更新过程,就看看浏览器当前帧是否还有空闲时间,如果有则继续渲染,没有的话 react 就中断执行,将控制权交给浏览器(除非有优先级高的任务和已超时的任务必须执行),等待下一帧浏览器有空闲时间再去恢复渲染,从上次中断的地方。
其中,维护每一个分片的数据结构,就是 Fiber,render 阶段会构建 fiber 树。和第二部分树对应的相同节点用 fiber 表示结构如下图所示:
可以看到,现在每个节点的连接通过 child 指针指向子节点,sibling 指向自己的兄弟节点,每个子节点有个 return 指针指向自己的父节点,这个 return 就是单纯的一个指针命名,这样整颗 fiber 树就通过链表的形式展现。
四、Fiber渲染
Fiber 的渲染分为两个阶段,Reconciliation(调和/render阶段) 和 Commit(提交阶段)。
协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)。
提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行。这个阶段必须同步执行,不能被打断。
按照之前的图中数据结构,我们先手动创建一颗 fiber 树(正常应该在render时候代码生成):
let A1 = { type: 'div', key: 'A1' }; let B1 = { type: 'div', key: 'B1', return: A1 } let B2 = { type: 'div', key: 'B2', return: A1 } let C1 = { type: 'div', key: 'C1', return: B1 } let C2 = { type: 'div', key: 'C2', return: B1 } A1.child = B1; B1.sibling = B2; B1.child = C1; C1.sibling = C2;
fiber 树按照深度优先遍历的规则进行渲染,如果到达某个节点后此节点无子节点则证明这个节点渲染完成,也是第一个渲染完成的节点,以此类推,如图所示(红色代表开始遍历,蓝色代表遍历完成):
下面通过代码实现,并结合 requestIdleCallback 来进行任务分片执行:
let nextUnitOfWork = null;//下一个执行单元 function workLoop(deadline) { while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && nextUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (!nextUnitOfWork) { console.log('render阶段结束了'); } else {//请求下次浏览器空闲的时候帮我调 requestIdleCallback(workLoop, { timeout: 1000 }); } } function performUnitOfWork(fiber) {//A1 B1 C1 C2 beginWork(fiber);//处理此fiber if (fiber.child) {//如果有儿子,返回大儿子 return fiber.child; }//如果没有儿子,说明此fiber已经完成了 while (fiber) { completeUnitOfWork(fiber); if (fiber.sibling) { return fiber.sibling;//如果说有弟弟返回弟弟 } fiber = fiber.return; } } function completeUnitOfWork(fiber) { console.log('结束', fiber.key);//C1 C2 B1 B2 A1 } function beginWork(fiber) { console.log('开始', fiber.key);//A1 B1 C1 C2 B2 } //上面手动创建的fiber nextUnitOfWork = A1; requestIdleCallback(workLoop, { timeout: 1000 });
本篇介绍了一些前提数据结构准备,下一篇继续实现真实数据的 render 和 commit。
3人赞