手写react核心API(一)

1,291 阅读28分钟

手写实现react系列,实现react的常用api;参照源码简化实现,抽取核心部分,相关函数命名与源码保持一致。

传送

手写react核心API(一)

手写react核心API(二)

网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!

自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!

(之前整理的不好,这是重新整理一遍的新版本)

一、前言

本文适用于有一定基础的读者!至少已经掌握了react的相关用法!

以及掌握js的关键知识点,包括但不限于:原型与原型链、继承、反向继承、单例、发布订阅、闭包、高阶函数(柯里化、compose、thunk……)、this指向、作用域、作用域链、事件机制、事件模型、装饰器、堆、栈、队列、深浅拷贝……等等!(文章中会穿插一些知识扫盲)

说明:本文只是对react常用api的手写实现,编译部分还是需要jsx去实现(后面如果有时间会整理一篇手写模板编译的)!所以需要依赖react脚手架配置,在其基础上用自己的代码实现一遍react(并不是真正的完全手写)!

1.导图

这是本文所实现的api以及用到的关键函数导图,命名和源码一致。简单做了一个思维导图,如果你愿意参照着手写实现,可以用作参考。

手写react导图.png

2.准备工作

由于新老版本jsx编译结果不同(例如新版jsx编译后是jsx.element格式的嵌套函数,旧版是React.createElement格式),而我这里采用的是旧版,所以需要安装v17版本,同时需要修改一些配置;如果你想跟着手写实现一遍,建议直接复制下面这几个文件代码,然后yarn 或 npm i 安装依赖。

1) package.json

{
  "name": "my-react",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.0.1",
    "lodash": "^4.17.21",
    "veract": "^1.0.6"
  },
  "scripts": {
    "start": "set DISABLE_NEW_JSX_TRANSFORM=true && react-app-rewired start",
    "build": "set DISABLE_NEW_JSX_TRANSFORM=true && react-app-rewired build",
    "test": "set DISABLE_NEW_JSX_TRANSFORM=true && react-app-rewired test",
    "eject": "set DISABLE_NEW_JSX_TRANSFORM=true && react-app-rewired eject"
  },
  "devDependencies": {
    "@babel/plugin-proposal-decorators": "^7.14.2", // 支持装饰器
    "customize-cra": "^1.0.0", // 重写脚手架配置
    "react-app-rewired": "^2.1.8" // 重载
  }
}

2) jsconfig.json

{
    "compilerOptions": {
        "experimentalDecorators": true // 是否使用js的实验语法 装饰器
    }
}

3) config-overrides.js

const {override,addBabelPlugin} = require('customize-cra');
module.exports = override(
    addBabelPlugin(
        [
            "@babel/plugin-proposal-decorators",{"legacy":true} // 装饰器编译插件 采用遗留版本
        ]
    )
);

4) 安装依赖

npm i 或者 yarn

安装完后,删掉没用的,然后在index.jsx中写点案例代码

3.功能拆分

大致分为:react、react-dom、component、constants、event、utils几个模块,里面具体的细化拆分,个人觉得怎么合理旧怎么拆(这些都是后话)

这里简单的创建以下几个js文件:

  1. constants.js --- 存放公共常量
  2. utils.js --- 存放工具函数
  3. react.js --- react功能的核心
  4. react-dom.js --- react-dom功能的核心
  5. event.js --- 合成事件代码
  6. component.js --- 类组件的核心

4.非关键代码

先把常量和utils需要用到的代码拿过来

  1. constants.js
/* 文本 */
export const REACT_TEXT = Symbol("REACT_TEXT");
/* forwardRef */
export const REACT_FORWARD_REF_TYPE = Symbol("react.forward_ref");
/* provider */
export const REACT_PROVIDER = Symbol("react.provider");
/* context */
export const REACT_CONTEXT = Symbol("react.context");
/* memo */
export const REACT_MEMO = Symbol("react.memo");
  1. utils.js
import { REACT_TEXT } from "./constants";

/**
 * @description    : vdom转换辅助函数
 * + 不管原来是什么样的元素,都转成对象的形式,方便后续的DOM-DIFF
 * + 所谓的React元素,也就是虚拟dom、vdom、vnode……都是同一个玩意
 * @param           { any } element jsx解析后的children内容
 * @return          { } vdom
 */
export function wrapToVdom(element) {
  if (typeof element === "string" || typeof element === "number") {
    // 如果children是字符串或者数字,就标记为文本节点
    return { type: REACT_TEXT, props: { content: element } }; //虚拟DOM.props.content就是此文件的内容
  } else {
    return element; // 否则说明是对象(jsx已经标记好了),返回原来的,无需再标记
  }
}

/* 判断是否对象 */
function isObj(target) {
  return typeof target === "object" && target !== null;
}

/**
 * @description    : 浅比较两个对象是否相等
 * @param           { object } obj1
 * @param           { object } obj2
 * @return          { boolean }
 */
export function shallowEqual(obj1 = {}, obj2 = {}) {
  if (obj1 === obj2) {
    return true;
  }
  if (!isObj(obj1) || !isObj(obj2)) {
    return false;
  }
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) {
    return false;
  }
  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }
  return true;
}

5.关于jsx

我们写的jsx,其实就是转换后的React.createElement,所以我们可以直接写React.createElement,效果也是一样的!

import React from "react";
import ReactDOM from "react-dom";

// 我们写的jsx 等同于下面的 React.createElement
// let element = (
//   <div className="title" style={{ color: "#fff", background: "#000" }}>
//     <span>hello</span>world
//   </div>
// );

// 等同于上面的jsx,效果是一样的
let element = React.createElement(
  "div",
  {
    className: "title",
    style: {
      color: "#fff",
      background: "#000",
    },
  },
  React.createElement("span", null, "hello"),
  "world"
);
console.log(JSON.stringify(element, null, 2)); // 控制台查看输出结果

ReactDOM.render(<element />, document.getElementById("root"));

注意: v18八本的 jsx编译后的vdom是被冻结的!!

  • 被冻结的原因:为了安全,以及规范
  • 就像你进学校,只能通过校门进去,不能翻墙进去!
  • 所以,如果你是用的v18版本,那么需要降低版本到v17!!
  • 我们实现手写需要对vdom进行修改,否则无法实现!

接下来正式开始进入正题!

二、初次渲染

前面说过jsx编译后会转换为React.createElement或者jsx/jsxs格式的嵌套调用函数

