术语

  • 渲染:生成用于显示的对象,以及将这些对象形成真实的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’));

  1. 通过参数的值创建节点
  2. 根据不同的节点,做不同的事情

    1. 文本节点:通过document.createTextNode创建真实的文本节点(但还没有加入到页面里)
    2. 空节点:什么都不做,只是占位
    3. React数组节点:遍历数组,将数组每一项(要有key)递归创建节点(返回步骤1反复操作,至遍历结束)
    4. DOM节点:通过document.createElement创建真实的DOM对象(挂到DOM节点的一个属性上),然后立即设置该真实DOM元素的各种属性,再遍历该节点的children属性,递归操作(即返回步骤1,反复操作,直至遍历结束)

      1. const app = <div className="assaf">
      2. <h1>
      3. 标题
      4. {['abc', null, <p>段落</p>]}
      5. </h1>
      6. <p>
      7. {undefined}
      8. </p>
      9. </div>;
      10. ReactDOM.render(app, document.getElementById('root'));
      11. console.log(app);

      以上代码生成的就是虚拟DOM树:
      VirtualDOM.jpeg

    5. 组件节点

      1. 组件节点它本身是不会产生真实DOM对象的,它只是用来占位,但它的内容会啊
      2. 函数组件:调用函数(该函数必须返回一个可以生成节点的内容—就是jsx),然后将该函数的返回结果递归生成节点(即回到第一步反复操作,直到遍历结束) ```jsx //App.js import React from ‘react’;

function Comp1(props){ return (

Comp1, {props.n}

);
}

function App(props){ return (

); }

const app = ReactDOM.render(app, document.getElementById(‘id’); console.log(app);

  1. ![函数节点虚拟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)
  2. 3. 类组件:
  3. 1. 创建该类的实例
  4. 1. 立即调用对象的生命周期方法:static getDerivedStateFromProps()
  5. 1. 运行该对象的render方法,拿到节点对象(再将该节点**递归**操作--回到第一步反复操作)
  6. 1. **再将该组件的componentDidMount加入到执行队列(先进先执行),以待将来执行。当整个虚拟DOM树全部构建完毕,并且将真实的DOM对象加入到容器后(未在页面上),执行该队列**
  7. ```jsx
  8. import React from 'react';
  9. class Comp1 extends React.Component{
  10. constructor(props){
  11. super(props);
  12. console.log(4, 'Comp1 constructor');
  13. }
  14. static getDerivedStateFromProps(props, state){
  15. console.log(5, 'Comp1 static getDerivedStateFromProps');
  16. return null;
  17. }
  18. componentDidMount(){
  19. console.log('b', 'Comp1 static getDerivedStateFromProps');
  20. }
  21. render(){
  22. console.log(6, 'Comp1 render');
  23. return (
  24. <h1>
  25. Comp1
  26. </h1>
  27. );
  28. }
  29. }
  30. class App extends React.Component{
  31. constructor(props){
  32. super(props);
  33. console.log(1, 'App constructor');
  34. }
  35. static getDerivedStateFromProps(props, state){
  36. console.log(2, 'App static getDerivedStateFromProps');
  37. return null;
  38. }
  39. componentDidMount(){
  40. console.log('a', 'App static getDerivedStateFromProps');
  41. }
  42. render(){
  43. console.log(3, 'App render');
  44. return (
  45. <div>
  46. <Comp1 />
  47. </div>
  48. );
  49. }
  50. }
  51. const app = (
  52. <div>
  53. <App />
  54. <App />
  55. </div>
  56. );
  57. ReactDOM.render(app, document.getElementById('id');
  58. console.log(app);

数字及字母打印顺序是:1 2 3 4 5 6 b a 1 2 3 4 5 6 b a
按上面的原理走嘛,没毛病。
类节点虚拟DOM.jpeg
两个的时候:
双类App虚拟DOM.jpeg

  1. 暂无..
    1. 生成出虚拟DOM树之后,将该树保存起来,以便后续使用
    2. 将之前生成的真实的DOM对象,加入到对应容器中

      更新节点与卸载节点

      更新的场景

  • 重新调用ReacDOM.render,完全重新生成节点树
    • 此时触发的是根结点的更新
  • 在类组件的实例对象中调用setState,会导致该实例所在的节点更新 ```jsx import React from ‘react’;

class CompA extends React.Component{ state = { a: 123, b: “abc” }

  1. componentDidUpdate(prevProps, prevState){
  2. console.log('CompA componentDidUpdate');

}

  1. render(){
  2. return (
  3. <div>
  4. {/* <h1>CompA</h1> */}
  5. <h1>{this.state.a}</h1>
  6. <CompB n={this.state.b} />
  7. <button
  8. onClick={() => {
  9. this.setState({
  10. a: 321,
  11. b: "cba",
  12. });
  13. }}
  14. >
  15. 点击
  16. </button>
  17. </div>
  18. );

} }

function CompB(props){ return (

{/

CompB

/}

{props.n}


); }

class CompC extends React.Component{

  1. componentDidUpdate(prevProps, prevState){
  2. console.log('CompC componentDidUpdate');//肯定先打印compc,再打印compa啊

}

  1. render(){
  2. let title = document.getElementById("title");
  3. if(title){
  4. console.log(title.innerHTML);
  5. }else{
  6. console.log(title);//null,因为首次渲染的时候此处还没添加到页面上,获取不到真实DOM
  7. }
  8. return (
  9. //<h1>CompC</h1>
  10. <h1>{this.props.n}</h1>
  11. );

} }

class App extends React.Component{

  1. render(){
  2. return (
  3. <div>
  4. <CompA />
  5. </div>
  6. );

} }

const app = ( );
ReactDOM.render(app, document.getElementById(‘id’); console.log(app);

  1. ![更新前.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)
  2. <a name="eJmoY"></a>
  3. ## 节点的更新
  4. 1. 如果调用的是ReactDOM.render,则进入根节点的**对比(diff)更新**
  5. 1. 如果调用的是类组件的setState
  6. 1. 运行生命周期函数,static getDedrivedStateFromProps
  7. 1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续
  8. 1. 运行render,得到一个新的节点,进入该新节点的**对比更新**(这件事肯定要递归)
  9. 1. 将自己的生命周期函数getSnapshotBeforeUpdate加入执行队列,以待将来执行
  10. 1. 将自己的生命周期函数componentDidUpdate加入执行队列,以待将来执行
  11. 1. (**不同名字的生命周期函数处于不同的队列**)
  12. 3. 后续步骤,无论上述哪种情况都要做以下流程:
  13. 1. **更新虚拟DOM树**
  14. 1. **完成真实的DOM更新。所以render的时候根本还没完成真实更新,那会只是记录,等待统一处理**
  15. 1. 依次调用执行队列中的componentDidMount(**有可能噢,因为可能有新的子组件要产生啊**)
  16. 1. 依次调用执行队列中的getSnapshotBeforeUpdate
  17. 1. 依次调用执行队列中的componentDidUpdate
  18. 1. 依次调用执行队列中的componentWillUnmount(**它自己肯定是不会卸载的,因为在它自己里面setState了。但子组件可能会啊**)
  19. <a name="h7mVH"></a>
  20. ## 对比更新【非常重要】
  21. **将新产生的节点,对比之前虚拟DOM中的节点,发现差异,完成更新**
  22. **问题是:对比之前DOM树中的哪个节点--它不知道自己之前在哪个位置??**
  23. 1. 假如是做一个唯一标识来匹配筛选,则需要遍历整个DOM树,当层次深的话,性能会很差
  24. **React为了提高对比效率,做出以下假设(并且事实证明绝大多数情况就是如此):**
  25. - **假设节点不会出现层次的移动(对比时,可以直接找到旧树中对应层的对应序号的节点进行对比)**
  26. - **不同的节点类型会生成不同的结构(后文再详解)**
  27. - **相同的节点类型:节点本身类型相同,如果是组件节点,组件类型(组件AB)也必须相同**
  28. - **如果是由React元素生成,type值还必须一致**
  29. - **其他的,都属于不相同的节点类型**
  30. - **比如两个元素都是<div></div>,即都是DOM节点,且都是div类型,这就是相同节点**
  31. - **多个兄弟通过唯一标识 key 来确定对比的新节点**
  32. - **key值的作用:用于通过旧节点寻找对应的新节点。如果某个旧节点有key值,则其更新时,会寻找相同层级中的相同key值的节点进行对比。没找到就会进入 ,没找到对比目标的流程中。**
  33. - **key值不能重复指的是同一个父亲同一层里面不能重复(就是兄弟节点中),key还要保持稳定**
  34. - **它就是为了尽量复用真实DOM**
  35. <a name="jiqiG"></a>
  36. ### 找到了对比目标
  37. <a name="XAFJ5"></a>
  38. #### 判断节点类型是否一致
  39. **(一)一致:**<br />**根据不同的节点类型,做不同的事情:**
  40. 1. 空节点:不做任何事情
  41. 1. **DOM节点:**
  42. 1. **直接重用之前的真实DOM对象**(因为之前就挂到了节点的属性上,可以从节点拿到)
  43. 1. **将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)**
  44. 1. 遍历该新的React元素的子元素,**然后递归对比更新**
  45. 3. 文本节点:
  46. 1. 直接重用之前的真实DOM的文本节点
  47. 1. 记录变化的nodeValue,同上
  48. 4. **组件节点:**
  49. 1. **函数组件节点**
  50. 1. 直接重新调用函数,得到一个节点对象,进入递归对比更新即可
  51. 2. **类组件节点**
  52. 1. 重用之前的实例,不用重新new,所以不用再次constructor
  53. 1. 运行生命周期函数,static getDedrivedStateFromProps
  54. 1. 运行shouldComponentUpdate,如果该函数**返回fasle则终止流程**,true则继续
  55. 1. 运行render,得到新的节点对象,**进入递归对比更新,**所以这儿容易考打印顺序
  56. 1. 将该对象的getSnapshotBeforeUpdate加入队列
  57. 1. 将该对象的componentsDidUpdate加入队列
  58. 5. 数组节点:
  59. 1. 遍历数组进行递归对比更新
  60. ![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 />**整体上,先创建全新的节点,卸载旧的节点,再挂载新节点到页面上**
  61. **先创建新节点:**<br />**进入新节点的挂载流程,**即 进入首次渲染新节点流程,跟前文中的完全一样。
  62. **再卸载旧节点:**
  63. 1. 文本节点、DOM节点、数组节点、空节点、函数组件节点:**直接放弃该整个节点**(就是说如果该节点有子节点,则须**递归卸载子节点**)
  64. 1. **类组件节点**
  65. 1. 直接放弃该节点
  66. 1. 调用该节点的componentWillUnmount生命周期函数
  67. 1. **递归卸载子节点**
  68. ```jsx
  69. import React, { Component } from 'react'
  70. class CompA extends Component{
  71. componentDidMount() {
  72. console.log("CompA 新组件挂载");
  73. }
  74. componentWillUnmount() {
  75. console.log("CompA 组件卸载");
  76. }
  77. render(){
  78. console.log("CompA render");
  79. return <CompAA />
  80. }
  81. }
  82. class CompB extends Component{
  83. componentDidMount() {
  84. console.log("CompB 新组件挂载");
  85. }
  86. componentWillUnmount() {
  87. console.log("CompB 组件卸载");
  88. }
  89. render(){
  90. console.log("CompB render");
  91. return <CompBB />
  92. }
  93. }
  94. class CompBB extends Component{
  95. componentDidMount() {
  96. console.log("CompBB 新组件挂载");
  97. }
  98. componentWillUnmount() {
  99. console.log("CompBB 组件卸载");
  100. }
  101. render(){
  102. console.log("CompBB render");
  103. return(
  104. <span>CompBB</span>
  105. )
  106. }
  107. }
  108. class CompAA extends Component{
  109. componentDidMount() {
  110. console.log("CompAA 新组件挂载");
  111. }
  112. componentWillUnmount() {
  113. console.log("CompAA 组件卸载");
  114. }
  115. render(){
  116. console.log("CompAA render");
  117. return(
  118. <span>CompAA</span>
  119. )
  120. }
  121. }
  122. export default class Update_Render_Unmount extends Component {
  123. state = {
  124. n: 0,
  125. }
  126. render() {
  127. if(this.state.n === 0){
  128. return (
  129. <div>
  130. <CompB />
  131. <button onClick={()=>{
  132. this.setState({
  133. n: 1
  134. })
  135. }}>
  136. 点击更新dom
  137. </button>
  138. </div>
  139. );
  140. }
  141. return (
  142. <div>
  143. <CompA />
  144. </div>
  145. );
  146. }
  147. }

点击前:
CompB render
CompBB render
CompBB 新组件挂载
CompB 新组件挂载
点击后:
CompA render
CompAA render
CompB 组件卸载
CompBB 组件卸载
CompAA 新组件挂载
CompA 新组件挂载
注意,对于显示隐藏的推荐做法:

  1. 如果只是做显示隐藏,尽量使用display:none,因为它并没有改变DOM的结构,不会影响后面的元素
  2. const h1 = this.state.visible ?

    标题

    : null;
    1. 因为null也是一个节点,只是没有被显示出来而已,它占位了

      没找到对比目标

  1. 新的DOM树中有节点被删除
  2. 新的DOM树有节点被添加

无论哪种情况,都执行下述步骤即可:

  1. 创建新加入的节点
  2. 卸载多余的旧节点