术语
- 渲染:生成用于显示的对象,以及将这些对象形成真实的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 =
3. 类组件:1. 创建该类的实例1. 立即调用对象的生命周期方法:static getDerivedStateFromProps()1. 运行该对象的render方法,拿到节点对象(再将该节点**递归**操作--回到第一步反复操作)1. **再将该组件的componentDidMount加入到执行队列(先进先执行),以待将来执行。当整个虚拟DOM树全部构建完毕,并且将真实的DOM对象加入到容器后(未在页面上),执行该队列**```jsximport 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} /><buttononClick={() => {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);
<a name="eJmoY"></a>## 节点的更新1. 如果调用的是ReactDOM.render,则进入根节点的**对比(diff)更新**1. 如果调用的是类组件的setState1. 运行生命周期函数,static getDedrivedStateFromProps1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续1. 运行render,得到一个新的节点,进入该新节点的**对比更新**(这件事肯定要递归)1. 将自己的生命周期函数getSnapshotBeforeUpdate加入执行队列,以待将来执行1. 将自己的生命周期函数componentDidUpdate加入执行队列,以待将来执行1. (**不同名字的生命周期函数处于不同的队列**)3. 后续步骤,无论上述哪种情况都要做以下流程:1. **更新虚拟DOM树**1. **完成真实的DOM更新。所以render的时候根本还没完成真实更新,那会只是记录,等待统一处理**1. 依次调用执行队列中的componentDidMount(**有可能噢,因为可能有新的子组件要产生啊**)1. 依次调用执行队列中的getSnapshotBeforeUpdate1. 依次调用执行队列中的componentDidUpdate1. 依次调用执行队列中的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,所以不用再次constructor1. 运行生命周期函数,static getDedrivedStateFromProps1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续1. 运行render,得到新的节点对象,**进入递归对比更新,**所以这儿容易考打印顺序1. 将该对象的getSnapshotBeforeUpdate加入队列1. 将该对象的componentsDidUpdate加入队列5. 数组节点:1. 遍历数组进行递归对比更新<br /><br />**(二)不一致:**<br />**整体上,先创建全新的节点,卸载旧的节点,再挂载新节点到页面上****先创建新节点:**<br />**进入新节点的挂载流程,**即 进入首次渲染新节点流程,跟前文中的完全一样。**再卸载旧节点:**1. 文本节点、DOM节点、数组节点、空节点、函数组件节点:**直接放弃该整个节点**(就是说如果该节点有子节点,则须**递归卸载子节点**)1. **类组件节点**1. 直接放弃该节点1. 调用该节点的componentWillUnmount生命周期函数1. **递归卸载子节点**```jsximport 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树有节点被添加
无论哪种情况,都执行下述步骤即可:
- 创建新加入的节点
- 卸载多余的旧节点