其实在此之前还有一个ast转化过程(babel-preset-react-app做的),这里跳过ast,直接从拼接好的函数调用开始实现。

下面开始实现一个简单的初次渲染!

简化的渲染流程图:

image.png

1.实现React.createElement

react.js中

/**
 * 创建vdom节点!
 * + 核心就是把字符串或者说数字类型的节点转换成对象的形式
 * + 多个儿子就是数组
 * + 一个儿子就是对象或者null
 * + 没有儿子就是undefined
 * @param {*} type 类型
 * @param {*} config 配置对象
 * @param {*} children  第一个儿子
 * @returns
 */
function createElement(type, config, children) {
  let ref; // 用来获取虚拟DOM实例的
  let key; // 用来区分同一个父亲的不同儿子的
  if (config) {
    // 从jsx编译结果中,拿到关键属性
    ref = config.ref;
    key = config.key;
  }
  let props = { ...config }; // props里面没有ref和key!
  if (arguments.length > 3) {
    //如果参数大于3个,说明有多个儿子,调用wrapToVdom将其变为有效的vdom对象
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    //children可能是一个字符串,也可能是一个数字,也可能是个null undefined,也可能是一个数组
    if (typeof children !== "undefined") props.children = wrapToVdom(children);
  }
  return {
    type,
    props,
    ref,
    key,
  };
}

/* 最后不要忘了导出 */
export default {
  createElement,
};

2.实现ReactDom.render

react-dom.js中

1) 全局变量

// 缓存当前挂载的实例,reducer需要用到这个属性
let mountingComponent = null;
// 这里存放着所有的状态,源码时fiber链表,这里用数组简单实现
let hookState = [];
// 当前的执行的hook的索引
let hookIndex = 0;
// 调度更新方法,数据变化后 能找到组件对应的此方法更新视图
let scheduleUpdate;

2) render

/**
 * 1渲染视图 2重置组件调度更新器
 * @param {} vdom  虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
  // 调用mount方法挂载/更新视图
  mount(vdom, container);
  scheduleUpdate = () => {
    /* 
      + 每次调用render时,将scheduleUpdate置为当前组件的的调度更新器
      + 重置hookIndex,每次组件更新,都需要重新计算hookState中的数据状态
      + 调用compareTwoVdom方法,进入节点比对(diff),更新视图
    */
    hookIndex = 0; //vdom并不指向当前的更新,而是指向根元素
    compareTwoVdom(container, vdom, vdom);
  };
}

/**
 * 比较新旧的虚拟DOM,找出差异,更新到真实DOM上
 * 这里只实现了浅比较,没有实现真正的dom-diff
 * @param {*} parentDOM 父元素
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 * @param {*} nextDOM dom插入的位置标记,例如[1,2,3]变[1,4,3] 4需要知道放在1的后面
 */
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
    // 更新逻辑
}

/* 最后不要忘了导出 */
export default {
  render,
};

3) mount

/**
 * 1把虚拟DOM转成真实DOM 2插入容器中
 * @param {} vdom  虚拟DOM
 * @param {} container  容器(创建出来的dom需要插入的父元素)
 */
function mount(vdom, container) {
  // 调用createDom方法,传入虚拟dom,根据虚拟dom,创建出真实dom
  let newDOM = createDOM(vdom);
  // 将得到的真实dom, 插入容器中(父元素)
  container.appendChild(newDOM);
  // 此时如果真实dom上,存在componentDidMount,就调用该生命周期函数!
  if (newDOM.componentDidMount) newDOM.componentDidMount();
}

4) createDOM

/**
 * 把虚拟DOM转成真实DOM
 * @param {*} vdom  虚拟DOM
 * @return {*} dom  真实DOM
 */
function createDOM(vdom) {
  /* 
    从虚拟dom上取出关键属性
    + type:有很多种类型
        + 文本类型
        + 函数组件
        + 类组件
        + memo组件 memo函数转化的组件
        + context组件 createContext
        + provider组件 Provider
        + forward组件 forwardRef转化的组件
        + ……
    + props 元素(组件)上的属性
    + ref 存放真实dom引用
  */
  let { type, props, ref } = vdom;
  let dom; // 真实DOM元素
  let prevComponent = mountingComponent; // 保存上一个挂载实例
  mountingComponent = vdom; // 指向最新的挂载实例

  /* 对type进行处理 */
  if (type && type.$$typeof === REACT_MEMO) {
    /* 挂载memo组件 */
    return mountMemoComponent(vdom);
  } else if (type && type.$$typeof === REACT_CONTEXT) {
    /* 挂载context组件 */
    return mountContextComponent(vdom);
  } else if (type && type.$$typeof === REACT_PROVIDER) {
    /* 挂载Provider组件 */
    return mountProviderComponent(vdom);
  } else if (type && type.$$typeof === REACT_FORWARD_REF_TYPE) {
    /* 挂载forwardRef组件 */
    return mountForwardComponent(vdom);
  } else if (type === REACT_TEXT) {
    /* 创建文本节点 */
    dom = document.createTextNode(props.content);
  } else if (typeof type === "function") {
    /* 
      type为 function 说明这是一个React函数组件的React元素
      + 不管是函数组件,还是类组件,其类型都是function,class最终也会被编译成function
    */
    if (type.isReactComponent) {
      /* 如果有 isReactComponent 标记,则挂载类组件*/
      return mountClassComponent(vdom);
    } else {
      /* 挂载函数组件 */
      return mountFunctionComponent(vdom);
    }
  } else if (typeof type === "string") {
    /* 创建dom元素 */
    dom = document.createElement(type);
  } else {
    throw new Error(`无法处理的元素类型`, type);
  }

  /* 对属性的处理 */
  if (props) {
    updateProps(dom, {}, props); // 根据虚拟DOM中的属性更新真实DOM属性
    if (typeof props.children == "object" && props.children.type) {
      /* 它是个对象 说明只有一个儿子,递归调用render继续处理子节点 */
      render(props.children, dom);
    } else if (Array.isArray(props.children)) {
      /* 
        如果是一个数组,需要循环数组,再递归调用render
        + 将当前创建出来的dom作为children的容器(container)
      */
      reconcileChildren(props.children, dom);
    }
  }
  mountingComponent = prevComponent; // 还原挂载实例

  vdom.dom = dom; // 让虚拟DOM的dom属生指向它的真实DOM
  if (ref) ref.current = dom; // 让ref.current属性指向真实DOM的实例
  return dom; // 最后返回创建好的dom
}

