添加快捷方式
分享
了解 React 设计
输入“/”快速插入内容
了解 React 设计
看别人代码最大的问题是:你不知道这个代码是为了解决什么问题而设计的,也就是你不知道上下文。所以这一篇文章会先解释 react 的一些设计出现的原因以及跟 vue 做对比,先有大概的理解:
🌰
文章目标:看完理解 concurrent, 双缓存 fiber 树, setState 之后全量 render 的设计
一、理解 concurrent 以及 优先级 出现原因
React 很多设计都是为了 concurrent 模式服务的,要了解 react 首先就要理解 concurrent 。
•
为什么会出现 concurrent:这里推荐阅读 kaosng 的这一篇
文章
:简单总结:
◦
JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待。在这样的机制下,如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿。
◦
Concurrent 就是解决上述问题的,将JavaScript 线程逻辑分为多个"执行单元",React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去,故任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。
◦
这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为
时间切片
而
时间切片
的关键是:将
同步的更新
变为
可中断的异步更新(concurrent)
。
那么问题来了,既然是可中断的结构,那么中断之后怎么决定执行哪一个任务?
•
为了解决上述问题,
react 引入优先级
。为每个任务增加一个优先级。也就是说高优先级优先处理,低优先处理。为了避免低优先级一直因为优先级过低产生饥饿现象,给每一个任务增加过期时间,过期之后提升优先级,解决饥饿现象。
二、理解为什么 vue 没有 concurrent 模式
为了解决 JavaScript 线程长时间地占用了主线程问题, react 采用【可中断 + 优先级 】的渲染结构。
但是 vue 利用自身优势采用了另外一种思路:
1.
利用模版编译的时候做了很多优化,过滤掉很多不需要 diff 的vNode。具体可以看
https://zhuanlan.zhihu.com/p/150732926
2.
响应式 setState 可以精准知道从哪一个节点开始向下 diff
三、理解为什么分为 render阶段和 commit阶段
•
Render 阶段指:diff, createFiber 等
•
Commit 阶段指:将 DOM 绘制到页面上
vue 就没有分开两个逻辑,为什么 react 需要呢?参考 vue patch 逻辑,假设我们不分为两个阶段,在render 的时候发现需要新建 DOM 节点就可以去新建,发现需要修改 DOM 就去修改。
假设我们将绘制DOM到页面上,绘制到一半突然中断了,过了好几秒之后才重新绘制。这样对于用户来说是一个很不连贯的事情。
•
为了解决上述问题,react 将patch 逻辑分为 render - commit 两个阶段。render 阶段是可中断的,commit 阶段是不可中断的。render去做 diff 等事情(可中断),commit 做绘制(不可中断)。
所以 react 为什么将 render和 commit 阶段分开的原因:
为了支持 render 中可中断模式,以及让 commit 中 DOM绘制变得连贯。
四、理解为什么会出现 fiber 结构
首先需要明确是:Fiber 只是一种比 vNode 更复杂的数据结构,本质没有什么区别,区别只在于 fiber 上需要记录的变量更多,比如:
•
Render 阶段记录父子兄弟关系,方便递归的时候回溯到父节点。
•
Diff 阶段记录哪一个节点需要重新绘制,上述我们已经知道 react 将 Render 和commit 分开了,那么假设我们在 render 阶段 diff 得出最小修改,那么在 commit 阶段怎么知道哪一些阶段需要变化,做什么变化呢?答案就是在 fiber 节点上做一些标记。
•
优先级记录,方便中断之后选择优先级最高的执行
•
......
所以我理解 fiber 的出现是很自然的事,为了承载记录更多的信息,就需要更加复杂的数据结构。那么不将信息放在 vNode 上面呢?其实我理解也是可以的🐶🐶🐶🐶🐶🐶
五、理解为什么会出现双缓存 fiber 结构,而 vue 不需要?
在解释为什么需要 双缓冲的 fiber 结构之前,我们先想一个问题:
•
假设一个低优先级任务做到一半,来了一个高优先级插队,那么低优先级的任务要如何处理呢?答案是
丢弃低优先级任务
。那为什么不是缓存起来呢?
◦
在多线程语言中,线程之间访问公共资源的时候是需要将资源锁定,直到访问结束才解锁,这是为了避免其他线程修改共享资源从而造成程序不可再现。
◦
同理,我们可以将低优任务和高优任务看作是两条线程,并且它们的执行顺序由React调度,不同优先级别的任务最后是作用在同一棵Fiber树上,那么它们势必会访问到共享资源。在高优任务中断低优任务后,低优任务无法判断之前完成的工作所引用的资源是否被修改了,而重做能够保证数据和视图的一致性。
回到我们问题:既然低优先级被打断之后需要丢弃,那么就证明需要恢复上一次的状态。
也就是我们必须为每一个节点做备份,因为随时准备着高优先级插队,插队了就需要将树恢复到上一次的状态。
那这个备份其实就是双缓冲 fiber 树了,每一个 fiber 的备份就是通过 alternate 连接,从而构成 workInprogress 树。
但是 vue 没有 concurrent 模式,所以不需要备份,自然不需要双缓冲。
六、理解为什么每次 setState 采用全量 render 但是 vue 不需要?
•
Vue 每次 setState 都是精准更新,从 setState 节点开始向下 diff 其子树。但是React 每次 setState 都是从最顶部的节点生成(新建 or 复用)workInprogressTree 后 diff 。为什么会有这种差距呢?
•
因为 react 存放任务优先级的机制。每次 set 之后会生成一个新任务。react 会根据当前任务情况(未完成的任务,上次被打断的任务等)计算一个更新优先级,如果当然任务的优先级等于这个更新优先级,就不会新启动一个任务,而是复用之前的任务。
也就是说一个任务中,存的更新可能不只是当前组件的 setState 引起的,还可能包括其他组件。因此不能直接从某个 setState 的组件开始更新,而是要从 root 开始遍历。
•
Vue 没有优先级机制,所以可以精准从 setState 节点更新。
七、理解 react 每次 setState 采用全量 render 不会很耗性能吗?
•
React 有 bailout 复用路径:简单来说就是会把不需要 改变的 fiber 节点直接复用(通过 expirationTime 判断)。通过 childExpirationTime 判断子孙节点是否需要 改变,不需要也直接复用。