简单手写实现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;