Preact源码阅读(二)- Diff流程

1,349 阅读10分钟

在Preact源码阅读(一)我们分析了render函数、diff函数的功能,分析了React Component的生命周期映射、父子组件的周期执行顺序,在本节中,我们将继续(一)中未分析的内容,介绍Preact的diff流程的处理流程、vnode到dom的转换及插入过程。let we go, 继续我们的源码阅读之旅。

1、关键子函数分析

在分析整体的流程之前,我们先看看几个关键的子函数,这可以帮助我们更好的理解整体的diff流程。我们主要分析与diff流程之外的函数,方便后续diff流程时,我们理解子函数的功能。

1.1 props

Preact props文件定义属性添加、比较、事件相关的一系列子功能,我们首先看下其包括的具体功能。

1.1.1 eventProxy

我们首先看下事件代理的功能,可以看到,当我们定义了options.event hooks时,会首先执行options.event hook,完成事件的代理。

function eventProxy(e) {
    this._listeners[e.type](options.event ? options.event(e) : e);
}

在(一)中我们也看到很多的options.*的hook功能,这里就分析下它的功能。options作为一个基础的工具库,可以自定义一系列的功能,比较典型的两种场景如下:

  • Preact官方的使用。一是,Preact到React的功能映射及转换,如compat/src/render.js里,定义了React的Event处理;二是,debug/test时的调用,方便debug、性能的测试。
  • 开发者自定义。当然,我们也可以自己定义options hook,具体的功能,可以参见preactjs options

1.1.2 setStyle

setStyle,用来设置元素的style属性。Preact对style属性分了4类情况:

  • 以'-'开头的元素。直接使用setProperty设置style的属性。
  • value为数值且需要设置px的属性,典型的如height、width等。这里的IS_NON_DIMENSIONAL包含一些不需要添加px的属性,如zoom、ine等。
  • value为null时,设置为''。
  • 默认逻辑,style[key]等于value。
function setStyle(style, key, value) {
    if (key[0] === '-') {
        style.setProperty(key, value);
    } else if (
        typeof value == 'number' &&
        IS_NON_DIMENSIONAL.test(key) === false
    ) {
        style[key] = value + 'px';
    } else if (value == null) {
        style[key] = '';
    } else {
        style[key] = value;
    }
}

1.1.3 setProperty

dom Property函数用来处理dom element的property, 其具体的处理流程如下:

  • className初始处理。svg图片,className => class, 非svg, class => className。
if (isSvg) {
    if (name === 'className') {
        name = 'class';
    }
} else if (name === 'class') {
    name = 'className';
}
  • Style。分为两种类型,string/object。
    • string时,dom.style.cssText = style;
    • object时。
      • oldValue string时,dom.style.cssText清空、oldValue为null。
      • oldValue为对象时。
        • oldValue有但value没有的属性,设置为''.
        • value设置新的style属性。
if (name === 'style') {
    s = dom.style;
    // string处理
    if (typeof value == 'string') {
        s.cssText = value;
    } else {
        // oldValue string
        if (typeof oldValue == 'string') {
            s.cssText = '';
            oldValue = null;
        }
        // oldValue object
        // oldValue有但value没有的属性,设置为''
        if (oldValue) {
            for (let i in oldValue) {
                if (!(value && i in value)) {
                    setStyle(s, i, '');
                }
            }
        }
        // 设置新的value属性
        if (value) {
            for (let i in value) {
                if (!oldValue || value[i] !== oldValue[i]) {
                    setStyle(s, i, value[i]);
                }
            }
        }
    }
}
  • 以on开头的属性,event的处理。区分onClick、onClickCapture两种场景。
    • value不存在时 ,移除name的事件。
    • value存在,dom添加event的listener。
else if (name[0] === 'o' && name[1] === 'n') {
    useCapture = name !== (name = name.replace(/Capture$/, ''));
    nameLower = name.toLowerCase();
    name = (nameLower in dom ? nameLower : name).slice(2);
    if (value) {
        if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
        (dom._listeners || (dom._listeners = {}))[name] = value;
    } else {
        dom.removeEventListener(name, eventProxy, useCapture);
    }
}
  • 正常操作。非SVG且name在dom且name!=list/tagName/form/type/size时,设置dom.name等于value。由于tagName、form、list等属性,在dom里只读属性,是不可以设置的,因此更新时,过滤这些属性的操作。
