buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

7.基本的dom-diff #7

Open buxuku opened 3 years ago

buxuku commented 3 years ago

在第一节的createElement里面,返回的对象,如果节点是文本类型的,其children值直接就是一个字符串或者数字.就好比下面这样.

{
    type: 'span',
    children: 'world',
}

在后面的createDom里面,我们也通过if (typeof vdom !== 'object')来判断一个节点是否是文本类型的节点.因为它不是一个对象,这样有一个小麻烦就是,我们没办法在最后把真实的Dom节点挂载到这个虚拟Dom上面.所以在接下来做dom-diff时,想对真实Dom进行增删改查操作时,对于文本类型的节点就比较痛苦了.所以在这之前,我们先做一个小的HACK,把文本类型的节点也返回一个对象形式的虚拟Dom.

新建src/constant/index.js文件,定义一个文本类型的节点,顺便把之前的forward类型也统一进来.

export const REACT_TEXT = Symbol('REACT_TEXT');
export const REACT_FORWARD_COMPONENT = Symbol('REACT_FORWARD_COMPONENT');

修改src/react/index.js文件里面的createElement方法

import {Component} from "./Component";
+import {wrapToVdom} from '../utils';
+import {REACT_FORWARD_COMPONENT} from "../constants";

 const createElement = (type, config = {}, ...children) => {
     const {ref, ...props} = config || {};
     if (children.length) {
-        props.children = children.length > 1 ? children : children[0];
+        props.children = children.length > 1 ? children.map(wrapToVdom) : wrapToVdom(children[0]);
     }
     return {
         $$typeof: Symbol.for('react.element'),
@@ -32,7 +34,7 @@ const createRef = () => {
  */
 function forwardRef(render) {
     return {
-        $$typeof: 'REACT_FORWARD_COMPONENT',
+        $$typeof: REACT_FORWARD_COMPONENT,
         render,
     }
 }

新建src/utils/index.js文件,实现wrapToVdom方法

import {REACT_TEXT} from '../constants'

/**
 * 判断是否是一个文本节点
 * @param element
 * @returns {boolean}
 */
function isTextElement(element) {
    return typeof element === 'string' || typeof element === 'number';
}

/**
 * 如果是文本节点,包装成一个虚拟DOM对象,方便挂载真实DOM节点
 * @param element
 * @returns {{type, props: {content}}|*}
 */
export function wrapToVdom(element) {
    return isTextElement(element) ? {
        type: REACT_TEXT, props: {content: element}
    } : element
}

同时修改src/react-dom/index.js文件里面的createDom方法

+import {wrapToVdom} from '../utils';
+import {REACT_FORWARD_COMPONENT, REACT_TEXT} from '../constants';

 function createDom(vdom) {
     let dom;
-    if (typeof vdom !== 'object') { // render可以直接渲染一个字符串或者数字,它不是一个React.element
-        dom = document.createTextNode(vdom);
-        return dom;
-    }
     const {type, props, ref} = vdom;
-    if(type && type.$$typeof === 'REACT_FORWARD_COMPONENT'){
+    if (type && type.$$typeof === REACT_FORWARD_COMPONENT) {
         return mountForwardComponent(vdom);
     }
+    if(type === REACT_TEXT){
+        dom = document.createTextNode(props.content);
+    }
    if (typeof type === 'string') {
        dom = document.createElement(type);
        renderAttributes(dom, props);
    }
    if (typeof type === 'function') {
        if (type.isReactComponent) { // 是一个类组件
            return mountClassComponent(vdom);
        }
-        let renderVdom = wrapToVdom(type(props));
+        let renderVdom = wrapToVdom(type(props));
        vdom.oldVdom = renderVdom;
        // 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
        return createDom(renderVdom);
    }

以及mountClassElement也用wrapToVdom进行包裹一下

function mountClassComponent(vdom) {
    const {type, props, ref} = vdom;
    const classInstance = new type(props);
-    const classInstanceVdom = wrapToVdom(classInstance.render());
+    const classInstanceVdom = wrapToVdom(classInstance.render());
    classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面;
    if (ref) {
        ref.current = classInstance;
    }
    return createDom(classInstanceVdom);
}

经过这样改造之后,我们就能也能正常渲染文本类型的节点了.

设计动机

从官方的文档设计动机,它提到了两点:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

官方认为,以上假设几乎在所有的场景都成立,也就是说对于第一点,如果元素的类型变了,或者说元素的层级变了,都将直接渲染整个Dom树,而对于对个子元素,我们可以通过key来识别是否有变更,包括位置的移动.在我们之前的更新方法里面,我们用的全量替换,基实就是针对第1种情况才使用的.而对于其它情况,我们继续顺着来梳理.

修改src/react-dom/index.js里面的compareTwoVdom方法.

export function compareTwoVdoms(oldVdom, newVdom){
-    let oldDom = findDom(oldVdom);
-    let newDom = createDom(newVdom);
-    oldDom.parentNode.replaceChild(newDom, oldDom);
+        if (!oldVdom && !newVdom) return; // 如果新旧都不存在虚拟Dom,则无须处理 比如自闭合标签没有children
+    if (oldVdom && newVdom && (oldVdom.type !== newVdom.type)) { // 如果节点类型变了,直接进行全量更新
+        let oldDom = findDom(oldVdom);
+        let newDom = createDom(newVdom);
+        oldDom.parentNode.replaceChild(newDom, oldDom);
+    } else {
+        updateElement(oldVdom, newVdom);
+    }

在这个方法里面,如果节点类型变了,就直接进行全量替换,否则,走更新的逻辑.

文本节点的更新

文本节点的更新相对比较简单,因为不涉及到元素的attribute属性的更新,直接判断一下文本内容有没变更,有变更就进行替换操作即可.

src/react-demo/index.js里继续实现updateElement方法

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
}

原生HTML标签类型更新

对于原生的HTML标签,如果标签类型没有变,就直接利用原来老的节点,并更新attribute属性,以及遍历子元素的变更.

更新attribute属性要考虑的主要就是属性的增删改行为,对属性的修改也是遵循只修改变动的部分.所以需要对新旧的props进行对比.

元素属性的更新

updateElement增加一个原生标签类型的判断

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
+    if (typeof oldVdom.type === 'string') { // 原生的HTML元素
+        const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
+        renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
    }
}

对原来的renderAttributes方法进行改造,主要就是增加属性删除的判断,以及新老属性值是否变化的判断.

function renderAttributes(dom, attributes = {}, oldAttributes = {}) {
    const newAttsrKeys = Object.keys(attributes);
    const oldAttrsKeys = Object.keys(oldAttributes);
    const deleteAttrsKeys = oldAttrsKeys.filter(item => !newAttsrKeys.includes(item));
    deleteAttrsKeys.forEach(item => dom.removeAttribute(item)); // 删除旧节点,新节点没有了的属性

    for (let key in attributes) {
        const value = attributes[key];
        if (key === 'children') continue; // 属性无值不处理,children也单独处理
        if (key === 'style') {
            if (oldAttributes.style) {
                const newStyleKeys = Object.keys(value);
                const oldStyleKeys = Object.keys(oldAttributes.style);
                const deleStyleKeys = oldStyleKeys.filter(item => !newAttsrKeys.includes(item));
                deleStyleKeys.forEach(item => dom.style[item] = ''); // 删除新节点不存在的样式
            }
            for (let attr in value) {
                const oldStyle = oldAttributes.style || {};
                if (value[attr] !== oldStyle[attr]) {
                    dom.style[attr] = value[attr];
                }
            }
        } else if (key.startsWith('on')) {
            addEvent(dom, key.toLocaleLowerCase(), value)
        } else if (value !== oldAttrsKeys[key]) { // 如果值更新了才进行更新
            dom[key] = value;
        }
    }
    return dom;
}

子节点的更新

对于子节点的对比,这里先采用一个简单粗暴的方法,假设新旧元素都处在相同位置上,就是新旧的进行一一对比,暂不考虑元素移动的问题.

src/react-dom/index.js实现一个updateChildren方法

/**
 * 循环对比子节点
 * @param parentDom
 * @param oldChildren
 * @param newChildren
 */
function updateChildren(parentDom, oldChildren, newChildren) {
    oldChildren = Array.isArray(oldChildren) ? oldChildren : [oldChildren];
    newChildren = Array.isArray(newChildren) ? newChildren : [newChildren];
    const maxLength = Math.max(oldChildren.length, newChildren.length);
    for (let i = 0; i < maxLength; i++) {
        compareTwoVdoms(oldChildren[i], newChildren[i], parentDom);
    }
}

updateElement中来调用这个方法

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
    if (typeof oldVdom.type === 'string') { // 原生的HTML元素
        const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
        renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
+        updateChildren(currentDom, oldVdom.props.children, newVdom.props.children);
    }
}

src/index.js中写一个组件来模拟元素属性的增删改操作,包括布尔类型属性的变更.

import React from './react';
import ReactDOM from './react-dom';

class Hello extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 1
        }
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        })
    }

    render() {
        const renderProps = {
            id: this.state.number,
            style: {
                color: 'red',
                backgroundColor: 'green',
            }
        };
        if(this.state.number % 2 === 0){
            renderProps.title = 'number';
            renderProps.style = {
                backgroundColor: 'yellow',
                fontSize: '20px',
            }
        }
        return (
            <div>
                <input id="input" readOnly={this.state.number % 2 === 0} />
                <p {...renderProps} readOnly>{this.state.number}</p>
                <button onClick={this.handleClick}>add</button>
            </div>
        );
    }
}

