Vue框架源码:源码剖析-虚拟DOM

115 阅读6分钟

课程回顾

  • 虚拟DOM库-Snabbdom
  • Vue.js 响应式原理模拟实现
  • Vue.js 源码剖析 - 响应式原理

什么是虚拟DOM

  • 虚拟DOM(Virtual DDOM)是使用JavaScript对象来描述真实DOM。程序的各种状态变化,首先作用于虚拟DOM,最终映射到真实DOM。像Vue这样的MVVM框架会屏蔽这些基本的DOM操作。
  • Vue借鉴了Snabbdom的虚拟DOM,比如模块机制、钩子函数、diff算法等等。在这基础上添加了Vue自身的特性,例如:指令和组件机制。

为什么使用虚拟DOM

  • 避免用户直接操作DOM,只需关注业务代码实现,也不用关注浏览器兼容性问题。提高开发效率。
  • 作为一个中间层可以跨平台,可以SSR、跨移动端平台。
  • 虚拟DOM不一定可以提高性能,首次渲染会增加开销,要创建js对象。复杂视图情况可以提升渲染性能。

Vue中的虚拟DOM,大部分和前面说的Snabbdom相似,只是针对一些步骤加入了自己的特性,本节课程重点讲:

  1. vm._render
  2. vm._update
  3. vm.patch
  4. patchVnode
  5. updateChildren

VNode的创建过程

createElement

在render方法的定义中,给出了createElement方法调用的地方,在render.js文件中定义了_render方法,在里边通过外界传入的render调用call方法,获取到vnode节点:

vnode = render.call(vm._renderProxy, vm.$createElement)

该render方法最终会返回一个根VNode。在上面代码里vm.$createElement就是h函数。在此处被调用了。

这里先着重记录下代码内部的normalizeChildren方法,文中老师说的是,使用normalizeChildren函数做处理,它最终会返回一个一维数组,方便后续的处理。

但我看了源码,觉得老师说少了,最终是循环遍历children数组,让所有的子节点最终都生成为文本节点。

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
    : undefined
}

看这个方法的递归调用,我觉得是遍历children下的每一层children,然后生成对应的VNode。

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]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        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
}

相比于Snabbdom的createElement,这里处理了组件的情况,及一些其它额外的事情。

处理Vnode-update(patch)

渲染watcher中传入的cb,就是被包裹的_update方法,在它源码内部是调用了__patch__方法,这估计和Snabbdom的patch方法差不多。

const vm: Component = this
const prevEl = vm.$el
// _vnode记录的是之前处理过的vnode对象
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.

if (!prevVnode) {
  // initial render
  // 初始化渲染时,没有老vnode,这里直接和el根节点作对比
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}

首次初始化,vm.$el会在patch方法中被转化为vnode,进行比较。然后将比对的差异更新到真实DOM上,并把比对的结果返回储存到vm.$el中来。

当patch函数处理完,会把最新的vnode存储到_vnode属性中。

Patch函数的初始化

该函数还区分环境:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch函数,由createPatchFunction方法生成,说明它也是一个高阶函数,是一个柯里化的函数。同时看参数,它也和Snabbdom一样,是模块化的。

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })
  • nodeOps:里面和Snabbdom很相似,都是存放的DOM操作方法,不同的是针对createElement方法做了针对select标签的处理。
export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
  • modules:这里拼接了基础模块平台模块,这些模块的基础操作也是处理属性、事件、样式等等,但Vue中多了一个transition,用于处理过渡动画。
// platformModules 平台模块内容
export default [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]

和Snabbdom一样,各个模块导出的都是生命周期钩子函数,基于钩子函数进行模块方法的执行

baseModules:基础模块,处理指令和ref的模块

// baseModules 基础模块内容
export default [
  ref,
  directives
]

createPatchFunction源码

Vue的钩子函数:

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

在Patch函数内部,定义的大部分方法和Snabbdom相似:

