Q&A
1、 请简述 React 16 版本中初始渲染的流程
jsx转换为react元素
babel-react将jsx调用React.createElement创建为虚拟DOM(react element是 一个用来描述react元素的对象)
render阶段(协调层): 此阶段负责创建Fiber数据结构并为Fiber节点打标记,标记当前Fiber节点要进行的DOM操作
大致流程:
- 首先为每一个
react元素创建fiber对象(workInProgress Fiber树) - 然后创建此
fiber对象对应的DOM对象,并为fiber对象添加effectTag属性,用于记录当前fiber要执行的DOM操作 - 在
render结束后,所有的fiber对象会被保存在fiberroot中
代码执行:
- 将子树渲染到容器中 (初始化
Fiber数据结构: 创建fiberRoot及rootFiber) - 判断是否为服务器端渲染 如果不是服务器端渲染,清空
container容器中的节点 - 通过实例化
ReactDOMBlockingRoot类创建LegacyRoot,创建LegacyRoot的Fiber数据结构 - 创建
container,创建根节点对应的fiber对象 - 获取
container的第一个子元素的实例对象 - 计算任务的过期时间,再根据任务过期时间创建
Update任务,将任务(Update)存放于任务队列(updateQueue)中。判断任务是否为同步 调用同步任务入口。 - 构建
workInProgress Fiber树
一些关键点:
- 在 render 阶段中,只会做一些复杂的运算,并不会真正的操作页面
- 在内存中做 新旧 fiber 对象比对,找出更新的 fiber 节点,或者是首次加载时 生成组装 html 片段
render阶段是可以被打断的,但初始渲染时不会被打断,因为要让用户尽快看到界面react里的时间分片的概念,分的就是复杂运算的部分render阶段也就是说可能会被高优先级的任务(例如界面事件)打断
commit阶段 (渲染层):commit阶段负责根据Fiber节点标记 (effectTag) 进行相应的DOM操作,可以分为三个子阶段
before mutation阶段(执行DOM操作前):处理类组件的getSnapShotBeforeUpdate生命周期函数mutation阶段(执行DOM操作): 根据effectTag执行DOM操作,向容器container中追加或插入节点layout阶段(执行DOM操作后):调用生命周期函数和钩子函数,执行render传递的回调函数
2、 为什么 React 16 版本中 render 阶段放弃了使用递归
- 在
React 15的版本中,采用了循环加递归的方式进行了virtualDOM的比对,由于递归使用JavaScript自身的执行栈,一旦开始就无法停止,直到任务执行完成。如果VirtualDOM树的层级比较深,virtualDOM的比对就会长期占用JavaScript主线程,由于JavaScript又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。 - 在
React 16的版本中,放弃了JavaScript递归的方式进行virtualDOM的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了virtualDOM比对造成页面卡顿的问题。 - 在
React中,官方实现了自己的任务调度库Scheduler。它可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,高优先级任务先执行,低优先级任务后执行。
3、 请简述 React 16 版本中 commit 阶段的三个子阶段分别做了什么事情
调用 commitRoot 表示进入 commit 阶段,commitRoot 更改任务优先级,默认任务优先级为 97 ,commit阶段不可被打断,要以最高优先级执行 commit 任务,使用 runWithPriority 改变任务优先级并调用 commitRootImpl 则开始 commit 阶段
before mutation阶段(执行 DOM 操作前) 主要调用类组件生命周期函数getSnapshotBeforeUpdate,并且把旧的props和旧的states传递进去mutation阶段(执行DOM操作) 调用commitMutationEffects,获取对象得effects,根据不同的effectTag执行不同的操作: 插入节点:commitPlacement、更新节点:commitWork、删除节点:commitDeletionlayout阶段(执行DOM操作后) 调用类组件的生命周期函数: 初次渲染阶段调用componentDidMount生命周期函数、更新阶段调用componentDidUpdate生命周期函数、执行渲染完成之后的回调函数,也就是render函数的第三个参数,并且更改this指向,指向render方法的第一个参数 调用函数组件的钩子函数: firstEffect:指向第一个更新的节点、nextEffect:指向下一个更新的节点
4、 请简述 workInProgress Fiber 树存在的意义是什么
React使用双缓存技术完成Fiber树的构建与替换,实现DOM对象的快速更新- 在
React中最多会同时存在两棵Fiber树,当前在屏幕中显示的内容对应的Fiber树叫做current Fiber树,当发生更新时,React会在内存中重新构建一颗新的Fiber树,这颗正在构建的Fiber树叫做workInProgress Fiber树。在双缓存技术中,workInProgress Fiber树就是即将要显示在页面中的Fiber树,当这颗Fiber树构建完成后,React会使用它直接替换current Fiber树达到快速更新DOM的目的,因为workInProgress Fiber树是在内存中构建的所以构建它的速度是非常快的。一旦workInProgress Fiber树在屏幕上呈现,它就会变成current Fiber树 - 在
current Fiber节点对象中有一个alternate属性指向对应的workInProgress Fiber节点对象,在workInProgress Fiber节点中有一个alternate属性也指向对应的current Fiber节点对象。
5、虚拟 DOM 是什么?原理、优缺点?应用场景?
虚拟 DOM: JS 对象,通常含有 tag(节点标签)、props(节点属性含事件监听)、shapeFlag(特征标签)、children(子节点 Array)。
诞生目的:浏览器标准 DOM 对象很复杂,使用虚拟 DOM + diff 算法减少 DOM 操作,同时引入 key 值,避免对比多个相同元素浪费性能
虚拟 DOM 的实现:
- compile,通过 h 函数把真实 DOM 编译成 vnode 虚拟节点对象。
- diff 算法,明确 oldVnode 和 newVnode 之间的变化。
- patch, 如果把这些变化用打补丁的方式(批量异步)更新到真实 dom 上去
优点:
保证性能下限 。降低 DOM 的操作范围(减小)与频次(合并),提升页面性能。
是不是所有的操作都是虚拟 DOM 更高效? 大量的直接操作 DOM 容易引起页面性能下降。这时 React 基于虚拟 DOM 的 diff 处理与批处理操作,可降低 DOM 的操作频次和范围,提升页面性能。但是在首次渲染或者微量 dom 操作的时候,虚拟 DOM 的性能就更慢一些。
规避 XSS 风险。 跨站脚本攻击属于代码注入攻击,没有直接的 HTML,都是通过转义的方式生成的。
虚拟 DOM 一定可以规避 XSS 吗? 虚拟 DOM 内部确保了字符转义,所以确实可以做到这点,但 React 存在风险,因为 React 留有 dangerouslySetInnerHTML API 绕过转义。
低成本实现跨平台(JS 对象可以与其他实体建立映射关系,如:IOS/安卓应用、小程序)
没有虚拟 DOM 不能实现跨平台吗? 比如 NativeScript 没有虚拟 DOM 层 ,它是通过提供兼容原生 API 的 JS API 实现跨平台开发。 那虚拟 DOM 的优势在哪里? 实际上它的优势在于跨平台的成本更低。在 React Native 之后,前端社区从虚拟 DOM 中体会到了跨平台的无限前景,所以在后续的发展中,都借鉴了虚拟 DOM。比如:社区流行的小程序同构方案,在构建过程中会提供类似虚拟 DOM 的结构描述对象,来支撑多端转换。
缺点:
- 内存占用较高。因为需要模拟整个网页的真实 DOM,而且由于是 Object,其内存占用肯定会有所上升。
- 严重依赖打包工具。需要用额外的创建函数 CreateElement,可以用 JSX / 模板 来简化,因为 JS 不认识 jsx / 模板 语法。
- 高性能应用场景存在难以优化的情况,类似像 Google Earth 一类的高性能前端应用在技术选型上往往不会选择 React。
其他应用场景:
- 记录真实 DOM 变更。可以应用于埋点统计与数据记录等。例如实现 web 页面录制与回放的rrweb, 将⻚⾯中的 DOM 以及⽤户操作保存为可序列化的数据,以实现远程回放.
6、vue2、vue3 、react17+ 在虚拟 dom 的 diff 算法?
简答
vue2 diff 算法:
- **双端比较:新列表****和旧列表两个列**表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。
- 非理想情况:当四次对比都没找到复用节点时,拿新列表的第一个节点去旧列表中找与其
key相同的节点。找到移动旧节点,找不到添加新节点到旧节点列表,之后新列表头指针向后移一位,依次查找。
vue3 diff 算法:
- 首先是相同的前置与后置元素的预处理,其次是最长递增子序列(借鉴于inferno)
react diff 算法:
- 递增法:通过对比新的列表中的节点,在原本的列表中的位置是否是递增,来判断当前节点是否需要移动。
- 优化与不足:
- **目前的 **
reactDiff的时间复杂度为O(m*n),我们可以用空间换时间,把key与index的关系维护成一个Map,从而将时间复杂度降低为O(n)。 - react 目前在节点移动情况下的算法性能不高,有可优化的空间。
vue2.x中的diff算法——双端比较,解决此问题。
- **目前的 **
详细介绍
掘金:React、Vue2、Vue3 的三种 Diff 算法
GitHub:三种 virtual-dom Diff 算法源码实现
7、react 和 vue 在技术层面的区别?
简答
- 相同点
- 都是创建 UI 界面的 JavaScript 库
- 都采用虚拟 DOM,提高渲染的速度
- 提供响应式和可组合性的视图组件(组件化),推崇单向数据流
- 独立的路由系统和全局状态的管理的处理来自于第三方库
- 不同点:
- 概念/框架层面:
- Vue.js 是一套构建用户界面的渐进式框架,采用自底向上增量开发的设计。
- React 是一个用于构建用户界面的开源 JavaScript 库。React 拥有较高的性能,代码逻辑非常简单。
- 数据流及响应式的不同: vue 是数据双绑,react 数据流向是单向的。监听数据变化的实现原理不同
- 模板语法不同:vue 是指令+模板语法,react 是 jsx 函数式编程
- 渲染区别:vue 是数据变化通知依赖项精确的驱动渲染,react 是需要调用 setState 时重新渲染全部子组件,但是可以通过 shouldComponentUpdate 等一些方法进行优化控制
- diff:Vue Diff 使用双向链表边对比边更新,react 的 diff 将需要更新的部分添加到任务队列进行批量更新
- 事件机制:vue 直接是原生事件,react 是合成事件:事件冒泡到根节点进行事件委托处理,且做了跨端兼容处理。
- 概念/框架层面:
Attraction11