三、「深入React源码」--- 手写实现类组件的状态更新

290 阅读9分钟

类组件的状态更新依赖setState方法。setState方法接收两种形式的参数:对象--同步更新;函数--异步更新。本文先做同步更新的处理,最后处理异步更新。

一、准备知识

1. 组件状态

  • 组件的数据来源:属性对象、状态对象
  • 属性是父组件传递过来的
  • 状态是自己内部的,改变状态唯一的方式时setState
  • 属性和状态的变化都会影响视图更新
  • 不要直接修改state,构造函数是唯一可以给this.state赋值的地方

2. 绑定事件

React中绑定事件和原生不一样:

  • 属性为驼峰命名而不是小写
  • 值是函数的引用地址而不是字符串

二、实现思路

1. 实现setState()

setState()方法的调用,实际上是实例去父类Component上拿到setState方法,因此:

component.js中,给Component类加上setState方法,入参为partialState(部分状态),来记录你需要更改的状态。内部调用更新器addState方法,委托给更新器去进行状态的更新。

2. 实现更新器-状态更新

Updater本质是一个类,继承Component组件实例。内部存放了组件实例、将要更新的状态队列pendingStates(因为setState不是立即修改,所以需要集合来保存所有需要更新的状态)。

  • addState:入参状态对象、把状态存进等待更新的数组,调用触发更新方法。

创建shoudlUpdate方法用于更新状态。状态的更新依赖于组件实例的state和最新的状态state,为方便复用我们抽出getState方法专门用于计算将要更新的新状态。状态的更新引起组件的更新,我们调用实例的forceUpdate强制更新方法。

  • emitUpdate:触发更新。调用更新组件方法。
  • shoudlUpdate:更新状态。解构出类的实例classInstance和等待生效的状态数组pendingStates,如果状态数组有值,调用shouldUpdate更新状态的方法,传入类的实例和最新状态。
  • getState:基于老状态的值获取新状态。解构出类的实例classInstance和等待更新的状态数组pendingStates,遍历pendingStates获取每一个要更新的值,从实例上解构出老状态state,进行状态合并,合并后清空pendingStates,返回合并后的state
  • shouldUpdate:入参类的实例、新状态。把新状态赋值给类的老状态state。调用Component实例的强制更新方法forceUpdate。

3. 实现forceUpdate--组件更新

怎么触发页面更新?每个页面对应的是一个div/span等(只能返回唯一的React元素)。因此:需要基于新的状态重新调用render方法,返回新的虚拟DOM ===> 把新的虚拟DOM转换为新的真实DOM ===> 把新的真实DOM替换掉老的页面结构

  • forceUpdate: 调用render方法获取基于新的状态要渲染新的虚拟DOM,把虚拟DOM转为真实DOM的方法createDOM,定义在react-dom.js,因此这一步骤在react-dom.js中实现。引入compareToVdom,传入需要的三个参数。
  1. 父真实DOM:oldDOM.parentNode
  2. 老的虚拟DOM:在第一次挂载mountClassCompoent的时候,要在类的实例上添加oldRenderVdom属性,记录老的虚拟DOM。之后就可以通过oldRenderVdom属性获取组件对应的老的虚拟DOM
  3. 新的虚拟DOM:调用组件的render方法获取 重新渲染之后,要把这次新的虚拟DOM赋值给oldRenderVdom属性作为下一次需要更新时老的虚拟DOM的值。
  • compareToVdom:新老虚拟DOM进行比较,返回新的虚拟DOM。入参为parentDOM父真实DOM、oldVdom老虚拟DOM、newVdom新的虚拟DOM。调用原生方法replaceChild把上一次虚拟DOM对应的真实DOM替换为新的DOM

4. 实现事件绑定

要想实现交互-->状态更新-->页面更新,首先需要绑定交互事件。事件是作为props传递给组件的,因此我们在updateProps中进行判断,进行事件的绑定

如果属性是以'on'开头说明属性是一个事件处理函数,拿本案例举例:点击事件就需要变成dom.onclick = 处理函数的形式

5. 实现异步更新

React源码中,setState支持两种传参形式:1.对象--同步;对象可以直接展开属性进行合并;2.函数--异步。函数要基于上个状态计算下个状态,最后进行合并。因此要把上面的代码做一点改进,也要支持函数参数。

  • getState: 在遍历每个属性时,先判断属性类型是否为'function',如果是,把状态传入函数调用,返回的新状态再重新赋值给nextState。接着进行状态的合并、清空将要更新的状态数组等。

