简单手写实现React类组件的state更新
继续学习写源码系列,本系列是学习系列。
本文的目标是,手写实现React
类组件的 state 更新。
TL;DR
- setState两件事,更新state,更新DOM
- 更新DOM,麻烦些,需要将旧DOM挂到组件实例上,新DOM重新生成,然后替换
准备工作
先将index.js
加入类组件
/// import React from './source/react';
// import ReactDOM from './source/react-dom';
import React from 'react';
import ReactDOM from 'react-dom';
class Count extends React.Component {
constructor(props) {
super(props);
this.state = { number: 1 };
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
};
render() {
return (
<div>
{this.state.number} <br />
<button onClick={this.handleClick}>增加</button>
</div>
);
}
}
const reactElement = <Count />;
ReactDOM.render(reactElement, document.getElementById('root'));
加了 state,点击数据变化。
分析 state
this.state
和this.setState
肯定在Component类
上定义了,不然没法直接使用setState
应该做了两件事:更新state
和更新DOM
实现 state
1. 更新state
这没啥,在原始类上,直接更改
class Component {
static isClassComponent = true;
constructor(props) {
this.props = props;
this.state = {};
}
setState(partialState) {
this.state = { ...this.state, ...partialState };
// this.updateDom()
}
}
2. 更新DOM
同样,也需要在原始类上,这边,暂不做DOM diff
,只是单纯的将组件的新DOM
替换旧DOM
。
于是:
- 必须知道组件生成的真实 DOM 的父节点
- state 修改之后,需要用新生成的 DOM,替换掉旧 DOM
问题在于,原始类里此时并不知道父节点,也不知道旧 DOM,更不知道新 DOM,=。=
想想怎么办呢????
回归到最开始的步骤,ReactDOM.render(<Count/>,root)
;
<Count/>
实际上就是React.createElement({type: class Count..,props://})
,既然是类,那么可以将真实的 DOM 以属性的形式添加到实例上,真实的 DOM 挂载之后,父节点可以通过 parentNode 获取。而新的 DOM,可以通过之前的方法createDOM(vdom)
去生成。
2.1 更新 react-dom.js
// {type:class xx,props:{}}
const handleClassType = (vdomClassType) => {
const { type: classFn, props } = vdomClassType;
const instance = new classFn(props);
// 这里添加
// 这里将本身的vdom绑定到实例上,再在vdom转化成dom的时候,再把dom挂上来
instance.curRenderVdom = vdomClassType;
const vdomReturn = instance.render(props);
return createElementDOM(vdomReturn);
};
export function createDOM(vdom) {
const type = getType(vdom);
const getDom = typeMap.get(type);
let DOM = getDom(vdom);
// 这里添加
// 这里DOM挂到vdom上,这样从instance.curRenderVdom.dom就可以拿到现存dom了
type !== 'text' && (vdom.dom = DOM);
console.log(vdom);
return DOM;
}
2.2 更新 react.js
setState(partialState) {
this.state = { ...this.state, ...partialState };
this.updateDom();
}
updateDom() {
// 找到组件挂载的元素,也就是父元素
// 重新生成组件的DOM,然后替换掉 现在的DOM,就更新啦
const curDom = this.curRenderVdom.dom;
const parentNode = curDom.parentNode;
console.log('setState', this.curRenderVdom, curDom);
// render是类组件必须有的,返回JSX,本质就是React.createElement(),返回值跟参数七七八八,也是有{type:class xx,props:{}}
const newVdom = this.render();
const newDom = createDOM(newVdom);
// 这里注意,需要手动将curRenderVdom替换
this.curRenderVdom = newVdom;
// 替换
parentNode.replaceChild(newDom, curDom);
}
将index.js
里面的react
和react-dom
重新换成我们自己写的,页面正常显示,✌️~~
补充
这里,React.createElement(参数)
,参数的类型大约四种,不同的种类,生成 DOM 的方式略有差别,所以分开了,贴上所有react-dom.js
的代码。
对了,还有事件属性,需要额外处理,也一并在下面补充了,不再赘述。
function render(vdom, container) {
// 本质就是挂载,因container本身存在于文档之中,所以挂载操作会触发渲染
mount(vdom, container);
}
function mount(vdom, container) {
// 将第一个参数变成真实DOM,插入到第二个参数挂载元素上
const DOM = createDOM(vdom);
container.append(DOM);
}
// vdom有四种类型
// 1. "直接就是字符串"
const handleTextType = (vdom) => document.createTextNode(vdom || '');
// 2. {type:'div',props:{...}} type是字符串,组件根元素是原生标签
const handleElementType = (vdom) => createElementDOM(vdom);
// 3. {type:function X(){ return <h1/>},props:{...}},type是函数,返回值才是vdom
const handleFunctionType = (vdom) => {
const { type: fn, props } = vdom;
const vdomReturn = fn(props);
return createElementDOM(vdomReturn);
};
// 4. {type: class x ...{ render(){return <h1/>} },props:{///}},type是函数,
// 但是静态属性有isClassComponent,实例的render函数返回值才是vdom
const handleClassType = (vdom) => {
const { type: classFn, props } = vdom;
const instance = new classFn(props);
// 这里将本身的vdom 绑定到实例上(不是vdomReturn,不然找不到curRenderVdom)
instance.curRenderVdom = vdom;
console.log('执行');
const vdomReturn = instance.render(props);
return createElementDOM(vdomReturn);
};
// 根据vdom得到类型,从而根据类型,调用相应的方法生成真实DOM
const getType = (vdom) => {
const isTextNode =
typeof vdom === 'string' || typeof vdom === 'number' || vdom == null;
if (isTextNode) return 'text';
const isObject = typeof vdom === 'object';
const isElementNode = isObject && typeof vdom.type === 'string';
if (isElementNode) return 'element';
const isFn = isObject && typeof vdom.type === 'function';
return isFn && vdom.type.isClassComponent ? 'class' : 'function';
};
const typeMap = new Map([
['text', handleTextType],
['element', handleElementType],
['function', handleFunctionType],
['class', handleClassType],
]);
export function createDOM(vdom) {
const type = getType(vdom);
const getDom = typeMap.get(type);
// @ts-ignore
let DOM = getDom(vdom);
// vdom和DOM一一对应,这里的vdom是四种类型
type !== 'text' && (vdom.dom = DOM);
console.log(vdom);
return DOM;
}
function createElementDOM(vdom) {
const { type, props } = vdom;
let DOM = document.createElement(type);
if (props) {
updateProps(DOM, props);
const { children } = props;
children && updateChildren(DOM, children);
}
return DOM;
}
function updateProps(DOM, props) {
// 正常遍历就好,特殊的特殊处理
for (const key in props) {
if (key === 'children') continue;
// 事件处理!!
if (/on[A-Z]+/.test(key)) {
DOM[key.toLowerCase()] = props[key];
continue;
}
if (key === 'style') {
updateStyle(DOM, props[key]);
continue;
}
DOM[key] = props[key];
}
function updateStyle(DOM, styleObj) {
for (const key in styleObj) {
DOM.style[key] = styleObj[key];
}
}
}
function updateChildren(DOM, children) {
// 单个节点,直接插入(挂载)到DOM上; 多个节点,遍历插入
const isOneChildren = !Array.isArray(children);
isOneChildren
? mount(children, DOM)
: children.forEach((child) => mount(child, DOM));
}
const ReactDOM = {
render,
};
export default ReactDOM;
转载自:https://juejin.cn/post/7143201165998555143