React 核心源码解析
课程目标
- 知道 react ⼤致实现思路,能对⽐ react 和 js 控制原⽣ dom 的差异,能口述⼀个简化版的 react。
- 知道 diff 算法⼤致实现思路。
- 对 state 和 props 有⾃⼰的使⽤⼼得,结合受控组件、HOC等特性描述,需要说明各种⽅案的适⽤场景。
- 能说明⽩为什么要实现 fiber。
- 能说明⽩为什么要实现 hook。
- 知道react不常⽤的特性,⽐如context,portal,Error boundaries。
知识要点
虚拟 DOM
不管是 React 还是 Vue,都有虚拟 DOM ,虚拟 DOM 一定快吗?
为什么要用虚拟 DOM?
- 某种程度上,保证,性能的一个下限;
- 中间层,VDOM -> fiber 对象 -> 真实 DOM
源码跟踪
render
1 | // react-dom/src/client/ReactDOMLegacy.js |
legacyRenderSubtreeIntoContainer
创建fiberroot的根节点
1 | // react-dom/src/client/ReactDOMLegacy.js |
updateContainer
1 | // react-reconciler/src/ReactFiberReconciler.old.js |
scheduleUpdateOnFiber
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
ensureRootIsScheduled
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
performSyncWorkOnRoot
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
renderRootSync
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
workLoopSync
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
performUnitOfWork
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
beginWork
1 | // react-reconciler/src/ReactFiberBeginWork.old.js |
completeWork
1 | // react-reconciler/src/ReactFiberCompleteWork.old.js |
diff 算法
react 是如何将 diff 算法的复杂度降下来的?
其实就是在算法复杂度、虚拟 dom 渲染机制、性能中找了⼀个平衡,react 采⽤了启发式的算法,做了如下最优假设:
- 如果节点类型相同,那么以该节点为根节点的 tree 结构,⼤概率是相同的,所以如果类型不同,可以直接「删除」原节点,「插⼊」新节点
- 跨层级移动⼦ tree 结构的情况⽐较少⻅,或者可以培养⽤户使⽤习惯来规避这种情况,遇到这种情况同样是采⽤先「删除」再「插⼊」的⽅式,这样就避免了跨层级移动
- 同⼀层级的⼦元素,可以通过 key 来缓存实例,然后根据算法采取「插⼊」「删除」「移动」的操作,尽量复⽤,减少性能开销
- 完全相同的节点,其虚拟 dom 也是完全⼀致的;
调度
concurrent 模式 / 18⾥⾯ – 调度。
scheduler – 有⼀个任务,耗时很⻓,filter。
--> 把任务,放进⼀个队列,然后开始以⼀种节奏进⾏执⾏。
使用 MessageChannel 模拟 requestIdleCallback
1 | /** |
为什么没有用 requestIdleCallback ?
- 兼容性;
- 50ms 渲染问题;
chrome 60hz 每16.666ms 执⾏⼀次事件循环。
为什么没有⽤ generator ?
why not use generator
为什么没有⽤ setTimeout ? – 4-5ms
补充知识点
react 和 Vue 的 diff 算法有什么区别?
相同点:
Vue 和react 的 diff 算法,都是不进⾏跨层级⽐较,只做同级⽐较。不同点:
- Vue 进⾏ diff 时,调⽤ patch 打补丁函数,⼀边⽐较⼀边给真实的DOM打补丁
- Vue 对⽐节点,当节点元素类型相同,但是 className 不同时,认为是不同类型的元素,删除重新创建,⽽ react 则认为是同类型节点,进⾏修改操作
- Vue 的列表⽐对,采⽤从两端到中间的⽅式,旧集合和新集合两端各存在两个指针,两两进⾏⽐较,如果匹配上了就按照新集合去调整旧集合,每次对⽐结束后,指针向队列中间移动;
- ⽽ react 则是从左往右依次对⽐,利⽤元素的 index 和标识 lastIndex 进⾏⽐较,如果满⾜ index < lastIndex 就移动元素,删除和添加则各⾃按照规则调整;
- 当⼀个集合把最后⼀个节点移动到最前⾯,react 会把前⾯的节点依次向后移动,⽽ Vue 只会把最后⼀个节点放在最前⾯,这样的操作来看,Vue 的 diff 性能是⾼于 react 的
⽬前的 diff 是什么?真的是 O(n) 吗?
准确的来说,React的 diff 的最好时间复杂度是 O(n), 最差的话,是 O(mn);
diff ⽐较的是什么?
比较的是 current fiber 和 vdom,比较之后生成 workingprogress fiber
为什么要有 key ?
在⽐较时,会以 key 和 type 是否相同进⾏⽐较,如果相同,则直接复制。
react 为什么要⽤ fiber ?
stack reconciler -> fiber reconciler
在 V16 版本之前协调机制是 Stack reconciler, V16 版本发布 Fiber 架构后是 Fiber reconciler。
在 setState 后,react 会⽴即开始 reconciliation 过程,从⽗节点(Virtual DOM)开始递归遍历,以找出不同。将所有的 Virtual DOM 遍历完成后,reconciler 才能给出当前需要修改真实 DOM 的信息,并传递给 renderer,进⾏渲染,然后屏幕上才会显示此次更新内容。
对于特别庞⼤的DOM树来说,reconciliation 过程会很⻓(x00ms),在这期间,主线程是被 js 占⽤的,因此任何交互、布局、渲染都会停⽌,给⽤户的感觉就是⻚⾯被卡住了。
在这⾥我们想解决这个问题的话,来引⼊⼀个概念,就是任务可中断,以及任务优先级,也就是说我们的 reconciliation 的过程中会⽣成⼀些任务和⼦任务,⽤户的操作的任务优先级是要⾼于 reconciliation 产⽣的任务的,也就是说⽤户操作的任务是可以打断 reconciliation 中产⽣的任务的,它会优先执⾏。
react 为什么要⽤ hook ?
- 在组件之间复用状态逻辑很难
Hook 使你在无需修改组件结构的情况下复用状态逻辑。 - 复杂组件变得难以理解
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) - 难以理解的 class
Hook 使你在非 class 的情况下可以使用更多的 React 特性
hook 是怎么玩的 ?
1 | function App () { |
类 React 最小实现
使用:
1 | import { render } from "./render"; |
code:
1 | const normalize = (children = []) => children.map(child => typeof child === 'string' ? createVText(child): child) |
1 | import { mount } from "./mount"; |
1 | import { patchProps } from "./patch"; |
1 | import { mount } from "./mount"; |
1 | import { mount } from './mount.js' |