术语
- 渲染:生成用于显示的对象,以及将这些对象形成真实的DOM对象
- React元素:React Element,通过React.createElement创建(它的语法糖:JSX)
- 包括普通元素()和组件元素(
A
)
- 包括普通元素(
- React节点:专门用于渲染到UI界面的对象,它不是DOM对象!!React会通过React元素,创建React节点
- 其实就是先生成react元素,然后将其变为React节点,再将这些节点进行渲染,最后生成真实DOM对象
- ReactDOM一定是通过React节点来进行渲染的!!
- 节点类型:
- React DOM节点(源码中是好像这个类:ReactDOMComponent):创建该节点的React元素的类型是一个字符串,比如:’div’
- React 组件节点(React Composite):创建该节点的React元素的类型是一个函数或是一个类,比如:class App。Fragment属于函数组件节点
- React 文本节点(React TextNode):由字符串、数字、表达式创建的节点
- React 空节点:由null、undefined、false、true创建的节点
- React 数组节点:该节点由一个数组创建
- 真实DOM(页面上显示的DOM):通过document.createElement创建的DOM元素
首次渲染—新节点渲染
ReactDOM.render(ele, document.getElementById(‘root’));
- 通过参数的值创建节点
根据不同的节点,做不同的事情
- 文本节点:通过document.createTextNode创建真实的文本节点(但还没有加入到页面里)
- 空节点:什么都不做,只是占位
- React数组节点:遍历数组,将数组每一项(要有key)递归创建节点(返回步骤1反复操作,至遍历结束)
DOM节点:通过document.createElement创建真实的DOM对象(挂到DOM节点的一个属性上),然后立即设置该真实DOM元素的各种属性,再遍历该节点的children属性,递归操作(即返回步骤1,反复操作,直至遍历结束)
const app = <div className="assaf">
<h1>
标题
{['abc', null, <p>段落</p>]}
</h1>
<p>
{undefined}
</p>
</div>;
ReactDOM.render(app, document.getElementById('root'));
console.log(app);
以上代码生成的就是虚拟DOM树:
组件节点
- 组件节点它本身是不会产生真实DOM对象的,它只是用来占位,但它的内容会啊
- 函数组件:调用函数(该函数必须返回一个可以生成节点的内容—就是jsx),然后将该函数的返回结果递归生成节点(即回到第一步反复操作,直到遍历结束) ```jsx //App.js import React from ‘react’;
function Comp1(props){ return (
Comp1, {props.n}
);}
function App(props){ return (
const app =
![函数节点虚拟DOM.jpeg](https://cdn.nlark.com/yuque/0/2021/jpeg/1413110/1627529054947-c76d7277-655b-4e3e-9886-c9e951e082ce.jpeg#clientId=ucc91b6fa-fbc2-4&from=ui&id=u8048bb77&margin=%5Bobject%20Object%5D&name=%E5%87%BD%E6%95%B0%E8%8A%82%E7%82%B9%E8%99%9A%E6%8B%9FDOM.jpeg&originHeight=2750&originWidth=2427&originalType=binary&ratio=1&size=2418937&status=done&style=none&taskId=ua01e8b79-b0fc-43da-a704-f043ae2ae41)
3. 类组件:
1. 创建该类的实例
1. 立即调用对象的生命周期方法:static getDerivedStateFromProps()
1. 运行该对象的render方法,拿到节点对象(再将该节点**递归**操作--回到第一步反复操作)
1. **再将该组件的componentDidMount加入到执行队列(先进先执行),以待将来执行。当整个虚拟DOM树全部构建完毕,并且将真实的DOM对象加入到容器后(未在页面上),执行该队列**
```jsx
import React from 'react';
class Comp1 extends React.Component{
constructor(props){
super(props);
console.log(4, 'Comp1 constructor');
}
static getDerivedStateFromProps(props, state){
console.log(5, 'Comp1 static getDerivedStateFromProps');
return null;
}
componentDidMount(){
console.log('b', 'Comp1 static getDerivedStateFromProps');
}
render(){
console.log(6, 'Comp1 render');
return (
<h1>
Comp1
</h1>
);
}
}
class App extends React.Component{
constructor(props){
super(props);
console.log(1, 'App constructor');
}
static getDerivedStateFromProps(props, state){
console.log(2, 'App static getDerivedStateFromProps');
return null;
}
componentDidMount(){
console.log('a', 'App static getDerivedStateFromProps');
}
render(){
console.log(3, 'App render');
return (
<div>
<Comp1 />
</div>
);
}
}
const app = (
<div>
<App />
<App />
</div>
);
ReactDOM.render(app, document.getElementById('id');
console.log(app);
数字及字母打印顺序是:1 2 3 4 5 6 b a 1 2 3 4 5 6 b a
按上面的原理走嘛,没毛病。
两个
- 重新调用ReacDOM.render,完全重新生成节点树
- 此时触发的是根结点的更新
- 在类组件的实例对象中调用setState,会导致该实例所在的节点更新 ```jsx import React from ‘react’;
class CompA extends React.Component{ state = { a: 123, b: “abc” }
componentDidUpdate(prevProps, prevState){
console.log('CompA componentDidUpdate');
}
render(){
return (
<div>
{/* <h1>CompA</h1> */}
<h1>{this.state.a}</h1>
<CompB n={this.state.b} />
<button
onClick={() => {
this.setState({
a: 321,
b: "cba",
});
}}
>
点击
</button>
</div>
);
} }
function CompB(props){ return (
CompB
/}{props.n}
class CompC extends React.Component{
componentDidUpdate(prevProps, prevState){
console.log('CompC componentDidUpdate');//肯定先打印compc,再打印compa啊
}
render(){
let title = document.getElementById("title");
if(title){
console.log(title.innerHTML);
}else{
console.log(title);//null,因为首次渲染的时候此处还没添加到页面上,获取不到真实DOM
}
return (
//<h1>CompC</h1>
<h1>{this.props.n}</h1>
);
} }
class App extends React.Component{
render(){
return (
<div>
<CompA />
</div>
);
} }
const app = (
ReactDOM.render(app, document.getElementById(‘id’);
console.log(app);
![更新前.jpeg](https://cdn.nlark.com/yuque/0/2021/jpeg/1413110/1627565116719-719c90e4-52e7-42ff-b0a2-508a0517fe94.jpeg?x-oss-process=image/auto-orient,1#clientId=u3e6d2c1b-3def-4&from=ui&id=qAKXJ&margin=%5Bobject%20Object%5D&name=%E6%9B%B4%E6%96%B0%E5%89%8D.jpeg&originHeight=3024&originWidth=4032&originalType=binary&ratio=1&size=7044212&status=done&style=none&taskId=u0c2b6911-e89b-413c-ac11-fa7a9175d75)![更新前2.jpeg](https://cdn.nlark.com/yuque/0/2021/jpeg/1413110/1627565130079-740b5a43-e0b4-4443-aeb1-36e7b99e23b3.jpeg#clientId=u3e6d2c1b-3def-4&from=ui&id=NUydD&margin=%5Bobject%20Object%5D&name=%E6%9B%B4%E6%96%B0%E5%89%8D2.jpeg&originHeight=1506&originWidth=1677&originalType=binary&ratio=1&size=650987&status=done&style=none&taskId=ua2adbaf2-4a85-432f-8e7e-21a63d62f25)
<a name="eJmoY"></a>
## 节点的更新
1. 如果调用的是ReactDOM.render,则进入根节点的**对比(diff)更新**
1. 如果调用的是类组件的setState
1. 运行生命周期函数,static getDedrivedStateFromProps
1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续
1. 运行render,得到一个新的节点,进入该新节点的**对比更新**(这件事肯定要递归)
1. 将自己的生命周期函数getSnapshotBeforeUpdate加入执行队列,以待将来执行
1. 将自己的生命周期函数componentDidUpdate加入执行队列,以待将来执行
1. (**不同名字的生命周期函数处于不同的队列**)
3. 后续步骤,无论上述哪种情况都要做以下流程:
1. **更新虚拟DOM树**
1. **完成真实的DOM更新。所以render的时候根本还没完成真实更新,那会只是记录,等待统一处理**
1. 依次调用执行队列中的componentDidMount(**有可能噢,因为可能有新的子组件要产生啊**)
1. 依次调用执行队列中的getSnapshotBeforeUpdate
1. 依次调用执行队列中的componentDidUpdate
1. 依次调用执行队列中的componentWillUnmount(**它自己肯定是不会卸载的,因为在它自己里面setState了。但子组件可能会啊**)
<a name="h7mVH"></a>
## 对比更新【非常重要】
**将新产生的节点,对比之前虚拟DOM中的节点,发现差异,完成更新**
**问题是:对比之前DOM树中的哪个节点--它不知道自己之前在哪个位置??**
1. 假如是做一个唯一标识来匹配筛选,则需要遍历整个DOM树,当层次深的话,性能会很差
**React为了提高对比效率,做出以下假设(并且事实证明绝大多数情况就是如此):**
- **假设节点不会出现层次的移动(对比时,可以直接找到旧树中对应层的对应序号的节点进行对比)**
- **不同的节点类型会生成不同的结构(后文再详解)**
- **相同的节点类型:节点本身类型相同,如果是组件节点,组件类型(组件A、B)也必须相同**
- **如果是由React元素生成,type值还必须一致**
- **其他的,都属于不相同的节点类型**
- **比如两个元素都是<div></div>,即都是DOM节点,且都是div类型,这就是相同节点**
- **多个兄弟通过唯一标识 key 来确定对比的新节点**
- **key值的作用:用于通过旧节点寻找对应的新节点。如果某个旧节点有key值,则其更新时,会寻找相同层级中的相同key值的节点进行对比。没找到就会进入 ,没找到对比目标的流程中。**
- **key值不能重复指的是同一个父亲同一层里面不能重复(就是兄弟节点中),key还要保持稳定**
- **它就是为了尽量复用真实DOM**
<a name="jiqiG"></a>
### 找到了对比目标
<a name="XAFJ5"></a>
#### 判断节点类型是否一致
**(一)一致:**<br />**根据不同的节点类型,做不同的事情:**
1. 空节点:不做任何事情
1. **DOM节点:**
1. **直接重用之前的真实DOM对象**(因为之前就挂到了节点的属性上,可以从节点拿到)
1. **将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)**
1. 遍历该新的React元素的子元素,**然后递归对比更新**
3. 文本节点:
1. 直接重用之前的真实DOM的文本节点
1. 记录变化的nodeValue,同上
4. **组件节点:**
1. **函数组件节点**
1. 直接重新调用函数,得到一个节点对象,进入递归对比更新即可
2. **类组件节点**
1. 重用之前的实例,不用重新new,所以不用再次constructor
1. 运行生命周期函数,static getDedrivedStateFromProps
1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续
1. 运行render,得到新的节点对象,**进入递归对比更新,**所以这儿容易考打印顺序
1. 将该对象的getSnapshotBeforeUpdate加入队列
1. 将该对象的componentsDidUpdate加入队列
5. 数组节点:
1. 遍历数组进行递归对比更新
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1413110/1627565393008-b2d19f1a-14eb-4e68-b66b-5535b0564dae.png#clientId=u3e6d2c1b-3def-4&from=paste&height=722&id=u14bc5c14&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1444&originWidth=1550&originalType=binary&ratio=1&size=4260936&status=done&style=none&taskId=u41658985-9aa0-49cf-96bb-9ba03cd2dff&width=775)<br />![更新.jpeg](https://cdn.nlark.com/yuque/0/2021/jpeg/1413110/1627565403076-d735e02a-04fa-4079-ae3a-653eeb639d4e.jpeg#clientId=u3e6d2c1b-3def-4&from=ui&id=u8bb6901c&margin=%5Bobject%20Object%5D&name=%E6%9B%B4%E6%96%B0.jpeg&originHeight=1506&originWidth=1677&originalType=binary&ratio=1&size=662295&status=done&style=none&taskId=u157b5dad-8f38-4e1e-a349-cb57dfd7cfe)<br />**(二)不一致:**<br />**整体上,先创建全新的节点,卸载旧的节点,再挂载新节点到页面上**
**先创建新节点:**<br />**进入新节点的挂载流程,**即 进入首次渲染新节点流程,跟前文中的完全一样。
**再卸载旧节点:**
1. 文本节点、DOM节点、数组节点、空节点、函数组件节点:**直接放弃该整个节点**(就是说如果该节点有子节点,则须**递归卸载子节点**)
1. **类组件节点**
1. 直接放弃该节点
1. 调用该节点的componentWillUnmount生命周期函数
1. **递归卸载子节点**
```jsx
import React, { Component } from 'react'
class CompA extends Component{
componentDidMount() {
console.log("CompA 新组件挂载");
}
componentWillUnmount() {
console.log("CompA 组件卸载");
}
render(){
console.log("CompA render");
return <CompAA />
}
}
class CompB extends Component{
componentDidMount() {
console.log("CompB 新组件挂载");
}
componentWillUnmount() {
console.log("CompB 组件卸载");
}
render(){
console.log("CompB render");
return <CompBB />
}
}
class CompBB extends Component{
componentDidMount() {
console.log("CompBB 新组件挂载");
}
componentWillUnmount() {
console.log("CompBB 组件卸载");
}
render(){
console.log("CompBB render");
return(
<span>CompBB</span>
)
}
}
class CompAA extends Component{
componentDidMount() {
console.log("CompAA 新组件挂载");
}
componentWillUnmount() {
console.log("CompAA 组件卸载");
}
render(){
console.log("CompAA render");
return(
<span>CompAA</span>
)
}
}
export default class Update_Render_Unmount extends Component {
state = {
n: 0,
}
render() {
if(this.state.n === 0){
return (
<div>
<CompB />
<button onClick={()=>{
this.setState({
n: 1
})
}}>
点击更新dom
</button>
</div>
);
}
return (
<div>
<CompA />
</div>
);
}
}
点击前:
CompB render
CompBB render
CompBB 新组件挂载
CompB 新组件挂载
点击后:
CompA render
CompAA render
CompB 组件卸载
CompBB 组件卸载
CompAA 新组件挂载
CompA 新组件挂载
注意,对于显示隐藏的推荐做法:
- 如果只是做显示隐藏,尽量使用display:none,因为它并没有改变DOM的结构,不会影响后面的元素
- const h1 = this.state.visible ?
标题
: null;
- 新的DOM树中有节点被删除
- 新的DOM树有节点被添加
无论哪种情况,都执行下述步骤即可:
- 创建新加入的节点
- 卸载多余的旧节点