/* 由于内容较多,以下是 下一篇中实现的方法 */
mountMemoComponent(vdom){ // 挂载memo组件  }
mountContextComponent(vdom){ // 挂载context组件  }
mountProviderComponent(vdom){ // 挂载Provider组件  }
mountForwardComponent(vdom){ // 挂载forwardRef组件  }
mountClassComponent(vdom){ // 挂载类组件  }
mountFunctionComponent(vdom){ // 挂载函数组件  }

5) updateProps

/**
 * 属性更新器,给dom添加props中的属性
 * @param {*} dom 真实dom节点
 * @param {*} oldProps 旧的props
 * @param {*} newProps 新的props
 */
function updateProps(dom, oldProps, newProps) {
  /* 这里省略属性比对,默认用最新的,如果你想加,可以自己加浅比较或深比较 */
  for (let key in newProps) {
    if (key === "children") {
      continue; // key如果是children,说明是子节点,不需要做任何梳理
    }
    if (key === "style") {
      // 给dom元素添加/修改样式
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (key.startsWith("on")) {
      /* 绑定事件,这里用的是自己实现的合成事件 */
      // 暂时注释掉,下一篇中实现合成事件绑定!
      // addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
    } else {
      /* 其他情况下,如果属性有值,就继续给dom添加属性值 */
      if (newProps[key]) dom[key] = newProps[key];
    }
  }
}

6) reconcileChildren

/**
 * 协调遍历子节点,递归调用render继续渲染
 * @param {*} vdom
 */
function reconcileChildren(childrenVdom, parentDOM) {
  /* 遍历子节点,将虚拟dom和父节点递归传递给render */
  for (let i = 0; i < childrenVdom.length; i++) {
    let childVdom = childrenVdom[i];
    render(childVdom, parentDOM);
  }
}

3.效果查看

1) 编写案例

// import React from "react";
// import ReactDOM from "react-dom";

// 用自己的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

// 模拟vdom创建
let element = React.createElement(
  "div",
  {
    className: "title",
    style: {
      color: "#fff",
      background: "#000",
      height: "200px",
    },
  },
  React.createElement("span", null, "hello"),
  "world"
);

// 实现渲染
ReactDOM.render(element, document.getElementById("root"));

2) 浏览器效果

image.png

三、实现函数组件挂载

react的中的组件 分为内置原生组件自定义组件

内置组件: p h1 span type字符串……

自定义组件:不管是类组件还是函数组件,类型都是一个函数

  • 类组件的父类Component的原型上有一个属性isReactComponent={}
  • 函数组件原型链上没有任何特殊属性,就是一个正常函数
  • 可以在控制台输出打印一下类组件和函数组件编译后的vdom到底长啥样

组件规则:

  • 自定义组件的名称必须是大写字母开头
  • 自定定组件的返回值有且只能一个根元素

函数组件原理:render调用在createDom创建真实dom,在createDom中如果vdom的type类型是函数,那么说明可能是函数组件,调用该函数并将props传过去,得到一个vdom,再递归调用createDom创建真实dom。

1.mountFunctionComponent

完善在上一篇中预留的函数组件挂载函数

调用组件并传递props,得到一个vdom,再递归调用createDom创建真实dom

/**
 * 函数组件的挂载逻辑
 * + 调用函数组件,传入props,得到jsx编译后的虚拟dom
 * + 保存一个旧的虚拟dom,用于更新时比对
 * + 继续调用createDOM 创建并渲染真实dom
 * @param {*} vdom
 */
function mountFunctionComponent(vdom) {
  /* 取出虚拟dom中的type和props */
  let { type, props } = vdom;
  /* 
    经过createDom中的判断,此时type一定是函数
    调用该函数,将props作为参数传过去,jsx会将返回值编译为虚拟dom
    最后得到的renderVdom就是函数返回值,是新的虚拟dom
  */
  let renderVdom = type(props);
  // 记录oldRenderVdom,用于更新时的dom比对
  vdom.oldRenderVdom = renderVdom;
  // 拿到函数组件的虚拟dom后,继续调用createDom方法,创建真实dom
  return createDOM(renderVdom);
}

函数组件挂载就实现了,是不是很简单~

这只是缩减版的代码,抽取了核心部分逻辑,少了一大堆判断~

2.创建并使用一个函数组件

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

function App(props) {
  return (
    <h1>
      <span>hello,</span>
      {props.name}
    </h1>
  );
  // 下面这种 和上面一样效果
  // return React.createElement("h1", null, "hello,", props.name); 
}

let element = React.createElement(App, { name: "张三" });

ReactDOM.render(element, document.getElementById("root"));

页面正常显示

image.png

四、实现类组件

前面说过,不管类组件还是函数组件,最终都是函数;

函数执行结果就是vdom,拿到vdom后再递归调用createDom函数创建真实dom

component.js中

1.Component

export class Component {
  // 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
  static isReactComponent = true;
  
  constructor(props) {
    this.props = props; // 初始化props
    this.state = {}; // 初始化state
    //每一个类组件的实例有一个updater更新器
    this.updater = new Updater(this);
  }
  
  // 类组件自带的setState方法
  setState(partialState, callback) {
    // 调用updater.addState,有一个批处理过程
    this.updater.addState(partialState, callback);
  }

  /**
   * 组件的更新
   * 1.获取 老的虚拟DOM React元素
   * 2.根据最新的属生和状态计算新的虚拟DOM
   * 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
   */
  forceUpdate() {
    // 组件更新逻辑
  }
}

在react.js中,引入并导出Component

import { Component } from "./Component";
// ……
export default {
  createElement,
  Component,
};

2.Updater

class Updater {
  constructor(classInstance) {
    this.classInstance = classInstance; // 初始化组件实例
    this.pendingStates = []; // 保存将要更新的队列
    this.callbacks = []; // 保存将要执行的回调函数
  }
  
  addState(partialState, callback) {
    /* 把改变数据的操作,放到队列中存起来 */
    this.pendingStates.push(partialState);
    // 如果传了第二个参数并且时函数,就把函数也放入队列
    if (typeof callback === "function") this.callbacks.push(callback);
    this.emitUpdate(); // 触发更新逻辑
  }
  
  //不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法
  emitUpdate(nextProps) {
    // 组件更新,会调用组件实例上的forceUpdate
  }
}

3.mountClassComponent

挂载类组件:new 类组件构造函数,执行实例上的render得到vdom,再递归调createDom创建真实dom

/**
 * 类组件的挂载逻辑
 * + 调用函数组件,传入props,得到jsx编译后的虚拟dom
 * + 保存一个旧的虚拟dom,用于更新时比对
 * + 继续调用createDOM 创建并渲染真实dom
 * @param {*} vdom
 */
function mountClassComponent(vdom) {
  /* 取出关键属性,此时type是构造函数 */
  let { type, props, ref } = vdom;
  // 初始化defaultProps,类组件中的默认props
  let defaultProps = type.defaultProps || {};
  // new 类组件构造函数,传入props,得到类组件实例
  let classInstance = new type({ ...defaultProps, ...props });
  if (type.contextType) {
    /* 
      对类组件context的处理:
      + 如果构造函数中有contextType属性,就将其_currentValue赋值给实例的context
      + contextType必须加static的原因就在这里,它是类组件自身的,而不是实例的!
    */
    classInstance.context = type.contextType._currentValue;
  }
  /* 记录vdom的实例,后面需要从该属性上拿到生命周期钩子,并执行钩子*/
  vdom.classInstance = classInstance;
  /* 如果实例上,有componentWillMount,就执行该钩子函数! */
  if (classInstance.componentWillMount) classInstance.componentWillMount();
  /* 调用实例的render方法,jsx会编译成虚拟dom对象 */
  let renderVdom = classInstance.render();
  /* 挂载的时候计算出虚拟DOM,然后挂到类的实例上,更新世用作比对 */
  classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
  /* ref.current指向类组件的实例 */
  if (ref) ref.current = classInstance;
  /* 调用createDom方法,创建真实dom */
  let dom = createDOM(renderVdom);
  /* 暂时把didMount方法暂存到dom上,前面的mount方法中会调用该钩子 */
  if (classInstance.componentDidMount) {
    /* 用bind确保其this指向始终是当前实例 */
    dom.componentDidMount = classInstance.componentDidMount.bind(classInstance);
  }
  return dom; // 返回真实dom
}

4.创建并使用一个类组件

写法和react类组件一样

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

class ClassComponent extends React.Component {
  render() {
    return (
      <h1 style={{ color: "red" }} className="title">
        <span>hello</span>
        {this.props.name}
      </h1>
    );
  }
}

let element = <ClassComponent name="张三" />;

ReactDOM.render(element, document.getElementById("root"));

看下效果

image.png

接下来开始实现类组件更新、合成事件、批处理!

在此之前,先来一个知识扫盲,已经掌握的同学可以跳过~

五、知识扫盲

你需要了解以下几个知识点:

  • 类组件中的方法,如果不是箭头函数,都会被编译成function声明的函数
  • setState的底层机制
    • setState自带partialState能力(部分更新)
    • v18:不管写在哪里,都会在下一个任务队列中执行
    • v18以前:在同步任务中,会做一次批处理(异步操作);在异步任务中,则跳过批处理(同步操作)

1.关于this问题

先来看一段代码

babel编译前的class,可以自己打开babel官网查看编译结果

class Component{
	tick(){}
  	tick1 = ()=>{}
}
const c = new Component()

babel编译后的class,耐心看注释!!

// 通过这个函数,对目标对象进行劫持
function _defineProperty(obj, key, value) {
  // 获取一个有效的key
  key = _toPropertyKey(key);
  // 再进行遍历添加属性!
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true,
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, "string");
  // symbol也能作为key,其他必须通过类型转换,变为string类型!
  return typeof key === "symbol" ? key : String(key);
}
/* 
  这个函数是为了:获取一个有效的key
  + 因为key有可能不是string(例如symbol),但是对象成员访问时,会把key隐式转换为string类型!
  + 在转换的过程中,需要看这个key有没有实现Symbol.toPrimitive方法!
  + 因为隐式转换最先找的,就是Symbol.toPrimitive这个方法!
*/
function _toPrimitive(input, hint) {
  if (typeof input !== "object" || input === null) return input;
  var prim = input[Symbol.toPrimitive];
  if (prim !== undefined) {
    var res = prim.call(input, hint || "default");
    if (typeof res !== "object") return res;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return (hint === "string" ? String : Number)(input);
}
class Component {
  constructor() {
    // 关键在这里:tick1 永远绑定在this上,this就是类的实例,所以通过this一定能拿到tick1
    _defineProperty(this, "tick1", () => {});
  }
  /* 
    这样写最后会作为Component类的原型方法,且是function类型的函数!
    而function是谁调用指向谁!所以这种函数,都需要加bind!!!
  */
  tick() {}
}
const c = new Component();

接着再看一下控制台输出:

image.png 结论:

  • 对象简写方式的函数,是属于class自身的,放在类的原型上!
  • 箭头函数,会通过defineProperty绑定到实例身上!!
  • 所以箭头函数的this,始终都是实例!!

2.关于setState

1) partialState

类组件中的setState自带partialState能力,也就是局部更新

也就是说:你更新的数据可以只传需要更新的,而不用传全部;[useState不具备这个能力,需要自己封装]

class ClassComponent extends React.Component {
  state = { date: new Date(), num: 1 };

  addNum = () => {
    // 这里只更新了num,底层会帮我们做 {...oldState,...newState} 这样的处理!
    this.setState({ num: this.state.num + 1 });
  };

  render() {
    return (
        <div>
          num: {this.state.num}
          <button onClick={this.addNum}>+</button>
        </div>
    );
  }
}

2) 批处理

先提一个问题:如果你在某个代码块中,调用了一万次setState,那么它需要执行一万次吗?需要更新一万次视图吗?这样合理吗?

上面问题的答案很明显,执行一万次是不合理的!那么我们如何能避免这个问题呢?最佳实践就是批处理。

我们把上面代码中的addNum改成下面这样:

state = { date: new Date(), num: 1 };
addNum = () => {
    // this.setState({ num: this.state.num + 1 });
    for (let i = 0; i < 20; i++) {
      this.setState({ num: this.state.num + 1 });
    }
};

请问render执行多少次? num最终是多少?答案是1次,num是2

3) 实现一个批处理

