react virtualDOM的生成和diff过程,个人笔记

90 阅读3分钟

什么是jsx

jsx其实也是js,react jsx经过babel编译后得到需要递归调用的React.createElement函数。

jsx = (<div className="container">
    <p>这是第一个子节点</p>
    <p>这是第二个子节点</p>
</div>)

// babel将上面的jsx编译后得到如下的函数调用
React.createElement(
    'div', 
    { className: 'container'}, 
    React.createElement('p', null, '这是第一个子节点'),
    React.createElement('p', null, '这是第二个子节点')
)

// React.createElement(type, attr, ...children)这个函数将出传入的转换为virtualDOM
// 那virtualDOM是上面样的呢?如下
virtualDom = {
    // type可以为dom标签名称,或组件(可以是react函数式组件,也可以是react类组件)
    type: 'div', 
    props: {
        children: [{
            type: 'p',
            props: null,
            children: [
                type: 'text',
                props: {
                    textContainer: '这是第一个子节点'
                }
            ]
        },{
            type: 'p',
            props: null,
            children: [
                type: 'text',
                props: {
                    textContainer: '这是第二个子节点'
                }
            ]
        }],
        className: 'container',
        ...等等,
    },
    children: [{
        type: 'p',
        props: null,
        children: [
            type: 'text',
            props: {
                textContainer: '这是第一个子节点'
            }
        ]
    },{
        type: 'p',
        props: null,
        children: [
            type: 'text',
            props: {
                textContainer: '这是第二个子节点'
            }
        ]
    }]
}

// 下面是createElement如何生成virtualDOM
React.createElement = function(type, props, ...children) {
    const childElements = [].concat(...children).map(child => {
    if (child instanceof Object) { 
        // 对象类型则返回
        return child
    } else {
        // 普通文本节点,处理为{type: 'text', props: { textContent: text }}
        return createElement("text", { textContent: child }) } 
    })
    return {
      type,
      props: Object.assign({ children: childElements }, props),
      children: childElements
    }
}

virtualDOM生成后如何diff

  • 1:当调用setState后,React.component会调用当前实例的render方法,获取最新的virtualDOM,newVirtualDOM = this.render();
  • 2: 那如何获取oldVirtualDOM呢?
  • 1:react在初始化第一次生成virtualDOM时,会将virtualDOM挂载在对应的dom节点对象下,只要拿到对应的dom节点对象就能获取到oldVirtualDOM
  • 2 那如何获取对应的dom节点对象,在初始化oldVirtualDOM时,react内部会调用new 我们当前的class组件,并调用了组件实例的setDom方法,将dom节点对象挂载在当前的class实例下

function diff(newVirtualDOM, oldVirtualDOM, oldDOM) {
    // 第一步:判断newVirtualDOM和oldVirtualDOM的type
    if (newVirtualDOM.type !== oldVirtualDOM.type) {
        cnost newDom = creatDom(newVirtualDOM)
        // 移除旧的节点
        unMount(oldVirtualDOM);
        mount(newVirtualDOM, oldDOM);
    } else { // 若是同一个组件
        /** 以下是props的diff算法
        * 1:先遍历新的props时,对比新的prop和旧的prop是否相等,不相等则替换prop
        * 2:新的props添加完成后,判断旧的props的长度是否大于旧的props长度,
        * 2:是的话,就说明需要删除后面(oldpropsLength - newpropsLength)个prop。
        */
        // 第一步:依据newVirtualDOM.props和oldVirtualDOM.props进行更新props
        const newpropsLength = Object.keys(newVirtualDOM.props).length;
        const oldpropsLength = Object.keys(oldVirtualDOM.props).length;
        for (let i = 0; i < newpropsLength.length; i ++) {
            // 新props的属性和旧pros属性名和值都相等
            if (newVirtualDOM[i].type !== oldVirtualDOM[i].type){
                updateProps(newVirtualDOM[i], oldDOM)
            }
        }
        
        // 若旧的oldVirtualDOM.props大于新的newVirtualDOM.props,则之后旧的props要删除
        for (let i = oldpropsLength.length - 1; i > newpropsLength.length - 1; i --) {
            delProps(oldVirtualDOM[i]);
       
       }
    }
    
    //接下来diff children
    /**
    * 1:先判断children是否包含key
    *   包含了:走包含key的diff算法
    *   不包含:走不包含key的算法
    */
    
    let keyElements = {};
    if (oldVirtualDOM && newVirtualDOM.type === oldVirtualDOM.type) {
        for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
            let domElement = oldDOM.childNodes[i]
            if (domElement.nodeType === 1) { 
                let key = domElement.getAttribute("key")
                if (key) {
                    keyElements[key] = domElement 
                } 
            } 
        }
    }
    const isHasKey = Object.keys(keyElements).length;
    if (isHasKey) {
        // 1: 遍历newVirtualDOM下children,获取key
        // 2: 根据key到keyElements寻找对应节点
        // 3: 没找到,则在当前索引下插入child节点
        // 这样只处理了新增和修改的
        for (let i = 0; i < newVirtualDOM.children.length - 1; i ++) {
            const key = newVirtualDOM.children[i].key;
            if (key) {
                let domElement = keyElements[key]
                // 若存在相同key的子元素,还需要判断新旧元素的顺序是否一致
                if (domElement) {
                    // 若顺序不相等,则插入新的dom节点
                    if (oldDOM.childNode[i] && oldDOM.childNode[i] !=== domElement) {
                        oldDOM.insertBefore(domElement, oldDOM.childNode[i]);
                    }
                }
            }
        }
        
        // 接下来处理删除的
        // 1: 遍历旧的oldDOM.childNodes
        // 2: 判断oldVirtualDOM.children中的key是否在newVirtualDOM下children中存在,
        // 如果不存在,则需要删除对应的childNode。调用umMount(childNode);
        // let oldChildKey = oldChild._virtualDOM.props.key
        const 
        for (let i = oldChild._virtualDOM, i < oldDOM._virtualDOM.children.length - 1; i ++) {
            const key = oldDOM._virtualDOM.children[i].props[key];
            // 寻找新的newVirtualDOM中是否包含旧的oldVirtualDOM中的key
            // 不包含则删除该节点
            cnost isFind = newVirtualDOM.children.find((child) => {
                return key === child.props.key;
            })
            
            if (!isFind) {
                // 卸载对应的节点,
                // unMount里还要地柜处理childNodes下的所有子组件的ref绑定,和所有子组件的事件监听,放在内存泄漏
                // 所有自组件的ref引用移除,dom事件监听解绑,则调用oldDOM.childNodes[i].remove();删除自身dom节点
                unMount(oldDOM.childNodes[i])
            }
        }
    } else {
    
    }
}

