1. Parcel 安装与使用 Parcel 是 Web 应用打包工具,适用于经验不同的开发者。它利用多核处理提供了极快的速度,并且不需要任何配置。
文档:https://www.parceljs.cn/getting_started.html 
 
本地安装:
1 yarn add parcel-bundler --dev 
 
安装 babel ,将 jsx 语法转化成 js 对象(虚拟DDM):
1 yarn add @babel/core @babel/plugin-transform-react-jsx @babel/preset-env --dev 
 
配置 .babelrc :
1 2 3 4 5 6 7 8 9 10 11 12 13 {   "presets" :  [      "evn"    ] ,    "plugins" :  [      [        "transform-react-jsx" ,        {          "prama" :  "React.createElement"        }      ]    ]  } 
 
2. JSX 的渲染 2.1 Babel 转义 先举个例子,当我们编写一个正常的 jsx 文件时,其结构是这样的:
1 2 3 4 5 6 7 8 9 10 import  React  from  "./lib/react" ;import  ReactDOM  from  "./lib/react-dom" ;const  ele = (  <div  className ="active"  title ="123" >      hello,<span  style ={{  color:  "red " }}> React!</span >    </div >  ); ReactDOM .render (ele, document .querySelector ("#app" ));
 
其中, babel 会对 jsx 部分进行转义,调用 react 的 createElement 方法去创建虚拟 DOM 树:
1 2 3 4 5 6 7 8 9 function  Test ( ){  const  flag = true    const  name = "ZhangSan"       return  <div  className ="active"  title ="123" >      hello<span  style ={{  color:  "red " }}> React!</span >      my name is {flag === true ? name : "LiSi"}   </div >  } 
 
Babel 转义后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 "use strict" ;function  Test ( ) {  const  flag = true ;   const  name = "ZhangSan" ;   return  React .createElement ("div" , {     className : "active" ,     title : "123"    }, "hello" , React .createElement ("span" , {     style : {       color : "red"      }   }, "React!" ), "my name is " , flag === true  ? name : "LiSi" ); } 
 
在 Babel 进行对 jsx 语法的转义过程中,也会对模板语法直接进行转义,调用其中使用的变量
 
同样的,我们可以不编写 JSX,直接调用 React.createElement() 方法来生成虚拟 DOM 树,然后再调用 ReactDOM.render() 来渲染虚拟 DOM 树:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  React  from  "./lib/react" ;import  ReactDOM  from  "./lib/react-dom" ;const  ele = React .createElement (  "div" ,   {     className : "active" ,     title : "123" ,   },   "hello," ,   React .createElement (     "span" ,     {       style : {         color : "red" ,       },     },     "React!"    ) ); ReactDOM .render (ele, document .querySelector ("#app" ));
 
在 Babel 转义后由于会转义为 React.createElement 因此必须把 React 引入到当前代码中,这也就是为什么我们即使在代码中并没有用到 React 对象,却仍要引用它的原因。
 
2.2 React.createElement React.createElement 方法会生成一个对象,这个对象包含了将来生成节点的类型、属性、内容(包含子节点),其是一个嵌套的结构,这就形成了一个树形结构,我们便将其称之为 虚拟DOM树 。
我们先来看下其 API 设计:
1 React .createElement (tagName, attribute, ...children)
 
tag 表示虚拟节点的类型,attribute 表示虚拟节点的属性,children 表示虚拟节点的子节点。这里要注意的是,如果子节点是文本节点,那么会直接传入一个字符串,如:
 
会被 Babel 转化为:
1 React .createElement ("h1" , null , "标题" )
 
实现这个方法其实也很简单,我们只需要返回一个对象就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 const  React  = {  createElement, }; function  createElement (tag, attrs, ...children ) {  return  {     tag,     attrs,     children,   }; } export  default  React ;
 
借用最初的例子:
1 2 3 4 5 const  dom = (  <div  className ="active"  title ="123" >      hello,<span  style ={{  color:  "red " }}> React!</span >    </div >  ) 
 
经 Babel 转义并使用调用 React.createElement() 后,打印出 dom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 {     "tag" : "div" ,     "attrs" : {         "className" : "active" ,         "title" : "123"      },     "children" : [         "hello," ,         {             "tag" : "span" ,             "attrs" : {                 "style" : {                     "color" : "red"                  }             },             "children" : [                 "React!"              ]         }     ] } 
 