let queue = []; // 用于存放待执行任务的队列
let callbacks = [] // 用于存放待执行回调函数的队列
let isBatchingUpdate = true; // 是否开启批处理的拦截器
let state = { number: 0 }; // 初始状态

// 遍历执行队列
function batchUpdate () {
  Promise.resolve().then(() => {
    // 将队列中的待执行任务全部取出,并一次性赋值给state
    queue.forEach(newSate => {
      state = { ...state, ...newSate }
    })
    // 执行回调函数队列中的函数,把最新的state传递回去
    callbacks.forEach(cb => cb(state))
    queue.length = 0 // 清空队列
    isBatchingUpdate = true // 重置状态
  })
}

// 把任务丢到队列
function setState (newSate, callback) {
  queue.push(newSate); // 将新数据放到数组中
  if (typeof callback === 'function') {
    callbacks.push(callback) // 将回调函数放入数组中
  }
  // 如果开启了批处理
  if (isBatchingUpdate) {
    isBatchingUpdate = false  // 立即把阀门关闭
    // 调用batchUpdate,在下一个微任务中执行队列中的任务!
    // 此时不管同步任务中调用多少次setState,都走不到这里!都只是在往队列中加数据!
    // 同时batchUpdate的微任务中,一定能拿到所有同步任务中调用setState后,存入队列的数据!
    batchUpdate()
  }
}

