React

https://yuchengkai.cn/react/2019-06-04.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E8%B0%83%E5%BA%A6%EF%BC%9F

JS 和 渲染引擎是互斥关系,JS执行代码 渲染引擎停止。复杂组件渲染 调用栈长 如有复杂操作,会阻塞引擎,用户体验差
时间分片 react 根据优先级分配expirationTime,过期时间到来前,处理优先级高的任务。高优先级也可以打断低优先级任务,造成某些生命周期函数多次执行,从而实现在不影响用户体验的情况下分段计算更新

react 调度

  1. 计算任务expriationTime
  2. 实现 requestidlecallback 的polyfill版本

expriationTime 计算方式

当前时间 + 常量: 当前时间取精度高点的 performance.now()

// 五种优先级
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
// 对应数值
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;

假设当前时间为 5000 并且分别有两个优先级不同的任务要执行。前者属于 ImmediatePriority,后者属于 UserBlockingPriority,那么两个任务计算出来的时间分别为 4999 和 5250。通过这个时间可以比对大小得出谁的优先级高,也可以通过减去当前时间获取任务的 timeout。

requestIdleCallback
RunTask [RunTask] UpdateRendering IdleCallback[IdleCallback]:frame #1
浏览器空闲时期一次调用,可在事件循环中执行任务不会对动画交互产生影响
这个函数的兼容性并不是很好,并且它还有一个致命的缺陷:

requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work.

也就是说 requestIdleCallback 只能一秒调用回调 20 次,这个完全满足不了现有的情况,由此 React 团队才打算自己实现这个函数
函数的核心只有一点,如何多次在浏览器空闲时且是渲染后才调用回调方法?
那么肯定得使用定时器了。在多种定时器中,唯有 requestAnimationFrame 具备一定的精确度,因此 requestAnimationFrame 就是当下实现 requestIdleCallback 的一个步骤
requestAnimationFrame 的回调方法会在每次重绘前执行,另外它还存在一个瑕疵:页面处于后台时该回调函数不会执行,因此我们需要对于这种情况做个补救措施

rAFID = requestAnimationFrame(function(timestamp) {
	// cancel the setTimeout
	localClearTimeout(rAFTimeoutID);
	callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
	// 定时 100 毫秒是算是一个最佳实践
	localCancelAnimationFrame(rAFID);
	callback(getCurrentTime());
}, 100);

当 requestAnimationFrame 不执行时,会有 setTimeout 去补救,两个定时器内部可以互相取消对方。

使用 requestAnimationFrame 只完成了多次执行这一步操作,接下来我们需要实现如何知道当前浏览器是否空闲呢?

[life of frame]https://yck-1254263422.cos.ap-shanghai.myqcloud.com/blog/2019-06-04-155147.png

大家都知道在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制。假设当前我们的浏览器支持 1 秒 60 帧,那么也就是说一帧的时间为 16.6 毫秒。如果以上这些操作超过了 16.6 毫秒,那么就会导致渲染没有完成并出现掉帧的情况,继而影响用户体验;如果以上这些操作没有耗时 16.6 毫秒的话,那么我们就认为当下存在空闲时间让我们可以去执行任务。

因此接下去我们需要计算出当前帧是否还有剩余时间让我们使用

简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。
得出下一帧时间以后,我们只需对比当前时间是否小于下一帧时间即可,这样就能清楚地知道是否还有空闲时间去执行任务。

那么最后一步操作就是如何在渲染以后才去执行任务。这里就需要用到事件循环的知识了

[事件循环机制]https://yck-1254263422.cos.ap-shanghai.myqcloud.com/blog/2019-06-04-155148.png

组件更新流程一

  1. setstate 批量更新如何实现
  2. fiber
  3. 调度任务

