响应式数据发生变化,触发渲染 watcher 的回调函数,进而执行组件的更新过程 组件更新过程 组件更新过程是新旧 vnode diff 的过程 新旧节点不相同:1. 创建新节点 2.更新父占位符节点 3.删除旧节点 新旧节点相同:1. 获取他们 children,根据不同情况做不同的更新逻辑 最复杂是新旧节点相同,而且都存在子节点,会执行 updateChildren diff 算法 源码 组件…

响应式数据发生变化,触发渲染 watcher 的回调函数,进而执行组件的更新过程

组件更新过程

  1. 组件更新过程是新旧 vnode diff 的过程
  2. 新旧节点不相同:1. 创建新节点 2.更新父占位符节点 3.删除旧节点
  3. 新旧节点相同:1. 获取他们 children,根据不同情况做不同的更新逻辑
  4. 最复杂是新旧节点相同,而且都存在子节点,会执行 updateChildren diff 算法

源码

  1. 组件更新调用了 vm._update 方法
// src/core/instance/lifecycle.js
export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
  updateComponent = () => {
    vm._update(vm._render(), hydrating);
  };
  // 渲染watcher
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate');
        }
      }
    },
    true /* isRenderWatcher */
  );
}
// src/core/instance/lifecycle.js
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  // 组件更新再次执行_update prevVnode是有的
  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);
  }
  // ...
};
  1. 组件更新调用 vm.__patch__方法
//  src/core/vdom/patch.js
return function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
    return;
  }

  let isInitialPatch = false;
  const insertedVnodeQueue = [];
  // 因为oldVnode已经定义,所以会走到sameVnode来判断是否是相同的vnode,进而走不同的更新逻辑
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  } else {
    // 组件更新时isRealElement原生节点为false
    const isRealElement = isDef(oldVnode.nodeType);
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 比对新旧vnode,sameVnode是否相同
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
    } else {
      if (isRealElement) {
        // ...
      }

      // replacing existing element
      const oldElm = oldVnode.elm;
      const parentElm = nodeOps.parentNode(oldElm);

      // create new node
      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
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0);
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode);
      }
    }
  }
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
};
  1. sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode

    //  src/core/vdom/patch.js
    function sameVnode(a, b) {
      // 如果两个vnode的key不同,就代表是不同的组件,key就是v-fof是key
      // 如果是同一个组件,并且还是同步组件, 则判断isComment、data,input类型是否相同
      // 如果是同一个组件,并且是异步组件,则判断asyncFactory是否相同
      return a.key === b.key && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)));
    }
    

新旧节点比较

新旧节点不相同

  1. 创建新节点
// 以当前旧节点为参考节点,创建新节点,插入到DOM中
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// 创建新节点
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)
);
  1. 更新父的占位符节点
if (isDef(vnode.parent)) {
  // 在render过程中,通过render函数生成的渲染vnode 指向父的vnode
  let ancestor = vnode.parent;
  // 找到当前vnode的父占位符节点
  // isPatchable是一直遍历组件vnode,找到真实的vnode(根vnode)
  const patchable = isPatchable(vnode);
  while (ancestor) {
    for (let i = 0; i < cbs.destroy.length; ++i) {
      // 先执行各个module的destroy钩子函数
      cbs.destroy[i](ancestor);
    }
    ancestor.elm = vnode.elm;
    // 如果占位符是一个可挂载的节点
    if (patchable) {
      for (let i = 0; i < cbs.create.length; ++i) {
        // 则执行module的create钩子函数
        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;
  }
}
  1. 删除旧节点
// 把oldVnode从当前DOM树中删除,如果父节点存在,则执行removeVnodes方法
if (isDef(parentElm)) {
  removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode);
}
// 遍历待删除的vnodes
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        // 从DOM中移除节点,并执行module的remove钩子函数,并对子节点递归调用removeAndInvokeRemoveHook函数
        removeAndInvokeRemoveHook(ch);
        // 执行module的destroy钩子函数,以及vnode的destory钩子函数,并对子vnode递归调用invokeDestroyHook
        // 在执行invokeDestroyHook会执行vnode的destory生命周期
        invokeDestroyHook(ch);
      } else {
        // 调用平台API把真正的DOM节点移除
        removeNode(ch.elm);
      }
    }
  }
}
// 从DOM中移除节点,并执行module的remove钩子函数,并对子节点递归调用removeAndInvokeRemoveHook函数
function removeAndInvokeRemoveHook(vnode, rm) {
  if (isDef(rm) || isDef(vnode.data)) {
    let i;
    const listeners = cbs.remove.length + 1;
    if (isDef(rm)) {
      // we have a recursively passed down rm callback
      // increase the listeners count
      rm.listeners += listeners;
    } else {
      // directly removing
      rm = createRmCb(vnode.elm, listeners);
    }
    // recursively invoke hooks on child component root node
    if (isDef((i = vnode.componentInstance)) && isDef((i = i._vnode)) && isDef(i.data)) {
      removeAndInvokeRemoveHook(i, rm);
    }
    for (i = 0; i < cbs.remove.length; ++i) {
      cbs.remove[i](vnode, rm);
    }
    if (isDef((i = vnode.data.hook)) && isDef((i = i.remove))) {
      i(vnode, rm);
    } else {
      rm();
    }
  } else {
    removeNode(vnode.elm);
  }
}
// 执行module的destroy钩子函数,以及vnode的destory钩子函数,并对子vnode递归调用invokeDestroyHook
function invokeDestroyHook(vnode) {
  let i, j;
  const data = vnode.data;
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
  }
  if (isDef((i = vnode.children))) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j]);
    }
  }
}