// 执行一百次setState
for (let i = 0; i < 100; i++) {
  setState({ number: state.number + 1 });
}
console.log(state); // 只执行依次!并且 无法拿到最新的!

setTimeout(() => {
  console.log(state); // 1 下一个任务队列(宏任务)中能拿到最新值
})

setState({ number: state.number + 1 }, (state) => {
  console.log('最新的state', state) // 回调函数中拿到最新值
});

setTimeout(() => {
  for (let i = 0; i < 100; i++) {
    setState({ number: state.number + 1 });
  }
  console.log(state) // 只执行一次! 并且仍然无法拿到最新值!
})

4) 关于传递函数和回调函数

setState可以传递函数,函数中的返回值作为新的state;

我们把上面的setNum改成这样:

state = { date: new Date(), num: 1 };
addNum = () => {
    for (let i = 0; i < 20; i++) {
      this.setState((oldState) => ({ num: oldState.num + 1 }));
    }
};

最终输入结果是21,而render还是只触发一次!

原理:函数中产生了一个新的闭包,oleState会被缓存起来,虽然同样批处理执行了20次,但是每一次的oldState都是上一次改变过的!

setState接收的第二个参数是一个函数,会在数据更新之后执行这个函数[类似vue的nextTick],这里不再赘述……

3.dom事件机制

dom事件机制可以参考我之前的文章:react合成事件原理

4.js事件循环机制

这里简单描述下js事件循环机制:

执行顺序:宏任务 > 同步任务 > 微任务 > 宏任务 > 同步任务 > 微任务 ……

举个例子:

假如你需要做三件事:追剧、泡方便面吃(10分钟)、洗衣服(50分钟)

如果你用同步的方式去做这三件事,那么你只能:

  • 1追剧,直到追完才能做其他(很可能饿死都没追完)
  • 2追完剧 泡方便面吃
  • 3吃完方便面 把衣服丢进洗衣机,等着衣服洗完

如果你用异步方式做这三件事:

  • 1先把方便面泡好(1分钟)
  • 2随后把衣服丢进洗衣机
  • 3回来方便面泡好了,一边看剧一边吃泡面,
  • 4看了一集电视剧后衣服洗好了;
  • 5最后你在50分钟内,剧也追了,面也吃了,衣服也洗了

我们假设追剧是同步任务,泡方便面是微任务,洗衣服是宏任务:

  • 执行同步任务时(追剧),发现有个微任务(泡方便面),就把方便面泡好(加入微任务队列)
  • 遇到一个宏任务(洗衣服),把衣服丢进洗衣机(加入宏任务队列)
  • 回来发现方便面泡好了,开始吃泡面(清空微任务队列)
  • 然后衣服洗好了,清空宏任务队列。
  • 继续执行下一轮同步任务(追剧)

总结:

  1. 微任务一定会在当前执行栈中的 所有同步任务执行完,才会执行!你不可刚泡好方便面就立即吃吧?
  2. 宏任务一定会在执行栈中的 所有微任务执行完后,才会执行!你不可能刚把衣服丢进洗衣机就拿出来凉吧?
  3. 宏任务执行完,如果里面又有新的同步任务,那么会继续执行新的任务队列!

最后:你了解上面的原理后,只需要记住哪些是宏任务,哪些是微任务就可以了!

  • 宏任务:定时器、script标签(就是最外层的宏任务)
  • 同步任务,就是我们写的普通代码,从上往下依次执行,遇到函数调用,就进入新的执行栈
  • 微任务:promise、nextTick、3个html观察器(IntersectionObserver、MutationObserver、ResizeObserver)……

六、实现setState及批处理

先看一张流程图

image.png

Component.js中

1.创建updateQueue对象

/* 
  更新队列对象
  + isBatchingUpdate:是否批量更新
  + updaters:待执行的更新队列
  + batchUpdate:执行更新队列的方法
*/
export let updateQueue = {
  isBatchingUpdate: false, // 是否批量更新
  updaters: [], // 队列
  batchUpdate () {
    for (let updater of updateQueue.updaters) {
      // 遍历队列, 执行队属性身上的updateComponent方法!!
      updater.updateComponent();
    }
    // 重置批量更新状态
    updateQueue.isBatchingUpdate = false;
    // 清空队列
    updateQueue.updaters.length = 0;
  },
};

可以看到:上面的代码中,updaters队列中存放的实际是updater,也就是我们的Updater构造类的实例

如何将updater放入updaters队列中?在哪里放入比较好?那就交给updater的emitUpdate方法去处理!

2.完善Updater

回到Updater构造类中,继续完善其实例方法

  • emitUpdate 发起更新函数

    • 判断是否批量更新:updateQueue.isBatchingUpdate === true
    • 如果是批量更新,就将将updater(也就是this)放入updaters队列中
    • 如果不是批量更新,就执行之前的updateComponent逻辑!
  • updateComponent 组件更新函数

  • shouldUpdate 是否更新函数

  • getState 获取最新状态函数

class Updater {
  constructor(classInstance) {
    this.classInstance = classInstance; // 初始化组件实例
    this.pendingStates = []; // 保存将要更新的队列
    this.callbacks = []; // 保存将要执行的回调函数
  }
  addState(partialState, callback) {
    /* 把改变数据的操作,放到队列中存起来 */
    this.pendingStates.push(partialState);
    // 如果传了第二个参数并且时函数,就把函数也放入队列
    if (typeof callback === "function") this.callbacks.push(callback);
    this.emitUpdate(); // 触发更新逻辑
  }