Fiber 重新实现了 React 的核心算法,带来了杀手锏增量更新功能

  • 新的数据结构fiber
  • 调度器
    数据结构 任务拆分粒度 fiber
    每个fiber是一个工作单元,执行更新流程就是反复寻找工作单元并执行,这样就实现了任务拆分
    拆分的目的就是 控制stack frame 调用栈的内容,随时随地执行。使得每运行一个工作单元后可以按照情况继续执行或者中断工作,中断的决定权在于调度算法
    fiber 内部存储很多上下文信息,可以认为是改版后的虚拟DOM,同样也对应了组件实例和DOM元素。同时fiber也会组成fiber tree,结构不是树,而是链表
{
  ...
  // 浏览器环境下指 DOM 节点
  stateNode: any,

  // 形成列表结构
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,

  // 更新相关
  pendingProps: any,  // 新的 props
  memoizedProps: any,  // 旧的 props
  // 存储 setState 中的第一个参数
  updateQueue: UpdateQueue<any> | null,
  memoizedState: any, // 旧的 state

  // 调度相关
  expirationTime: ExpirationTime,  // 任务过期时间

  // 大部分情况下每个 fiber 都有一个替身 fiber
  // 在更新过程中,所有的操作都会在替身上完成,当渲染完成后,
  // 替身会代替本身
  alternate: Fiber | null,

  // 先简单认为是更新 DOM 相关的内容
  effectTag: SideEffectTag, // 指这个节点需要进行的 DOM 操作
  // 以下三个属性也会形成一个链表
  nextEffect: Fiber | null, // 下一个需要进行 DOM 操作的节点
  firstEffect: Fiber | null, // 第一个需要进行 DOM 操作的节点
  lastEffect: Fiber | null, // 最后一个需要进行 DOM 操作的节点,同时也可用于恢复任务
  ....
}
Fiber 和 fiber 不是同一个概念。前者代表新的调和器,后者代表 fiber node,也可以认为是改进后的虚拟 DOM

调度器

有更新任务发生时候,调度器按照策略给任务分配优先级,必然动画优先级高,离屏元素的更新优先级会低
通过优先级我没获取更新任务必须执行的截止时间,优先级高,截止时间近,反之亦然。截止时间用来判断任务是否过期,如果过期任务会马上执行
通过调度器实现 requestIdleCallback 函数来做到在浏览器空闲的时候执行更新任务
1.简单说就是 通过定时器,获取每一帧结束时间,得到结束时间,就好判断当前距离结束时间的一个差值
如果还没有到结束时间,意味着我们可以继续执行更新任务;如果已经过了结束时间,那么意味着当前帧没有时间给我执行任务,必须将执行权交给浏览器,即打断更新任务
2.新的任务进来,调度器会根据两者优先级进行判断,如果高,就会打断当前任务开始新的任务 就是计算出来的截止时间

diff策略
调和的过程
组件更新到底还是DOM更新,会有两个阶段
1.调和阶段,虚拟DOM的diff算法
2.提交阶段,将调和阶段的diff内容体现到DOM上

class Test extends React.Component {
  state = {
    data: [{ key: 1, value: 1 }, { key: 2, value: 2 }]
  };
  componentDidMount() {
    setTimeout(() => {
      const data = [{ key: 0, value: 0 }, { key: 2, value: 2 }]
      this.setState({
        data
      })
    }, 3000);
  }
  render() {
    const { data } = this.state;
    return (
      <>
        { data.map(item => <p key={item.key}>{item.value}</p>) }
      </>
    )
  }
}
// 具体 循环?寻找工作单元表现 
while (nextUnitOfWork !== null && !shouldYield()) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

while循环只有找不到工作单元或者需要打断的时候才会终止。找不到=》工作单元循环完成;打断=》调度器触发
更新操作开始时候,root永远是第一个工作单元,无论之前有没有被打断过工作
循环?寻找工作单元的流程比较简单,就是自顶向下再向上的一个循环,循环的规则如下
1.root永远是第一个工作单元,不管之前是否打断
2.首先判断当前节点是否存在第一个子节点,存在的话,就是下一个工作单元,并让下一个工作节点继续执行该规则,不存在就跳到3
3.判断当前节点是否slibing。如果就2,否则4
4.回到父节点 判断父节点是否存在,如果存在执行3,否则5
5.当前工作单元为null,完成整个循环

