Vue初始化(3)

874 阅读3分钟

前言


上一章Vue初始化(2),我们分析了_render和$createElement两个核心函数进行了分析,接下来我们将分析一下初始化过程的最后阶段,在下面我还是贴上渲染watcher的核心函数:

updateComponent = () => {    // 函数定义
   vm._update(vm._render(), hydrating)
}

_update


相关代码再/src/instance/leftcyle.js下

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this // 组件实例
    const prevEl = vm.$el // 组件挂载的DOM
    const prevVnode = vm._vnode // 组件VNode对象 初始化时值为null
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode // 将当前的vnode赋值给vm._vnode 用于判断是更新还是初始化
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render 初始化渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates 更新渲染
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // 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.
  }
  // ...
}

从代码中可以看到,通过判断vm._vnode是否存在,当prevVnode不存在时,则进入初始化渲染的逻辑,将vm.$el和vnode传入执行了vm.__patch__函数。

patch


我们先找到__patch__函数的入口文件(src/platforms/web/runtime)

import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

path定义在src/platforms/web/patch.js下

import * as nodeOps from 'web/runtime/node-ops' // 封装了DOM操作
import { createPatchFunction } from 'core/vdom/patch' // 将VNode映射成真实DOM
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 })

我们着重分析createPatchFunction在初始化过程中做了什么事情。 代码在src/vdom/patch.js下:

return function patch (oldVnode, vnode, hydrating, removeOnly) { // 初始化过程 oldVnode = vm.$el(DOM元素#app)
    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 {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) { 
      // 如果oldVnode不是一个真实的元素,并且oldVnode和vnode是同一个"元素节点"(sameVnode可以自行查看)
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {  // 初始化走到这个分支
        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 ' +
                '<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
          oldVnode = emptyNodeAt(oldVnode) // 为oldVnode创建一个新的空VNode
        }

        // replacing existing element
        const oldElm = oldVnode.elm // oldVnode.elm 保存着div#appDOM元素
        const parentElm = nodeOps.parentNode(oldElm) // 获取到div#app的父级,在这个例子中可以认为就是body元素

        // 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([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

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

代码中我都附有注释,所以在初始化中,会通过createElm这个函数进行节点创建,接下来我们来看看createElm函数是如何进行元素的新增的

createElm


function createElm (
  vnode, // 当前传入的vnode
  insertedVnodeQueue, [] 空数组
  parentElm, // 父元素
  refElm, 当前元素的相邻节点 
  nested,
  ownerArray,
  index
) {
  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.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
	// 初始化会走到这一步
  const data = vnode.data // vm.$createElement 的data参数
  const children = vnode.children // 当前节点的子节点集合
  const tag = vnode.tag // 当前节点的tagName
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    vnode.elm = vnode.ns // 通过封装好的DOM操作生成DOM元素
      ? 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)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
      createChildren(vnode, children, insertedVnodeQueue)
      if (appendAsTree) {
        if (isDef(data)) {
          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) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
  	// 注释节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm) // 将子元素插入到父元素当中 parentElm: 'body' vnode.elm: 'div#app'
  } else {
   // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

createElea方法在初始化过程中,就先通过nodeOps.createElement方法为当前VNode节点生成一个新的DOM节点,其次再通过createChildren方法来来VNode树进行递归调用,我们看下createChildren的实现

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

createChildren的逻辑比较简单,将子元素插入到了父元素中(递归调用) * 首先,会判断children是否是数组 * 如果是数组,则循环遍历数组中的每一项,递归调用creteElm函数 * 否则,将子元素视为文本节点,通过调用createTextNode生成文本节点,插入到当前vnode.elm中 最后初始流程会走到这里

// destroy old node 删除旧节点
  if (isDef(parentElm)) { // true
    removeVnodes([oldVnode], 0, 0) // 删除旧节点
  } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
  }

removeVnodes会把之前页面中的div#app元素进行删除,而在createElm中新创建的div#app及其子元素会被保留下来,最后将视图渲染了出来,这就是Vue初始化过程中所做的事情。 以下我附上我在源码学习过程中的思维导图,希望能够帮助到大家,更好地理解这个过程: init_.png 到这里,Vue初始化的流程我们就已经大致地分析完了,如果文中有何不足或是不对之处,还望指出

参考文章