Vue2.x 源码剖析之虚拟 DOM

Study Notes

本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。

Vue.js 源码剖析-虚拟 DOM

什么是虚拟 DOM

虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组件机制。

Vue 1.x 中细粒度监测数据的变化,每一个属性对应一个 watcher,开销太大 Vue 2.x 中每个组件对应一个 watcher,状态变化通知到组件,再引入虚拟 DOM 进行比对和渲染

为什么要使用虚拟 DOM

  • 使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如何操作 DOM,从而提高开发效率

  • 作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR、Weex。

  • 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM,如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM 之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM

vue 虚拟 DOM 使用

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level, // 标签名称
      this.$slots.default, // 子节点数组
    );
  },
  props: {
    level: {
      type: Number,
      required: true,
    },
  },
});

createElement

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar',
      },
    }),
  ],
);

虚拟 DOM 创建过程

vue 虚拟 DOM 源码分析

createElement

在 vm._render() 中调用了用户传递的(或者编译生成的) render 函数,这个时候传递了 createElement

src/core/instance/render.js

// 对编译生成的render进行渲染的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 对手写render函数进行渲染的方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

src/core/vdom/create-element.js

使用 createElement 创建 VNode,并返回给 vm._update,最终传递给 Watcher 对象

export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number,
): VNode {
  // 如果data存在,且存在__ob__属性,创建一个空虚拟DOM节点
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data,
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context,
      );
    return createEmptyVNode();
  }
  // object syntax in v-bind
  // 如果data存在,并且存在is属性,将其赋值给tag(标签)
  // <component v-bind:is="currentTabComponent"></component>
  if (isDef(data) && isDef(data.is)) {
    tag = data.is;
  }
  // 如果不存在tag,创建一个空虚拟DOM节点
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode();
  }
  // warn against non-primitive key
  if (
    process.env.NODE_ENV !== 'production' &&
    isDef(data) &&
    isDef(data.key) &&
    !isPrimitive(data.key)
  ) {
    // 避免使用非原始值作为key
    warn(
      'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
      context,
    );
  }
  // support single function children as default scoped slot
  // 如果children是一个数组,并且数组的第一位元素是一个函数
  if (Array.isArray(children) && typeof children[0] === 'function') {
    data = data || {};
    data.scopedSlots = { default: children[0] };
    children.length = 0;
  }
  // ALWAYS_NORMALIZE代表用户传入的render
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 当手写 render 函数的时候调用
    // 判断 children 的类型,如果是原始值的话转换成 VNode 的数组
    // 如果是数组的话,继续处理数组中的元素
    // 如果数组中的子元素又是数组(slot template),递归处理
    // 如果连续两个节点都是字符串会合并文本节点
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 将二维数组转换为一维数组并返回
    // 如果 children 中有函数组件的话,函数组件会返回数组形式
    // 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
    children = simpleNormalizeChildren(children);
  }
  let vnode, ns;
  if (typeof tag === 'string') {
    let Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    //  如果是浏览器的保留标签,创建对应的 VNode
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      // 创建vnode对象
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context,
      );
    } else if (
      isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
    ) {
      // component
      // 如果是自定义组件
      // 查找自定义组件构造函数的声明
      // 根据Ctor创建组件的VNode
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined, undefined, context);
    }
  } else {
    // direct component options / constructor
    // 如果tag不是字符串,即代表其是一个组件
    // 创建组件的VNode
    vnode = createComponent(tag, data, context, children);
  }
  if (isDef(vnode)) {
    if (ns) applyNS(vnode, ns);
    return vnode;
  } else {
    return createEmptyVNode();
  }
}

normalizeChildren

export function normalizeChildren(children: any): ?Array<VNode> {
  // 如果children是原始值,则创建文本虚拟DOM节点,并返回
  // 如果是数组,使用normalizeArrayChildren方法,递归children并创建的文本虚拟DOM节点的一维数组,并返回
  // 否则返回undefined
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
    ? normalizeArrayChildren(children)
    : undefined;
}

normalizeArrayChildren

