Vue源码解析系列——diff算法篇:diff算法(一)

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

写在前面

有了前面数据驱动、组件化、响应式原理篇的知识储备,(没看过的同学可以戳这:《Vue源码分析系列》)这时我们就有足够的的理论去支持我们研究Vue里面大名鼎鼎的diff算法了。

_update

diff算法体现在页面的更新过程中,所以我们直接进入_update查看:

const prevVnode = vm._vnode;
 if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
}

由于是页面更新过程,所以vm._vnode是有值的,所以最后调用vm.__patch__(prevVnode, vnode)

patch

之前我们在patch的执行过程有提到过最后__patch__调用的是createPatchFunction的返回值函数:

/**
   * 使用函数柯里化牵引着一些平台化的dom操作
   */
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    //没有找到新节点,但是有旧节点,就删除该节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
      return;
    }

    let isInitialPatch = false;
    const insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      //没有找到就节点(也有可能是组件),就代表是个新增节点
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      //判断是不是真实的dom
      const isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        //如果节点相同(节点内部更改)
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        //如果节点不同,为新vnode创建新dom节点,删除旧的dom节点
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.

          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true);
              return oldVnode;
            } else if (process.env.NODE_ENV !== "production") {
              warn(
                "The client-side rendered virtual DOM tree is not matching " +
                  "server-rendered content. This is likely caused by incorrect " +
                  "HTML markup, for example nesting block-level elements inside " +
                  "

, or missing . Bailing hydration and performing " + "full client-side render." ); } } // either not server-rendered, or hydration failed. // create an empty node and replace it //将真实的dom转换为一个VNode oldVnode = emptyNodeAt(oldVnode); } // replacing existing element //真实的dom => #app const oldElm = oldVnode.elm; //父节点 => body const parentElm = nodeOps.parentNode(oldElm); // create new node //深度优先创建真实DOM createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent; //是否可挂载 const patchable = isPatchable(vnode); while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor); } ancestor.elm = vnode.elm; if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor); } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert; if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i](); } } } else { registerRef(ancestor); } ancestor = ancestor.parent; } } // destroy old node //在dom上删除老的节点 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm; };

前面的我不多赘述了,想知道的可以直接去看我之前的:patch的执行过程。
我们这边直接进入这个判断if (!isRealElement && sameVnode(oldVnode, vnode)),由于是页面更新过程,所以isRealElementfalse,之后按照一个算法比较新旧vnode是否相同,如果相同,说明是vnode内部可能有变化,就调用patchVnode,进入patchVnode

patchVnode

patchVnode这个方法有点长,我一段一段拆开来分析。
前面一些无关紧要的逻辑我直接就略过了,一直到这里:

let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
  //组件vnode,执行prepatch钩子
  i(oldVnode, vnode);
}

判断这个vnode是不是有data.hook,有的话说明是一个组件vnode(占位符vnode),调用prepatchhook。这个prepatchhook等我们把patchVnode分析完毕后再回来分析。(因为在prepatch中会递归调用patchVnode
继续向下阅读patchVnode

const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
  //如果有data,执行update钩子
  for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
  if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}

判断有没有vnode.data且这个vnode是一个可挂载的vnode,(isPatchable方法感兴趣的童鞋可以去关注下,我觉得挺有意思的),如果是的话就执行update hook。
继续:

if (isUndef(vnode.text)) {
  //新节点不是文本节点
   if (isDef(oldCh) && isDef(ch)) {
     //新旧节点都存在children就执行updateChildren
     if (oldCh !== ch)
       updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
   } else if (isDef(ch)) {
     //新节点增加了children
     if (process.env.NODE_ENV !== "production") {
       checkDuplicateKeys(ch);
     }
     //如果老节点是文本节点,清除DOM上的老节点的文本gggggggggf
     if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
     //新节点做DOM插入
     addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
   } else if (isDef(oldCh)) {
     //新节点删除了children,删除老的DOM元素
     removeVnodes(oldCh, 0, oldCh.length - 1);
   } else if (isDef(oldVnode.text)) {
     //新节点和老节点都没有children,且老节点有文本节点
     //删除DOM上的老文本节点
     nodeOps.setTextContent(elm, "");
   }
} else if (oldVnode.text !== vnode.text) {
  //文本节点更新
  nodeOps.setTextContent(elm, vnode.text);
}

这部分有很多的判断,这些判断在这里不好描述,我全部写在上面的注释里面了。
需要特别关注的是:

//新旧节点都存在children就执行updateChildren
if (oldCh !== ch)
  updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);

如果新旧节点都存在children就执行updateChildren,这个updateChildren的实现是diff算法里非常复杂的部分,我们后面专开一章节来研究他。

继续向下阅读patchVnode

if (isDef(data)) {
  if (isDef((i = data.hook)) && isDef((i = i.postpatch)))
    i(oldVnode, vnode);
}

最后执行了一个postpatchhook。

prepatch hook

回到之前提到过的prepatch hook,定义在create-component.js中:

prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions;
  const child = (vnode.componentInstance = oldVnode.componentInstance);
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  );
},

调用了updateChildComponent,进入updateChildComponent

updateChildComponent

export function updateChildComponent(
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== "production") {
    isUpdatingChildComponent = true;
  }

  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren.

  // check if there are dynamic scopedSlots (hand-written or compiled but with
  // dynamic slot names). Static scoped slots compiled from template has the
  // "$stable" marker.
  const newScopedSlots = parentVnode.data.scopedSlots;
  const oldScopedSlots = vm.$scopedSlots;
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  );

  // Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  const needsForceUpdate = !!(
    renderChildren || // has new static slots
    vm.$options._renderChildren || // has old static slots
    hasDynamicScopedSlot
  );

  vm.$options._parentVnode = parentVnode;
  vm.$vnode = parentVnode; // update vm's placeholder node without re-render

  if (vm._vnode) {
    // update child tree's parent
    vm._vnode.parent = parentVnode;
  }
  vm.$options._renderChildren = renderChildren;

  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject;
  vm.$listeners = listeners || emptyObject;

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false);
    const props = vm._props;
    const propKeys = vm.$options._propKeys || [];
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i];
      const propOptions: any = vm.$options.props; // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm);
    }
    toggleObserving(true);
    // keep a copy of raw propsData
    vm.$options.propsData = propsData;
  }

  // update listeners
  listeners = listeners || emptyObject;
  const oldListeners = vm.$options._parentListeners;
  vm.$options._parentListeners = listeners;
  updateComponentListeners(vm, listeners, oldListeners);

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context);
    vm.$forceUpdate();
  }

  if (process.env.NODE_ENV !== "production") {
    isUpdatingChildComponent = false;
  }
}

平时我们更新组件主要是因为组件中props的变化,所以我们这边主要关注props的逻辑:

const props = vm._props;
const propKeys = vm.$options._propKeys || [];
for (let i = 0; i < propKeys.length; i++) {
  const key = propKeys[i];
  const propOptions: any = vm.$options.props; // wtf flow?
  props[key] = validateProp(key, propOptions, propsData, vm);
}

这里对props[key]进行了赋值,同时会触发propssetter,会通知props中的依赖(组件的render watcher),进行更新页面,同时又会触发组件的_update,重复触发了以上所有逻辑。

总结

这篇文章主要是分析了diff算法的浅层,浅层次的diff算法逻辑还是比较简单的,更加深层次的updateChildren我们放在下一篇文章中去解析。

你可能感兴趣的:(vue,源码)