1.请简述 React 16 版本中初始渲染的流程
- jsx 转换成 react 元素
- babel-react 会将jsx 转换为 React.createElement 函数调用
- React.createElement 会 jsx 转换成 react element (react element 就是 一个用来描述react 元素的对象。)
- render (协调层)此阶段负责创建 Fiber 数据结构并为 Fiber 节点打标记,标记当前 Fiber 节点要进行的 DOM 操作。
- 首先为每一个react 元素构建 fiber 对象 (workInProgress Fiber 树)创建 此 fiber 对象对应的 DOM 对象,为 fiber 对象添加 effectTag 属性(用来记录当前 Fiber 要执行的 DOM 操作),然后在render 结束后, fiber 会被保存到 fiberroot 中。
- commit 阶段 (渲染层)
- 渲染器根据协调器为 Fiber 节点打的标记,同步执行对应的DOM操作。
- 先获取render阶段的工作成果,即保存在fiberRoot对象中的重新构建的workInProgress fiber树,fiberRoot.finishedWork根据fiber对象中的effectTag属性进行相应的DOM操作,如果存在类组件,调用对应的生命周期函数,如果存在函数组件,调用对应的useEffect钩子函数。
2.为什么 React 16 版本中 render 阶段放弃了使用递归
在 React 15 的版本中,采用了循环加递归的方式进行了 virtualDOM 的比对,由于递归使用 JavaScript 自身的执行栈,一旦开始就无法停止,直到任务执行完成。如果 VirtualDOM 树的层级比较深,virtualDOM 的比对就会长期占用 JavaScript 主线程,由于 JavaScript 又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。
在 React 16 的版本中,放弃了 JavaScript 递归的方式进行 virtualDOM 的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了 virtualDOM 比对造成页面卡顿的问题。
3.请简述 React 16 版本中 commit 阶段的三个子阶段分别做了什么事情
调用 commitRoot 表示进入 commit 阶段,commitRoot 更改任务优先级,默认任务优先级为 97 ,commit阶段不可被打断,要以最高优先级执行 commit 任务,使用 runWithPriority 改变任务优先级并调用 commitRootImpl 则开始 commit 阶段:
- 第一个子阶段:before mutation 阶段(执行 DOM 操作前)
- 执行commitBeforeMutationEffects,处理类组件的 getSnapShotBeforeUpdate 生命周期函数(更新阶段执行的钩子函数)
- 给实例对象下面添加属性 reactInternalSnapshotBeforeUpdate,存储快照,在执行生命周期函数 componentDidUpdate 时,在第三个参数当中我们要通过实例对象下面的 reactInternalSnapshotBeforeUpdate 属性 获取快照
- 第二个子阶段:mutation 阶段(执行 DOM 操作)
- 执行commitMutationEffects,,将 workInProgress Fiber 树变成 current Fiber 树,进行真实 DOM 操作
- 获取 effectTag 根据它执行 DOM 操作,Placement(针对该节点及子节点进行插入操作)、PlacementAndUpdate(插入并更新 DOM)、Hydrating(服务器端渲染)、HydratingAndUpdate(服务器端渲染)、Update(更新 DOM)、Deletion(删除 DOM)
- 第三个子阶段:layout 阶段(执行 DOM 操作后)
- 如果类组件中调用了生命周期函数 或者函数组件中调用了 useEffect 则调用 commitLayoutEffectOnFiber 处理
- 类组件:获取类组件实例对象,初始渲染阶段调用挂载在实例对象身上的 componentDidMount 生命周期函数;更新阶段获取旧的 props ,获取旧的 states,通过实例对象调用 componentDidUpdate 传递旧的 props、旧的state、和实例对象挂载的 __reactInternalSnapshotBeforeUpdate 快照;获取 任务队列,调用 commitUpdateQueue 去处理 render 函数的第三个参数 callback ,渲染完成要执行的回调函数
- 函数内部组件处理钩子函数:当函数组件调用钩子函数useEffect 时,调用它传入的 callback ,将 callback 的返回值(清理函数)存储在 effect.destory 中,当组件被清理时调用 destory 清理函数
4.请简述 workInProgress Fiber 树存在的意义是什么
在 React 中最多会同时存在两棵 Fiber 树,当前在屏幕中显示的内容对应的 Fiber 树叫做 current Fiber 树,当发生更新时,React 会在内存中重新构建一颗新的 Fiber 树,这颗正在构建的 Fiber 树叫做 workInProgress Fiber 树。在双缓存技术中,workInProgress Fiber 树就是即将要显示在页面中的 Fiber 树,当这颗 Fiber 树构建完成后,React 会使用它直接替换 current Fiber 树达到快速更新 DOM 的目的,因为 workInProgress Fiber 树是在内存中构建的所以构建它的速度是非常快的。
在 current Fiber 节点对象中有一个 alternate 属性指向对应的 workInProgress Fiber 节点对象,在 workInProgress Fiber 节点中有一个 alternate 属性也指向对应的 current Fiber 节点对象。两个 Fiber 节点通过 alternate 属性连接
React 应用的根节点通过 current 指针在不同 Fiber 树的 rootFiber 间切换来实现 Fiber 树的切换。当 workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的 current 指针指向 workInProgress Fiber 树,此时它就变为 current Fiber 树。每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成DOM更新。
由于有两颗 Fiber 树,实现了异步中断时,更新状态的保存,中断回来以后可以拿到之前的状态。并且两者状态可以复用,节约了从头构建的时间。workInProgress Fiber 避免了频繁大量的 DOM 操作,先对比