三、实现类组件状态更新

1. src/index.js

import React from "./react";
import ReactDOM from "./react-dom";
/**
 * 1.组件的数据来源的有两个,一个是来自于父组件的属性,组件内可以通过this.props获取 属性是父组件的不能修改
 * 2.组件的状态 state 状态对象是自己内部初始化的,可以修改,唯一能修改状态的方法就是setState
 * React中绑定事件和原生不一样1,属性不是小写,而是驼峰命名 2.值不是字符串而是函数的引用地址
 * 调用setState和直接 修改state有区别
 * 不管改属性或者setState改状态都会引起组件的重新刷新,让视图更新
 */
class Counter extends React.Component {
  constructor(props) {
    super(props);
    //唯一能给state赋值的地方只有构造函数
    this.state = { number: 0, title: "计数器" };
  }
  handleClick = (event) => {
    //更改状态的时候 ,只需要传递更新的变量即可。
    //调用了setState之后,状态this.state并没有立刻修改,面是等handleClick执行完了之后才去更新
    debugger;
    this.setState({ number: this.state.number + 1 });
    console.log(this.state);
  };
  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}
ReactDOM.render(<Counter />, document.getElementById("root"));

2. src/component.js

import { compareToVdom } from "./react-dom";

/** 更新器 */
class Updater {
  constructor(classInstance) {
    // 保存实例
    this.classInstance = classInstance;
    // 等待更新的状态数组
    this.pendingStates = [];
  }

  addState(partialState) {
    this.pendingStates.push(partialState);
    // 触发更新
    this.emitUpdate();
  }

  emitUpdate() {
    this.updateComponent();
  }

  updateComponent() {
    // 解构出实例、等待更新的状态
    let { classInstance, pendingStates } = this;
    if (pendingStates.length > 0) {
      shouldUpdate(classInstance, this.getState());
    }
  }

  /** 基于老状态和pendingStates获取新状态 */
  getState() {
    let { classInstance, pendingStates } = this;
    let { state } = classInstance; //老状态
    // 每个分状态 ==> 合并属性
    pendingStates.forEach((nextState) => {
      state = { ...state, ...nextState };
    });
    // 清空等待更新的分状态数组
    pendingStates.length = 0;
    return state;
  }
}

function shouldUpdate(classInstance, nextState) {
  classInstance.state = nextState; // 先把新状态赋值给实例的state
  classInstance.forceUpdate(); // 让类的实例强行更新
}

class Component {
  // 当子类继承父类的时候,父类的静态属性也是可以继承的
  // 函数组件和类组件编译后,都会变成函数,因此加上isReactComponent属性来区分是函数组件还是类组件
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {}; // 初始值
    this.updater = new Updater(this);
  }

  /** 更新分状态 */
  setState(partialState) {
    this.updater.addState(partialState);
  }

  /** 根据新的属性状态,计算新的要渲染的虚拟DOM */
  forceUpdate() {
    //获取老的虚拟DOM
    let oldRenderVdom = this.oldRenderVdom;
    //获取老的真实DOM
    let oldDOM = oldRenderVdom.dom;
    // 基于新的属性和状态,计算新的真实DOM
    let newRenderVdom = this.render();
    compareToVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
    // 使下次更新时以上次的新DOM为比较
    this.oldRenderVdom = newRenderVdom;
  }
}

export { Component };

3. src/react.js

import { wrapToVdom } from "./utils";
import { Component } from "./component";