新旧节点相同

  1. 调用 patchVnode 方法
// src/core/vdom/patch.js
// patchVnode就是把新的vnode patch到旧的vnode
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  if (oldVnode === vnode) {
    return;
  }
  const elm = (vnode.elm = oldVnode.elm);
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }
    return;
  }
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  // 核心步骤1:执行prepatch钩子函数
  // prepatch是拿到新的vnode组件配置以及组件实例,去执行updateChildComponent方法,对子components做更新
  let i;
  const data = vnode.data;
  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode);
  }
  // 核心步骤2:执行update钩子函数
  if (isDef(data) && isPatchable(vnode)) {
    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);
  }
  // 核心步骤3:完成patch过程
  const oldCh = oldVnode.children;
  const ch = vnode.children;
  if (isUndef(vnode.text)) {
    // 如果1.	新老子节点都存在且不相同时
    // 都存在children
    if (isDef(oldCh) && isDef(ch)) {
      // 使用updateChildren来更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    } else if (isDef(ch)) {
      // 如果新的子节点存在,表示旧节点不需要了,如果旧节点是文本节点,将文本清除
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
      // 通过addVnodes将新节点插入到新节点elm下
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 如果老的子节点存在,则表明更新的是空节点,需要将旧的节点通通删掉
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 当旧节点是文本节点的时候,清除其节点文本内容
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text);
  }
  // 核心步骤4:执行postpatch钩子函数
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
  }
}
  • prepatch 方法
// prepatch是拿到新的vnode组件配置以及组件实例,去执行updateChildComponent方法
// src/core/vdom/create-component.js
const componentVNodeHooks = {
  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 方法
// src/core/instance/lifecycle.js
// 更新vm的一系列属性没比如slot,listener,props等更新
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
  const hasChildren = !!(
    renderChildren || // has new static slots
    vm.$options._renderChildren || // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject
  ); // has old scoped slots

  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 (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context);
    vm.$forceUpdate();
  }

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

新旧节点相同的 updateChildren 逻辑

// 递归patch vnode的过程
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  //  旧节点开始的位置
  let oldStartIdx = 0;
  // 新节点开始的位置
  let newStartIdx = 0;
  // 旧节点最后的位置
  let oldEndIdx = oldCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  // 新节点最后的位置
  let newEndIdx = newCh.length - 1;
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly;

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh);
  }
  // 如果旧节点开始位置小于等于旧节点最后位置,新节点开始位置小于等于新节点最后位置
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
          oldCh[idxInOld] = undefined;
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // 如果oldstartIndex>oldEndIdx 做插入动作
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}
<template>
  <div id="app">
    <div>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.val }}</li>
      </ul>
    </div>
    <button @click="change">change</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      items: [{ id: 0, val: 'A' }, { id: 1, val: 'B' }, { id: 2, val: 'C' }, { id: 3, val: 'D' }]
    };
  },
  methods: {
    change() {
      this.items.reverse().push({ id: 4, val: 'E' });
    }
  }
};
</script>

上例中 updateChildren diff 算法

  1. 步骤 1

    初始化 vnode

步骤 1

  1. 步骤 2

    执行 while 循环,走到 sameVnode(oldEndVnode, newStartVnode)

步骤 2

// Vnode moved left
//如果oldEndVnode与newStartVnode相等,移动oldEndVnode到最左边,并且让oldEndIdx--,让newStartIdx++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
  1. 步骤 3

    执行 while 循环,走到 sameVnode(oldEndVnode, newStartVnode)

步骤 3

// Vnode moved left
//如果oldEndVnode与newStartVnode相等,移动oldEndVnode到最左边,并且让oldEndIdx--,让newStartIdx++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
  1. 步骤 4

    执行 while 循环,走到 sameVnode(oldEndVnode, newStartVnode)

步骤 4

// Vnode moved left
//如果oldEndVnode与newStartVnode相等,移动oldEndVnode到最左边,并且让oldEndIdx--,让newStartIdx++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
  1. 步骤 5

    执行 while 循环,走到 sameVnode(oldStartVnode, newStartVnode)

步骤 5

//如果oldStartVnode与newStartVnode相等,并且让oldStartIdx++,让newStartIdx++,不移动vnode
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
  1. 步骤 6

    不执行 while 循环,走到 if (oldStartIdx > oldEndIdx)

步骤 6

// 此时oldstartIndex>oldEndIdx 做插入动作,把newEndVnode插入到oldStartIndex位置
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);

暂无评论