ReactDOM.render(
    <Hello/>, document.getElementById('root')
);

QQ20210526-221702-HD

可以看到,现在Dom节点的更新只更新了变动的部分.

元素的挂载与卸载

对于如下一个render方法里面

render() {
    const renderChild = () => {
        if(this.state.number % 2 === 0){
            return <input />
        }
    }
    return (
        <div>
            {renderChild()}
            {this.state.number % 2 === 0 && <input />}
            <p {...renderProps} readOnly>{this.state.number}</p>
            <button onClick={this.handleClick}>add</button>
        </div>
    );
}

number不管等于1,它都将有4个子元素,比如等于1的时候.它的子元素如下:

image-20210526234107995

所以对于这样的子元素的挂载与卸载是可以通过前后对比判断出来的.这里注意到如果子元素如果是布尔值, null, undefined类型的,不进行真实Dom的渲染的.

src/utils/index.js增加一个判断方法

/**
 * 判断一个虚拟节点是否不需要渲染成真实Dom
 * @param element
 * @returns {boolean}
 */
export function isNotNeedRender(vdom){
    return vdom === null || vdom === undefined || typeof vdom === 'boolean';
}

然后调整一下src/react-dom/index.js

-import {wrapToVdom} from '../utils';
+import {wrapToVdom, isNotNeedRender} from '../utils';
import {REACT_FORWARD_COMPONENT, REACT_TEXT} from '../constants';