2.3 ReactDOM.render React 生成了虚拟 DOM 树,那么 ReactDOM 就需要将虚拟 DOM 树渲染为 html 节点,其核心就是调用 ReactDOM.render 函数。
我们先来看一下 ReactDOM.render 函数的 API:
1 ReactDOM .render (vnode, container);
 
其中,vnode 就是虚拟 DOM 树,container 就是由虚拟 DOM 树生成真实 html 节点后,节点挂载的目标父节点,其是一个 HTMLElement。
其实现也并不复杂,只需分如下几步:
判断 vnode 类型,如果是字符串,就创建文本节点,并将文本节点挂载到目标父节点中; 
如果不是字符串,那就根据 tag 名称,调用 document.createElement 生成真实节点; 
为真实节点添加属性; 
使用递归,遍历子节点,将当前生成的真实节点作为子节点的目标父节点,调用 render 函数渲染子节点; 
调用 appendChild 方法将生成的节点挂载到目标父节点中。 
 
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 const  ReactDOM  = { render };function  render (vnode, container ) {  if  (vnode === undefined ) {     return ;   }      if  (typeof  vnode === "string" ) {          const  textNode = document .createTextNode (vnode);     return  container.appendChild (textNode);   }      const  { tag, attrs, children } = vnode;   const  dom = document .createElement (tag);   if  (attrs) {     for  (let  key in  attrs) {       const  value = attrs[key];       setAttribute (dom, key, value);     }   }      if  (children && children instanceof  Array ) {     children.forEach ((child ) =>  render (child, dom));   }   return  container.appendChild (dom); } function  setAttribute (dom, key, value ) {     if  (key === "className" ) {     key = "class" ;   }      if  (/on\w+/ .test (key)) {     key = key.toLowerCase ();     dom[key] = value || "" ;   }      else  if  (key === "style" ) {          if  (!value || typeof  value === "string" ) {       dom.style .cssText  = value || "" ;     }          else  if  (value && typeof  value === "object" ) {       for  (let  key in  value) {         if  (typeof  value === "number" ) {           dom.style [key] = value[key] + "px" ;         } else  {           dom.style [key] = value[key];         }       }     }   }      else  {     if  (key in  dom) {       dom[key] = value || "" ;     }     if  (value) {       dom.setAttribute (key, value);     } else  {       dom.removeAttribute (key);     }   } } export  default  ReactDOM ;
 
3. 组件的实现 在上一节中,我们实现了 render 函数,render 函数的第一个参数可以传入一个虚拟节点。但是,在实际的 React 中,第一个参数还可以传入一个函数组件,因此我们以此为切入点,探讨一下 React 中组件的渲染原理。
3.1 让 render 函数支持传入组件 我们先来看一下经过 babel 转义的组件 jsx 长什么样子:
1 2 3 4 5 6 7 8 9 10 11 function  Home ( ) {  return  (     <div  className ="active"  title ="123" >        hello, <span > react</span >      </div >    ); } const  title = "active" ;console .log (<Home  name ={title}  />  );
 
输出结果:
我们可以发现,函数组件被处理为虚拟节点对象后,tag 中包含了改组件的渲染函数,因此我们可以通过 render 函数来判断 tag 属性来判断渲染对象到底是 HTMLElement 还是 React 组件,同时我们将 render 函数进行一下简单的拆分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function  render (vnode, container ) {  return  container.appendChild (_render (vnode)); } function  _render (vnode ) {  if  (vnode === undefined  || vnode === null  || typeof  vnode === "boolean" ) {     return ;   }      if  (typeof  vnode.tag  === "function" ) {          const  comp = createComponent (vnode.tag , vnode.attrs );          setComponentProps (com, vnode.attrs );          return  comp.base ;   }      if  (typeof  vnode === "string" ) {        }         return  dom; } 
 