else if (
    name !== 'list' &&
    name !== 'tagName' &&
    name !== 'form' &&
    name !== 'type' &&
    name !== 'size' &&
    !isSvg &&
    name in dom
) {
    dom[name] = value == null ? '' : value;
}
  • 除以上及value非函数、name不等于"dangerouslySetInnerHTML"的处理。
    • www.w3.org/1999/xlink 属性处理,value为null/false时,removeAttributeNS对应的属性;否则,setAttributeNS对应的属性。
    • value为null、value为false且非ARIA-attributes属性节点时,removeAttribute该属性。
      • ARIA属性存在为false的场景,因此,需要过滤该类型的属性。
    • 除以上的情况,直接设置setAttribute(name, value).
else if (typeof value != 'function' && name !== 'dangerouslySetInnerHTML') {
    if (name !== (name = name.replace(/^xlink:?/, ''))) {
        if (value == null || value === false) {
            dom.removeAttributeNS(
                'http://www.w3.org/1999/xlink',
                name.toLowerCase()
            );
        } else {
            dom.setAttributeNS(
                'http://www.w3.org/1999/xlink',
                name.toLowerCase(),
                value
            );
        }
    } else if (
        value == null ||
        (value === false &&
            !/^ar/.test(name))
    ) {
        dom.removeAttribute(name);
    } else {
        dom.setAttribute(name, value);
    }
  }
}

1.1.4 diffProps

在分析完基础的子函数后,我们看下diffProps函数的核心功能,其主要是vnode节点的props diff并将变更应用到对应的dom节点上。

  • 除children/key外,oldProps有但newProps没有的属性,调用setProperty设置为null.
  • 递归处理newProps的属性,满足如下三种条件下,setProperty i更新为新的newProps[i]。
    • 除children/value/key/checked外
    • 非hydrate模式,或hydrate下newProps[i]为函数
    • oldProps、newProps属性不想等。
export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
    let i;
    // 处理oldProps有但newProps没有的props
    for (i in oldProps) {
        // 除children/key外,i不在newProps,设置为null
        if (i !== 'children' && i !== 'key' && !(i in newProps)) {
            setProperty(dom, i, null, oldProps[i], isSvg);
        }
    }
    
    // 处理新的props
    for (i in newProps) {
        // 除children/value/key/checked属性外
        // 非hydrate模式,或hydrate下newProps[i]为函数
        // oldProps[i] !== newProps[i]
        if (
            (!hydrate || typeof newProps[i] == 'function') &&
            i !== 'children' &&
            i !== 'key' &&
            i !== 'value' &&
            i !== 'checked' &&
            oldProps[i] !== newProps[i]
        ) {
            // 设置i属性,更新为newProps[i]
            setProperty(dom, i, newProps[i], oldProps[i], isSvg);
        }
    }
}

1.2 diffchildren

diffChildren包含了核心的diffChildren功能,这里主要看一下toChildArray的功能。

1.2.1 toChildArray

toChildArray功能主要是将children转换为数组,并且对children的子元素做toChildArray的递归处理。

export function toChildArray(children) {
    if (children == null || typeof children == 'boolean') {
        return [];
    } else if (Array.isArray(children)) {
        return EMPTY_ARR.concat.apply([], children.map(toChildArray));
    }
    return [children];
}

1.3 applyRef/unmount/removeNode

1.3.1 applyRef

触发/更新ref对象,若为函数,则执行ref(value), 否则设置ref.current = value.

export function applyRef(ref, value, vnode) {
  try {
    if (typeof ref == 'function') ref(value);
    else ref.current = value;
  } catch (e) {
    options._catchError(e, vnode);
  }
}

1.3.2 unmount

unmount为节点的卸载函数,skipRemove标识父节点,是否从当前DOM分离。

  • 清空当前节点/组件的refs。
  • vnode.type不为组件时,重置skipRemove。
  • 设置vnode.dom/vnode._nextDom为undefined。
  • 若当前节点为component时,执行componentWillUnmount函数、设置r.base = r._parentDom = null。
  • 递归处理vnode._children, 卸载当前元素的子节点。
  • dom存在时,remove当前Dom Element。
