Vue2 挂载过程源码解读

184 阅读3分钟

本文我将继续讲述Vue在挂载中发生的那些事儿

mountComponent

首先来看一下上篇文章说到的mountComponent

// src\core\instance\lifecycle.js
export function mountComponent (vm, el, hydrating) {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher

// src\core\observer\watcher.js
export default class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if (typeof expOrFn === 'function') {
      // 如果是函数,则赋值给getter(expOrFn === updateComponent)
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // 初始化组件 lazy 为 false,执行 get
    this.value = this.lazy
      ? undefined
      : this.get()
  }

}

可以看出,挂载时,我们首先在vm上追加当前节点,然后注册一个updateComponent函数,注意此时并每有调用,随后,我们新建了一个Watcher类,进行了初始化的操作,其中可以看到我们转化expOrFn函数后赋值给getter,最后调用get函数获取值,也就是调用了updateComponent。综上,我们可以看到挂载时,新建了一个Watcher以进行响应式处理,然后调用了updateComponent函数。

updateComponent

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

可以看到这边首先是执行了render函数获取其返回值,然后调用了update函数进行初始化或更新操作。在Vue2 初始化实例过程源码解析(一)中我们通过compileToFunctions函数将template转化为一个render函数,该函数的职责为生成虚拟DOM。所以可得,vm._render的最终返回值为虚拟DOM,而我们的vm._update则是对比新旧(初始化即为创建)虚拟DOM,对比并更新。接下来,让我们来一起看看update函数是如何起作用的吧。

update

// src\core\instance\lifecycle.js
  Vue.prototype._update = function (vnode, hydrating) {
    const vm = this
    // 获取原节点
    const prevEl = vm.$el
    // 获取原vnode
    const prevVnode = vm._vnode
    // 通过闭包缓存当前的vm
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // 通过闭包取出先前的vm
    restoreActiveInstance()
    // 需要清除原先保存的vnode
    if (prevEl) {
      prevEl.__vue__ = null
    }
    // 更新节点
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // 节点赋值
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

可以看到关键点就在于vm.__proto__函数了,显而易见的,传递的第一个参数为真实节点即为初始化操作,传递的是虚拟DOM即为更新操作。而这个函数又是在哪里进行的安装的呢?大家可以这样考虑一下,因为Vue是一个支持多平台的框架,而对于节点的操作,每个平台都有其不同的操作方法,这样想的话,是不是在运行时进行会更好呢?因此,实际的函数安装,是写在/runtime的包中的,具体路径我在Vue2 初始化实例过程源码解析(一)中也有提到。

patch

// src\platforms\web\runtime\patch.js
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'

const modules = platformModules.concat(baseModules)
export const patch = createPatchFunction({ nodeOps, modules })

显然,它用了一个工厂函数创建了patch,其中nodeOps是平台特有的节点操作,modules则是一些指令方法。

createPatchFunction

进入到这个函数,你会很惊奇的发现,这个函数有730+行,这是Vue2源码最长的一个函数,但是也不用着急,实际上的处理也很简单。既然说这是一个工厂函数,那我们直接来看看他返回的内容。

// src\core\vdom\patch.js
export function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 如果vnode不存在(只有在我们手动调用销毁方法才会进入这边
    if (isUndef(vnode)) {
      // 销毁存在的oldVnode
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    // 新增vnode插入队列
    const insertedVnodeQueue = []
    // 如果oldVnode不存在,则空挂载
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      // 创建元素
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 如果oldValue是虚拟DOM,并且和vnode是相同节点
        // 不仅仅是key相同,还有tag、inputType相同,是否为异步组件等等等等
        // 这是对比更新的函数,在这里用到了diff算法
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 挂载到真实的节点上
          // SSR相关
          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
            }
          }
          // 用真实节点创建一个空的虚拟DOM
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 保存节点
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 创建一个新节点
        createElm(
          vnode,
          // 需要新增的节点
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          // 节点插入位置
          nodeOps.nextSibling(oldElm)
        )

        // 递归地更新父占位符节点元素
        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)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 销毁旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

这里是patch过程的所有操作的大致内容,接下来让我们进入函数中,细看创建与更新。

createElm

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested
    // 如果是组件创建,直接执行createComponent
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      // 创建根据节点类型element
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      // 设置样式等
      setScope(vnode)
      
      // 创建子节点并插入
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    } else if (isTrue(vnode.isComment)) {
      // 创建并插入注释
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      // 创建并插入文本
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

patchVnode

  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      // 如果相同不用更新
      return
    }

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

    // 获取当前节点并把节点赋值给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
    }

    // 为静态树重用元素
    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)
    }

    // 获取两者的children递归对比
    const oldCh = oldVnode.children
    const ch = vnode.children
    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)
    }
    if (isUndef(vnode.text)) {
      // 如果vnode没有text
      if (isDef(oldCh) && isDef(ch)) {
        // 如果两者都有children,则对比他们的子元素
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // oldVnode没有children,vnode有children,先清空文本内容,再添加子元素
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // oldVnode有children,vnode为空,删除oldVnode的子元素
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有text,vnode为空,清空文本内容
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // vnode有text,并且和oldVnode不相同,更新文本
      nodeOps.setTextContent(elm, vnode.text)
    }
    // 钩子
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren

在这个函数中,进行的是数组与数组的对比,也是最复杂的对比,其中的对比用到的是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

    const canMove = !removeOnly
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 当前头节点已经被移动了,需要调整位置
        oldStartVnode = oldCh[++oldStartIdx]
      } else if (isUndef(oldEndVnode)) {
        // 当前尾节点已经被移动了,需要调整位置
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 如果两个头节点相同,直接patch这俩节点,并且移动指针
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 如果两个尾节点相同,直接patch这俩节点,并且移动指针
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // 如果旧的头节点和新的尾节点相同,patch这俩节点,将头节点移动到尾部
        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)) {
        // 如果旧的尾节点和新的头节点相同,patch这俩节点,将尾节点移动到头部
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 以上的头尾比较都不行,则进行key对比,首先是寻找到当前的node的key
        // 初始化时需要创建对应的key
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 寻找到相应key的vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          // 如果没有寻找到,直接创建新的vnode
          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 {
            // 如果key相同但是元素节点不同,强制创建vnode覆盖
            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)
    }
  }

以上就是Vue2的挂载的全过程,挂载的过程也是源码阅读中较为重要,难度最大的部分,需要我们细细反复精读。