function sameVnode (a, b) {
  /** 
   * 相比于Snabbdom,相同点:
   * 都比较了key,tag是否相同
   * 不同点:
   * 还判断了很多额外的东西
   * 
  */
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

createElm函数

它的作用是,把VNode转化成真实DOM,然后挂载到DOM树上来。

createElm(
  vnode: any,  虚拟节点
  insertedVnodeQueue: any,  inserted钩子队列
  parentElm: any, 父节点元素
  refElm: any, 
  nested: any, 
  ownerArray: any, 子节点数组
  index: any 
): void {
// ``` //
  
// 先判断vnode中是否有elm属性,如果有,则说明该vnode层级被渲染过 
// ownerArray代表该vnode是否有子节点
if (isDef(vnode.elm) && isDef(ownerArray)) {
  // This vnode was used in a previous render!
  // now it's used as a new node, overwriting its elm would cause
  // potential patch errors down the road when it's used as an insertion
  // reference node. Instead, we clone the node on-demand before creating
  // associated DOM element for it.
  // 如果有elm属性,并且有子节点,则要克隆一份。
  vnode = ownerArray[index] = cloneVNode(vnode)
}

vnode.isRootInsert = !nested // for transition enter check
// 处理组件情况
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}

// ``` //

}

这个函数内部调用了createChildren方法,里边进行了重复Key值的判断,使用方法就是对象方式。

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

更多详情,看代码注释

patchVNode函数

详情看代码,原理参考Snabbdom。

function patchVnode(
oldVnode,
 vnode,
 insertedVnodeQueue,
 ownerArray,
 index,
 removeOnly
) {
  if (oldVnode === vnode) {
    return;
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

  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.
  // 如果新旧 VNode 都是静态的,那么只需要替换componentInstance
  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)) {
    // 调用 cbs 中的钩子函数,操作节点的属性/样式/事件....
    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);
  }

  // 新节点没有文本
  if (isUndef(vnode.text)) {

    // 老节点和老节点都有有子节点
    // 对子节点进行 diff 操作,调用 updateChildren
    if (isDef(oldCh) && isDef(ch)) {

      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);

    } else if (isDef(ch)) {
      // 新vnode有子节点,老vnode没有子节点
      if (process.env.NODE_ENV !== "production") {
        checkDuplicateKeys(ch);
      }

      // 先清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);

    } else if (isDef(oldCh)) {

      // 老节点有子节点,新的没有子节点
      // 直接删除老节点中的子节点
      removeVnodes(oldCh, 0, oldCh.length - 1);

    } else if (isDef(oldVnode.text)) {
      // 老节点有文本,新节点没有文本
      // 清空老节点的文本内容
      nodeOps.setTextContent(elm, "");
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新老节点都有文本节点
    // 修改文本
    nodeOps.setTextContent(elm, vnode.text);
  }

  // 如果有用户定义的data,则判断执行对应钩子函数
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch)))
      i(oldVnode, vnode);
  }
}

updateChildren函数

详情参看Snabbdom的相关解析

// diff 算法
// 更新新旧节点的子节点
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);
  }

  // diff 算法
  // 当新节点和旧节点都没有遍历完成
  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,
        newCh,
        newStartIdx
      );
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      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,
        newCh,
        newStartIdx
      );
      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,
            newCh,
            newStartIdx
          );
          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];
    }
  }
  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(oldCh, oldStartIdx, oldEndIdx);
  }
}
function sameVnode(a, b) {
  /**
   * 相比于Snabbdom,相同点:
   * 都比较了key,tag是否相同
   * 不同点:
   * 还判断了很多额外的东西
   *
   */
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
     (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  );
}

没设置Key的情况

从示例看,更新了3次DOM,插入了1次DOM,总共执行4次DOM操作。

设置Key的情况

从示例看,只执行了1次插入的操作,Dom操作少很多。

总结:设置key,能更好的利用updateChildren的特性,从头到尾遍历比对不成功,能尽快的从尾到头进行遍历,能最大限度的重用Dom。

没设置key,则可近似看为只要判断tag相同,则会进行patchVnode,增加了不必要的比对、更新次数。设置了key,则会提前判断为比对不一致。

总结

Patch的核心是进行:对数据的更新、真实DOM的操作。所有的虚拟DOM相关的操作,最终由这里挂载到页面上。