export function unmount(vnode, parentVNode, skipRemove) {
  let r;
  if (options.unmount) options.unmount(vnode);
  if ((r = vnode.ref)) {
    if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode);
  }
  let dom;
  if (!skipRemove && typeof vnode.type != 'function') {
    skipRemove = (dom = vnode._dom) != null;
  }

  vnode._dom = vnode._nextDom = undefined;
  if ((r = vnode._component) != null) {
    if (r.componentWillUnmount) {
      try {
        r.componentWillUnmount();
      } catch (e) {
        options._catchError(e, parentVNode);
      }
    }
    r.base = r._parentDom = null;
  }
  if ((r = vnode._children)) {
    for (let i = 0; i < r.length; i++) {
      if (r[i]) unmount(r[i], parentVNode, skipRemove);
    }
  }
  if (dom != null) removeNode(dom);
}

1.3.3 removeNode

调用DOM removeChildren,完成node节点的删除。

export function removeNode(node) {
  let parentNode = node.parentNode;
  if (parentNode) parentNode.removeChild(node);
}

1.4 getDomSibling

getDomSibling函数用来获取索引为index的兄弟节点、当前节点的相邻节点。

  • childIndex等于null时,获取当前节点的相邻节点。
  • childIndex存在时,从索引开始寻找children[i]._dom存在的节点,返回该节点的_dom.
  • 在上一步,若仍然未找到_dom,只能从当前节点的父节点继续查询,寻找关联的节点。
export function getDomSibling(vnode, childIndex) {
  if (childIndex == null) {
   return vnode._parent
      ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
      : null;
  }
  let sibling;
  for (; childIndex < vnode._children.length; childIndex++) {
    sibling = vnode._children[childIndex];
    if (sibling != null && sibling._dom != null) {
      return sibling._dom;
    }
  }

  return typeof vnode.type == 'function' ? getDomSibling(vnode) : null;
}

2、整体的diff流程

preact的diff流程采用深度搜索的方法,从跟节点出发,递归遍历children节点/组件,在叶节点完成dom的生成与转换,从而完成diff的流程。我们先看下diffElementNodes/diffChildren的功能,后续在分析preact整体的diff流程。

2.1 diffElementNodes

通过函数的名称,我们就能知道diffElementNodes用来进行element node的比较,生成dom节点。 diffElementNodes的入参数如下:

diffElementNodes的函数处理流程如下:

2.1.1 初始化

参数初始化。初始化oldProps、newProps、isSvg等属性。

let i;
let oldProps = oldVNode.props;
let newProps = newVNode.props;
isSvg = newVNode.type === 'svg' || isSvg;

2.1.2 excessDomChildren

excessDomChildren属性用于hydration模式下现有DOM节点的复用,现从整体的角度去看其基本功能及作用。 如下,现以一个简单的Demo,说明excessDomChildren的整体功能。

当excessDomChildren存在时,寻找当前nodes的值,与dom比较,复用现有的dom节点(diffElementNodes)。依据节点类型分为三类:

  • 节点类型为文本节时(nodeType === 3), 复用现有的文本节点。
  • 节点类型为非文本节点时, child.localName === newVNode.type, 即标签名称相同时, 如div、span元素。
  • dom节点与child相同时,复用现有的节点。
  if (excessDomChildren != null) {
    for (i = 0; i < excessDomChildren.length; i++) {
      const child = excessDomChildren[i];
      if (
        child != null &&
        ((newVNode.type === null
          ? child.nodeType === 3
          : child.localName === newVNode.type) ||
          dom == child)
      ) {
        dom = child;
        excessDomChildren[i] = null;
        break;
      }
    }
  }

excessDomChildren存在时,当前节点的children diff时,会设置excessDomChildren为当前节点的childNodes。

if (excessDomChildren != null) {
  excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
} 

excessDomChildren的重置,在diffchildren函数里,清除excessDomChildren[i]的子节点。

  // Remove children that are not part of any vnode.
  if (excessDomChildren != null && typeof newParentVNode.type != 'function') {
    for (i = excessDomChildren.length; i--; ) {
      if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
    }
  }

