手写react7-生命周期和domdiff

82 阅读7分钟

在实现剩下两个hook(componentWillReceiveProps和componentWillUnmount)之前,需要先实现domdiff

\

  • dom-diff核心是比较新旧虚拟DOM的差异,然后把差异同步到真实DOM节点上,主要有以下几种情况

  • 1 老新都没有

  • 2 老有新没有

  • 3.老没有新有

  • 4.老新都有

老新都没有的情况很简单,不再赘述

老的有,新的没有的情况会进入unMountVdom(oldVdom); 在这个方法里面我们可以执行卸载的钩子

export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
    //老新都没有,什么都不需要做
    if (!oldVdom && !newVdom) {
        return null;
        //如果老的有,新的没有 卸载老节点
    } else if (oldVdom && !newVdom) {
        unMountVdom(oldVdom);
        //如果老的没有,新有的
}

在卸载的方法unMountVdom里面,需要拿到组件实例对象(类组件的话)上的卸载的钩子执行,所以需要建立vdom和类组件实例的引用关系:

function mountClassComponent(vdom) {
    let { type: ClassComponent, props, ref } = vdom;
    let classInstance = new ClassComponent(props);
    //如果类组件的虚拟DOM有ref属性,那么就把类的实例赋给ref.current属性
    if (ref) ref.current = classInstance;
    if (classInstance.componentWillMount) {//组件将要挂载
        classInstance.componentWillMount();
    }
    //把类组件的实例挂载到它对应的vdom上
    vdom.classInstance = classInstance;
function unMountVdom(vdom) {
    let { props, ref } = vdom;
    let currentDOM = findDOM(vdom);//获取此虚拟DOM对应的真实DOM
    //vdom可能是原生组件span 类组件 classComponent 也可能是函数组件Function
    if (vdom.classInstance && vdom.classInstance.componentWillUnmount) {
        vdom.classInstance.componentWillUnmount();
    }
    if (ref) {
        ref.current = null;
    }
    //取消监听函数
    Object.keys(props).forEach(propName => {
        //我们现在用了合成事件
        if (propName.slice(0, 2) === 'on') {
            delete currentDOM._store;
        }
    });
    //如果此虚拟DOM有子节点的话,递归全部删除
    if (props.children) {
        //得到儿子的数组
        let children = Array.isArray(props.children) ? props.children : [props.children];
        children.forEach(unMountVdom);
    }
    //把自己这个虚拟DOM对应的真实DOM从界面删除
    currentDOM.parentNode.removeChild(currentDOM);
}

接下来的一种情况是老的没有,新的有,这种情况实际就是创建新的节点,同时也需要执行新节点创建的hook

export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
    //老新都没有,什么都不需要做
    if (!oldVdom && !newVdom) {
        return null;
        //如果老的有,新的没有 卸载老节点
    } else if (oldVdom && !newVdom) {
        unMountVdom(oldVdom);
        //如果老的没有,新有的
    } else if (!oldVdom && newVdom) {
        let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
        if (nextDOM) {
            parentDOM.insertBefore(newDOM, nextDOM)
        } else {
            parentDOM.appendChild(newDOM);//添加到父节点上
        }
        if (newDOM._componentDidMount) newDOM._componentDidMount();

最后一种,老的新的都有的情况,在代码中又体现为新老的类型不同和新老的类型相同

先来看类型不同的情况:

export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
    //老新都没有,什么都不需要做
    if (!oldVdom && !newVdom) {
        return null;
        //如果老的有,新的没有 卸载老节点
    } else if (oldVdom && !newVdom) {
        unMountVdom(oldVdom);
        //如果老的没有,新有的
    } else if (!oldVdom && newVdom) {
        let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
        if (nextDOM) {
            parentDOM.insertBefore(newDOM, nextDOM)
        } else {
            parentDOM.appendChild(newDOM);//添加到父节点上
        }
        if (newDOM._componentDidMount) newDOM._componentDidMount();
        //如果老的有,新的也有,但是类型不同
    } else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
        unMountVdom(oldVdom);//删除老的节点
        let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
        if (nextDOM) {
            parentDOM.insertBefore(newDOM, nextDOM)
        } else {
            parentDOM.appendChild(newDOM);//添加到父节点上
        }
        if (newDOM._componentDidMount) newDOM._componentDidMount();

再来看类型相同的情况,这时就需要进入更详细的对比:

我们封了一个方法专门处理这种情况

export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
    //老新都没有,什么都不需要做
    if (!oldVdom && !newVdom) {
        return null;
        //如果老的有,新的没有 卸载老节点
    } else if (oldVdom && !newVdom) {
        unMountVdom(oldVdom);
        //如果老的没有,新有的
    } else if (!oldVdom && newVdom) {
        let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
        if (nextDOM) {
            parentDOM.insertBefore(newDOM, nextDOM)
        } else {
            parentDOM.appendChild(newDOM);//添加到父节点上
        }
        if (newDOM._componentDidMount) newDOM._componentDidMount();
        //如果老的有,新的也有,但是类型不同
    } else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
        unMountVdom(oldVdom);//删除老的节点
        let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
        if (nextDOM) {
            parentDOM.insertBefore(newDOM, nextDOM)
        } else {
            parentDOM.appendChild(newDOM);//添加到父节点上
        }
        if (newDOM._componentDidMount) newDOM._componentDidMount();
        //如果老的有,新的也有,并且类型也一样,只需要更新就可以,就可以复用老的节点了
    } else {//进入 深度对比子节点的流程
        updateElement(oldVdom, newVdom);
    }
}

