preact系列之diffChildren

194 阅读4分钟

阅读须知

  • 文章参考的preact版本是10.5.13
  • 文章会省略大部分逻辑,比如hydrating,context,isSvg等。所以如果有大佬点进来需谨慎。
  • 简略版本代码

diffChildren概览

  • diffChildren循环处理children节点
    • 依据不同的类型,创建生成不同的childVNode
    • 对key节点处理,判断复用或者重置oldVNode
    • 调用diff对比子节点
    • newDom插入到parentDom
  • 卸载旧节点
export function diffChildren(
    parentDom, // 当前children的父节点,如果是组件那么父节点就是往上找到第一个真实dom
    renderResult,
    newParentVNode,
    oldParentVNode,
    commitQueue,
    oldDom,  
) {
    let i, j, oldVNode, childVNode, newDom, firstChildDom;
    let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;

    let oldChildrenLength = oldChildren.length;

    newParentVNode._children = [];
    
    // 处理子节点
        // 1. 创建childVNode
        // 2. key对比进行节点复用
        // 3. 继续diff子节点
        // 4. 替换节点到dom上面
    
    // 对不需要的节点/组件 进行卸载处理
        // 执行oldChildren的vNode的卸载生命周期
}

处理子节点

for (i = 0; i < renderResult.length; i++) {
    childVNode = renderResult[i];
    
    // 依据不同的类型,创建生成不同的childVNode
    // 对key这种情况节点进行对比 复用或者重置oldVNode
    // diff 递归对比子节点
    // 进行dom的替换
}

创建childVNode

  • childNode为null、typeof childVNode == 'boolean'。该类型的节点,设置为null,不渲染。
  • typeof childVNode 为string/number的节点,将创建为文本节点(vnode.type = null)。
  • childVNode为数组时,将创建为Fragment节点(vnode.type = Fragment)。
  • childVNode已创建时,直接克隆新的vNode。
//childVNode设置为null,不渲染
if (childVNode == null || typeof childVNode == 'boolean') {
    childVNode = newParentVNode._children[i] = null;
}

// 文本节点会创建一个type==null的vNode
else if (
    typeof childVNode == 'string' ||
    typeof childVNode == 'number' ||
    typeof childVNode == 'bigint'
) {
    childVNode = newParentVNode._children[i] = createVNode(
        null,
        childVNode,
        null,
        null,
        childVNode
    );
}

// 子节点是数组创建一个type==Fragment的vNode
else if (Array.isArray(childVNode)) {
    childVNode = newParentVNode._children[i] = createVNode(
        Fragment,
        { children: childVNode },
        null,
        null,
        null
    );
}

// VNode is already in use, clone it。eg:
//   const reuse = <div />
//   <div>{reuse}<span />{reuse}</div>
else if (childVNode._depth > 0) {
    childVNode = newParentVNode._children[i] = createVNode(
        childVNode.type,
        childVNode.props,
        childVNode.key,
        null,
        childVNode._original
    );
} 

else {
    childVNode = newParentVNode._children[i] = childVNode;
}

if (childVNode == null) {
    continue;
}

// 绑定父VNode,深度比父节点+1
childVNode._parent = newParentVNode;
childVNode._depth = newParentVNode._depth + 1;

key节点对比

  • 新旧节点key和type相同,oldChildren[i] = undefined 表示复用不需要卸载(后续卸载逻辑体现)
  • 不同就遍历oldChildren,寻找key/value适配的节点,
    • 若找到,oldChildren[i] = undefined 表示复用不需要卸载
    • 找不到证明旧节点没有可复用,需要创建。设置oldVNode = null,后续diffElementNodes根据这个条件会创建真实dom。
oldVNode = oldChildren[i];
// 如果新节点和旧节点相同,oldChildren[i] = undefined
if (
    oldVNode === null ||(oldVNode &&
    childVNode.key == oldVNode.key &&
    childVNode.type === oldVNode.type)
) {
    oldChildren[i] = undefined;
} else {
    // 新旧节点不同:
    // 1. 如果在oldChildren中找到 oldVnode赋值为找到的某一个 从而复用oldVnode的dom
    // 2. 如果在oldChildren找不到 直接oldVnode=null后续赋值{} 会导致后面diffElementNodes从oldVnode拿不出dom所以会重新创建

    for (j = 0; j < oldChildrenLength; j++) {
        oldVNode = oldChildren[j];
        if (
            oldVNode &&
            childVNode.key == oldVNode.key &&
            childVNode.type === oldVNode.type
        ) {
            oldChildren[j] = undefined;
            break;
        }
        oldVNode = null;
    }
}

oldVNode = oldVNode || EMPTY_OBJ;

diff 对比子节点

diff(
    parentDom,
    childVNode,
    oldVNode,
    commitQueue,
    oldDom,
);

dom的替换

