React 虚拟DOM和性能优化
DOM
DOM 是对结构化文本的抽象表达,web 环境中,DOM 是对 html 文本的抽象描述,每个 html 元素对应一个 DOM 节点,html 元素的层级关系也体现在了 DOM 树上。
在 DOM 进行增删改操作,都会引起浏览器对网页的重新布局和重新渲染,这个过程很耗时。
通过 React 执行机制了解虚拟 DOM
React 视图
1 | // index.jsx |
babel-preset-react-app 编译 JSX
const element = createElement(type, props, …children)
type: The type argument must be a valid React component type(tag name like ‘div’ or React Component or ‘Fragment’)
props: The props argument must either be an object or null
optional: …children,当前元素子节点
1 | // babel-compile.js |
虚拟 DOM
前端性能优化的重要一条:尽量减少对DOM操作,React 执行效率高的原因在于它的虚拟DOM机制
软件开发中遇到的所有问题都可以通过增加一层抽象解决
虚拟 DOM 是 DOM 效率低下的抽象。虚拟 DOM 是用 javascript 描述的DOM 元素。
1 | // console.log(babel-compile.js) -> get virtualDOM |
flowchart LR A["index.js"] -->|依赖 babel-preset-react-app 编译| B[babel-compile.js] B -->|执行| C[virtualDOM]
diff 算法
react 的调合过程(Reconciliation)react 采用声明式的 API 描述 UI 结构,组件状态或属性更新,组件的 react 方法会返回一个新的虚拟 DOM 对象表述新的 ui 结构。 Diff 算法中,比较新的虚拟 DOM 和 旧的虚拟 DOM,再将 diff 的结果更新到真实 DOM 上。
React 基于两个假设,降低算法的时间复杂度,进行比较:
- 如果两个元素的类型不同,那么它们将生成两个不同的树
- 为列表中元素设置 key 属性,key 标识对应元素在多次 render 过程中是否发生变化
react 从根结点比较两棵树的差异,根结点不同,两棵树不同,具体如下:当根结点是不同类型时
根结点变化,react 会认为新的树和旧的树完全不同,不会继续比较其它属性和子节点,直接拆掉整个树重建。重建后的 DOM 树会整体更新到真实的 DOM 树中。需要操作大量 DOM 更新效率低。
虚拟 DOM 结点类型分为两类: - 像
div/p
等 DOM 元素类型; - react 组件类型,如 react 自定义组件。
旧的 react 组件实例在 componentWillUnMount 中调用,新的组件实例在 componentWillMount 和 componentDidMount 中调用。
包裹组件的 dom 元素变量,a 组件改变 b 组件,或组件中的元素变化,都是结点类型发生变化。当根结点相同的 DOM 类型时
根节点相同,属性不同,react 只更新需拟 DOM 树和真实 DOM 树对应的节点。当根结点是相同组件类型时
如果两个根节点是相同类型的组件,对应的组件实例不会被销毁,只会执行更新操作。并且在 componentWillReceiveProps 和 componentWillUpdate 中调用组件,更新变化的属性。
比较完根节点,react 会以同样的方式递归比较子节点,当然子节点又是它子节点的根节点。如此递归,走到比较完两棵树上的所有节点,计算出差异更新到 DOM 树。
对于列表,react 提供了 key 的属性,key 是为了帮助 react 提高 diff 算法效率的。每次渲染后,只要 key 不变,react 认为是只一节点。如下 code,<li key='first'>first</li>
与<li key='second'>second</li>
两个元素 key 值没有变,只是位置变化,react 判断出为新增节点。从而避免了大量渲染。不要用 index 索引做 key 值,一旦数组开头新增,大量 key 失败,从而引用大量重新渲染1
2
3<li key='third'>third</li>
<li key='first'>first</li>
<li key='second'>second</li>性能优化
- 使用生产环境版本库
npm run start
使用的是开发环境版本的 react 库npm run build
构建生产环境 react 库,其它第三方也执行生产环境版本构建(生产环境 NODE_ENV = ‘production’)
一般第三方库都会根据 process.env.NODE_ENV 这个变量决定开发环境和生产环境执行
如果是自己编写 Webpack 的构建配置,在生产环境构建时,需要在 Webpack 的配置项中包含以下插件配置:1
2
3
4
5
6
7
8plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new UglifyJSPlugin()
] - 避免不必要的组件渲染
重写 shouldComponentUpdate 方法避免不必要的组件渲染 - 使用 key
性能检测工具
- React Develop Tools for Chrome 检测页面使用的 react 代码版本是生产环境版本,如果地址栏 react icon 是黑色,代表当前为生产版本 react,红色则为开发环境 react
- chrome performance tab 观察组件挂载、更新、卸载过程及各阶段的时间
- 确保运行在开发模式下
- 打开 Chrome 开发者工具,切换到 performance 窗口,单击 Record 按钮开始统计。
- 在页面执行需要分析的操作,最好不要超过 20 秒,否则导致 chrome 卡死。
- 单击 stop 按钮结束统计,user timing 查看统计结果
- why-did-you-update
用来比较 state 和 props 的变化,从而发现组件 render 方法不必要的调用
- 安装
npm i why-did-you-update --save-dev
- 使用
1
2
3
4
5import React from 'react';
if(process.env_NODE_ENV !== 'production') {
const { whyDidYouUpdate } = require('why-did-you-update')
whyDidYouUpdate(React)
}
JSX 底层处理机制
flowchart LR a[JSX视图代码] -->|编译| b[虚拟 DOM 对象 VirtualDOM] b -->|构建| c[真实 DOM]
第一次渲染页面直接从 vitureDom –> 真实 Dom;第一次以后更新需要 DOM-DIFF 对比,计算出PATCH(两次视图差异),渲染 Patch。
%%{init: {"flowchart": {"htmlLabels": false}} }%% flowchart TB subgraph 第一次渲染 a["第一次渲染"] --> b["VirtualDOM"] end subgraph 第二次渲染 a2["第一次渲染第二次渲染"] --> b1["new VirtualDOM"] b1 --> diff diff{对比 VirtualDOM && new VirtualDOM} --> patch["get 补丁包"] end b -->|渲染 virtualDOM| c["真实 DOM"] patch -->|渲染补丁包|c
Virtural DOM 的工作原理
由 ReactNode 节点元素组成的树,一个 ReactNode 实例表示一个轻量的、无状态的、不可变的虚拟 DOM 元素。
虚拟 DOM 树,最终由 ReactDOM.render 函数最终渲染到浏览器 DOM 树上,形成最终的界面效果。
React 中最主要的类型就是 ReactElement,是 ReactNode 的主要数据来源。
React Element 四个属性:type , key, props, ref
虚拟 dom 的 diff 算法复杂度 $O(n^3)$,经过优化,最终把复杂度降低到 On
操作 DOM(Document Object Model) 的几种方式
JavaScript 原生获取 DOM
1 | document.getElementById('id') |
jQuery 获取 DOM
1 | $('#id') |
以上两种方式都是直接操作 DOM 元素达到视图更新的效果
Vue 更新视图的方式
1 | <template> |
Vue 改变数据
this.name = 'zhangLP'
触发视图改变
React 更新视图
1 | import React, {useState} from 'react' |
React 通过 setState() 更新状态,改变数据,触发视图更新
React + Vue ** 两个框架都是通过操作虚拟 DOM,完成视图更新的,具体如下
graph LR subgraph 框架内完成 A[虚拟 DOM] --> B(计算变更) end B(计算变量) --> |DIFF|C[操作真实 DOM] C[操作真实 DOM] --> D[视图更新]
what’s the Virtual DOM
1 | <body style="background-color: rgb(255, 255, 255);"> |
用 JS 模拟 DOM 生成的 DOM 结构
1 | { |