class组件调和过程,
1.生命周期函数的处理
1.1 最先 compnentWillReceiveProps 触发条件有两个
1.1.1props前后有差别
1.1.2没有使用getDerivedStateFromProps 或者 getSnapshotBeforeUpdate 这两个新的周期函数。使用其中一个compnentWillReceiveProps不会被触发
满足上面条件函数就会被调用,react16中不被建议使用。调和阶段有可能被打断,因此该函数会重复调用

//凡是调和阶段调用的生命周期函数都不被建议使用

then 需要处理 getDerivedStateFromProps函数获取到的最新的state
then 判断是否需要更新组件
1.2.1 判断是否存在shouldCompnentUpdate函数,存在就调用
1.2.2不存在函数就判断当前组件是否继承PureComponent。如果是,就浅判断前后的props及state得到结果
如果需要更新组件,先调用componentWillUpdate,然后处理componentDidUpdate getSnapshotBeforeUpdate函数
注意,调和阶段不会调用上面两个函数,而是打上tag,以便将来使用位运算知晓是否需要使用他们。effectTag这个属性在整个更新流程中重要。凡是涉及函数延迟调用、devTool处理、DOM更新都会使用effecttag

if(typeof instance.componentDidUpdate === 'function'){
    workInProgress.effectTag |=Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function'){
    workInProgress.effectTag|=Snapshot;
}

2.调和子组件,也就是diff算法
处理完成生命周期后,会调用render函数获取新的child,用于之后与老的child对比
熟悉三个对象
returnFiber 父组件
currentFirstChild 父组件第一个child。
newChild 刚刚render出来的内容?
会判断newchild类型,知道类型就可以进行相关diff策略。可能是一个Fragment类型 or obj number string。处理简单
重点在 可迭代对象 array iterator。两者逻辑一致

第一轮遍历
逻辑核心,复用和当前节点索引一致的老节点,如果出现不能复用就跳出遍历
逻辑规则

  • 新节点都是文本节点,可以复用,文本节点不需要key
  • 其他一律通过key判断是否相同来复用或者创建节点
// 简化后的代码
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // 找到下一个老的子节点
  nextOldFiber = oldFiber.sibling;
  // 通过 oldFiber 和 newChildren[newIdx] 判断是否可以复用
  // 并给复用出来的节点的 return 属性赋值 returnFiber
  const newFiber = reuse(
    returnFiber,
    oldFiber,
    newChildren[newIdx]
  );
  // 不能复用,跳出
  if (newFiber === null) {
    break;
  }
}

回到上例中,老的第一个节点key为1,新的为0,不相同跳循环,吃啥newIdx仍为0

第二轮遍历
第一轮完成的结果可能?

  • newChild 遍历完成
  • 老节点遍历完成
    出现newChild遍历完成只需要将剩余老节点删除。删除逻辑就是设置 effectTag为Deletion
    当出现渲染阶段处理的节点,会将节点放入父节点的efect链表中,如要删除的节点就会放入链表
    当出现老节点遍历完成,只用把剩余新节点全部创建完成

第三轮遍历
找到复用的老节点,并移动,不能复用就创建新的
如何复用并移动,
1.3.1 剩余老节点到map中
剩余老节点

<p key={1}>1</p>
<p key={2}>2</p>

map的结构为

// 节点的key作为map的key
// 如果节点不存在key 那么index为key
const map = {
    1:{},
    2:{}
}

在遍历过程中寻找新的节点key是否存在map中,有就复用,没有就创建。其实在部分复用及创建的逻辑和第一轮一致。
如果成功复用,需要将key从map中删除,并给复用的节点移动未知。移动位置,移动位置不涉及DOM操作,给effectTag赋值为Placement

0120-0205
7271
8054
783
4600/783