手写实现react系列,实现react的常用api;参照源码简化实现,抽取核心部分,相关函数命名与源码保持一致。
传送
网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!
自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!
(之前整理的不好,这是重新整理一遍的新版本)
一、前言
本文适用于有一定基础的读者!至少已经掌握了react的相关用法!
以及掌握js的关键知识点,包括但不限于:原型与原型链、继承、反向继承、单例、发布订阅、闭包、高阶函数(柯里化、compose、thunk……)、this指向、作用域、作用域链、事件机制、事件模型、装饰器、堆、栈、队列、深浅拷贝……等等!(文章中会穿插一些知识扫盲)
说明:本文只是对react常用api的手写实现,编译部分还是需要jsx去实现(后面如果有时间会整理一篇手写模板编译的)!所以需要依赖react脚手架配置,在其基础上用自己的代码实现一遍react(并不是真正的完全手写)!
1.导图
这是本文所实现的api以及用到的关键函数导图,命名和源码一致。简单做了一个思维导图,如果你愿意参照着手写实现,可以用作参考。
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文件:
constants.js--- 存放公共常量utils.js--- 存放工具函数react.js--- react功能的核心react-dom.js--- react-dom功能的核心event.js--- 合成事件代码component.js--- 类组件的核心
4.非关键代码
先把常量和utils需要用到的代码拿过来
- 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");
- 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,直接从拼接好的函数调用开始实现。
下面开始实现一个简单的初次渲染!
简化的渲染流程图:
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) 浏览器效果
三、实现函数组件挂载
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"));
页面正常显示
四、实现类组件
前面说过,不管类组件还是函数组件,最终都是函数;
函数执行结果就是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"));
看下效果
接下来开始实现类组件更新、合成事件、批处理!
在此之前,先来一个知识扫盲,已经掌握的同学可以跳过~
五、知识扫盲
你需要了解以下几个知识点:
- 类组件中的方法,如果不是箭头函数,都会被编译成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();
接着再看一下控制台输出:
结论:
- 对象简写方式的函数,是属于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分钟内,剧也追了,面也吃了,衣服也洗了
我们假设追剧是同步任务,泡方便面是微任务,洗衣服是宏任务:
- 执行同步任务时(追剧),发现有个微任务(泡方便面),就把方便面泡好(加入微任务队列)
- 遇到一个宏任务(洗衣服),把衣服丢进洗衣机(加入宏任务队列)
- 回来发现方便面泡好了,开始吃泡面(清空微任务队列)
- 然后衣服洗好了,清空宏任务队列。
- 继续执行下一轮同步任务(追剧)
总结:
- 微任务一定会在当前执行栈中的 所有同步任务执行完,才会执行!你不可刚泡好方便面就立即吃吧?
- 宏任务一定会在执行栈中的 所有微任务执行完后,才会执行!你不可能刚把衣服丢进洗衣机就拿出来凉吧?
- 宏任务执行完,如果里面又有新的同步任务,那么会继续执行新的任务队列!
最后:你了解上面的原理后,只需要记住哪些是宏任务,哪些是微任务就可以了!
- 宏任务:定时器、script标签(就是最外层的宏任务)
- 同步任务,就是我们写的普通代码,从上往下依次执行,遇到函数调用,就进入新的执行栈
- 微任务:promise、nextTick、3个html观察器(IntersectionObserver、MutationObserver、ResizeObserver)……
六、实现setState及批处理
先看一张流程图
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
}
七、实现合成事件
完善合成事件处理:
- 将props属性处理中,对事件的绑定方式进行修改
- 创建event.js,用来写合成事件相关的逻辑
- 实现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"));
八、实现组件更新
回到react-dom.js中
所有的更新处理,都从compareTwoVdom函数开始!在前面实现render时,我们已经创建好了该函数,现在开始对该函数进行完善!
触发组件更新的流程图
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组件 }
结语
到这里已经实现了以下功能:
- 实现React.createElement
- 实现ReactDom.render
- 实现函数组件挂载
- 实现类组件挂载
- 知识扫盲
- class中的this问题
- setState特性解读
- dom事件机制
- js事件循环机制
- 实现setState及批处理
- 实现合成事件
- 实现函数组件更新
- 实现类组件更新
由于字数限制,第一部分到这里结束;下一篇继续实现更多核心功能!
谢谢观看!