React.component = class Component {
    constructor(props) {
        this.props = props
    };
    
    setState(options){
        this.state = options;
        const newVirtualDOM = this.render();
        // 获取当前组件对应的dom节点
        const dom = this.getDom();
        // 通过dom获取oldVirtualDOM
        const oldVirtualDOM = dom._virtualDOM
        /**
        * 新的virtualDOM: newVirtualDOM
        * 旧的virtualDOM: oldVirtualDOM
        * 组件挂载的dom节点:this.dom
        */
        diff(newVirtualDOM, oldVirtualDOM, this.dom);
    }
    
    setDom(dom) {
        this.dom = dom;
    }
    
    getDom() {
        return this.dom;
    }
}

class childClass extends React.component {
    state = {
        cont = 0;
    }
    onClick() {
        this.setState({ count: 1 })
    }
    
    render() {
        return (
            const count = this.state.count
            <div>
                <div> {count}</div>
                <button onClick={this.onClick.bind(this)}></button>
            </div>
        )
    }

}
// 遍历virtualDOM时如果判断当前virtualDOM的type时class组件,
// 则会实例化组件new virtualDOM.type(virtualDOM.props);
// 我们上面有提到virtualDOM.type可以是标签名称,函数式组件的函数,或者是具有render方法的calss
// 如果判断到virtualDOM.type是函数且原型上具有render方法,则说明是class组件,就会调用
// buildStatefulComponent实例化这个组件,得到Virtual DOM
function buildStatefulComponent(virtualDOM) { 
    // 实例化react的class组件
    const component = new virtualDOM.type(virtualDOM.props);
    // 调用组件的render方法,获取最新的virtualDOM
    const nextVirtualDOM = component.render()
    // 将component挂载在virtualDOM下,之后可以通过virtualDOM获取对应的组件实例
    nextVirtualDOM.component = component
    // 返回virtualDOM
    return nextVirtualDOM 
}

function mountComponent(virtualDOM, container) { 
    let nextVirtualDOM = null 
    // 是否函数组件
    if (isFunctionalComponent(virtualDOM)) { 
        // 调用函数组件获取最新virtualDOM
        nextVirtualDOM = buildFunctionalComponent(virtualDOM) 
    } else { 
        // 调用class组件获取最新virtualDOM
        nextVirtualDOM = buildStatefulComponent(virtualDOM)
    } 
    if (isFunction(nextVirtualDOM)) { 
        mountComponent(nextVirtualDOM, container) 
    } else { 
        mountNativeElement(nextVirtualDOM, container) 
    } 
}

function mountNativeElement(virtualDOM, container) {
    const component = virtualDOM.component
    if (component) { 
        // 将组件对应的com阶段,调用setDom挂载到实例的this.dom下。
        component.setDOM(container) 
    } 
}