newDom = childVNode._dom;
if (!newDom) {
  firstChildDom = firstChildDom || newDom;
  
  // 组件只有在孩子节点相同才需要递归比较子节点
  if (
    typeof childVNode.type === 'function' &&
    childVNode._children === oldVNode._children
  ) {
    childVNode._nextDom = oldDom = reorderChildren(
      childVNode,
      oldDom,
      parentDom,
    );
  } 
  // 其他都直接替换
  else {
    oldDom = placeChild(
      parentDom,
      childVNode,
      oldVNode,
      oldChildren,
      newDom,
      oldDom,
    );
  }
}

reorderChildren(用于组件)

  • 组件会去递归调用,dom节点会调用placeChild
  function reorderChildren(childVNode, oldDom, parentDom) {
   for (let tmp = 0; tmp < childVNode._children.length; tmp++) {
       let vnode = childVNode._children[tmp];
       if (vnode) {
           vnode._parent = childVNode;
   
           if (typeof vnode.type == 'function') {
               oldDom = reorderChildren(vnode, oldDom, parentDom);
           } else {
               oldDom = placeChild(
                   parentDom,
                   vnode,
                   vnode,
                   childVNode._children,
                   vnode._dom,
                   oldDom
               );
           }
       }
   }
   return oldDom;
}

placeChild方法 (用于dom节点)

  • appendChild情况:
    • 首次渲染的时候没有oldVNode,所以也就没有oldDom。
    • 新建一个节点,这个节点不是复用旧节点(key旧节点复用),也没有oldDom。
  • insertBefore的情况
    • 节点被复用了,key相同导致节点被复用,此时不需要删除节点,插入旧节点前面。
  • 返回oldDom的下一个节点,作为下一个节点的oldDom
function placeChild(
    parentDom,
    childVNode,
    oldVNode,
    oldChildren,
    newDom,
    oldDom
) {
    let nextDom;
    // todo:不知道为什么只对Fragments起作用
    if (childVNode._nextDom !== undefined) {
        nextDom = childVNode._nextDom;
        childVNode._nextDom = undefined;
    } else if (
        oldVNode == null || 
        newDom != oldDom ||  
        newDom.parentNode == null
    ) {
        // 首次渲染或者新增节点oldDom是不存在  但是可能是交换节点所以要保证父节点不一样
        outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
            parentDom.appendChild(newDom);
            nextDom = null;
        } 
        
        // 主要对应上文key复用旧节点,主要是交换节点的逻辑
        else { 
            // 当前兄弟节点是否找到newDom节点,若找到,中断执行。
            // todo:不知道为什么是j+=2
            for (
                let sibDom = oldDom, j = 0;
                (sibDom = sibDom.nextSibling) && j < oldChildren.length;
                j += 2
            ) {
                if (sibDom == newDom) {
                    break outer;
                }
            }
            parentDom.insertBefore(newDom, oldDom);
            nextDom = oldDom;
        }
    }

    // 返回dom的下一个节点
    if (nextDom !== undefined) {
        oldDom = nextDom;
    } else {
        oldDom = newDom.nextSibling;
    }

    return oldDom;
}

节点appendChild逻辑(图一)和insertBefore逻辑(图2) image.png

image.png

替换逻辑

  • 赋值firstChildDom,进行dom的替换。
if (newDom != null) {
    if (firstChildDom == null) {
        firstChildDom = newDom;
    }
    
    // 组件的替换
    if (
        typeof childVNode.type == 'function' &&
        childVNode._children != null && // Can be null if childVNode suspended
        childVNode._children === oldVNode._children
    ) {
        childVNode._nextDom = oldDom = reorderChildren(
            childVNode,
            oldDom,
            parentDom
        );
    } 
    
    // 普通节点的替换
    else {
        oldDom = placeChild(
            parentDom,
            childVNode,
            oldVNode,
            oldChildren,
            newDom,
            oldDom
        );
    }
}

// _dom赋值第一个子节点
newParentVNode._dom = firstChildDom;

卸载处理

  • 对vNode节点以及其子节点递归执行componentWillUnMount,
  • 在dom树上移除oldChildren
for (i = oldChildrenLength; i--; ) {
    if (oldChildren[i] != null) {
        unmount(oldChildren[i], oldChildren[i]);
    }
}
export function unmount(vnode, parentVNode, skipRemove) {
    let r;
    
    let dom;
    if (!skipRemove && typeof vnode.type != 'function') {
        skipRemove = (dom = vnode._dom) != null;
    }

    // 执行vNode的componentWillUnmount
    if ((r = vnode._component) != null) {
        if (r.componentWillUnmount) {
            try {
                r.componentWillUnmount();
            } catch (e) {
                options._catchError(e, parentVNode);
            }
        }
        r.base = r._parentDom = null;
    }
    
    // 递归执行子组件的componentWillUnmount
    if ((r = vnode._children)) {
        for (let i = 0; i < r.length; i++) {
            if (r[i]) unmount(r[i], parentVNode, skipRemove);
        }
    }
    
    // 在dom树上移除oldChildren
    if (dom != null) removeNode(dom);
}