简介
本文教你如何实现一个类 react 15
的框架,在实现的过程中了解 react
的生命周期函数,异步setState
这些是如何实现的。
JSX
JSX
是一种 JavaScript
的语法扩展,运用于React架构中。在 react
中 jsx
会被转换为虚拟DOM。
什么是虚拟DOM呢。简单理解就是一个格式固定了的对象。
const title = <div className="name">111</div>;
比如这段代码在 react
编译的时候,会转换为一个方法,这个方法的返回值就是虚拟DOM。
const title = React.createElement("div", attrs:{className:"name"}, "111");
// 虚拟dom
const DOM = {
tag:'div',
attrs:{
className:"name"
},
children:["111"]
}
搭建项目
-- 目录 --
│ dist #打包后的文件
│ node_modules #npm下载的包文件
│ src #开发代码
│ babel.config.json #babel 插件功能配置
│ package.json #初始化项目文件
│ webpack.dev.config.js #webpack开发配置文件
我们需要在编译时把 jsx
自动转为虚拟DOM,要使用@babel/plugin-transform-react-jsx
这个插件。
使用时,只需要在 babel
配置中把它放出来。
// babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
// 配置 遇到 jsx 类型的数据 自动转换为 React.createElement
"pragma": "React.createElement"
}
]
]
}
// package.json 初始化项目
{
"name": "react-simulation-15",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --config webpack.dev.config.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/plugin-transform-react-jsx": "^7.12.12",
"babel-loader": "^8.2.2",
"webpack": "^5.11.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {}
}
// webpack.dev.config.js 启动配置
const path = require("path");
module.exports = {
// 模式 开始模式 会自动预设安装插件 模式不同安装插件不同
// 可以使用 node 自带的 process.env.NODE_ENV 来获取所在的环境
mode: 'development',// production 生产模式 development 开发模式
/* 入口 打包开始的文件*/
entry: path.join(__dirname, "src/index.js"),
/* 输出到dist目录,输出文件名字为dist.js */
output: {
path: path.join(__dirname, "dist"),
filename: 'dist.js',
},
/* cacheDirectory是用来缓存编译结果,下次编译加速 */
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ["babel-loader?cacheDirectory=true"],
include: path.join(__dirname, "src"),
},
]
},
// webpack-dev-server
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true, // gzip压缩
host: "0.0.0.0", // 允许ip访问
hot: true, // 热更新
historyApiFallback: true, // 解决启动后刷新404
port: 8111, // 端口
},
};
根据插件的要求我们需要自定义一个 React.createElement
,在 src/index.js
中编写代码。
- 编译时插件会直接调用该方法并传入多个参数,可以理解为使用了回调函数。
const React = {};
// tag DOM节点的标签名
// attrs 节点上的所有属性
// children 子节点
React.createElement = function(tag, attrs, ...children) {
return {
tag,
attrs,
children
};
};
console.log(<div>111</div>)
然后在dist
文件夹下添加index.html
。启动项目,console.log
打印成功,表示我们项目初始化成功。
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title></head>
<body><div id="root"></div><script src="dist.js"></script></body>
</html>
ReactDOM.render
ReactDOM.render
是 react
的入口方法。接下来就是实现这个入口函数。
ReactDOM.render(
<div>111</div>,
document.getElementById('root')
);
从这段代码可以看出,第一个参数是一个jsx
( 虚拟DOM ),第二个参数是一个真实DOM。我们通过虚拟DOM渲染成的真实DOM都将放入第二个参数中(简称容器)。
那么 render
这个方法,第一步要做的就是把虚拟DOM转换为真实DOM节点然后放入容器中。
const ReactDOM = {};
/**
*
* @param {*} vDom 虚拟DOM
* @param {*} container 容器
*/
ReactDOM.render = function (vDom, container) {
container.innerHTML = ""; // 清空容器
// 放入容器中
return container.appendChild(initComponent(vDom));
};
/**
* 创建真实节点
* @param {*} vDom 虚拟DOM
* @param {*} container 容器
*/
function initComponent(vDom) {
// 错误节点 修改为空
if (vDom === null || vDom === undefined || typeof vDom === "boolean") vDom = "";
// 文本返回文本节点
if (typeof vDom === "number" || typeof vDom === "string") {
vDom = String(vDom);
let textNode = document.createTextNode(vDom);
return textNode;
}
// 虚拟DOM 生成真实节点
const dom = document.createElement(vDom.tag);
// 添加属性
setAttr(dom, vDom.attrs);
if (vDom.children) {
// 有子节点 重复操作
vDom.children.forEach((child) => dom.appendChild(initComponent(child)));
}
return dom;
}
当然只是创建真实DOM节点是不够的,还要在节点上为它添加上属性。
/**
* 修改属性
* @param {*} dom 真实节点
* @param {*} attrs 属性 数组对象
*/
function setAttr(dom, attrs) {
Object.keys(attrs || []).forEach((key) => {
const value = attrs[key];
if (key === "style") {
// 样式
if (value && typeof value === "object") {
for (let name in value) {
dom.style[name] = value[name];
}
} else {
dom.removeAttribute(key);
}
} else if (/on\w+/.test(key)) {
// 事件处理 直接赋值
key = key.toLowerCase();
dom[key] = value || "";
if (!value) {
dom.removeAttribute(key);
}
} else {
// 当值为空 删除属性
if (value) {
dom.setAttribute(key, value);
} else {
dom.removeAttribute(key);
}
}
});
}
// ----------------- 使用 -----------------
ReactDOM.render(
<div name="111">111</div>,
document.getElementById('root')
);
启动项目页面展示 111
,到这我们实现了浏览器基础节点展示,下面开始加入组件的转换。
加入组件和实现生命周期函数
用 JSX
编写组件后,@babel/plugin-transform-react-jsx
会帮我们把第一个参数tag
变成function
这个字符串,用于我们区分是否是组件。
有状态的组件都有一个基类 React.Component
,主要用用初始化 state & props
的数据 和 setState
的功能(执行后更新组件)。这里只是实现一个简单的功能后续会修改。
// 基类
React.Component = class Component {
constructor(props = {}) {
this.state = {};
this.props = props;
}
setState(stateChange) {
// 保存上一次的状态
this.oldState = JSON.parse(JSON.stringify(this.state));
// 合并 state
Object.assign(this.state, stateChange);
// 更新组件状态
renderComponent(this);
}
render() {
throw "组件无渲染!!!";
}
}
在 react
中组件分为两种:继承了基类的 和 未继承的无状态组件。
添加 createComponent
公用方法。用于处理传入组件,实例化一个新组件出来。
/**
* 创建组件
* @param {*} component 函数组件
* @param {*} props 属性值
*/
function createComponent(component, props) {
let comp;
// 根据原型判断 是否是 继承基类的组件
if (component.prototype && component.prototype.render) {
// 返回实例化组件
comp = new component(props);
} else {
comp = new React.Component(props);
// 修改构造函数 取消默认的 state --取消组件的状态
comp.constructor = component;
// 当执行 render 默认执行函数 并获取 return 中的 jsx
comp.render = function () {
return this.constructor(props);
};
}
return comp;
}
组件的渲染过程中是有声明周期函数的,当 props
修改后我们需要实现这个生命周期。
添加 setComponentProps
用于修改组件的 props
。
/**
* 修改组件属性值
* @param {*} component 函数组件
* @param {*} props 属性值
*/
function setComponentProps(component, props) {
// 是否保存 真实DOM 后面生成节点时 创建
if (!component.DOM) {
// 声明周期函数 初始化 第一次加载组件执行
if (component.willMount) component.willMount();
} else if (component.base && component.receiveProps) {
// 后续修改 props 执行
component.receiveProps(props);
}
// 修改保存 props
component.props = props;
// 生成对应真实DOM
renderComponent(component);
}
除了修改 props
的生命周期函数,在生成真实DOM前后也有其他生命周期函数。
添加 renderComponent
创建组件真实DOM 替换 旧DOM,并调用不同阶段的声明周期函数。
/**
* 生成对应真实DOM 并替换 旧DOM
* @param {*} component 函数组件
*/
function renderComponent(component) {
let DOM;
// 获取 组件的虚拟DOM
const vDom = component.render();
// 生命周期函数 修改 真实节点创建前的 什么周期函数
if (component.DOM && component.willUpdate) component.willUpdate();
// 判断组件 是否继续进行 更新操作的函数
if (component.DOM && component.shouldUpdate) {
// 如果组件经过了初次渲染,是更新阶段,那么可以根据这个生命周期判断是否更新
let result = true;
// 根据 组件返回状态判断 是否终止
result =
component.shouldUpdate &&
component.shouldUpdate(component.props, component.state);
if (!result) {
// 终止更新 不修改对应的 state
component.state = JSON.parse(JSON.stringify(component.oldState));
component.prevState = JSON.parse(JSON.stringify(component.oldState));
return;
}
}
// 得到真实DOM
DOM = initComponent(vDom);
// DOM = diffNode(component.DOM, vDom);
if (component.DOM) {
// 真实DOM 生成后 执行
if (component.didUpdate) component.didUpdate();
} else if (component.didMount) {
// 第一次 DOM加载完后执行
component.didMount();
}
// 当存在真实DOM节点 新真实DOM 替换 旧的DOM
if (component.DOM && component.DOM.parentNode) {
component.DOM.parentNode.replaceChild(DOM, component.DOM);
}
// 绑定真实DOM
component.DOM = DOM;
// DOM绑定 本次组件
DOM._component = component;
}
最后修改 initComponent
添加组件函数的对比渲染。
/**
* 创建真实节点
* @param {*} vDom 虚拟DOM
* @param {*} container 容器
*/
function initComponent(vDom) {
// 错误节点 修改为空
if (vDom === null || vDom === undefined || typeof vDom === "boolean")
vDom = "";
// 文本返回文本节点
if (typeof vDom === "number" || typeof vDom === "string") {
vDom = String(vDom);
let textNode = document.createTextNode(vDom);
return textNode;
}
// 组件DOM
if (typeof vDom === "object" && typeof vDom.tag === "function") {
//先创建组件
const component = createComponent(vDom.tag, vDom.attrs);
// 设置属性
setComponentProps(component, vDom.attrs);
//返回的是真实dom对象
return component.DOM;
}
// 默认节点
if (typeof vDom === "object" && typeof vDom.tag === "string") {
// 虚拟DOM 生成真实节点
const dom = document.createElement(vDom.tag);
// 添加属性
setAttr(dom, vDom.attrs);
if (vDom.children) {
// 有子节点 重复操作
vDom.children.forEach((child) => dom.appendChild(initComponent(child)));
}
return dom;
}
}
修改使用框架的代码,启动项目就可以测试了。
// ----------------- 使用 -----------------
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 0,
};
}
// shouldUpdate(po, st) {
// console.log("st", st, po);
// if (st.num > 3) {
// return false;
// }
// return true;
// }
willMount() {
console.log("初始化");
}
but() {
this.setState( {num:this.state.num + 1})
}
render() {
return (
<h1>
<button type="button" onclick={this.but.bind(this)}>+</button>
{this.state.num}, {this.props.name}
</h1>
);
}
}
function App() {
return (
<div>
<Home key={1} name={`你好`} />
<Home key={2} name={"你好"} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
到这我们实现了展示组件,组件的生命周期函数,setState
功能。
实现diff算法
接下来就是实现react
中的diff算法
,当然这里的算法非常简单。复杂的算法 diff 算法原理概述。
这里实现得比较简单,简单介绍下diff算法
中的几种对比:
- 纯文本或者数字的对比
- 组件对比
- 浏览器节点对比
- 子节点的对比,等
/**
* 节点对比
* @param {*} dom 真实DOM
* @param {*} vDom 虚拟DOM
*/
function diffNode(dom, vDom) {
let newDom = dom;
// 错误节点 修改为空
if (vDom === null || vDom === undefined || typeof vDom === "boolean")vDom = "";
// 文本对比
if (typeof vDom === "number" || typeof vDom === "string") {
vDom = String(vDom);
// 如果当前的DOM就是文本节点,则直接更新内容
if (dom && dom.nodeType === 3) {
if (dom.textContent !== vDom) {
dom.textContent = vDom;
}
} else {
// 创建节点
newDom = document.createTextNode(vDom);
// 判断当前 真实DOM 是否存在 如果是 就替换节点
if (dom && dom.parentNode) {
dom.parentNode.replaceChild(dom, newDom);
}
}
return newDom;
}
// 组件对比
if (typeof vDom === "object" && typeof vDom.tag === "function") {
return diffComponent(newDom, vDom);
}
// 默认节点对比
if (typeof vDom === "object" && typeof vDom.tag === "string") {
// 判断 节点 类型 是否相同
if (!dom || !isSameNodeType(dom, vDom)) {
// 创建 新节点
newDom = document.createElement(vDom.tag);
if (dom) {
if (vDom.children) {
// 当 虚拟DOM有子节 时 将原来的子节点移到新节点下
[...dom.childNodes].map(newDom.appendChild);
}
if (dom.parentNode) {
// 移除掉原来的DOM对象
dom.parentNode.replaceChild(newDom, dom);
}
}
// 修改属性
diffAttributes(newDom, vDom);
}
//
if (
(vDom.children && vDom.children.length > 0) ||
(newDom.childNodes && newDom.childNodes.length > 0)
) {
diffChildren(newDom, vDom.children);
}
return newDom;
}
}
在节点对比过程中,会多次使用相同的功能和判断,抽取为公用函数。
/**
* 删除组件 并调用离开生命周期
* @param {*} component
*/
function unmountComponent(component) {
if (component.willUnmount) component.willUnmount();
removeNode(component.DOM);
}
/**
* 删除 真实节点
* @param {*} dom
*/
function removeNode(dom) {
if (dom && dom.parentNode) {
dom.parentNode.removeChild(dom);
}
}
/**
* 判断真实节点 和 虚拟DOM 类型是否相同
* @param {*} dom
* @param {*} VDOM
*/
function isSameNodeType(dom, VDOM) {
if (typeof VDOM === "string" || typeof VDOM === "number") {
return dom.nodeType === 3;
}
if (typeof VDOM.tag === "string") {
return dom.nodeName.toLowerCase() === VDOM.tag.toLowerCase();
}
return dom && dom._component && dom._component.constructor === VDOM.tag;
}
组件对比,当组件类型相同修改属性,不同相同删除原来的组件,创建一个新的组件返回。
/**
* 组件类型 处理
* @param {*} dom 真实DOM
* @param {*} vDom 虚拟DOM
*/
function diffComponent(dom, vDom) {
// 之前的虚拟DOM
let comp = dom && dom._component;
if (comp && comp.constructor === vDom.tag) {
// 之前的虚拟DOM 和 现在的是同一个
// 修改 props 属性
setComponentProps(comp, vDom.attrs);
// 获取最新的 真实DOM
dom = comp.DOM;
} else {
// 当两次虚拟DOM 不同 删除之前的组件
if (comp) {
unmountComponent(comp);
}
// 先创建新组件
const component = createComponent(vDom.tag, vDom.attrs);
// 设置属性 生成组件DOM
setComponentProps(component, vDom.attrs);
// 获取最新的 真实DOM
dom = component.DOM;
}
return dom;
}
浏览器节点的属性修改。
/**
* 修改节点 属性
* @param {*} dom 真实DOM
* @param {*} vDom 虚拟DOM
*/
function diffAttributes(dom, vDom) {
const olds = {}; // 旧DOM的属性
const attrs = vDom.attrs; // 虚拟DOM的属性
for (let i = 0; i < dom.attributes.length; i++) {
const attr = dom.attributes[i];
olds[attr.name] = undefined;
}
// 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)
setAttr(dom, olds);
// 更新新的属性值
setAttr(dom, attrs);
}
子DOM节点的对比,这里写得很简单。
/**
* 子组件对比
* @param {*} dom
* @param {*} vchildren
*/
function diffChildren(dom, vchildren) {
// 获取原来的节点
const domChildren = dom.childNodes;
const keyed = {};
// 将有key的节点获取
if (domChildren.length > 0) {
for (let i = 0; i < domChildren.length; i++) {
const child = domChildren[i];
const key = child._component?.props?.key;
if (key) {
keyed[key] = child;
}
}
}
// 子节点 对比
if (vchildren && vchildren.length > 0) {
for (let i = 0; i < vchildren.length; i++) {
const vchild = vchildren[i];
const key = vchild.attrs?.key;
let child; // 旧的真实节点
// 如果有key,找到对应key值的节点
if (key) {
if (keyed[key]) {
child = keyed[key];
keyed[key] = undefined;
}
}
console.log(child,vchild,keyed)
// 对比节点返回 新节点
let newChild = diffNode(child, vchild);
// 获取当前 虚拟DOM对应的真实节点
const f = domChildren[i];
if (newChild && newChild !== dom && newChild !== f) {
if (!f) {
// 如果更新前的对应位置为空,说明此节点是新增的
dom.appendChild(newChild);
} else if (newChild === f.nextSibling) {
// 如果更新后的节点和更新前对应位置的下一个节点一样,说明当前位置的节点被移除了
removeNode(f);
} else {
// 将更新后的节点移动到正确的位置
// 在已有节点之前插入
// 注意insertBefore的用法,第一个参数是要插入的节点,第二个参数是已存在的节点
dom.insertBefore(newChild, f);
if (!child) {
removeNode(f);
}
}
}
}
}
}
开始diff对比,要修改 ReactDOM.render
和 renderComponent
/**
*
* @param {*} vDom 虚拟DOM
* @param {*} container 容器
*/
ReactDOM.render = function (vDom, container) {
container.innerHTML = ""; // 清空容器
// 放入容器中
// return container.appendChild(initComponent(vDom));
return container.appendChild(diffNode(null, vDom));
};
/**
* 生成对应真实DOM 并替换 旧DOM
* @param {*} component 函数组件
*/
function renderComponent(component) {
...
// 得到真实DOM
// DOM = initComponent(vDom);
DOM = diffNode(component.DOM, vDom);
...
}
现在我们实现了diff算法,这里主要是使用key来判断之前的组件是否存在,存在就使用之前的DOM只修改属性。
异步state
在进入下面之前先了解浏览器的事件循环机制。简单来说就是 js
在执行过程中有两个队列( 队列的特点是先进先出 ),一个宏任务队列,一个微任务队列。他们的执行顺序是,主任务执行过程中把对应的宏任务(setTimeout
等),微任务(Promise.then()
等)分别放入各自的队列中。主任务执行完后,在宏任务队列中执行一个宏任务,然后去执行所有的微任务。这里要注意的是,是所有的微任务都会执行。重复这个步骤直到没有任务。JavaScript 运行机制详解
之前我们实现的 setState
每次执行都会更新组件,如果一个操作中多次使用 setState
对性能影响较大。所以我们需要优化。先把一次操作中的所有 setState
都放入数组中,把要更新的组件也放入另个数组中(数组中,组件不添加重复的),然后把 setState
合并 和 更新组件的方法 flush()
放入微任务中,就能保证在主任务执行完后在执行这个微任务。
let setStateQueue = []; // state队列
let renderQueue = []; // 组件队列
/**
* 合并本次的所有 state
* @param {*} stateChange
* @param {*} component
*/
function enqueueSetState(stateChange, component) {
// 合并操作的微任务只需要执行一次
if (setStateQueue.length === 0) {
defer(flush);
}
// 合并 state 队列
setStateQueue.push({ stateChange, component });
// 只放入不存在的组件
if (!renderQueue.some((item) => item === component)) {
renderQueue.push(component);
}
}
/**
* 执行合并 state 和 更新组件
* 并清空 state队列 和 组件队列
*/
function flush() {
let item, component;
while ((item = setStateQueue.shift())) {
const { stateChange, component } = item;
// 是否存在 上一个 state 不存在 添加 第一个
if (!component.prevState) {
component.prevState = Object.assign({}, component.state);
}
// 判断是否是方法
if (typeof stateChange === "function") {
// 合并 方法 返回的值 未最新 state
Object.assign(
component.state,
stateChange(component.prevState, component.props)
);
} else {
// 合并state
Object.assign(component.state, stateChange);
}
// 更新 下一次 prevState 为最新
component.prevState = component.state;
}
while ((component = renderQueue.shift())) {
// 更新组件
renderComponent(component);
}
}
// 事件执行机制 主任务全部执行完后 执行 微任务
function defer(fn) {
return Promise.resolve().then(fn);
}
然后修改基类 React.Component
加入异步state
...
setState(stateChange) {
// 保存上一次的状态
this.oldState = JSON.parse(JSON.stringify(this.state));
// 合并 state
// Object.assign(this.state, stateChange);
// 更新组件状态
// renderComponent(this);
// 异步state
enqueueSetState(stateChange, this);
}
...
现在 异步state
加入成功了。修改执行一次操作中加入多次setState
就可以测试了,只更新一次表示成功。
class Home extends React.Component {
...
but() {
for (let i = 0; i < 10; i++) {
this.setState((pre) => {
return { num: 1 + pre.num };
});
}
}
...
}
这篇文章的代码:源码地址