/**
 * 将虚拟DOM渲染到真实DOM节点里面
 * @param vdom
 * @param container
 */
function render(vdom, container) {
-    if (vdom === null || vdom === undefined) return; // 如果vdom不存在,则不需要创建真实dom;
+    if(isNotNeedRender(vdom)) return // 如果vdom不存在,则不需要创建真实dom;
    const dom = createDom(vdom);
    container.appendChild(dom);
}

/**
 * 将虚拟DOM转换成真实DOM
 * @param vdom
 */
function createDom(vdom) {
+    if(isNotNeedRender(vdom)) return
    let dom;

    const {type, props, ref} = vdom;

compareTwoVdom方法里面,增加对新旧节点挂载与卸载的判断,这里需要注意的地方就是,如果老节点没有,新节点有,那么这个新节点有可能是在当前位置进行插入,也有可能是插入到最后一个元素,判断的依据就是看旧的Dom树后面还有没有子元素.同时compareTwoVdom需要接收一个父元素的参数,在保证在oldVdom不存在的情况下通过父元素来完成插入操作.

-export function compareTwoVdoms(oldVdom, newVdom) {
-    if(!oldVdom && !newVdom) return;
+export function compareTwoVdoms(oldVdom, newVdom, parentDom, nextDom) {
    if (!oldVdom && !newVdom) return;
    if (oldVdom && newVdom && (oldVdom.type !== newVdom.type)) { // 如果节点类型变了,直接进行全量更新
-        let oldDom = findDom(oldVdom);
-        let newDom = createDom(newVdom);
+        const oldDom = findDom(oldVdom);
+        const newDom = createDom(newVdom);
        oldDom.parentNode.replaceChild(newDom, oldDom);
+    } else if (oldVdom && !newVdom) {
+        const oldDom = findDom(oldVdom);
+        oldDom.parentNode.removeChild(oldDom);
+    } else if (!oldVdom && newVdom) {
+        const newDom = createDom(newVdom);
+        if (nextDom) { // 如果后面有节点,则应该进行插入操作
+            parentDom.insertBefore(newDom, nextDom);
+        } else {
+            parentDom.appendChild(newDom);
+        }
    } else {
        updateElement(oldVdom, newVdom);
    }
}

function updateChildren(parentDom, oldChildren, newChildren) {
    oldChildren = Array.isArray(oldChildren) ? oldChildren : [oldChildren];
    newChildren = Array.isArray(newChildren) ? newChildren : [newChildren];
    const maxLength = Math.max(oldChildren.length, newChildren.length);
    for (let i = 0; i < maxLength; i++) {
+        const nextDom = oldChildren.find((item, index) => index > i && item && findDom(item));
-        compareTwoVdoms(oldChildren[i], newChildren[i]);
+        compareTwoVdoms(oldChildren[i], newChildren[i], parentDom, findDom(nextDom));
    }
}

src/index.js里面的render方法修改如下,来模拟一下组件的挂载与卸载操作.

render() {
    const renderProps = {
        id: this.state.number,
        style: {
            color: 'red',
            backgroundColor: 'green',
        }
    };
    if(this.state.number % 2 === 0){
        renderProps.title = 'number';
        renderProps.style = {
            backgroundColor: 'yellow',
            fontSize: '20px',
        }
    }
    const renderChild = () => {
        if(this.state.number % 2 === 0){
            return <input />
        }
    }
    return (
        <div>
            {true}
            {renderChild()}
            {this.state.number % 2 === 0 && <input />}
            <p {...renderProps} readOnly>{this.state.number}</p>
            <button onClick={this.handleClick}>add</button>
        </div>
    );
}

类组件的更新

前面我们已经实现了类组件state的变更更新组件,这里的类组件更新处理的是父组件的更新,导致了类组件类型的子元素的更新.类组件的更新,就是传入新的props,再次调用实例上面的render方法,重新新的虚拟Dom,然后用新的虚拟Dom和老的虚拟Dom继续进行对比更新即可.

updateElement方法里面增加一个类组件的判断

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
    if (typeof oldVdom.type === 'string') { // 原生的HTML元素
        const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
        renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
        updateChildren(currentDom, oldVdom.props.children, newVdom.props.children);
+    } else if(typeof oldVdom.type === 'function'){
+        if (oldVdom.type.isReactComponent) {
+            updateClassComponent(oldVdom, newVdom)
+        }
    }
}

继续在src/react-dom/index.js里面实现updateClassComponent这个方法

function updateClassComponent(oldVdom, newVdom) {
    const classInstance = newVdom.classInstance = oldVdom.classInstance;
    classInstance.updater.emitUpdate(newVdom.props);
}

在这个方法里面,我们要调用实例上面的更新器,去触发这一次更新,并把最新的props传递过去.为了能获取到组件的实例,我们在原来的mountClassComponent方法里面把实例挂到虚拟Dom上面

function mountClassComponent(vdom) {
    const {type, props, ref} = vdom;
    const classInstance = new type(props);
    const classInstanceVdom = wrapToVdom(classInstance.render());
    classInstance.oldVdom = vdom.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面;
+    vdom.classInstance = classInstance;
    if (ref) {
        ref.current = classInstance;
    }
    return createDom(classInstanceVdom);
}

src/react/Updater.js里面,增加一个emitUpdate这个方法,来授受stateprops变更进行更新的触发操作.

addState(partialState) {
-    if (!updateTracker.isBatchingUpdate) { //如果不是批量更新,则直接更新组件
-        this.updateComponent()
-    } else if (!this.batchTracking) { // 如果还没有添加进updateTracker队列中,刚添加进去
+    if (!this.batchTracking) { // 如果还没有添加进updateTracker队列中,刚添加进去
        updateTracker.updaters.push(this);
        this.batchTracking = true;
    }
+    this.emitUpdate(this.componentInstance.props);
}

+emitUpdate(nextProps) {
+    this.nextProps = nextProps;
+    if (!updateTracker.isBatchingUpdate) { //如果不是批量更新,则直接更新组件
+        this.updateComponent()
+    }
+}

updateComponent() {
-       onst {componentInstance, pendingState} = this;
-    if (pendingState.length) {
+    const {componentInstance, pendingState, nextProps} = this;
+    if (pendingState.length || nextProps) {
        componentInstance.state = this.getState();
+        componentInstance.props = nextProps;
        componentInstance.forceUpdate();
    }
    this.batchTracking = false;
}

好,写到这里,终于出现了一个小bug,在原来的updateTracker.isBatchingUpdate = false;这个操作是放在batchUpdate这个方法里面的,这就会导致一个问题,父组件一state变更了,触发更新操作,这个isBatchingUpdate会直到父组件更新完毕了才会重置会false,所以就会导致父组件里面的类组件类型的子组件无法进行更新.所以需要把这一句重置放到事件执行完毕之后.

修改src/react/Updater.js

export let updateTracker = {
    isBatchingUpdate: false,
    updaters: [],
    batchUpdate() {
        for (let updater of updateTracker.updaters) {
            updater.updateComponent();
        }
-        updateTracker.isBatchingUpdate = false;
        updateTracker.updaters.length = 0;
    }
};

移动到src/react/event.js里面

function dispatchEvent(event){
    updateTracker.isBatchingUpdate = true;
    let { target, type } = event;
    let eventType = `on${type}`;
    let syntheticEvent = createSyntheticEvent(event);
    // 模拟事件冒泡,一直向父级元素进行冒泡
    while(target){
        let { store } = target;
        let handler = store && store[eventType];
        handler && handler.call(target, syntheticEvent);
        target=target.parentNode;
    }
-    updateTracker.isBatchingUpdate = false;
    updateTracker.batchUpdate();
}

这里还注意到有一个问题,咱们之前的state触发类组件的更新,在forceUpdate里面是通过this.oldVdom来获取之前render出来的旧的虚拟Dom,通过它是可以递归查到的真实的Dom的,而在updateChildren里面有这一么句const nextDom = oldChildren.find((item, index) => index > i && item && findDom(item));,这里面的findDom(item)传入的item如果刚好是一个类组件的虚拟Dom的话,那么咱们之前的findDom就查不到真实的Dom了.因为它不是实例上面的oldVdom,所以有两个方案,一个是在mountClassComponent的时候,需要把render出来的虚拟Dom也挂载到原来的vdom上面.这样改的话,同时也还需要在forceUpdate的里面,在最后把最新生成的newVdom再次更新回vdom上面.以确保能够通过它获取到最新的真实Dom.

即修改mountClassComponent这个方法和Componennt这个父类里面的forceUpdate方法

function mountClassComponent(vdom) {
    const {type, props, ref} = vdom;
    const classInstance = new type(props);
    const classInstanceVdom = wrapToVdom(classInstance.render());
-    classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面;
+    classInstance.oldVdom = vdom.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面;
    vdom.classInstance = classInstance;
    if (ref) {
        ref.current = classInstance;
    }
    return createDom(classInstanceVdom);
}

forceUpdate() {
    const oldVdom = this.oldVdom;
    const newVdom = this.render();
    compareTwoVdoms(oldVdom, newVdom)
+    newVdom.oldVdom = newVdom; // 保证oldVdom能查找到最新的真实Dom.因为newVdom上面挂载的可能是一个新的真实Dom了.
    this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面
}

但也有另外一个改法,就是让oldVdom还是依然只挂载的实例上面,改造一个findDom这个方法

function findDom(vdom) {
    if (!vdom || typeof vdom !== 'object') return;
    const {type} = vdom;
    let dom;
    /**
     * 比如render(){return <Demo />}这里面render出来的还不是最终的虚拟dom;
     */
    if (typeof type === 'function') {
        // vdom可能是一个函数或者类组件,需要继续递归查找真实的DOM节点.
+        if(vdom.classInstance){
+            dom = findDom(vdom.oldVdom);
+        }else{
            dom = findDom(vdom.oldVdom);
+        }
    } else {
        dom = vdom.dom;
    }
    return dom;
}

这里我们就采用后面这种改法.

src/index.js增加一个Counter的类组件,测试一下父子组件props更新和state更新是否正常.

class Counter extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            count: 1,
        }
    }
    handleClick = () => {
        this.setState({
            count: this.state.count + 1,
        })
    }
    render(){
        return <div>
            <p>parent:{this.props.number}</p>
            <p>{this.state.count}</p>
            <button onClick={this.handleClick} >Add Count</button>
        </div>
    }
}