3.2 createComponent 在 _render() 函数中,如果传入的是一个函数或 class 组件,首先要实现一个 createComponent 方法,来将组件进行 实例化 ,最终的实例化对象上会有一个 render() 方法来生成具体的虚拟 DOM 对象。
以下是 createComponent 方法的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 function  createComponent (comp, props ) {  let  inst;   if  (comp.prototype   && comp.prototype  .render ) {     inst = new  comp (props);    } else  {     inst = new  Component (props);      inst.constructor  = comp;      inst.render  = function  ( ) {       return  this .constructor (props );     };    }   return  inst; } 
 
如果我们传入的是一个 class 组件,那么直接将其进行实例化,注意此时组件就会执行构造函数的 constructor 部分,如进行 state 的初始化 ,最终实例化后的对象上会挂载一个 render 方法(1);
但如果我们传入的是一个函数组件,我们要将其构造为一个 class 组件,在构造为一个 class 组件之前,我们需要首先声明 Component 类:
1 2 3 4 5 6 7 8 9 class  Component  {  constructor (props = {} ) {     this .props  = props;     this .state  = {};   } } export  default  Component ;
 
首先我们实例化一个 Component 对象,作为我们即将改造的“初始对象”,此时要注意将组件属性 props 传入,这样在组件对象上才能取到传入的 props(2);之后我们将函数组件的函数体挂载到生成的 Component 对象的 constructor 上,我们这一步是改写了生成的 Component 对象的构造方法(3),目前来看意义不大;之后,我们将生成的 Component 对象的 render() 方法改写为函数组件的函数体(4),这样就将一个函数组件改写为了 class 组件。
3.3 setComponentProps setComponentProps 方法负责对组件的 props 进行更新,并触发组件的渲染:
1 2 3 4 export  function  setComponentProps (comp, props ) {  comp.props  = props;   renderComponent (comp); } 
 
其实,在目前组件执行初次渲染时 comp.props = props 的执行是没有意义的,因为在执行 createComponent 组件实例化时,就已经完成了对 props 的挂载。我们在这里再重新挂载一次是因为该方法不仅在组件初始化时调用,也会在组件更新时调用。当组件更新时,直接调用该方法就可以直接完成 props 的更新以及组件的重新渲染。
 
3.4 renderComponent 在调用 renderComponent 之前,我们已经完成了对函数组件、class 组件的实例化,并且将外部传入的组件属性挂载到了实例化对象的 props 属性上,同时实例化好的组件对象上有用 render() 方法,执行后可以返回一个虚拟节点对象。
因此,在 renderComponent 方法中,我们主要是调用组件的 render() 函数(1),然后再将生成的虚拟节点对象传入到 _render() 函数中,渲染为真实的 DOM 对象,并将 DOM 对象挂载到组件实例的 base 属性上(2):
1 2 3 4 function  renderComponent (comp ) {  const  renderer = comp.render ();    comp.base  = _render (renderer);  } 
 
4. 生命周期 在上一节中,我们已经完善了组件的渲染过程,那么在本章节中,我们将还原 React 组件生命周期函数 的调用过程。我们将着重探讨 componentWillMount componentDidMount componentWillUpdate componentDidUpdate 这四个生命周期函数。
4.1 componentWillMount 与 componentDidMount 
UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。
 
根据以上的特性我们很容易判断出 componentWillMount 的执行位置,让其在 renderComponent() 之前执行并且仅执行一次即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function  _render (vnode ) {     if  (typeof  vnode.tag  === "function" ) {     const  comp = createComponent (vnode.tag , vnode.attrs );     if  (!comp.base ) {       comp?.componentWillMount ();     }      renderComponent (comp);     return  comp.base ;   }    } 
 
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
 
componentDidMount 要在组件完成挂载时执行,且只执行一次,那么在 renderComponent 过程中,我们可以将其放置在组件渲染完成并且是初次挂载时执行。
!!! 此处教程有误,componentDidMount 应该在组件 DOM 挂载到页面上后再执行,按照下面的写法显然实在 DOM 被挂载之前执行  !!!
 
1 2 3 4 5 6 7 8 9 10 11 export  function  renderComponent (comp ) {     const  renderer = comp.render ();   let  base = _render (renderer);   if  (!comp.base ) {     comp?.componentDidMount ();   }   comp.base  = base; } 
 
