Skip to content

03.React DOM DIFF算法源码实现

具体代码演化过程请观看视频,这里呈现关键代码:

js
// utils.js
export const REACT_TEXT = Symbol('react.text')///
export const MOVE = Symbol('dom.diff.move')//
export const CREATE = Symbol('dom.diff.create')//
export const toVNode = (node) => {//
    return typeof node === 'string' || typeof node === 'number' ? {//
        type: REACT_TEXT, props: {text: node}//
    } : node//
}//
js
// react.js
import { REACT_ELEMENT, REACT_FORWARD_REF, toVNode } from './utils' ///
if(arguments.length > 3){
        props.children = Array.prototype.slice.call(arguments, 2).map(toVNode);///
    }else{
        props.children = toVNode(children)///
    }
js
// Component.js
update(){
        // 1. 获取重新执行render函数后的虚拟DOM 新虚拟DOM
        // 2. 根据新虚拟DOM生成新的真实DOM
        // 3. 将真实DOM挂载到页面上
        let oldVNode = this.oldVNode; // TODO: 让类组件拥有一个oldVNode属性保存类组件实例对应的的虚拟DOM
        let oldDOM = findDomByVNode(oldVNode) // TODO: 将真实DOM保存到对应的虚拟DOM上
        let newVNode = this.render()
        updateDomTree(oldVNode, newVNode, oldDOM) ////
        this.oldVNode = newVNode
    }
js
// react-dom.js
import { REACT_ELEMENT, REACT_FORWARD_REF, MOVE, CREATE, REACT_TEXT  } from './utils'///
function createDOM(VNode){
    // 1.创建元素 2.处理子元素 3.处理属性值
    ...
    if (type === REACT_TEXT){ ////
        dom = document.createTextNode(props.text);///
    } if(type && VNode.$$typeof === REACT_ELEMENT){///
        dom = document.createElement(type)///
    }///

    if(props){
        if(typeof props.children === 'object' && props.children.type){
            mount(props.children, dom)
        }else if(Array.isArray(props.children)){
            mountArray(props.children, dom)
        }////
        /////
    }
    setPropsForDOM(dom, props)
    VNode.dom = dom
    ref && (ref.current = dom)
    return dom
}

function mountArray(children, parent){
    if(!Array.isArray(children)) return
    for(let i = 0; i < children.length; i++){
        children[i].index = i;////
        mount(children[i], parent)////
    }
}


function updateChildren(parentDOM, oldVNodeChildren, newVNodeChildren) {
    oldVNodeChildren = (Array.isArray(oldVNodeChildren) ? oldVNodeChildren : [oldVNodeChildren]).filter(Boolean);
    newVNodeChildren = (Array.isArray(newVNodeChildren) ? newVNodeChildren : [newVNodeChildren]).filter(Boolean);
    
    // 利用Map数据结构为旧的虚拟DOM数组找到key和节点的关系,为后续根据key查找是否有可复用的虚拟DOM创造条件
    let lastNotChangedIndex = -1;
    let oldKeyChildMap = {};
    oldVNodeChildren.forEach((oldVNode, index) => {
        let oldKey = oldVNode && oldVNode.key ? oldVNode.key : index;
        oldKeyChildMap[oldKey] = oldVNode;
    });

    // 遍历新的子虚拟DOM树组,找到可以复用但需要移动的、需要重新创建的、需要删除的节点,剩下的都是不用动的节点
    let actions = [];
    newVNodeChildren.forEach((newVNode, index) => {
        if(typeof newVNode !== 'string'){
            newVNode.index = index;
        }
        let newKey = newVNode.key ? newVNode.key : index;
        let oldVNode = oldKeyChildMap[newKey];
        if (oldVNode) {
            deepDOMDiff(oldVNode, newVNode);
            if (oldVNode.index < lastNotChangedIndex) {
                actions.push({
                    type: MOVE,
                    oldVNode,
                    newVNode,
                    index
                });
            }
            delete oldKeyChildMap[newKey]
            lastNotChangedIndex = Math.max(lastNotChangedIndex, oldVNode.index);
        } else {
            actions.push({
                type: CREATE,
                newVNode,
                index
            });
        }
    });

    // 可以复用但需要移动位置的节点,以及用不上需要删除的节点,都从父节点上移除
    let VNodeToMove = actions.filter(action => action.type === MOVE).map(action => action.oldVNode);
    let VNodeToDelete = Object.values(oldKeyChildMap)
    VNodeToMove.concat(VNodeToDelete).forEach(oldVChild => {
        let currentDOM = findDomByVNode(oldVChild);
        currentDOM.remove();
    });

    // 对需要移动以及需要新创建的节点统一插入到正确的位置
    actions.forEach(action => {
        debugger
        let { type, oldVNode, newVNode, index } = action;
        let childNodes = parentDOM.childNodes;
        const getDomForInsert = () => {
            if(type === CREATE){
                return createDOM(newVNode)
            }
            if(type === MOVE){
                return findDomByVNode(oldVNode)
            }
        }
        let childNode = childNodes[index];
        if (childNode) {
            parentDOM.insertBefore(getDomForInsert(), childNode)
        } else {
            parentDOM.appendChild(getDomForInsert());
        }
    });
}
function updateClassComponent(oldVNode, newVNode) {
    const classInstance = newVNode.classInstance = oldVNode.classInstance;
    classInstance.updater.launchUpdate();
}
function updateFunctionComponent(oldVNode, newVNode) {
    let oldDOM = findDomByVNode(oldVNode);
    if (!oldDOM) return;
    const { type, props } = newVNode;
    let newRenderVNode = type(props);
    updateDomTree(oldVNode.oldRenderVNode, newRenderVNode, oldDOM);
    newVNode.oldRenderVNode = newRenderVNode;
}