class Hello extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 1
        }
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        })
    }

    render() {
        const renderProps = {
            id: this.state.number,
            style: {
                color: 'red',
                backgroundColor: 'green',
            }
        };
        if(this.state.number % 2 === 0){
            renderProps.title = 'number';
            renderProps.style = {
                backgroundColor: 'yellow',
                fontSize: '20px',
            }
        }
        const renderChild = () => {
            if(this.state.number % 2 === 0){
                return <input />
            }
        }
        return (
            <div>
                {true}
                {renderChild()}
                {this.state.number % 2 === 0 && <input />}
                <p {...renderProps} readOnly>{this.state.number}</p>
                <button onClick={this.handleClick}>add number</button>
                <Counter number={this.state.number} />
            </div>
        );
    }
}

函数组件的更新

函数组件的更新和类组件的更新类似,调用新的props执行函数返回的虚拟Dom,然后再和老的虚拟Dom进行递归对比.

src/react-dom/index.js增加一个updateFunctionComponent方法

function updateFunctionComponent(oldVdom, newVdom){
    const {type, props} = newVdom;
    const newRenderVdom = type(props);
    compareTwoVdoms(oldVdom.oldVdom, newRenderVdom);
    newVdom.oldVdom = newRenderVdom;
}

并在updateElement中增加这一个判断

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
    if (typeof oldVdom.type === 'string') { // 原生的HTML元素
        const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
        renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
        updateChildren(currentDom, oldVdom.props.children, newVdom.props.children);
    } else if(typeof oldVdom.type === 'function'){
        if (oldVdom.type.isReactComponent) {
            updateClassComponent(oldVdom, newVdom)
+        }else{
+            updateFunctionComponent(oldVdom, newVdom);
        }
    }
}