updateElement中,如果新老节点都是纯文本节点,直接替换文本

function updateElement(oldVdom, newVdom) {
    //如果新老节点都是纯文本节点的
    if (oldVdom.type === REACT_TEXT) {
        if (oldVdom.props.content !== newVdom.props.content) {
            let currentDOM = newVdom.dom = findDOM(oldVdom);
            currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
        }

如果是原生DOM节点,需要更新节点的属性:

function updateElement(oldVdom, newVdom) {
    //如果新老节点都是纯文本节点的
    if (oldVdom.type === REACT_TEXT) {
        if (oldVdom.props.content !== newVdom.props.content) {
            let currentDOM = newVdom.dom = findDOM(oldVdom);
            currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
        }
        //此节点是下原生组件 span div而且 类型一样,说明可以复用老的dom节点
    } else if (typeof oldVdom.type === 'string') {
        let currentDOM = newVdom.dom = findDOM(oldVdom);//获取老的真实DOM,准备复用
        updateProps(currentDOM, oldVdom.props, newVdom.props);//直接用新的属性更新老的DOM节点即可
        updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
function updateProps(dom, oldProps, newProps) {
    for (let key in newProps) {
        if (key === 'children') {//children
            continue;//此处忽略子节点的处理
        } else if (key === 'style') {//style
            let styleObj = newProps[key];
            for (let attr in styleObj) {
                dom.style[attr] = styleObj[attr];
            }
        } else if (key.startsWith('on')) {
            //dom[key.toLocaleLowerCase()] = newProps[key];
            addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
        } else {
            dom[key] = newProps[key];//className
        }
    }
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
    oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : oldVChildren ? [oldVChildren] : [];
    newVChildren = Array.isArray(newVChildren) ? newVChildren : newVChildren ? [newVChildren] : [];
    let maxChildrenLength = Math.max(oldVChildren.length, newVChildren.length);
    //oldChildren=3 newChildren=2   oldChildren=2 newChildren=3
    for (let i = 0; i < maxChildrenLength; i++) {
        //试图取出当前的节点的下一个,最近的弟弟真实DOM节点
        let nextVdom = oldVChildren.find((item, index) => index > i && item && findDOM(item));
        compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom));
    }
}

如果是组件节点

function updateElement(oldVdom, newVdom) {
    //如果新老节点都是纯文本节点的
    if (oldVdom.type === REACT_TEXT) {
        if (oldVdom.props.content !== newVdom.props.content) {
            let currentDOM = newVdom.dom = findDOM(oldVdom);
            currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
        }
        //此节点是下原生组件 span div而且 类型一样,说明可以复用老的dom节点
    } else if (typeof oldVdom.type === 'string') {
        let currentDOM = newVdom.dom = findDOM(oldVdom);//获取老的真实DOM,准备复用
        updateProps(currentDOM, oldVdom.props, newVdom.props);//直接用新的属性更新老的DOM节点即可
        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);
        }
    }
}

初次render调用堆栈:

render(vdom, parentDOM) ->

createDOM(vdom) ->

mountClassComponent(vdom)

vdom.classInstance = classInstance

renderVdom = classInstance.render()

vdom.oldRenderVdom = renderVdom

createDOM(renderVdom)

类组件通过事件触发setState再更新的调用堆栈:

dispatchEvent ->

updateQueue.isBatchingUpdate=true

eventHandler.call(target, syntheticEvent) ->

this.setState({...}) ->

this.updater.addState(partialState) ->

this.emitUpdate()

updateQueue.updaters.push(this)

updateQueue.isBatchingUpdate=false

updateQueue.batchUpdate() ->

updater.updateComponent() ->

shouldUpdate(classInstance, nextProps, this.getState()) ->

classInstance.props=nextProps

classInstance.state=nextState

classInstance.forceUpdate()

let newRenderVdom = this.render()

compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)

\