  /* 
    发起更新函数
    + 不管状态和属性的变化 都会让组件刷新,
    + 不管状态变化和属性变化 都会执行此方法
  */
  emitUpdate(nextProps) {
    /* 属性变化时,会传递新的props */
    this.nextProps = nextProps;
    if (updateQueue.isBatchingUpdate) {
      /* 如果当前处于批量更新模式,那么就把此updater实例添加到updateQueue里去 */
      updateQueue.updaters.push(this);
    } else {
      /* 非批处理模式,直接让组件更新 */
      this.updateComponent();
    }
  }

  /* 组件更新函数 */
  updateComponent() {
    /* 取出关键数据,组件实例、要改变的state、新的props */
    let { classInstance, pendingStates, nextProps } = this;
    /* 在更新前还要进行是否应该更新的判断 */
    if (nextProps || pendingStates.length > 0) {
      /* 
        有新的状态或者 新的props,说明应该更新
        + 传入实例以及新的props,调用getState获取最新状态并传递给shouldUpdate
      */
      shouldUpdate(classInstance, nextProps, this.getState());
    }
  }

  /* 
    计算新状态,对partialState的处理:
    + 根据老状态,和pendingStates计算出新的状态
    + 进行状态合并,实现'部分更新'的功能
  */
  getState() {
    /* 取出关键属性 */
    let { classInstance, pendingStates } = this;
    /* 先获取老的原始的组件状态 */
    let { state } = classInstance;
    /* 遍历执行新状态队列 */
    pendingStates.forEach((nextState) => {
      /* 如果传入的是函数,旧将函数的返回值作为新的state */
      if (typeof nextState === "function") {
        nextState = nextState(state);
      }
      /* 最后合并新旧state,用新的替换旧的 */
      state = { ...state, ...nextState };
    });
    /* 清空等待更新的队列 */
    pendingStates.length = 0;
    /* 执行回调函数,并传入最新状态 */
    this.callbacks.forEach((callback) => callback(state));
    /* 清空回调函数队列 */
    this.callbacks.length = 0;
    return state; // 返回新状态
  }
}

3.完善Component

export class Component {
  // 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
  static isReactComponent = true;
  constructor(props) {
    this.props = props; // 初始化props
    this.state = {}; // 初始化state
    //每一个类组件的实例有一个updater更新器
    this.updater = new Updater(this);
  }
  // 类组件自带的setState方法
  setState(partialState, callback) {
    // 调用updater.addState,有一个批处理过程
    this.updater.addState(partialState, callback);
  }

  /**
   * 组件的更新
   * 1.获取 老的虚拟DOM React元素
   * 2.根据最新的属生和状态计算新的虚拟DOM
   * 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
   */
  forceUpdate() {
    let oldRenderVdom = this.oldRenderVdom; // 老的虚拟DOM
    let oldDOM = findDOM(oldRenderVdom); // 根据老的虚拟DOM查到老的真实DOM
    if (this.constructor.contextType) {
      /* 
        给实例的context赋值!这一步是对上下文的处理
        + 如果构造函数身上有contextType属性,就将其_currentValue作为实例的context
      */
      this.context = this.constructor.contextType._currentValue;
    }
    /* 
      调用实例的render,计算新的虚拟DOM
      + 类组件的更新,是调用render,并不是重新new 构造函数!
      + 所以类组件中除了render以外的,更新时都不会重新创建!
      + 这也是为什么createRef在类组件中不会重新执行,而在函数组件中每次都会创建一个新的原因
      + 以及更多的特性……都是因为这个
    */
    let newRenderVdom = this.render();

    let extraArgs; // 快照的返回值
    if (this.getSnapshotBeforeUpdate) {
      /* 
        在更新前,调用getSnapshotBeforeUpdate生命周期钩子函数
        + 如果存在getSnapshotBeforeUpdate,就调用该钩子函数,将返回值赋值给extraArgs
      */
      extraArgs = this.getSnapshotBeforeUpdate();
    }

    /* 拿到新的虚拟dom后,开始走更新逻辑,调用compareTwoVdom进行vdom比对 */
    compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);

    /* 重置旧的vdom,将新的作为下一次的旧的,用于下一次的更新 */
    this.oldRenderVdom = newRenderVdom;

    if (this.componentDidUpdate) {
      /* 
        触发componentDidUpdate生命周期钩子函数
        + 将最新的props和state,以及上面的更新快照传递过去
      */
      this.componentDidUpdate(this.props, this.state, extraArgs);
    }
  }
}

4.辅助函数findDOM

/**
 * 根据vdom返回真实DOM
 * @param {*} vdom
 */
export function findDOM(vdom) {
  let type = vdom.type; // 取出type属性
  let dom; // 虚拟dom对应的真实dom
  if (typeof type === "string" || type === REACT_TEXT) {
    dom = vdom.dom; // 如果type是html元素或者文本节点,一定能拿到它的dom(之前绑定过)
  } else {
    // 可能函数组件 类组件 provider context forward……需要递归查询
    dom = findDOM(vdom.oldRenderVdom);
  }
  return dom; // 返回找到的真实dom
}

七、实现合成事件

完善合成事件处理:

  1. 将props属性处理中,对事件的绑定方式进行修改
  2. 创建event.js,用来写合成事件相关的逻辑
  3. 实现addEvent、dispatchEvent、createSyntheticEvent

1.修改updateProps事件处理

将上面代码中addEvent的注释去掉

function updateProps (dom, oldProps, newProps) {
  for (let key in newProps) {
      /* ------------------------------------------------ */
    } else if (key.startsWith("on")) {
      /* 
        绑定事件,这里用的是自己实现的合成事件 
        执行合成事件,传入: dom、事件名(小写)、事件绑定的handler
      */
      addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
    } else {
      /* ------------------------------------------------ */
  }
}

接下来我们需要实现addEvent方法!

2.addEvent

创建event.js > 实现addEvent方法

// 这里一定要先引入updateQueue队列,其数据基于闭包原理是同步更新的
import { updateQueue } from "./Component";

/**
 * @description: 添加事件处理函数,做合成事件处理!
 * @param {*} dom 事件源
 * @param {*} eventType 事件类型
 * @param {*} handler 事件触发的绑定函数
 * @return {*} void
 */