function createElement(type, config, children) {
  //children永远都是数组
  let ref, key;
  if (config) {
    delete config.__source; // source:bable编译时产生的属性
    delete config.__self;
    ref = config.ref; // ref可以用来引用这个真实DOM元素
    key = config.key; // 用来进行DOM-DIFF优化的,是用来唯一标识某个子元素的
    delete config.ref;
    delete config.key;
  }
  let props = { ...config };
  if (arguments.length > 3) {
    // 如果入参多余3个,说明有多个子元素,截取后,以数组形式保存
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else if (arguments.length === 3) {
    props.children = wrapToVdom(children); // 可能是React元素对象,也可能是string/number/null/und
  }
  return {
    type,
    ref,
    key,
    props,
  };
}
const React = {
  createElement,
  Component,
};
export default React;

4. react-dom.js

import { REACT_TEXT } from "./constants";

/**
 *把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM/React元素
 * @param {*} container 真实DOM容器
 */
function render(vdom, container) {
  mount(vdom, container);
}

/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {
  //把虚拟DOM变成真实DOM
  let newDOM = createDOM(vdom);
  //把真实DOM追加到容器上
  parentDOM.appendChild(newDOM);
}

/**
 * 把虚拟DOM变成真实DOM
 * @param {*} vdom 虚拟DOM
 * @return 真实DOM
 */
function createDOM(vdom) {
  if (!vdom) return null; // null/und也是合法的dom

  let { type, props } = vdom;
  let dom; //真实DOM
  if (type === REACT_TEXT) {
    // 如果元素为文本,创建文本节点
    dom = document.createTextNode(props.content);
  } else if (typeof type === "function") {
> > >     if (type.isReactComponent) {
> > >       // 说明这是一个类组件
> > >       return mountClassComponent(vdom);
> > >     } else {
      // 函数组件
      return mountFunctionComponent(vdom);
    }
  } else if (typeof type === "string") {
    //创建DOM节点 span div p
    dom = document.createElement(type);
  }

  // 处理属性
  if (props) {
    //更新DOM的属性 后面我们会实现组件和页面的更新。
    updateProps(dom, {}, props);
    let children = props.children;
    //如果说children是一个React元素,也就是说也是个虚拟DOM
    if (typeof children === "object" && children.type) {
      //把这个儿子这个虚拟DOM挂载到父节点DOM上
      mount(children, dom);
    } else if (Array.isArray(children)) {
      reconcileChildren(children, dom);
    }
  }
  vdom.dom = dom; // 给虚拟dom添加dom属性指向这个虚拟DOM对应的真实DOM
  return dom;
}

/** 挂载类组件 */
function mountClassComponent(vdom) {
  let { type: ClassComponent, props } = vdom;
  // 把类组件的属性传递给类组件的构造函数,
  // 创建类组件的实例,返回组件实例对象
  let classInstance = new ClassComponent(props);
  //可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
  let renderVdom = classInstance.render();
  //在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
  classInstance.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
  let { type: functionComponent, props } = vdom;
  //获取组件将要渲染的虚拟DOM
  let renderVdom = functionComponent(props);
  return createDOM(renderVdom);
}

/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
  children.forEach((childVdom) => mount(childVdom, parentDOM));
}

/**
 * 把新的属性更新到真实DOM上
 * @param {*} dom 真实DOM
 * @param {*} oldProps 旧的属性对象
 * @param {*} newProps 新的属性对象
 */
function updateProps(dom, oldProps, newProps) {
  for (let key in newProps) {
    if (key === "children") {
      // 子节点另外处理
      continue;
    } else if (key === "style") {
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
> > >     } else if (/^on[A-Z].*/.test(key)) {
> > >       // 绑定事件 ==> dom.onclick = 事件函数
> > >       dom[key.toLowerCase()] = newProps[key];
> > >     } else {
      dom[key] = newProps[key];
    }
  }

  for (let key in oldProps) {
    //如果说一个属性老的属性对象里有,新的属性没有,就需要删除
    if (!newProps.hasOwnProperty(key)) {
      dom[key] = null;
    }
  }
}

/**
 * @param {*} parentDOM 父真实DOM
 * @param {*} oldVdom 老的虚拟DOM
 * @param {*} newVdom 新的虚拟DOM
 */
export function compareToVdom(parentDOM, oldVdom, newVdom) {
  // 获取oldRenderVdom对应的真实DOM
  let oldDOM = oldVdom.dom;
  // 根据新的虚拟DOM得到新的真实DOM
  let newDOM = createDOM(newVdom);
  // 把老的真实DOM替换为新的真实DOM
  parentDOM.replaceChild(newDOM, oldDOM);
}

const ReactDOM = {
  render,
};
export default ReactDOM;

四、总结

就本文计数器案例来讲,当点击加号触发事件回调:

updateProps绑定事件 --> setState更改状态 --> updater.addState传入需要更新的状态数组 --> pendingStates.push把分状态添加到队列 --> emitUpdate触发更新 --> updateComponent更新组件 --> shouldUpdate更新状态 --> getState合并状态 --> shouldUpdate赋值新状态 --> forceUpdae强制更新 --> compareToVdom替换为新的组件