前言
哈喽,大家好,我是 Baker !🎉
对于前端的 Vue 和 React 相信大家并不陌生,这两个库有着截然不同的设计思想和发展目标,对于我们上层使用者来说,研究它们的差异不仅让我们更加深入的去理解这些库的设计思想,也能帮助我们在开发中更有依据的去选择合适的框架。
本篇文章就两者的更新机制来浅淡一下它们的区别,因为是浅淡并且也受限于篇幅,所以这里并不会就某些技术细节进行展开,如果大家想要深入去了解两者技术的实现,可以尝试去读一下源码,或者期待一波博主后续的更新🌹
因为不涉及技术细节,大家放轻松,悠闲自在的观看即可。
好啦,让我们开始吧!
Vue 更新机制
Vue 使用著名的响应式系统来收集依赖和派发更新,当模板中数据发生变化时,组件的 render 函数会被作为数据的依赖而被触发,只不过这个触发并不是立刻的,因为模板中会引用很多数据,render 同时是这些所有数据的依赖项,如果 render 每次都立刻执行,则会造成多次重复渲染而消耗性能。
实际上 render 是被 update 调用的,而 update 又是 Watcher 调用的,而 Watcher 在收到 Dep 的派发更新时会把自身交给 Scheduler ,由 Scheduler 负责对其去重并通过 nextTick()
将这些 Watcher 包装成微任务放入到事件循环中等待调用。
render 执行输出的结果是一颗新的虚拟 DOM 树,然后 update 会通过 patch 函数将它与旧的虚拟 DOM 树进行对比,diff 和真实 DOM 的操作过程既是在 patch 函数中进行。
所以,其实 Vue 的整个更新任务(构建虚拟 DOM ,diff,操作真实 DOM)可以算作一个整体,这个整体被当作微任务来处理,这也就是 Vue 异步更新的原理。
最后附上一张 Vue 官方文档的流程图:
React 更新机制
这里以 React16 之后的 Fiber 架构为例。
React 没有 Vue 的响应式系统,它的更新主要是靠用户手动的调用副作用函数(比如 setState 等)来触发( Vue 则是系统自动触发)。所有的更新任务(如调用 setState )会被 render 阶段的 Scheduer 进行调度,它会将多次的 setState 调用合并为一次更新操作,render 阶段可以随时被打断(如遇到高优先级任务、当前事件循环没有足够的时间了、发生了其它错误等)。
Scheduer 调度器负责对任务进行调度,内部会通过任务的优先级(会通过一系列的优先级设置任务的 delay ),以及当前事件循环空闲的时间等来判断当前任务是否可以执行,如果可以执行则会通过 MessageChannel 将任务包装成一个宏任务推入到事件循环当中等待执行,在任务执行时通过 render 阶段的 Reconciler 来进行协调。
Scheduer 判断当前任务是否可以执行的调度可以通过原生的 requestIdleCallback 来进行简单模拟,因为这个 Api 就是在浏览器空闲时期被调用。
Reconciler 通过递和归两个阶段来创建新的 Fiber Tree 和进行副作用的收集(这个副作用指的是 DOM 的更新操作)( diff 发生在此),至此 render 阶段结束,之后就来到了 commit 阶段,该阶段只有一个 Readerer 执行,并且是同步执行,Readerer 主要就是根据 Fiber Tree 标记的副作用来进行真实 DOM 的创建、更新和删除操作。
区别
其实大体上 Vue 和 React 的更新机制都是构建虚拟DOM、diff、操作真实DOM这三个主要过程,它们的主要区别主要体现在触发更新的机制和一些细节上:
-
触发更新机制:Vue 是通过响应式系统自动及时的进行触发,而 React 则是通过用户更改状态的操作然后进行一系列调度来触发更新。
-
任务的区别:Vue 会将任务包装成微任务,而 React 则是将其包装成宏任务。
虽然都是异步任务,但它们有很大区别。在事件循环中,如果有微任务存在则会先一直执行微任务,直到把微任务队列清空,然后再执行宏任务,并且在每个宏任务执行完毕后,会立即检查并执行所有微任务,然后再进行下一个宏任务的执行。
先明确一点:异步任务执行时是由主线程进行执行的,所以此时它们已经相当于是同步执行了(这个异步实际指的是异步任务在任务队列里面等待的时候不会影响主线程的执行)
微任务执行时不会穿插其它任务(比如浏览器渲染),所以当有大量微任务堆积时可能就会阻塞浏览器渲染(异步任务),但执行完一个宏任务时如果遇到浏览器需要渲染,则不会继续执行下一个宏任务而是转去进行浏览器渲染然后开启新的一轮事件循环。
- 因为 React 的 Fiber 架构的出现就是为了能够随时打断,把控制权交给主线程,所以 React 采用的是宏任务,而不是会一股脑 “ 全冲完 ” 的微任务,这样可以避免微任务过多而导致的任务堆积和性能问题。
- 也正是因为 Vue 的理念是追求响应性和即时效果并避免过多的渲染,所以它采用微任务,及时把更新任务处理完,最后让浏览器渲染一次即可。
- 其实现如今,React 和 Vue 都不是完全使用某一种任务,在一些情况下 React 也会使用微任务,Vue 亦是如此,它们的目标都是想要结合自身情况来创造一个更优秀的框架。
-
diff 算法的不同:Vue 采用双端对比,而 React 使用的是 Reconciliation 算法。
Reconciliation 算法是React 整体的更新策略,并不只是简单的 diff 算法。简单来说 React 的 diff 过程会对单节点和多节点分别计算(都是遍历 Fiber 链表进行的),多节点的情况会分两轮遍历,第一轮遍历会尝试逐个的复用节点,第二轮遍历处理上一轮遍历中没有处理完的节点。
React 不使用双端 diff 的原因:
React 源码中的原话:This algorithm can’t optimize by searching from boths ends since we don’t have backpointers on fibers. I’m trying to see how far we can get with that model. If it ends up not being worth the tradeoffs, we can add it later.(这种算法不能通过从两端搜索来优化,因为我们在 Fiber 对象上没有反向指针。我正试着看看我们能用这个模型走多远,如果这种方式不理想,以后再考虑实现两端搜索。)
并且源码中还提到,React 认为对于列表反转和需要进行双端搜索的场景是少见的,所以综合以上情况, React 暂时还不会使用双端 diff。
结语
还是那句话,本篇文章只是浅淡,Vue 和 React 的更新机制非常复杂,其中涉及的知识点很多,比如 Vue 的响应式原理、Vue 双端 diff 流程、React Fiber 架构、Fiber 双缓冲、React Reconciler 递和归两阶段流程等等。
本文简单梳理了一下它们更新机制的整体流程,希望能基于此来为大家展开一个清晰的脉络,如果在阅读过程中发现了问题,欢迎评论区留言交流!
如果本篇文章对你起到了帮助,还请大家不要吝啬手中的点赞、评论、收藏和关注,我们下次再见!
最近忙着秋招,更新频率虽然降低了,但是我写文章的热情可是一点没减,期待下次好文相见!