目录
概要
- 《React踩坑笔记 —— 差分算法(二)》
React提供了声明式的API,以至于我们不需要担心每次更新具体发生了什么更改。这使得我们开发应用变得很容易,但始终无法清楚React内部是如何实现的。本文解释了在 “差分算法” 中如何做出选择,使得组件更新,在足够快的高性能应用中仍然可以预测。
在理解 “差分算法” 之前,首先我们需要去理解:
- document与Dom nodes的关系;
- document与react组件的关系;
- react组件与react元素树的关系;
render()
函数的作用与生命周期;ReactDOM.render()
函数的作用。
Document与React元素树
JSX代码
import React from "react";
import ReactDOM from 'react-dom';
const First = () => (<div>
<h1>第一棵树</h1>
<div>附加信息</div>
</div>);
const Second = () => (<div>
<h1>第二棵树</h1>
<Third />
</div>);
const Third = () => (<div>
<h1>第三棵树</h1>
<div>附加信息</div>
</div>);
const App = () => (<div>
<First />
<Second />
</div>);
ReactDOM.render(<App />, document.getElementById('root'));
图示
说明
万变不离其宗,对于一个HTML页面,document与Node永远会是它的基础组成部分(实际上document也只是一个特别的Node),如果把document看做一张纸,那么Node可以看成点缀在纸上的文字、图片或符号。
在React中,React组件是由真实的Dom Node组成,最终会被转化为Dom Node并在document中渲染出来(事件\样式\选择器,只属于真实的DOM Node)。
当你使用React的时候,你可以认为:
- 每一个React组件都对应一棵React元素树。
- 或者说,在某一时间点上每一个
render()
函数都创建了一棵React元素树。 - 这里的
render()
函数,见下文。 - 每一个React组件可以由一个或多个React组件以及
HTMLElement
组成,即是 —— 每一棵树都可以作为另一棵树的子树,正如上图所示:<Third />是<Second />的子树,<Second />和<First />又都是<App />的子树。 - 在上图中,<App />组件所对应的React元素树,是最大的一棵树。通过API ——
ReactDOM.render(element, container[, callback])
将这棵树挂载到容器(container)
中。 - 所谓
容器(container)
,不过是document中某个指定的Div或其它块级元素。 - 所以,无论通过
JavaScript选择器
还是dom操作插件
还是React提供的Refs
,对Dom node的操作都会被保留在这个document中,并在组件的state
或props
发生改变的时候参与 “差分算法”。 - 所以,如果当前页面被刷新,那么原来的document也不复存在:
- React组件会被重新挂载
- Dom node会被重新插入
- 如果你想保留某些状态,你需要高于页面的存储方式,例如sessionStorage( 标签页 )、localStorage( 浏览器 )、cookie( 浏览器 )、Web SQL( 浏览器 )、服务器数据( 任何地方 )
- 路由(Router),只会引起某些组件的卸载或挂载,而不会颠覆整个document。
render()
对于Function组件(无状态组件)
就是自身,对于Class组件(有状态组件)
就是从React.Component
继承的render()
方法。
返回值类型
当render()
被调用时,可以依赖props
和state
来构造返回值,其返回值类型包括如下:
- React elements. 典型地通过JSX语法创建,例如
<div />
、<MyComponent />
- Arrays and fragments. 由于 “差分算法” ,React要求
render()
必须返回单元素(single),React提供了<></>和<React.Fragment>允许开发者从render()
中返回多元素 - Portals. 有时我们需要将子组件渲染到父组件外的某个Dom node上,例如对话框、悬浮卡、提示信息。Portals 为我们都提供了首选方式。
- String and numbers. 字符串和数字被以文本节点(Text nodes)的形式渲染到Dom中
- Booleans or null. 返回Null表示什么都不做。 (大多数情况是返回
test && <Child />
,这里test
是布尔值(boolean)
纯函数
render()
应该是一个纯函数,返回值只依赖于state
和props
,并且不会产生副作用,副作用包括:
- 修改
state
和props
- 调用非纯函数。如
Date.now()
或Math.random()
- 网络请求,路由跳转
- 浏览器交互。
setTimeout()
、console.log()
如果你需要浏览器交互或网络请求,可以选择在componentDidMount()
或其它合适的声明周期函数中执行。有些条件没有做硬性要求,但是保证render()
作为纯函数,能够提高组件更新的性能。
生命周期
组件在挂载(Mounting)和更新(Updating)时,render()
都会被执行,并产生一棵新的React元素树,然后执行 “ 差分算法” 更新UI(用户界面)。声明周期执行顺序如下。详情可见官方文档《component-lifecycle》
Mounting
- constructor()
- static getDerivedStateFromProps()
- UNSAFE_componentWillMount()
- render()
- componentDidMount()
Updating
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- UNSAFE_componentWillUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
ReactDOM.render()
ReactDOM.render(element, container[, callback])
该方法将整个React元素树渲染到document中指定的<Div />
容器中,并返回根组件(对应最大元素树)的引用(Function 组件 —— 无状态组件,没有实例(Instance)所以返回null)。
如果这个React元素(根组件)先前已经被渲染到容器中,那么将对它执行更新,按照 “差分算法” 去呈现它。
如果这个可选的参数—— 回调函数,被提供。那么会在组件被渲染或更新后调用。
Note
ReactDOM.render()
控制着容器(container)节点的内容,第一次调用时,任何存在的Dom元素都会被替换,之后再调用会使用React Dom diffing algorithm(差分算法)进行高效更新。ReactDOM.render()
不修改容器节点(只修改容器的子节点)。ReactDOM.render()
当前返回根组件(root ReactComponent )实例的引用。然而,返回值的使用已经被遗留了,应该避免去使用它,因为在未来的React版本中,在某些情况下React会采用异步的方式去渲染组件。如果你真的需要引用根组件实例,首选解决方案是为你的根组件添加《callback ref》。- 另外,使用
ReactDOM.render()
去融合服务端渲染容器(server-rendered container)已经被遗弃了,并将在React 17被移除,代替使用hydrate()。