function deepDOMDiff(oldVNode, newVNode) {
    let diffTypeMap = {
        ORIGIN_NODE: typeof oldVNode.type === 'string', // 原生节点
        CLASS_COMPONENT: typeof oldVNode.type === 'function' && oldVNode.type.isReactComponent,
        FUNCTION_COMPONENT: typeof oldVNode.type === 'function',
        TEXT: oldVNode.type === REACT_TEXT
    }
    let DIFF_TYPE = Object.keys(diffTypeMap).filter(key => diffTypeMap[key])[0]
    switch (DIFF_TYPE) {
        case 'ORIGIN_NODE':
            let currentDOM = newVNode.dom = findDomByVNode(oldVNode);
            setPropsForDOM(currentDOM, newVNode.props)
            updateChildren(currentDOM, oldVNode.props.children, newVNode.props.children);
            break;
        case 'CLASS_COMPONENT':
            updateClassComponent(oldVNode, newVNode);
            break;
        case 'FUNCTION_COMPONENT':
            updateFunctionComponent(oldVNode, newVNode);
            break;
        case 'TEXT':
            newVNode.dom = findDomByVNode(oldVNode);
            newVNode.dom.textContent = newVNode.props.text;
            break;
        default:
            break;
    }
}
function removeVNode(vNode) {
    const currentDOM = findDomByVNode(vNode);
    if (currentDOM) currentDOM.remove();
}
// 开始dom-diff
export function updateDomTree(oldVNode, newVNode, oldDOM) {
    const typeMap = {
        NO_OPERATE: !oldVNode && !newVNode,
        ADD: !oldVNode && newVNode,
        DELETE: oldVNode && !newVNode,
        REPLACE: oldVNode && newVNode && oldVNode.type !== newVNode.type // 类型不同
    }
    let UPDATE_TYPE = Object.keys(typeMap).filter(key => typeMap[key])[0]

    switch (UPDATE_TYPE) {
        case 'NO_OPERATE':
            break
        case 'DELETE':
            removeVNode(oldVNode);
            break
        case 'ADD':
            oldDOM.parentNode.appendChild(createDOM(newVNode));
            break
        case 'REPLACE':
            removeVNode(oldVNode);
            // 这里直接追加到尾巴上
            oldDOM.parentNode.appendChild(createDOM(newVNode));
            break
        default:
            // 深度的 dom-diff,新老虚拟DOM都存在且类型相同
            deepDOMDiff(oldVNode, newVNode)
            break;
    }
js
// index.js
import React from './react';
import ReactDOM from './react-dom';

class MyClassComponent extends React.Component{
    isReset = false
    oldArr = ['A', 'B', 'C', 'D', 'E']
    newArr = ['C', 'B', 'E', 'F', 'A']
    constructor(props) {
        super(props);
        this.state = { arr: this.oldArr };
    }
    updateShowArr(){
        this.setState({
            arr: this.isReset ? this.oldArr : this.newArr
        })
        this.isReset = !this.isReset
    }
    render(){
        return <div>
                    <div className='test-class' style={
                        {
                            color: 'red', 
                            cursor: 'pointer', 
                            border: '1px solid gray', 
                            borderRadius: '6px',
                            display: 'inline-block',
                            padding: '6px 12px'
                        }
                    } onClick={ () => this.updateShowArr() }>Change The Text</div>
                    <div>
                        {
                            this.state.arr.map(item => {
                                return <div key={item}>{item}</div>
                            })
                        }
                    </div>
            </div>
    }
}
ReactDOM.render(<MyClassComponent />, document.getElementById('root'))

基于 VitePress 构建