export function addEvent (dom, eventType, handler) {
  let store; // 用于存放dom身上绑定的所有事件的handler
  if (dom.store) {
    // 如果dom上已经有store对象,就取出来用
    store = dom.store;
  } else {
    // 如果没有store,就创建一个对象
    dom.store = store = {};
  }
  // 将事件放入store对象中 store.onclick = ()=>{}
  store[eventType] = handler;
  // 这里做了一个去重,如果一个元素绑定多个onClick事件,实际只绑定一个
  if (!document[eventType]) {
    /* 
      绑定的dispatchEvent函数中去对事件做统一处理!
      这里也可以改为addEventListaner……
      + v17以前:事件委托在document上
      + v17以后:事件委托在root元素上
    */
    document[eventType] = dispatchEvent;
  }
}

3.dispatchEvent

接着需要实现dispatchEvent方法,派发事件

/**
 * @description: 事件派发
 * @param {*} event 事件源
 * @return {*} void
 */
function dispatchEvent (event) {
  // 解构出原生事件的event上的target和type属性
  let { target, type } = event;
  let eventType = `on${type}`; // 此时type为click 需 拼接为 onclick
  updateQueue.isBatchingUpdate = true; // 切换为批量更新模式
  // 获取合成事件
  let syntheticEvent = createSyntheticEvent(event);
  //模拟事件冒泡的过程
  while (target) {
    // 拿到上面addEvent函数中绑定在dom身上的store
    let { store } = target;
    // 取出store中的handler
    let handler = store && store[eventType];
    // 执行handler,并把合成事件对象传递过去!!
    handler && handler.call(target, syntheticEvent);
    // 将target修改为parentNode,进入下一个while循环, 继续执行冒泡事件
    // 直到target是null,它的parent为undefiined,循环结束!!!
    target = target.parentNode;
  }
  // 重置批量更新状态
  updateQueue.isBatchingUpdate = false;
  // 执行批量更新!!!
  updateQueue.batchUpdate();
}

4.createSyntheticEvent

可以看到,上面的dispatchEvent派发事件时,实际传递给handler的是合成事件对象(react的合成事件对象,并不是原生dom事件对象!),合成事件对象中,会做很多处理:

  • 阻止冒泡的兼容处理
  • 阻止默认事件的兼容处理
  • ……
/**
 * @description    : 创建合成事件
 * 在源码里此处做了一些浏览器兼容性的适配
 * 例如: 对事件冒泡的兼容处理, 对阻止默认事件的兼容处理等...
 * @param           { } event
 * @return          { }
 */
function createSyntheticEvent (event) {
  let syntheticEvent = {}; // 合成事件对象
  // 遍历原生事件对象,赋值给合成事件对象,源码中还会做一些特殊处理
  for (let key in event) {
    syntheticEvent[key] = event[key];
    // 做兼容处理...省略,后面再完善
  }
  return syntheticEvent; // 返回合成事件对象
}

5.查看效果

此时可以对点击事件稍作修改,查看一下效果

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

class ClassComponent extends React.Component {
  state = { date: new Date(), num: 1 };

  addNum = (event) => {
    console.log("合成事件对象", event);
    for(let i = 0; i < 10; i++){
        // 批处理
        this.setState({num:this.state.num + 1)
    }
  };

  render () {
    console.log('render')
    return (
      <div id="abc">
        { this.state.num }
        <button onClick={this.addNum}>addNum</button>
      </div>
    );
  }
}

let element = React.createElement(ClassComponent);
ReactDOM.render(element, document.getElementById("root"));

image.png

八、实现组件更新

回到react-dom.js中

所有的更新处理,都从compareTwoVdom函数开始!在前面实现render时,我们已经创建好了该函数,现在开始对该函数进行完善!

触发组件更新的流程图

类组件更新流程.png

1.完善compareTwoVdom

/**
 * 比较新旧的虚拟DOM,找出差异,更新到真实DOM上
 * 这里只实现了浅比较,没有实现真正的dom-diff
 * @param {*} parentDOM 父元素
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 * @param {*} nextDOM dom插入的位置标记,例如[1,2,3]变[1,4,3] 4需要知道放在1的后面
 */
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
  if (!oldVdom && !newVdom) {
    // 如果老的虚拟DOM是null,新的虚拟DOM也是null,不需要做任何处理
  } else if (oldVdom && !newVdom) {
    /* 老的为不null,新的为null,需要销毁老组件 */
    let currentDOM = findDOM(oldVdom); // 找到老的真实dom
    if (oldVdom.classInstance && oldVdom.classInstance.componentWillUnmount) {
      /* 在卸载前,触发组件将要卸载的生命周期钩子函数 */
      oldVdom.classInstance.componentWillUnmount();
    }
    currentDOM.parentNode.removeChild(currentDOM); // 把老的真实DOM删除
  } else if (!oldVdom && newVdom) {
    //如果老的没有,新的有,就根据新的组件创建新的DOM并且添加到父DOM容器中
    let newDOM = createDOM(newVdom); // 创建真实dom
    if (nextDOM) {
      // 插入前看一下有没有位置标记,如果有,就放入指定的位置
      parentDOM.insertBefore(newDOM, nextDOM);
    } else {
      parentDOM.appendChild(newDOM); // 没有标记位置时,直接放到最后
    }
    /* 新增的组件,需要触发生命周期钩子函数componentDidMount */
    if (newDOM.componentDidMount) newDOM.componentDidMount();
  } else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
    //新老都有,但是type不同(例如新的是div 旧的是p)也不能复用,则需要删除老的,添加新的
    let oldDOM = findDOM(oldVdom); // 先获取 老的真实DOM
    let newDOM = createDOM(newVdom); // 创建新的真实DOM

    /* 在卸载旧的组件前,需要执行生命周期钩子函数componentWillUnmount */
    if (oldVdom.classInstance && oldVdom.classInstance.componentWillUnmount) {
      oldVdom.classInstance.componentWillUnmount(); // 执行组件卸载方法
    }
    /* 通过老的父节点,用新的把旧的替换掉 */
    oldDOM.parentNode.replaceChild(newDOM, oldDOM);
    /* 新的挂载完成后,需要执行生命周期钩子函数componentDidMount */
    if (newDOM.componentDidMount) newDOM.componentDidMount();
  } else {
    // 老的有,新的也有,且类型也一样,需要复用老节点,进行深度的递归dom diff了
    updateElement(oldVdom, newVdom);
  }
}

2.完善updateElement