src/index.js里面再拆分一个函数组件出来,测试一下函数组件的更新,以及涉及到元素类型变化的情况.

import React from './react';
import ReactDOM from './react-dom';

function Number(props) {
    return <p>parent:{props.number}</p>
}

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
        }
    }

    handleClick = () => {
        this.setState({
            count: this.state.count + 1,
        })
    }

    render() {
        return this.props.number % 2 === 0 ? <div>
            <Number number={this.props.number}/>
            <p>this.state:{this.state.count}</p>
            <button onClick={this.handleClick}>Add Count</button>
        </div> : <h1>
            <Number number={this.props.number}/>
            <p>this.state:{this.state.count}</p>
            <button onClick={this.handleClick}>Add Count</button>
        </h1>
    }
}

class Hello extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 1
        }
    }

    handleClick = () => {
        this.setState({
            number: this.state.number + 1,
        })
    }

    render() {
        const renderProps = {
            id: this.state.number,
            style: {
                color: 'red',
                backgroundColor: 'green',
            }
        };
        if (this.state.number % 2 === 0) {
            renderProps.title = 'number';
            renderProps.style = {
                backgroundColor: 'yellow',
                fontSize: '20px',
            }
        }
        const renderChild = () => {
            if (this.state.number % 2 === 0) {
                return <input/>
            }
        }
        return (
            <div>
                {true}
                {renderChild()}
                {this.state.number % 2 === 0 && <input/>}
                <p {...renderProps} readOnly>{this.state.number}</p>
                <button onClick={this.handleClick}>add number</button>
                <Counter number={this.state.number}/>
            </div>
        );
    }
}

ReactDOM.render(
    <Hello/>, document.getElementById('root')
);

看看效果是否是一致呢?

QQ20210531-221508

buxuku commented 3 years ago

Component父类的forceUpdate里面的render也需要用wrapToVdom来处理一下.

import {compareTwoVdoms} from '../react-dom';
+import {wrapToVdom} from '../utils';
import {Updater} from "./Updater";

/**
 * React.Component父类
 */
export class Component {
    static isReactComponent = {};

    constructor(props) {
        this.props = props;
        this.state = {};
        this.updater = new Updater(this)
    }

    setState(partialState) {
        this.updater.addState(partialState)
    }

    forceUpdate() {
        const oldVdom = this.oldVdom;
-        const newVdom = this.render();
+        const newVdom = wrapToVdom(this.render());
        compareTwoVdoms(oldVdom, newVdom)
        this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面
    }
}