function normalizeArrayChildren(
  children: any,
  nestedIndex?: string,
): Array<VNode> {
  const res = [];
  let i, c, lastIndex, last;
  for (i = 0; i < children.length; i++) {
    c = children[i];
    // 如果c为undefined或者null,或者是Boolean类型,则跳过该循环,执行下一次循环
    if (isUndef(c) || typeof c === 'boolean') continue;
    lastIndex = res.length - 1;
    last = res[lastIndex];
    //  nested
    // 如果c是一个数组,递归
    if (Array.isArray(c) && c.length > 0) {
      c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`);
      // merge adjacent text nodes
      // 合并相邻的文本节点
      if (isTextNode(c[0]) && isTextNode(last)) {
        // 创建文本虚拟DOM节点
        res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
        // 删除数组的第一个元素
        c.shift();
      }
      // 合并两个数组
      res.push.apply(res, c);
    } else if (isPrimitive(c)) {
      // 如果c是原始值
      // r如果last是文本节点
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        // 合并相邻的文本节点
        // 这对于SSR水化来说是必需的
        // 因为文本节点在呈现为HTML字符串时基本上已经合并
        res[lastIndex] = createTextVNode(last.text + c);
      } else if (c !== '') {
        // convert primitive to vnode
        // 创建文本虚拟DOM节点,并添加到res数组中
        res.push(createTextVNode(c));
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        // 合并相邻的文本节点
        res[lastIndex] = createTextVNode(last.text + c.text);
      } else {
        // default key for nested array children (likely generated by v-for)
        if (
          isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)
        ) {
          c.key = `__vlist${nestedIndex}_${i}__`;
        }
        res.push(c);
      }
    }
  }
  return res;
}

update

  • update 方法的作用是通过 patch 方法把 VNode 渲染成真实的 DOM

  • 首次渲染和数据更新都会调用_update

  • src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  if (vm._isMounted) {
    callHook(vm, 'beforeUpdate');
  }
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  const prevActiveInstance = activeInstance;
  activeInstance = vm;
  vm._vnode = vnode;
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // 如果当前vue实例不存在vnode,则代表是首次渲染
  if (!prevVnode) {
    // initial render
    // 这时使用vm.__patch__方法传入真实DOM(vm.$el),并转换为虚拟DOM,与传入的vnode进行比较
    // 返回真实DOM赋值给vm.$el
    vm.$el = vm.__patch__(
      vm.$el,
      vnode,
      hydrating,
      false /* removeOnly */,
      vm.$options._parentElm,
      vm.$options._refElm,
    );
    // no need for the ref nodes after initial patch
    // this prevents keeping a detached DOM tree in memory (#5851)
    vm.$options._parentElm = vm.$options._refElm = null;
  } else {
    // updates
    // 使用vm.__patch__方法传入新旧vnode进行比较
    // 返回真实DOM赋值给vm.$el
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  activeInstance = prevActiveInstance;
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null;
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm;
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el;
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
};

patch

功能

  • 如果是首次渲染的话,会把真实 DOM 先转换成
    VNode
  • 传入新旧 VNode,对比差异,把差异渲染到 DOM
  • 返回新的 VNode 的真实 DOM

patch 初始化过程

// __patch__方法将虚拟dom转换为真实dom
Vue.prototype.__patch__ = inBrowser ? patch : noop;
/* @flow */
// nodeOps里是各种DOM操作函数
import * as nodeOps from 'web/runtime/node-ops';
import { createPatchFunction } from 'core/vdom/patch';
// 指令和钩子函数
import baseModules from 'core/vdom/modules/index';
// DOM节点的属性/事件/样式的操作
import platformModules from 'web/runtime/modules/index';

// the directive module should be applied last, after all
// built-in modules have been applied.
// 合并指令和钩子函数和DOM节点的属性/事件/样式的操作
const modules = platformModules.concat(baseModules);

export const patch: Function = createPatchFunction({ nodeOps, modules });
export function createPatchFunction(backend) {
  let i, j;
  const cbs = {};

  // modules 节点的属性/事件/样式的操作
  // nodeOps 节点操作
  const { modules, nodeOps } = backend;

  for (i = 0; i < hooks.length; ++i) {
    // 初始化create、activate、update、remove、destroy钩子函数回调数组
    // cbs['update'] = []
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 把模块中的钩子函数全部设置到 cbs 中,将来统一触发
        // cbs['update'] = [updateAttrs, updateClass, update...]
        cbs[hooks[i]].push(modules[j][hooks[i]]);
      }
    }
  }

...

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {...}
}

patch 执行过程

function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  // 如果新的虚拟节点不存在,并且旧的虚拟节点存在,则执行destroy钩子函数,并直接返回,阻止向下执行
  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
    // 标记当前虚拟节点已创建,只存储在内存中,未挂载到DOM树上
    isInitialPatch = true;
    // 将新的虚拟节点转换为真实DOM
    createElm(vnode, insertedVnodeQueue, parentElm, refElm);
  } else {
    // 如果存在nodeType,则是真实DOM
    const isRealElement = isDef(oldVnode.nodeType);
    // 如果不是真实DOM,并且新旧虚拟节点相同
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      // 调用patchVnode,通过diff算法,对比新旧节点的差异,并更新
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
    } else {
      // 否则
      // 如果是真实DOM
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        // 如果是元素节点,并且该节点存在data-server-rendered属性
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          // 移除data-server-rendered属性
          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 ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.',
            );
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        // 将真实DOM转换为虚拟节点并赋值给旧的虚拟节点
        oldVnode = emptyNodeAt(oldVnode);
      }
      // replacing existing element
      // 获取旧的虚拟节点的真实DOM元素
      const oldElm = oldVnode.elm;
      // 获取旧的虚拟节点的父元素节点
      const parentElm = nodeOps.parentNode(oldElm);
      // 调用createElm方法将新的虚拟节点转换为真实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),
      );

      if (isDef(vnode.parent)) {
        // component root element replaced.
        // update parent placeholder node element, recursively
        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;
        }
      }

      // 如果存在旧的虚拟节点的父元素节点
      if (isDef(parentElm)) {
        // 移除DOM树上对应的旧虚拟节点的真实DOM节点
        removeVnodes(parentElm, [oldVnode], 0, 0);
      } else if (isDef(oldVnode.tag)) {
        // 如果不存在旧的虚拟节点的父元素节点,并且存在tag
        // 触发destroy钩子函数
        invokeDestroyHook(oldVnode);
      }
    }
  }

  // 遍历insertedVnodeQueue数组,执行insert钩子函数
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
}

createElm

将虚拟节点转换为真实 DOM,并挂载到 DOM 树上

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  vnode.isRootInsert = !nested; // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }

  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  // 如果是标签节点
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        inPre++;
      }
      if (
        !inPre &&
        !vnode.ns &&
        !(
          config.ignoredElements.length &&
          config.ignoredElements.some((ignore) => {
            return isRegExp(ignore) ? ignore.test(tag) : ignore === tag;
          })
        ) &&
        config.isUnknownElement(tag)
      ) {
        warn(
          'Unknown custom element: <' +
            tag +
            '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
          vnode.context,
        );
      }
    }
    // 如果存在ns属性,创建一个具有指定的命名空间URI和限定名称的元素,否则创建创建元素,并赋值给vnode.elm
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
    // 设置样式的作用域
    setScope(vnode);

    /* istanbul ignore if */
    if (__WEEX__) {
      // in Weex, the default insertion order is parent-first.
      // List items can be optimized to use children-first insertion
      // with append="tree".
      const appendAsTree = isDef(data) && isTrue(data.appendAsTree);
      if (!appendAsTree) {
        if (isDef(data)) {
          // 触发create钩子函数
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        // 将元素节点挂载到父元素节点上
        insert(parentElm, vnode.elm, refElm);
      }
      // 创建子节点真实DOM元素
      createChildren(vnode, children, insertedVnodeQueue);
      if (appendAsTree) {
        if (isDef(data)) {
          // 触发create钩子函数
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        // 将元素节点挂载到父元素节点上
        insert(parentElm, vnode.elm, refElm);
      }
    } else {
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      insert(parentElm, vnode.elm, refElm);
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      inPre--;
    }
  } else if (isTrue(vnode.isComment)) {
    // 如果是注释节点
    // 创建注释节点,并赋值给vnode.elm
    vnode.elm = nodeOps.createComment(vnode.text);
    // 将元素节点挂载到父元素节点上
    insert(parentElm, vnode.elm, refElm);
  } else {
    // 否则
    // 创建文本节点,并赋值给vnode.elm
    vnode.elm = nodeOps.createTextNode(vnode.text);
    // 将元素节点挂载到父元素节点上
    insert(parentElm, vnode.elm, refElm);
  }
}

patchVnode

对比新旧节点的差异,并更新

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;
  }

  let i;
  const data = vnode.data;
  // 执行用户传过来的钩子函数
  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode);
  }

  // 获取旧虚拟节点的子节点
  const oldCh = oldVnode.children;
  // 获取新虚拟节点的子节点
  const ch = vnode.children;
  if (isDef(data) && isPatchable(vnode)) {
    // 执行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);
  }
  // 如果新虚拟节点不存在text属性
  if (isUndef(vnode.text)) {
    // 新旧虚拟节点都存在子节点
    if (isDef(oldCh) && isDef(ch)) {
      // 如果新旧虚拟节点的子节点不一致,调用 updateChildren方法,对子节点进行 diff 操作,并更新
      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    } else if (isDef(ch)) {
      // 如果新虚拟节点存在子节点,旧虚拟节点不存在子节点
      // 如果旧虚拟节点存在text属性,清空旧节点 DOM 的文本内容
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
      // 为子节点创建真实DOM元素,并挂载到DOM树上
      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)) {
      // 如果新旧虚拟节点不存在子节点,并且旧虚拟节点存在text属性
      // 清空旧节点 DOM 的文本内容
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // 如果新旧虚拟节点的text属性都存在,并且不一致
    // 修改文本内容
    nodeOps.setTextContent(elm, vnode.text);
  }
  // 触发用户传入的postpatch钩子函数
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
  }
}

updateChildren

该方法与 Snabbdom 中的 updateChildren 整体算法 一致。

在 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判断当前的新旧虚拟节点是否是相同节点,sameVnode() 中会首先判断 key 是否相同。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>key</title>
  </head>
  <body>
    <div id="app">
      <button @click="handler">按钮</button>
      <ul>
        <!-- <li v-for="value in arr">{{value}}</li> -->
        <li v-for="value in arr" :key="value">{{value}}</li>
      </ul>
    </div>
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          arr: ['a', 'b', 'c', 'd'],
        },
        methods: {
          handler() {
            this.arr.splice(1, 0, 'x');
            // this.arr = ['a', 'x', 'b', 'c', 'd']
          },
        },
      });
    </script>
  </body>
</html>

当没有设置 key 的时候,在 updateChildren 中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作

当设置 key 的时候,在 updateChildren 中比较子节点的时候,因为 oldVnode 的子节点的 b,c,d 和 newVnode 的 b,c,d 的 key 相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上,DOM 操作只有一次插入操作。

总结

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容