手写React(三)实现setState及类组件更新
从零到1,手写实现react!
本文接着前面的手写类组件+函数组件,继续实现类组件的更新
传送
还是那句话:
网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!
自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!
我这个一步步手写实现React的笔记,希望可以帮到你。
一、前言
在开始手写之前,你可能需要了解以下几个知识点:
- 类组件中的方法,如果不是箭头函数,都会被编译成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(() => {
queue.forEach(newSate => {
state = { ...state, ...newSate }
})
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
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],这里不再赘述
二、实现state/setState
回到正文:在前面的基础上,开始实现Component类的state和setState
1.完善Component
export class Component {
// 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
static isReactComponent = true;
constructor(props) {
this.props = props; // 初始化props
this.state = {}; // 初始化state
this.updater = new Updater(this); // 初始化组件的更新器
}
/* 实现setState方法,将接收的参数,传递给updater.addState */
setState(partialState, callback) {
this.updater.addState(partialState, callback);
}
}
2.Updater
更新操作其实是放在updater中去处理的,现在开始实现这个构造函数
1) 创建Update
class Updater {
constructor(classInstance) {
// 初始化类组件实例,每个updater对应一个类组件
this.classInstance = classInstance;
// 初始化setState更新队列
this.pendingState = [];
// 初始化setState的回调函数队列
this.callbacks = [];
}
}
2) addState
将更新操作放入队列中,然后调用emitUpdate方法处理更新逻辑
class Updater {
// ……
/* 实现addState方法 */
addState(partialState, callback) {
// 将setState放入队列中
this.pendingState.push(partialState);
if (typeof callback === "function") {
// 如果第二个参数是函数,继续放入回调函数队列中
this.callbacks.push(callback);
}
// 执行更新操作
this.emitUpdate();
}
// ……
}
3) emitUpdate
class Updater {
// ……
/* 不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法 */
emitUpdate() {
// 这里后面会有一些更新判断,先跳过
this.updateComponent(); // 让组件更新
}
// ……
}
4) updateComponent
处理组件更新,更新前需要先调用getState方法,获取最新的state状态
class Updater {
// ……
/* 处理组件更新 */
updateComponent() {
const { classInstance, pendingState } = this;
// 如果当前更新队列中有数据才执行
if (pendingState.length > 0) {
// 将组件实例,和最新的状态,传递给shouldUpdate函数,开始组件更新
shouldUpdate(classInstance, this.getState());
}
}
// ……
}
5) getState
执行传入的setState,计算出最新的state
class Updater {
// ……
/* 根据老状态,和pendingStates这个更新队列,计算新状态 */
getState() {
let { classInstance, pendingState } = this;
let { state } = classInstance; // 取出组件实例上的state
// 遍历执行队列
pendingState.forEach((nextState) => {
// 如果传递的是函数,就执行该函数,并把state传递过去
if (typeof nextState === "function") {
nextState = nextState(state);
}
// 最终合并state
state = { ...state, ...nextState };
});
pendingState.length = 0; // 清空队列
classInstance.state = state; // 重新赋值
this.callbacks.forEach((callback) => callback()); // 执行回调队列
this.callbacks.length = 0; // 清空回调队列
return state; // 返回新的状态
}
// ……
}
6) shouldUpdate
后面这个函数中,会处理是否更新组件的逻辑
/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
console.log('状态修改完毕,开始更新组件')
}
7) 事件处理
回到react-dom中,找到updateProps方法,需要对传入的onXxx事件做一下简单处理
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
// …… 如果传入的属性 是以on开头 就进行事件绑定
} else if (key.startsWith("on")) {
// 先简单处理下事件绑定,后面再改为合成事件!
dom[key.toLocaleLowerCase()] = newProps[key]; //dom.onclick=handleClick
} else {
// ……
}
}
8) 查看效果
回到index.jsx中,创建一个案例,点击按钮改变state,测试一下
// 用自己封装的
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 = () => {
this.setState({ num: this.state.num + 1 }, () => {
console.log(this.state.num);
});
};
render() {
return (
<div id="abc">
num: {this.state.num}
<button onClick={this.addNum}>+</button>
</div>
);
}
}
let element = React.createElement(ClassComponent);
/*
注意: v18八本的 jsx编译后的vdom是被冻结的!!
+ 被冻结的原因:为了安全,以及规范
+ 就像你进学校,只能通过校门进去,不能翻墙进去!
+ 所以,如果你是用的v18版本,那么需要降低版本到v17!!
*/
ReactDOM.render(element, document.getElementById("root"));
可以看到,回调函数中打印的state已经发生改变,这一步已经简单实现了!后面再来完善批处理!
三、实现组件更新
我们先对上面的shouldUpdate方法进行完善
1.完善shouldUpdate
/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
// 将实例上的state,改为最新的state,这里后面再做比对优化……
classInstance.state = nextState;
// 调用组件实例的forceUpdate方法,更新组件
classInstance.forceUpdate();
}
2.完善真实dom查找
上面可以看到,我们接着要实现forceUpdate方法,但是在实现此方法之前,还要做一些事:
- 在每个vdom上,都添加一个dom属性,存放它的真实dom!
- 组件在作为vdom的type时,并没有真实dom,需要添加oldRenderVdom属性,以便于查找其真实dom
1) 完善createDom
回到react-dom.js中,在每次真实dom创建完后,都将真实dom赋值给虚拟dom的dom属性!
function createDom(vdom) {
// ……
/*
将真实dom挂载再vdom身上,
面组件更新时,子节点需要用将真实dom作为parent,然后才能操作dom更新
后面如果设置了ref 还会把这个dom给ref
*/
vdom.dom = dom;
return dom;
}
2) 完善mountClassComponent
function mountClassComponent(vdom) {
let { type, props } = vdom;
// new 类组件构造函数 得到实例
let classInstance = new type(props);
// 执行实例上面的render方法,拿到vdom
let renderVdom = classInstance.render();
/* ------------------------------------------------- */
// 将虚拟dom挂载到实例上,这样后面才能通过这个属性 找到对应的真实dom!
classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
/* ------------------------------------------------- */
// 递归创建真实dom
return createDom(renderVdom);
}
3 实现findDOM
用于递归查找当前vdom的真实dom,我们将新的真实dom替换旧的真实dom的时候,肯定需要找到之前的真实dom才能替换!!
到react-dom.js中,创建此方法
/*
查找vdom身上的真实dom!
这个真实dom,就是更新时的容器!
只有找到这个容器,我们才知道新的元素因该放在哪里!!!
*/
export function findDOM(vdom) {
let { type, oldRenderVdom } = vdom;
let dom;
if (isBaseType(type) || type === REACT_TEXT) {
// 原生标签
dom = vdom.dom; // 这个dom 在createDom时创建的!
} else if (oldRenderVdom) {
/*
可能是函数组件/类组件,需要递归调用,因为组件 作为vdom的时候,还没有真实dom,
组件需要被调用一次, 再递归调用createDom的时候,才有真实dom!
并且 组件 可能会嵌套!!例如:
const Child = () => <div>111</div>;
const Parent = () => <Child />;
此时Parent身上没有真实dom! 它的dom还是一个vdom!所以需要递归继续找!
*/
dom = findDOM(oldRenderVdom);
}
return dom;
}
4.实现forceUpdate
回到Component中,我们开始来实现组件的强制更新函数
export class Component {
// ……
/*
强制更新一次组件,组件更新:
+ 1.获取 老的虚拟DOM <React元素>
+ 2.根据最新的属生和状态计算新的虚拟DOM
+ 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
*/
forceUpdate() {
// 获取到老的虚拟dom
let oldRenderVdom = this.oldRenderVdom;
// 找到老的真实dom
let oldDOM = findDOM(oldRenderVdom);
// 在调用forceUpdate之前,state已经更新完毕,此时拿到新的虚拟dom
let newRenderVdom = this.render();
// 更新视图: 比对新旧虚拟dom,创建出新的真实dom, 插入到旧的真实dom上
compareTwoVdom(oldDOM, oldRenderVdom, newRenderVdom);
// 更新完后,将新的虚拟dom作为下一次更新的旧的虚拟dom
this.oldRenderVdom = newRenderVdom;
}
// ……
}
5.实现compareTwoVdom
在这里有一个diff算法 比对新旧虚拟dom,我们后面再一步步实现! 先暴力更新处理一次!
- 根据传入的旧的虚拟dom,找到旧的真实dom
- 根据传入的新的虚拟dom,创建新的真实dom
- 将新的真实dom,替换调旧的真实dom
/**
* @description : 比较新旧虚拟dom,找出差异,更新真实dom
* @param { } parentDom
* @param { } oldVdom
* @param { } newVdom
* @return { }
*/
export function compareTwoVdom(parentDom, oldVdom, newVdom) {
// 找到原来的真实dom
let oldDom = findDOM(oldVdom);
// 创建出新的真实dom
let newDom = createDom(newVdom);
// 用新的 替换旧的!后面再做diff 做节点比对。。。
parentDom.parentNode.replaceChild(newDom, oldDom);
console.log('组件更新完毕!')
}
6.看下效果
点击 + 按钮,视图实现更新!
7.完整代码如下
index.jsx
// import React from "react";
// import ReactDOM from "react-dom/client";
// 用自己封装的
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 = () => {
this.setState({ num: this.state.num + 1 }, () => {
console.log("最新的state", this.state.num);
});
};
render() {
return (
<div id="abc">
num: {this.state.num}
<button onClick={this.addNum}>+</button>
</div>
);
}
}
let element = React.createElement(ClassComponent);
/*
注意: v18八本的 jsx编译后的vdom是被冻结的!!
+ 被冻结的原因:为了安全,以及规范
+ 就像你进学校,只能通过校门进去,不能翻墙进去!
+ 所以,如果你是用的v18版本,那么需要降低版本到v17!!
*/
ReactDOM.render(element, document.getElementById("root"));
Component.js
/*
* @Description : 类组件实现
* @Author : zhangyuru
* @FilePath : Component.js
*/
import { compareTwoVdom, findDOM } from "./react-dom";
/**
* @description : Component父类
* @return { void }
*/
export class Component {
// 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
static isReactComponent = true;
constructor(props) {
this.props = props; // 初始化props
this.state = {}; // 初始化state
this.updater = new Updater(this); // 初始化组件的更新器
}
/* 实现setState方法,将接收的参数,传递给updater.addState */
setState(partialState, callback) {
this.updater.addState(partialState, callback);
}
/*
强制更新一次组件,组件更新:
+ 1.获取 老的虚拟DOM <React元素>
+ 2.根据最新的属生和状态计算新的虚拟DOM
+ 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
*/
forceUpdate() {
// 老的虚拟dom
let oldRenderVdom = this.oldRenderVdom;
// 找到老的真实dom
let oldDOM = findDOM(oldRenderVdom);
// 在调用forceUpdate之前,state已经更新完毕,此时拿到新的虚拟dom
let newRenderVdom = this.render();
// 更新视图: 比对新旧虚拟dom,创建出新的真实dom, 插入到旧的真实dom上
compareTwoVdom(oldDOM, oldRenderVdom, newRenderVdom);
// 更新完后,将新的虚拟dom作为下一次更新的旧的虚拟dom
this.oldRenderVdom = newRenderVdom;
}
}
class Updater {
constructor(classInstance) {
// 初始化类组件实例,每个updater对应一个类组件
this.classInstance = classInstance;
// 初始化setState更新队列
this.pendingState = [];
// 初始化setState的回调函数队列
this.callbacks = [];
}
/* 实现addState方法 */
addState(partialState, callback) {
// 将setState放入队列中
this.pendingState.push(partialState);
if (typeof callback === "function") {
// 如果第二个参数是函数,继续放入回调函数队列中
this.callbacks.push(callback);
}
// 执行更新操作
this.emitUpdate();
}
/* 不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法 */
emitUpdate() {
// 这里后面会有一些更新判断,先跳过
this.updateComponent(); // 让组件更新
}
/* 处理组件更新 */
updateComponent() {
const { classInstance, pendingState } = this;
// 如果当前更新队列中有数据才执行
if (pendingState.length > 0) {
// 将组件实例,和最新的状态,传递给shouldUpdate函数,开始组件更新
shouldUpdate(classInstance, this.getState());
}
}
/* 根据老状态,和pendingStates这个更新队列,计算新状态 */
getState() {
let { classInstance, pendingState } = this;
let { state } = classInstance; // 取出组件实例上的state
// 遍历执行队列
pendingState.forEach((nextState) => {
// 如果传递的是函数,就执行该函数,并把state传递过去
if (typeof nextState === "function") {
nextState = nextState(state);
}
// 最终合并state
state = { ...state, ...nextState };
});
pendingState.length = 0; // 清空队列
this.callbacks.forEach((callback) => callback()); // 执行回调队列
this.callbacks.length = 0; // 清空回调队列
return state; // 返回新的状态
}
}
/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
classInstance.state = nextState;
classInstance.forceUpdate();
}
constans.js
/*
* @Description : 手写React的常量存放
* @Author : zhangyuru
* @FilePath : contants.js
*/
/* 表示这是一个文本类型的元素 在源码里没有这样一个类型 */
export const REACT_TEXT = Symbol("REACT_TEXT");
react-dom.js
/*
* @Description : 手写react-dom
* @Author : zhangyuru
* @FilePath : react-dom.js
*/
import { REACT_TEXT } from "./contants";
import { isBaseType } from "./utils";
/**
* @description : render方法,创建真实dom,插入到指定容器中
* @param { } vdom
* @param { } container
* @return { } void
*/
function render(vdom, container) {
let newVdom = createDom(vdom); // 调用此方法创建真实dom
container.appendChild(newVdom); // 将真实dom插入到容器中
}
/**
* @description : 根据vdom的描述 创建真实dom
* @param { } vdom
* @return { } 真实dom元素
*/
function createDom(vdom) {
let { type, props } = vdom;
let dom;
if (type === REACT_TEXT) {
// 类型是文本节点,直接创建文本
dom = document.createTextNode(props.content);
} else if (typeof type === "function") {
// 可能是类组件,也可能是函数组件
if (type.isReactComponent) {
// 有isReactComponent属性 说明它是一个类组件
return mountClassComponent(vdom);
} else {
// 否则就是普通函数组件
return mountFunctionComponent(vdom);
}
} else if (typeof type === "string") {
// 类型是string,说明它是一个标签名,创建对应元素
dom = document.createElement(type);
} else if (isBaseType(vdom)) {
// 新版本编译出来的vdom可能是一个string或者number类型,当作文本处理就好
dom = document.createTextNode(vdom);
} else {
throw new Error("无法处理元素类型", type);
}
if (props) {
// 最props进行处理,根据vdom的描述,挂载/更新元素的属性
updateProps(dom, {}, props);
// 对children进行处理
if (typeof props?.children === "object" && !!props?.children?.type) {
// 如果children是对象[只有一个儿子],递归调render继续创建
render(props.children, dom);
}
if (Array.isArray(props?.children)) {
// 如果children是数组 需要遍历再递归创建
reconcileChidren(props.children, dom);
}
if (isBaseType(props.children)) {
// 新版本编译出来的children有可能是string或者number,也当作文本处理
render(props.children, dom);
}
}
/*
将真实dom挂载再vdom身上,
面组件更新时,子节点需要用将真实dom作为parent,然后才能操作dom更新
后面如果设置了ref 还会把这个dom给ref
*/
vdom.dom = dom;
return dom;
}
/**
* @description : 挂载类组件
* @param { } vdom
* @return { } vdom
*/
function mountClassComponent(vdom) {
let { type, props } = vdom;
// new 类组件构造函数 得到实例
let classInstance = new type(props);
// 执行实例上面的render方法,拿到vdom
let renderVdom = classInstance.render();
/* ------------------------------------------------- */
// 将虚拟dom挂载到实例上,这样后面才能通过这个属性 找到对应的真实dom!
classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
/* ------------------------------------------------- */
// 递归创建真实dom
return createDom(renderVdom);
}
/**
* @description : 处理函数组件
* @param { } vdom
* @return { } vdom
*/
function mountFunctionComponent(vdom) {
const { type, props } = vdom;
// type类型是函数,就调用该函数,将props传过去
let renderVdom = type(props);
// 将函数返回的vdom,继续递归创建真实dom
return createDom(renderVdom);
}
/**
* @description : 循环创建/更新子节点
* @param { } children
* @param { } parentDom
* @return { } void
*/
function reconcileChidren(children, parentDom) {
for (let i = 0; i < children.length; i++) {
let childVdom = children[i];
render(childVdom, parentDom); // 递归render
}
}
/**
* @description : 根据vdom的描述,挂载/更新元素的属性
* @param { } dom
* @param { } oldProps 暂时没用,后面会用作diff
* @param { } newProps 新的props
* @return { } void
*/
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
continue; // 暂时跳过,后面会单独处理子节点
}
if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else if (key.startsWith("on")) {
// 先简单处理下事件绑定,后面再改为合成事件!
dom[key.toLocaleLowerCase()] = newProps[key]; //dom.onclick=handleClick
} else {
if (newProps[key]) dom[key] = newProps[key];
}
}
}
/*
查找vdom身上的真实dom!
这个真实dom,就是更新时的容器!
只有找到这个容器,我们才知道新的元素因该放在哪里!!!
*/
export function findDOM(vdom) {
let { type, oldRenderVdom } = vdom;
let dom;
if (isBaseType(type) || type === REACT_TEXT) {
// 原生标签
dom = vdom.dom; // 这个dom 在createDom时创建的!
} else if (oldRenderVdom) {
/*
可能是函数组件/类组件,需要递归调用,因为组件 作为vdom的时候,还没有真实dom,
组件需要被调用一次, 再递归调用createDom的时候,才有真实dom!
并且 组件 可能会嵌套!!例如:
const Child = () => <div>111</div>;
const Parent = () => <Child />;
此时Parent身上没有真实dom! 它的dom还是一个vdom!所以需要递归继续找!
*/
dom = findDOM(oldRenderVdom);
}
return dom;
}
/**
* @description : 比较新旧虚拟dom,找出差异,更新真实dom
* @param { } parentDom
* @param { } oldVdom
* @param { } newVdom
* @return { }
*/
export function compareTwoVdom(parentDom, oldVdom, newVdom) {
// 找到原来的真实dom
let oldDom = findDOM(oldVdom);
// 创建出新的真实dom
let newDom = createDom(newVdom);
// 用新的 替换旧的!后面再做diff 做节点比对。。。
parentDom.parentNode.replaceChild(newDom, oldDom);
console.log('组件更新完毕!')
}
const ReactDOM = {
render,
};
export default ReactDOM;
react.js
/*
* @Description : 手写react
* @Author : zhangyuru
* @FilePath : react.js
*/
import { wrapToVdom } from "./utils";
import { Component } from "./Component";
/**
* @description : 实现createElement方法
* @param { } type 元素类型
* @param { } config 元素属性
* @param { } children 子元素
* @return { } vdom
*/
function createElement(type, config, children) {
if (config) {
delete config.__source;
delete config.__self;
delete config.ref;
delete config.key;
}
// 将接受的第二个参数转为props
let props = { ...(config || {}) };
// 对children的处理
if (arguments.length > 3) {
// 如果参数个数大于3个,说明不止一个子节点,截取子节点并用wrapToVdom处理
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
// props.children = Array.prototype.slice.call(arguments, 2);
} else {
// 只有一个子节点的情况下,继续用wrapToVdom处理
props.children = wrapToVdom(children);
}
// 返回值后面会继续扩展
return { type, props };
}
const React = {
createElement,
Component,
};
export default React;
utils.js
/*
* @Description : 手写React的工具函数
* @Author : zhangyuru
* @FilePath : utils.js
*/
import { REACT_TEXT } from "./contants";
/**
* @description : 创建vdom对象 标记子节点类型
* @param { } element
* @return { } vdom
*/
export function wrapToVdom(element) {
if (typeof element === "string" || typeof element === "number") {
// 虚拟DOM.props.content就是此元素的内容
return { type: REACT_TEXT, props: { content: element } };
} else {
return element;
}
}
/* ---------------------utils中的辅助函数----------------------- */
/**
* @description : 判断是不是能被有效渲染的初始值
* @param { any } target
* @return { boolean } boolean
*/
export function isBaseType(target) {
return typeof target === "string" || typeof target === "number";
}
转载自:https://juejin.cn/post/7274163003157725184