现在以某个demo,我们看下excessDomChildren是怎么复用现有的dom,完成节点的更新及替换的。默认插入的节点为body,render时excessDomChildren的结构[text、div.hello-world、script、text]。 如下图,demo excessDomChildren节点的处理与比较流程如下:

  • 比较div.hello-world节点时,excessDomChildren为初始化的document.body.childNodes节点。
    • excessDomChildren不为null, 匹配中child.localName(div) === newVNode.type div匹配,dom = document.body.childNodes.div节点,设置对应excessDomChildren[i]为null。
    • excessDomChildren不为null,当前子节点的excessDomChildren设置为[text, div, text('world')], 调用diffChildren完成childredn节点的匹配。
  • 比较div节点,适配div元素。
    • 此时excessDomChildrenan按照匹配规则,适配div节点,dom = excessDomChildren.div。
    • excessDomChildren不为null,当前子节点的excessDomChildren设置为[text('world')], 调用diffChildren完成childredn节点的匹配。
  • 比较world文本节点,适配文本节点。
    • 此时excessDomChildrenan按照匹配规则,适配文本节点, dom = excessDomChildren[0], 完成文本节点的适配。
    • world文本节点无子节点,返回到上一轮的节点diff。
  • div兄弟节点world, 适配excessDomChildrenan的文本节点。
    • 按照适配规则,文本节点适配(text('')),并没有适配最后一个节点。 excessDomChildren主要目的就是更好的复用已有的dom元素,提高节点创建的损耗,更快的渲染出页面。

2.1.3 新节点的创建

当dom为null时,表明当前的节点为新添加的节点,如isA & <A />的场景,此时依据节点的类型,分三类进行处理:

  if (dom == null) {
    if (newVNode.type === null) {
      return document.createTextNode(newProps);
    }
    dom = isSvg
      ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
      : document.createElement(
          newVNode.type,
          newProps.is && { is: newProps.is }
        );
    excessDomChildren = null;
    isHydrating = false;
  }
  • 文本节点。type 为null时,标识为文本节点,使用document.createTextNode(newProps)完成文本节点的创建。
  • SVG。调用document.createElementNS('www.w3.org/2000/svg', newVNode.type)创建SVG元素。
  • element,如div/span/input等。调用document.createElement完成节点的创建。
  • 重置excessDomChildren、isHydrating。dom为新增的元素,没必要复用现有的节点,所有设置excessDomChildren为null。创建新的节点,现有的元素节点无法复用,isHydrating设置为false。

2.1.4 props及children的处理

diffElementNodes在props/children的处理,可以分为如下的两个方面:

  • 文本节点,props的直接替换。
if (newVNode.type === null) {
  if (oldProps !== newProps && dom.data != newProps) {
    dom.data = newProps;
  }
}
  • 非文本节点,依据dangerouslySetInnerHTML分成两种模式,处理props及children.
    • 若使用dangerouslySetInnerHTML, 计算生成新的html。
let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
// 非isHydrating模式下,获取旧的props,isHydrating模式下不处理旧props。
if (!isHydrating) {
  // 获取dom attributes,拷贝到oldProps里。
  if (excessDomChildren != null) {
    oldProps = {};
    for (let i = 0; i < dom.attributes.length; i++) {
      oldProps[dom.attributes[i].name] = dom.attributes[i].value;
    }
  }
  // newHtml与newHtml存在且不想等,设置dom.innerHTML
  if (newHtml || oldHtml) {
    if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
      dom.innerHTML = (newHtml && newHtml.__html) || '';
    }
  }
}
  • newProps与oldProps比较。
diffProps(dom, newProps, oldProps, isSvg, isHydrating);
  • Children diff。dangerouslySetInnerHTML模式下,设置_chilren=[],否则比较当前节点的children。
if (newHtml) {
  newVNode._children = [];
} else {
  i = newVNode.props.children;
  // children的diff,开启新一轮递归
  diffChildren(
    dom,
    Array.isArray(i) ? i : [i],
    newVNode,
    oldVNode,
    globalContext,
    newVNode.type === 'foreignObject' ? false : isSvg,
    excessDomChildren,
    commitQueue,
    EMPTY_OBJ,
    isHydrating
  );
}
  • 非isHydrating模式下,比较属性的value、checked属性,一般用于input/textarea等节点。
if (!isHydrating) {
  if (
    'value' in newProps &&
    (i = newProps.value) !== undefined &&
    i !== dom.value
  ) {
    setProperty(dom, 'value', i, oldProps.value, false);
  }
  if (
    'checked' in newProps &&
    (i = newProps.checked) !== undefined &&
    i !== dom.checked
  ) {
    setProperty(dom, 'checked', i, oldProps.checked, false);
  }
}
}
  • 返回生成的dom节点。

2.2 diffChildren

diffChildren的入参与diff、diffElementNodes类似,其具体的参数如下:

2.2.1 初始化参数

diffChildren定义了常用的参数,包括oldNode、childVNode、newDom、firstChildDom、refs等。

