buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

4.组件的全量更新 #4

Open buxuku opened 3 years ago

buxuku commented 3 years ago

组件渲染出来了,也能响应事件了,接下来一个重点就是组件的更新了,毕竟React是数据驱动UI的,那么数据变了,我们怎么是需要去进行UI的更新的呢?

触发组件的更新有以下三种情况:

React采用的是Dom-diff来更新UI,也就是通过新的虚拟DOM和旧的虚拟DOM进行对比,找出差异点,然后将对应的改变在真实的DOM上进行替换操作.

我们先来解决第一个场景: state变更了,自动调用组件的forceUpdate方法来更新组件.

src/react/Componennt.js里面的Component父类增加两个API,一个setState,一个forceUpdate,在接收到新的state的时候,更新组件的State,并调用forceUpdate来更新组件.

export class Component {
    static isReactComponent = {};

    constructor(props) {
        this.props = props;
+        this.state = {};
    }
+    setState(partialState) {
+        this.state = {...this.state, ...partialState};
+        this.forceUpdate()
+    }
+    forceUpdate(){
+                // TODO
+    }
}

forceUpdate中,要实现组件的更新,我们要拿到旧的虚拟DOM,以及state更新之后的新的虚拟DOM,然后将它对比之后,将差异替换到真实的DOM节点上面去.新的虚拟DOM和之前的createDom里面一样,调用render方法即可取到,那旧的虚拟DOM呢?以及如何找其对应的真实DOM呢?

在最开始生成虚拟DOM的时候,我们可以将把挂载到我们的实例(或者方法,如果是函数组件)上面.以及把生成出来的真实DOM挂载到它的虚拟DOM上面.这样就可以在每次重新渲染的时候,找到旧的虚拟DOM和旧的真实DOM了.

对原来的createDommountClassComponennt方法进行修改

function createDom(vdom) {
    let dom;
    if (typeof vdom !== 'object') { // render可以直接渲染一个字符串或者数字,它不是一个React.element
        dom = document.createTextNode(vdom);
        return dom;
    }
    const {type, props} = vdom;
    if (typeof type === 'string') {
        dom = document.createElement(type);
        renderAttributes(dom, props);
    }
    if (typeof type === 'function') {
        if (type.isReactComponent) { // 是一个类组件
            return mountClassComponent(vdom);
        }
+        let renderVdom = type(props);
+        vdom.oldVdom = renderVdom;
        // 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
        return createDom(renderVdom);
    }
    if (props) {
        const {children} = props;
        if (Array.isArray(children)) {
            reconcileChildren(children, dom);
        } else {
            render(children, dom);
        }
    }
+    vdom.dom = dom; // 将真实DOM绑定到它的虚拟DOM上面
    return dom;
}

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

接下来既可来实现forceUpdate方法.从实例上获取到上一步挂载的oldVdom,以及调用render获取到的新的虚拟DOM.

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

对比两颗虚拟DOM,将差异来更新到真实DOM上面.这里先不考虑Dom-diff这个难点,如果直接进行全量更新,那么可以直接用新的真实DOM替换掉老的真实DOM即可.

src/react-dom/index.js增加compareTwoVdom方法

/**
 * 比较两次虚拟DOM的差异,并将差异更新到真实DOM节点上面去
 * @param parentNode
 * @param oldVdom
 * @param newVdom
 */

export function compareTwoVdoms(oldVdom, newVdom){
    let newDom = createDom(newVdom);
    oldVdom.dom.parentNode.replaceChild(newDom, oldDom);
}

注意,这里有一个小坑,oldVdom.dom就一定是真实节点吗?在前面的createDom里面的增加的vdom.dom = dom知道, Dom是挂在虚拟Dom上面的,但在这个方法里面传入的oldVdom是实例上面的,

const classInstanceVdom = classInstance.render();
classInstance.oldVdom = classInstanceVdom;

可以看到,它来源于实例的render方法,但注意,render返回的可能会另外一个组件,那它会继续调用createDom方法,所以最后真实的Dom是挂在最后渲染它的虚拟Dom上面的.如下图这样,真实Dom是挂在Child这个组件上面的.

edOMNG

完了,Child如果再返回的是别外一个组件,组件套组件,组件何其多,那如何定位最终的真实dom?

还好,每个组件上面都挂了一个oldVdom,而这个oldVdom要么挂有真实dom,要么就指向下一个oldVdom,于是可以增加一个函数来递归查找最终的真实dom即可.

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

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

将前面的compareTwoVdoms方法修改如下

export function compareTwoVdoms(oldVdom, newVdom){
    let oldDom = findDom(oldVdom);
    let newDom = createDom(newVdom);
    oldDom.parentNode.replaceChild(newDom, oldDom);
}

修改src/index.js文件如下

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

function Count({number}){
    return <p>countNumber: {number}</p>
}

class Hello extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
        }
    }
    handleAdd = () => {
        this.setState({
            count: this.state.count + 1,
        });
    }

    render() {
        return (
            <div>
                number: {this.state.count}
                <Count number={this.state.count} />
                <p>
                    <button onClick={this.handleAdd}>+</button>
                </p>
            </div>
        )
    }
}

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

运行发现,点击按钮,我们的类组件通过State, 函数组件通过props都可以实现更新了.