4.2 componentWillUpdate 与 componentDidUpdate 要执行这两个方法,我们首先要实现组件内部 state 的更新以及重新渲染,我们先编写一个如下的 demo 组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class  Home  extends  React.Component  {  constructor (props ) {     super (props);     this .state  = {       num : 0 ,     };   }      handleClick ( ) {     this .setState ({       num : this .state .num  + 1 ,     });   }   render ( ) {     return  (       <div  id ="home" >          <h1 > {this.props.title}</h1 >          <span > hello, react!</span >          <button  onClick ={this.handleClick.bind(this)} >            Click me! ({this.state.num})         </button >        </div >      );   } } 
 
当用户点击按钮后,state 中存放的 num 变量就会被加一,然后页面会触发重新渲染,来展示整个页面。
要实现 state 的变更以及重新渲染,我们首先要扩展一下 Component 组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 + import { renderComponent } from "../react-dom/index";   class Component {     constructor(props = {}) {       this.props = props;       this.state = {};     } +   setState(stateChange) { +     Object.assign(this.state, stateChange); // (1) +     renderComponent(this); // (2) +   }   } export default Component; 
 
我们为 Component 添加 setState 方法,此时我们为了方便编写 demo,只是简单的将 state 进行了浅拷贝并覆盖值(1),实际的 setState 操作是异步的。进行了赋值操作之后,我们重新调用 renderComponent 方法对组件进行重新渲染,并将当前组件作为渲染对象传入(2)。
此时我们点击按钮后,会发现页面上的 DOM 结构并不会改变,但是在 renderComponent 方法中打印出当前的组件对象,其 base 上挂载的 HTMLElement 的确是已经发生了更新,这就说明我们并没有将更新后的 HTMLElement 挂载到页面上。
更新节点的方法其实也很简单,我们只需要获取到当前组件的父节点,然后使用 replaceChild() 方法,替换父节点的内容为最新的组件节点就可以了(1)。
1 2 3 4 5 6 7 8 9 10 11 12   export function renderComponent(comp) {     // 对组件进行渲染,获取虚拟节点对象     const renderer = comp.render();     let base = _render(renderer); +   // 节点替换 +   if (comp?.base?.parentNode) { +     comp.base.parentNode.replaceChild(base, comp.base); // (1) +   }     comp.base = base;   } 
 
这时,我们根据 componentWillUpdate 与 componentDidUpdate 的定义,就很容易得知其执行的位置:
当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。
 
componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19   export function renderComponent(comp) {     const renderer = comp.render();     let base = _render(renderer); +   if (comp.base) { +     comp?.componentWillUpdate(comp.props, comp.state); +   } else { +     comp.base = base; // 组件必须在挂载后再触发 componentDidMount +     comp?.componentDidMount(); +   }     if (comp?.base?.parentNode) {       comp.base.parentNode.replaceChild(base, comp.base); +     comp.base = base; +     comp?.componentDidUpdate();     } -   comp.base = base;   } 
 
5. Diff 算法的实现 截至目前,我们已经刨析了 jsx 的渲染以及组件 state 的更新,那么接下来我们会进一步对渲染流程进行优化。
在前面的写法中,每当 state 改变触发组件重新渲染时,都会从头开始进行渲染,这样对性能的损耗是很大的。为了优化 DOM 结构的更新性能,react 引入了 diff 算法,这个算法会对比每个同级节点的变更 ,如果当前节点与之前相较发生了变更,就会更新当前节点与其子节点,这比重新渲染整个 DOM 结构要高效的多。
总而言之,我们的diff算法有两个原则:
对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM 
只对比同一层级的变化 
 
5.1 起步 先修改render函数,将_render方法渲染的方式改为我们即将写的diff算法方式
/react-dom/index.js
1 2 3 4 5 6 7 8 9 import  diff from  './diff' ;const  ReactDOM  = {    render } function  render (vnode, container ) {         return  diff (dom,vnode,container); } 
 
由上面的diff()可以看出,传入了 真实DOM对象,虚拟DOM对象,根元素
5.2 实现 实现一个diff算法,它的作用是对比真实的DOM和虚拟DOM,最后返回更新后的DOM
/react-dom/diff.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export  function  diff (dom,vnode,container ) {         let  ret =  diffNode (dom,vnode);     return  ret; } function  diffNode (dom,vnode ){	 } 
 
接下来实现这个方法
在这之前先来回忆一下我们虚拟DOM的结构:
虚拟DOM的结构可以分为三种,分别表示文本,原生DOM节点以及组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 {     tag : 'div' ,     attrs : {         className : 'container'      },     children : [] } "hello,world" {     tag : ComponentConstrucotr ,     attrs : {         className : 'container'      },     children : [] } 
 
5.3 对比文本节点 首先考虑最简单的文本节点,如果当前的DOM就是文本节点,则直接更新内容,否则就新建一个文本节点,并移除原来的DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function  diffNode (dom,vnode ) {    let  out = dom;     if (typeof  vnode === 'string' ){              if (dom && dom.nodeType  === 3 ){           if (dom.textContent  !== vnode) dom.textContent  = vnode;       } else {                  out = document .createTextNode (vnode);         if (dom && dom.parentNode ){             dom.parentNode .replaceChild (out,dom);         }       }         return  out;     }     return  out; } 
 
文本节点十分简单,它没有属性,也没有子元素
5.4 对比组件 之前也说过,react组件分为函数组件和类组件,我们定制一个方法diffComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import  setComponentProps from  "./index.js" function  diffNode (dom,vnode ){              if  (typeof  vnode.tag  === 'function' ) {         return  diffComponent (dom, vnode);     }      } function  diffComponent (dom, vnode ) {    let  comp = dom;          if  (comp && comp.constructor  === vnode.tag ) {                  setComponentProp (comp, vnode.attrs );                  dom = comp.base ;     } else  {                           if  (comp) {             unmountComponent (comp);             comp = null ;         }                           comp = createComponent (vnode.tag , vnode.attrs );                  setComponentProp (comp, vnode.attrs );         dom = comp.base ;     }     return  dom; } 
 
5.5 对比非文本DOM节点 如果vnode表示的是一个非文本DOM节点,分两种情况分析:
情况一: 如果真实DOM不存在,表示此节点是新增的
1 2 3 if (!dom){    updateDOM = document .createElement (vnode.tag ); } 
 
情况二:如果真实DOM存在,需要对比属性和对比子节点
5.5.1 对比属性 找出来节点的属性以及事件监听的变化 单独起一个diffAttributes方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import  { setAttribute } from  './index' ;function  diffNode (dom,vnode ){    let  out = dom; 	                 if (!dom){       out = document .createElement (vnode.tag ); 	  }          diffAttributes (out,vnode);          return  out; } function  diffAttributes (dom, vnode ) {         const  oldAttrs = {};     const  newAttrs = vnode.attrs ;           const  domAttrs = dom.attributes ;          [...domAttrs].forEach (item  =>  {         oldAttrs[item.name ] = item.value ;     })               for  (let  key in  oldAttrs) {         if  (!(key in  newAttrs)) {                          setAttribute (dom, key, undefined );         }     }          for  (let  key in  newAttrs) {         if  (oldAttrs[key] !== newAttrs[key]) {             console .log (dom, newAttrs[key], key);                          setAttribute (dom, key, newAttrs[key]);         }     } } 
 
5.5.2 对比子节点 节点对比完成之后,接下来对比它的子节点
这个时候会有一个问题,前面我们实现的不同的diff算法,都是明确知道哪一个是真实DOM和虚拟DOM对比,但是子节点childrens是一个数组,他们可能改变顺序,或者数量有所变化,我们很难确定是和虚拟DOM对比的是哪一个?
思路:给节点设置一个key值,重新渲染时对比key值相同的节点,这样我们就能找到真实DOM和哪个虚拟DOM进行对比了
 
对比子节点的方法有点复杂,在这里理解一下原理
1 2 3 4 5 function  diffNode (dom,vnode ){     if  (vnode.childrens  && vnode.childrens .length  > 0  || (out.childNodes  && out.childNodes .length  > 0 )) {         diffChildren (out, vnode.childrens );     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 function  diffChildren (dom, vchildren ) {  const  domChildren = dom.childNodes ;   const  children = [];   const  keyed = {};      if  (domChildren.length  > 0 ) {     [...domChildren].forEach ((item ) =>  {              const  key = item.key ;       if  (key) {                  keyed[key] = item;       } else  {                  children.push (item);``        }     });   }   if  (vchildren && vchildren.length  > 0 ) {     let  min = 0 ;     let  childrenLen = children.length ;      [...vchildren].forEach ((vchild, i ) =>  {              const  key = vchild.key ;       let  child;       if  (key) {                  if  (keyed[key]) {           child = keyed[key];           keyed[key] = undefined ;         }       } else  if  (childrenLen > min) {                           for  (let  j = min; j < childrenLen; j++) {           let  c = children[j];           if  (c) {             child = c;             children[j] = undefined ;             if  (j === childrenLen - 1 ) {               childrenLen--;             }             if  (j === min) {               min++;             }             break ;           }         }       }              child = diffNode (child, vchild);              const  f = domChildren[i];       if  (child && child !== dom && child !== f) {                  if  (!f) {           dom.appendChild (child);                    } else  if  (child === f.nextSibling ) {           dom.removeChild (f);                    } else  {                      dom.insertBefore (child, f);         }       }     });   } } 
 
整个流程如下:
最后再修改renderComponent方法的两个地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25   export function renderComponent(comp) { +   let base     // 对组件进行渲染,获取虚拟节点对象     const renderer = comp.render(); -   let base = _render(renderer);     if (comp.base) {       // 组件更新时引发重新渲染       comp?.componentWillUpdate(comp.props, comp.state); +     base = diffNode(comp.base,renderer);       comp.base = base; +     comp?.componentDidUpdate();     } else {       // 初次渲染 +     base = _render(renderer);       comp.base = base;       comp?.componentDidMount();     } -   // 组件 state 发生变化后,重新渲染节点,需要进行节点替换 -   if (comp?.base?.parentNode) { -     comp.base.parentNode.replaceChild(base, comp.base); -     comp?.componentDidUpdate(); -   }   } 
 
6. 异步 setState 在 React 中,为了优化性能 setState 的操作是异步的,当我们在一个 for 循环中直行 setState,会出现以下情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 constructor (props ) {  super (props);   this .state  = {     num : 0 ,   }; } componentDidMount ( ) {  for  (let  i = 0 ; i < 5 ; i++) {     console .log (this .state .num );     this .setState ({       num : this .state .num  + 1 ,     });   } } 
 
输出结果始终为初始化的 state 值:
 
同时 setState 也支持传入一个函数,在该函数中,可以获取到上一次更新 state 后的状态(prevState):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 constructor (props ) {  super (props);   this .state  = {     num : 0 ,   }; } componentDidMount ( ) {  for  (let  i = 0 ; i < 5 ; i++) {     this .setState ((prevState )=> {       console .log (prevState.num );              return  {         num : prevState.num  + 1        }     })     } } 
 
输出结果可以获取到每次 state 改变前的值:
 
之所以会这样,是因为 setState 始终是一个异步的操作,此时在循环中取到的 state 都是在循环执行时获取到的组件 state。但是 setState 中如果传入一个函数,那么在函数中可以获取到上次组件更新的 prevState,也就是在传入函数执行时,组件最新的 state,这的确很神奇。
那么知道的上述的具体表现后,我们再来探讨一下为什么 setState 要是一个异步操作,其是怎么优化的,其主要分如下几步:
当 react 进行 setState 操作时,会重新渲染组件,渲染组件会消耗大量的性能,为了减少性能损耗,react 会将 当前同步任务队列  中的所有 setState 操作都暂存在一个 执行队列  中,我们将其定为 setStateQueue,但并不立即执行。同时创建一个 渲染队列 ,将要改变 state 的组件全部存放在渲染队列中,同时 合并渲染队列中重复的组件 ; 
等待一段时间过后,react 会将直行队列 setStateQueue 中的 setState 操作,但此时只会改组件的 state ,组件并没有直行实质性的渲染; 
等 setStateQueue 队列执行完毕之后,开始对渲染队列 renderQueue 的组件直行渲染操作,这样就可以实现改变多个 state 但只渲染一次,从而优化性能。 
 
6.1 创建 setStateQueue 与 renderQueue setStateQueue 负责存储该轮更新时执行的 setState 操作,renderQueue 负责存储在该轮更新后应该渲染的组件。我们要创建这两个队列,并创建加入队列的方法 enqueueSetState(stateChange, component),并在组件更新 state 时调用该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const  setStateQueue = [];const  renderQueue = [];export  function  enqueueSetState (stateChange, component ) {     setStateQueue.push ({     stateChange,     component,   });         let  r = renderQueue.some ((item ) =>  {     return  item === component;   });   if  (!r) {     renderQueue.push (component);   } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  { enqueueSetState } from  "./set_state_queue" ;class  Component  {  constructor (props = {} ) {     this .props  = props;     this .state  = {};   }   setState (stateChange ) {                         enqueueSetState (stateChange, this );   } } export  default  Component ;
 
6.2 创建清空队列的方法 假设我们已经等待了 一定的时间 ,到达了某一时刻,那么我们就要清空 setStateQueue 与 renderQueue 了。因此我们要创建一个清空队列的方法,将其命名为 flush:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function  flush ( ) {  let  item;      while  ((item = setStateQueue.shift ())) {     const  { stateChange, component } = item;          if  (!component.prevState ) {       component.prevState  = Object .assign ({}, component.state );     }          if  (typeof  stateChange === "function" ) {       Object .assign (         component.state ,         stateChange (component.prevState , component.props )       );     } else  {              Object .assign (component.state , stateChange);     }     component.prevState  = component.state ;   }      let  component;   while  ((component = renderQueue.shift ())) {     renderComponent (component);   } } 
 
该清空队列的方法实际上就是实现了我们前面原理分析的第 2 与第 3 步操作:
6.3 寻找合适的更新时机 前面我们一直在强调 setState 是一个异步操作,其原因就在这儿,我们虽然将所有的 setState 行为已经组件都存放在队列里了,但最重要是寻找到一个时间点去清空队列。这个时间点既不能太快(要后置于当前 JS 的同步任务),又不能太慢(保证让用户无感知)。
最简单的我们就使用一个定时器来实现:
1 2 3 4 5 6 7 8 9 10 export  function  enqueueSetState (stateChange, component ) {     if  (setStateQueue.length  === 0 ) {     setTimeout (() =>  {       flush ();     }, 0 );   }    } 
 
当更新队列为空时,我们开启定时器,由于定时器是一个异步任务,后面更新 setStateQueue 与 renderQueue 的操作会继续执行。同时需要添加一个判断队列是否为空时候才开启定时器,保证开启定时器的操作只在“一轮更新”的最开始直行,只要是在该轮直行的 enqueueSetState 操作都不会在开启定时器,保证了定时器的唯一性。
当定时器到达时间阈值时,标志着该轮更新“到点了”,开始清空队列并执行渲染操作,后续再有更新 state 操作的话就会被延迟到“下一轮”更新中。
当然,使用定时器的方法并不是最优雅的,某些浏览器 setTimeout 的最小触发时间为 4ms,如果我们只想同步任务直行完成之后就直行组件更新,连 4ms 都不想等,或者说总是想要优先于主任务队列中的所有 setTimeout 行为的话,该怎么办呢?
这时候就要利用到任务轮询中的 微任务  了,这也是微任务的应用点之一。我们都知道微任务属于异步任务,会延后于同步任务队列,但是又会优先于宏任务队列,而 Promise.resolve().then() 是一个最典型的微任务,那么我们只需要将 setTimeout 改成微任务就行了:
1 2 3 4 5 6 7 8 export  function  enqueueSetState (stateChange, component ) {     if  (setStateQueue.length  === 0 ) {     return  Promise .resolve ().then (flush);   }    } 
 
这样的话,我们就不会跟其他的 setTimeout 行为发生冲突了,也可以利用这点来让某些行为在组件更新后发生,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class  Home  extends  React.Component  {  constructor (props ) {     super (props);     this .state  = {       num : 0 ,     };   }      handleClick ( ) {     this .setState ({       num : this .state .num  + 1 ,     });     setTimeout (() =>  {                     const  btn = document .querySelector ("#btn" );       console .log ("btn: " , btn.innerHTML );     }, 0 );   }   render ( ) {     return  (       <button  id ="btn"  onClick ={this.handleClick.bind(this)} >          Click me! ({this.state.num})       </button >      );   } } 
 
7. 参考 本文主要参考如下视频与文章,以及 React 官网以及具体源码,结合一些个人理解而编写:
配套源码:https://github.com/EsunR/Study-Book/tree/master/React/React%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90