接着上面的分析,setState之后导致的重新渲染执行的compareTwoVdom,其参数oldRenderVdom, newRenderVdom通常情况下以真实DOM居多,所以从这个入口进到compareTwoVdom时,既有真实DOM对应的虚拟DOM的情况,也有类组件或函数组件对应的虚拟DOM的情况

遍历到普通DOM属性的情况

compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)

updateElement(oldVdom, newVdom)

updateProps(currentDOM, oldVdom.props, newVdom.props)

updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)

for (let i=0; i<maxChildrenLength; i++) {

compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom))

\

遍历到类组件的情况

compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)

updateElement(oldVdom, newVdom)

updateClassComponent(oldVdom, newVdom)

classInstance.updater.emitUpdate(newVdom.props)

属性更新导致的重新渲染通常是由于setState改变了props,这个时候通常都是遍历虚拟DOM树到类组件的时候,递归调用compareTwoVdom进入的:

compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)

updateElement(oldVdom, newVdom)

updateClassComponent(oldVdom, newVdom)

classInstance.updater.emitUpdate(newVdom.props)

this.updateComponent()

classInstance.forceUpdate()

compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)

对于普通DOM对象,vdom对象上挂载着vdom对应的DOM对象

对于类组件,vdom对象上挂载着类组件的实例classInstance和调用render渲染出来的oldRenderVdom

父子组件更新顺序:

Counter 1.constructor
Counter 2.componentWillMount
Counter 3.render
ChildCounter 1.componentWillMount
ChildCounter 2.render
ChildCounter 3.componentDidMount
Counter 4.componentDidMount

react16去掉了willxxx的生命周期函数,添加了getDerivedStateFromProps、getSnapshotBeforeUpdate两个新的生命周期函数

组件创建、更新时都会走到getDerivedStateFromProps这个钩子中

class ChildCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { number: 0 };
    }
    static getDerivedStateFromProps(nextProps, prevState) {
        const { number } = nextProps;
        return { number: number * 2 };
    }
class Component {
    static isReactComponent = true //当子类继承父类的时候 ,父类的静态属性也是可以继承的
    constructor(props) {
        this.props = props;
        this.state = {};
        this.updater = new Updater(this);
    }
    setState(partialState) {
        this.updater.addState(partialState);
    }
    //根据新的属性状态计算新的要渲染的虚拟DOM
    forceUpdate() {
        let oldRenderVdom = this.oldRenderVdom;//上一次类组件render方法计算得到的虚拟DOM
        let oldDOM = findDOM(oldRenderVdom);//获取 oldRenderVdom对应的真实DOM
        if(this.constructor.getDerivedStateFromProps){
            let newState = this.constructor.getDerivedStateFromProps(this.props,this.state);
            if(newState)
                this.state = newState;
        }

getDerivedStateFromProps主要用来代替之前的componentWillReceieProps

对于某些情况,例如向一个div中插入子节点,导致div高度增加,当高度增加到大于容器高度时,会出现滚动条,如果用户再滚动一段距离,随着子节点还在不断插入,上卷距离(scrollTop)不变,内容就会不断变化,所以我们需要手动修改上卷距离,就可以在getSnapshotBeforeUpdate这个hook中完成

		// DOM更新前
		getSnapshotBeforeUpdate() {//很关键的,我们获取当前rootNode的scrollHeight,传到componentDidUpdate 的参数perScrollHeight
        return {prevScrollTop:this.wrapper.current.scrollTop,prevScrollHeight:this.wrapper.current.scrollHeight};
    }
		// DOM更新后
    componentDidUpdate(pervProps, pervState, {prevScrollHeight,prevScrollTop}) {
        //当前向上卷去的高度加上增加的内容高度
        this.wrapper.current.scrollTop = prevScrollTop + (this.wrapper.current.scrollHeight - prevScrollHeight);
    }

getSnapshotBeforeUpdate的实现:

时机在getDerivedStateFromProps之后,还要把返回结果传给componentDidUpdate

    forceUpdate() {
        let oldRenderVdom = this.oldRenderVdom;//上一次类组件render方法计算得到的虚拟DOM
        //let oldDOM = oldRenderVdom.dom;
        let oldDOM = findDOM(oldRenderVdom);//获取 oldRenderVdom对应的真实DOM
        if(this.constructor.contextType){
            this.context = this.constructor.contextType._currentValue;
        }
        if(this.constructor.getDerivedStateFromProps){
            let newState = this.constructor.getDerivedStateFromProps(this.props,this.state);
            if(newState)
                this.state = newState;
        }
        let snapshot = this.getSnapshotBeforeUpdate&&this.getSnapshotBeforeUpdate();
        //然后基于新的属性和状态,计算新的虚拟DOM
        let newRenderVdom = this.render();
        compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
        this.oldRenderVdom = newRenderVdom;
        if (this.componentDidUpdate) {
            this.componentDidUpdate(this.props, this.state, snapshot);
        }