let i, j, oldVNode, childVNode, newDom, firstChildDom, refs;
// 获取旧节点的_children, 不存在时,设置为空数组。
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// 获取旧节点的children长度
let oldChildrenLength = oldChildren.length;

2.2.2 oldDom设置

oldDom不存在的场景,只在调用render或diffElementNodes才会设置为EMPTY_OBJ, 此时,需复用现有excessDomChildren、oldParentVNode,得到新的oldDom.

if (oldDom == EMPTY_OBJ) {
    //excessDomChildren存在,oldDom设置为首个节点。
    if (excessDomChildren != null) { 
      oldDom = excessDomChildren[0];
    } else if (oldChildrenLength) {
      // oldChildren存在时,取虚拟节点dom
      oldDom = getDomSibling(oldParentVNode, 0);
    } else {
      // 不存在,设置为null
      oldDom = null;
    }
}

2.2.3 循环处理children

接下来就是循环处理renderResult,将新的子节点添加到newParentVNode._children里。

newParentVNode._children = [];
for (i = 0; i < renderResult.length; i++) {
}

下面按照节点的创建、XX的流程来介绍children节点的处理流程。

2.2.3.1 节点的创建

针对生成的节点,依据不同的类型,创建生成不同的vnode。

  • childNode为null、typeof childVNode == 'boolean'。该类型的节点,设置为null,不渲染。
  • typeof childVNode 为string/number的节点,将创建为文本节点(vnode.type = null)。
  • childVNode为数组时,将创建为Fragment节点(vnode.type = Fragment)。
  • childVNode._dom存在或者childVNode._component不为null(组件)时,重新创建childVNode.type节点。
  • childVNode已创建时,直接复制到newParentVNode._children上即可。
childVNode = renderResult[i];
if (childVNode == null || typeof childVNode == 'boolean') {
  childVNode = newParentVNode._children[i] = null;
} else if (typeof childVNode == 'string' || typeof childVNode == 'number') {
  childVNode = newParentVNode._children[i] = createVNode(
    null,
    childVNode,
    null,
    null,
    childVNode
  );
} else if (Array.isArray(childVNode)) {
  childVNode = newParentVNode._children[i] = createVNode(
    Fragment,
    { children: childVNode },
    null,
    null,
    null
  );
} else if (childVNode._dom != null || childVNode._component != null) {
  childVNode = newParentVNode._children[i] = createVNode(
    childVNode.type,
    childVNode.props,
    childVNode.key,
    null,
    childVNode._original
  );
} else {
  childVNode = newParentVNode._children[i] = childVNode;
}

完成节点的创建后, 若为null,跳过处理。不为null,绑定parentNode和设置_depth。

// childVNode节点为null,跳过处理
if (childVNode == null) {
  continue;
}
// 绑定父节点和设置_depth。
childVNode._parent = newParentVNode;
childVNode._depth = newParentVNode._depth + 1;

2.2.3.2 key节点处理

针对设置了key值的节点,preact会走专门的处理逻辑。

  • oldNode存在且新旧节点key/type相等时,设置oldChildren[i] = undefined(后续删除使用)。
  • 其他场景下,递归处理oldChildren, 寻找key/value适配的节点,若找到,设置该节点为undefined。
oldVNode = oldChildren[i];
if (
  oldVNode === null ||
  (oldVNode &&
    childVNode.key == oldVNode.key &&
    childVNode.type === oldVNode.type)
) {
  oldChildren[i] = undefined;
} else {
  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;
  }
}

2.2.3.3 diff的调用

oldNode为null/undefined时,重置为EMPTY_OBJ,调用diff函数生成newDom。 oldVNode = oldVNode || EMPTY_OBJ;

newDom = diff(
  parentDom,
  childVNode,
  oldVNode,
  globalContext,
  isSvg,
  excessDomChildren,
  commitQueue,
  oldDom,
  isHydrating
);

2.2.3.4 refs的处理

childVNode的ref添加到refs里,oldVNode的ref绑定节点设置null。

if ((j = childVNode.ref) && oldVNode.ref != j) {
  if (!refs) refs = [];
  if (oldVNode.ref) refs.push(oldVNode.ref, null, childVNode);
  refs.push(j, childVNode._component || newDom, childVNode);
}

2.2.3.5 newDom的插入