/**
 * 比对新旧虚拟dom,根据type执行对应的更新函数
 * + 组件标记的属性,都放在.$$typeof上!
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 */
function updateElement(oldVdom, newVdom) {
  if (oldVdom.type && oldVdom.type.$$typeof === REACT_MEMO) {
    /* 类型是memo组件 */
    updateMemoComponent(oldVdom, newVdom);
  } else if (oldVdom.type && oldVdom.type.$$typeof === REACT_PROVIDER) {
    /* 类型是provider组件 */
    updateProviderComponent(oldVdom, newVdom);
  } else if (oldVdom.type && oldVdom.type.$$typeof === REACT_CONTEXT) {
    /* 类型是context组件 */
    updateContextComponent(oldVdom, newVdom);
  } else if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT) {
    /* 
        类型是文本节点
        + 找到旧虚拟dom的真实dom,并赋值一份给新虚拟dom的真实dom属性
        + 同步给currentDOM变量,基于对象引用地址的原理做修改
    */
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    if (oldVdom.props.content !== newVdom.props.content) {
      /* 文本节点无需创建新的dom元素,直接替换文本内容即可! */
      currentDOM.textContent = newVdom.props.content;
    }
  } else if (typeof oldVdom.type === "string") {
    /* 
      + type是字符串,则说明是html标签(源码有严格的判断,这里省略掉,可以自己加) 
      + 让新的虚拟DOM的真实DOM属性等于老的虚拟DOM对应的那个真实DOM
    */
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    /* 更新属性:用新的属性更新DOM的老属性 */
    updateProps(currentDOM, oldVdom.props, newVdom.props);
    /* 更新子节点:需要将真实dom 作为子节点的容器,继续比对子节点 */
    updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
  } else if (typeof oldVdom.type === "function") {
    // 如果类型是函数,则判断是函数组件还是类组件
    if (oldVdom.type.isReactComponent) {
      /* 类组件身上有isReactComponent标记 */
      updateClassComponent(oldVdom, newVdom); // 更新类组件
    } else {
      updateFunctionComponent(oldVdom, newVdom); // 更新函数组件
    }
  }
}

3.完善updateChildren

/**
 * 比对更新子节点,这里不是真的doom-diff哈!
 * @param {*} parentDOM 父节点的真实dom,作为子节点的容器
 * @param {*} oldVChildren 旧的子节点
 * @param {*} newVChildren 新的子节点
 */
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  /* 先将子节点转为数组,有可能只有一个子节点的时候,也要转为数组 */
  oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren];
  newVChildren = Array.isArray(newVChildren) ? newVChildren : [newVChildren];
  /* 新旧子节点谁的长度大,旧以谁作为循环的标准 */
  let maxLength = Math.max(oldVChildren.length, newVChildren.length);
  /* 遍历子节点 */
  for (let i = 0; i < maxLength; i++) {
    /* 
      在更新前,需要找到标记位置,例如:
      + 旧的是[1,2,3],新的是[1,4,3],我需要知道4应该放在哪个位置!
      + 如果不找这个位置,那4永远只能插入到最后,显然是不行了!
      找当前的虚拟DOM节点这后的最近的一个真实DOM节点作为位置标记!
    */
    let nextVNode = oldVChildren.find(
      (item, index) => index > i && item && findDOM(item)
    );
    /* 递归调用compareTwoVdom方法,继续新的比对更新! */
    compareTwoVdom(
      parentDOM,
      oldVChildren[i],
      newVChildren[i],
      nextVNode && findDOM(nextVNode) // 如果位置标记有值,旧取它的真实dom
    );
  }

上面代码中,已经把函数调用写好了,接下来一一实现相关方法即可!

4.实现函数组件更新

实现updateFunctionComponent方法

/**
 * 更新函数组件
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 */
function updateFunctionComponent(oldVdom, newVdom) {
  /* 第一步:找到旧的真实dom的父节点,作为容器 */
  let parentDOM = findDOM(oldVdom).parentNode;
  /* 
    第二布:从新的虚拟dom中取出type和props
    + type就是函数组件的函数
    + 将props传递给函数,jsx会编译出新的虚拟dom!
  */
  let { type, props } = newVdom;
  let renderVdom = type(props); // 执行函数 得到新的虚拟dom
  /* 第三步:递归调用 compareTwoVdom 继续比对更新! */
  compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  /* 第四步:将新的renderVdom,赋值给oldRenderVdom,作为下一次更新的旧的虚拟dom! */
  newVdom.oldRenderVdom = renderVdom;
}

4.实现类组件更新

实现updateClassComponent方法

/**
 * 更新类组件
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 */
function updateClassComponent(oldVdom, newVdom) {
  /* 
    + 从旧的虚拟dom上取出类组件实例classInstance,并赋值给新的虚拟dom 
    + 这个classInstance是在mountClassComponent的时候,挂载到vdom上的!
  */
  let classInstance = (newVdom.classInstance = oldVdom.classInstance);
  /* 
    旧虚拟dom的oldRenderVdom赋值给 新虚拟dom的oldRenderVdom,用于下一次更新
    oldRenderVdom这个属性,是forceUpdate中调用this.render得到的!也是jsx编译出来的!
  */
  newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
  /* 
    触发组件的生命周期钩子componentWillReceiveProps
    + 此更新可能是由于父组件更新引起的,父组件在重新渲染的时候,给子组件传递新的属性
  */
  if (classInstance.componentWillReceiveProps) {
    classInstance.componentWillReceiveProps();
  }
  // 调用组件实例的updater方法,将新的props传递过去,递归继续更新!
  classInstance.updater.emitUpdate(newVdom.props);
}

5.预处理其他更新函数

为保证代码能运行,需要对其他更新函数进行预设,这里先创建好函数,后面再一一实现!

function updateMemoComponent(oldVdom, newVdom){ // 更新memo组件 }
function updateProviderComponent(oldVdom, newVdom){ // 更新provider组件 }
function updateContextComponent(oldVdom, newVdom){ // 更新context组件 }

结语

到这里已经实现了以下功能:

  1. 实现React.createElement
  2. 实现ReactDom.render
  3. 实现函数组件挂载
  4. 实现类组件挂载
  5. 知识扫盲
    • class中的this问题
    • setState特性解读
    • dom事件机制
    • js事件循环机制
  6. 实现setState及批处理
  7. 实现合成事件
  8. 实现函数组件更新
  9. 实现类组件更新

由于字数限制,第一部分到这里结束;下一篇继续实现更多核心功能!

谢谢观看!