diff函数返回的新dom节点, 依据是否为null, 分为两类:

  • newDom不为null时,有如下的处理步骤:
    • 设置firstChildDom等于首个不为null的dom节点。
    • 调用placeChild,将dom节点插入到parentDom中。
    • 针对select.options特殊处理,设置其value为''。
    • newParentVNode.type为函数时,设置newParentVNode._nextDom。
  • newDom为null时,针对oldDom的某个场景特殊处理,oldDom获取下一个节点的dom。
if (newDom != null) {
  if (firstChildDom == null) {
    firstChildDom = newDom;
  }
  oldDom = placeChild(
    parentDom,
    childVNode,
    oldVNode,
    oldChildren,
    excessDomChildren,
    newDom,
    oldDom
  );

  if (newParentVNode.type == 'option') {
    parentDom.value = '';
  } else if (typeof newParentVNode.type == 'function') {
    newParentVNode._nextDom = oldDom;
  }
} else if (
  oldDom &&
  oldVNode._dom == oldDom &&
  oldDom.parentNode != parentDom
) {
  oldDom = getDomSibling(oldVNode);
}
placeChild主要负责将新生成的节点插入、移动,完成parentDom的生成。
function placeChild(
  parentDom,
  childVNode,
  oldVNode,
  oldChildren,
  excessDomChildren,
  newDom,
  oldDom
) {
  let nextDom;
  // Fragment下,_nextDom不为null。
  if (childVNode._nextDom !== undefined) {
    nextDom = childVNode._nextDom;
    childVNode._nextDom = undefined;
  // 当前仅当excessDomChildren、oldNode都为null时,两者才相等
  // 新旧dom不相等、newParent.parentNode为null
  } else if (
    excessDomChildren == oldVNode ||
    newDom != oldDom ||
    newDom.parentNode == null
  ) {
    // oldDom为null时,oldDomParentNode != parentDom时,将newDom插入到newDom
    outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
      parentDom.appendChild(newDom);
      nextDom = null;
    } else {
      // oldChildren会存在空的text节点,因此只需要length/2的次数即可
      // 当前兄弟节点是否找到newDom节点,若找到,中断执行
      for (
        let sibDom = oldDom, j = 0;
        (sibDom = sibDom.nextSibling) && j < oldChildren.length;
        j += 2
      ) {
        if (sibDom == newDom) {
          break outer;
        }
      }
      // 在oldDom前插入newDom节点
      parentDom.insertBefore(newDom, oldDom);
      nextDom = oldDom;
    }
  }

  if (nextDom !== undefined) {
    oldDom = nextDom;
  } else {
    oldDom = newDom.nextSibling;
  }
  return oldDom;
}

2.2.4 卸载处理

在完成children的diff之后,将会对excessDomChildren、oldChildren、ref做处理,卸载已有的节点、更新新添加的节点。

// 更新newParentVNode._dom为更新后的dom
newParentVNode._dom = firstChildDom;

// 非组件状态下,remove excessDomChildren不为null的节点
if (excessDomChildren != null && typeof newParentVNode.type != 'function') {
    for (i = excessDomChildren.length; i--; ) {
      if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
    }
}

// 卸载oldChildren里的节点
for (i = oldChildrenLength; i--; ) {
if (oldChildren[i] != null) unmount(oldChildren[i], oldChildren[i]);
}
// 应用refs,卸载或者触发更新
if (refs) {
    for (i = 0; i < refs.length; i++) {
      applyRef(refs[i], refs[++i], refs[++i]);
    }
}

2.3 diff的整体流程

Preact的diff流程,采取深度搜索比较的形式,从上到下,diff、diffChildren、diffElementNode循环调用,完整整个vnode dom tree的比较和处理。以如下的一个例子,我们看下初始化阶段其基本的diff处理流程。 如下图,其基本的调用流程如下,实线代表函数调用,虚线表示返回及dom的处理。我们可以看到,Preact从App节点开始,使用深度搜索的方式,递归调用diffChildren、diff、diffElementNodes三个函数,完成节点的diff、虚拟节点的创建、dom的生成,最终将内容插入到页面中。

3、总结

本节主要介绍了preact 10.4.6的diff流程,并以一个简单的demo,演示了render阶段节点的创建、比较、Dom的生成与创建流程。preact采取深度搜索的形式完成vnode的比较,在此基础上,使用了裁剪(key)、DOM节点复用等多种方式,优化现有的diff方式,提高dom diff的效率。

4、参考文档