源码分析:new Vue() 数据如何渲染到页面,以超简单代码为例(4)

212 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

接上一节 new Vue() 数据如何渲染到页面,以超简单代码为例3,继续分析new Vue()过程:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

上一节分析到了执行updateComponent函数了,我们知道Vue._render是返回了一个vnode,我们继续往下分析:

updateComponent = () => {
  // 第二个参数是false,这里的vm是Vue
  vm._update(vnode, hydrating)
}

Vue._update()

Vue._update函数定义在src/core/instance/lifecycle.js中:

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // 指向Vue
    const vm: Component = this
    // 这里是div #app
    const prevEl = vm.$el
    // 下面这几个参数与本次分析关联不大
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    // Vue实例赋值给activeInstance
    activeInstance = vm
    // vnode赋值给_vnode
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // 初始渲染 执行__patch__函数
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      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.
  }
}

其实_update这个函数的核心逻辑就是执行了__patch__函数,定义在src/platforms/web/runtime/index.js中:

import { patch } from './patch'
// install platform patch function
// 浏览器环境为patch函数
Vue.prototype.__patch__ = inBrowser ? patch : noop

继续查找patch函数,定义在src/platforms/web/runtime/patch.js

/* @flow */

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

到这里发现,patch最终是createPatchFunction函数,调用这个函数的时候传入了两个参数,nodeOps,modules,这样做的目的是什么?这里用到了一个函数柯里化的技巧,将不同平台的逻辑对象当做参数传入给了createPatchFunction,这样就避免了最终执行patch函数的时候需要针对不同平台写一堆的判断逻辑,同时这样做代码拆分,也有利于代码维护。 其中nodeOps是封装了各种DOM操作,modules是针对各种属性操作的封装。 我们继续往下看createPatchFunction,定义在src/core/vdom/patch.js中:

createPatchFunction

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

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  // 拿到传入的对象
  const { modules, nodeOps } = backend
  
  // 这个地方是将modules上定义的跟class,style,events相关的hooks函数推入cbs对象统一管理
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  
  ......(这之中定义了很多辅助函数)
  // 执行的__patch__最终就是这个函数,本次传入的参数分别为(div#app, vnode,false,false)
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 未定义vnode,则进入这个逻辑
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    // oldVnode有定义,进入else逻辑
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 判断oldvnode是否是真实DOM节点
      const isRealElement = isDef(oldVnode.nodeType)
      // 不是真实节点的时候,进入diff算法,与本次分析无关
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, 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
          // 将真实DOM当参数传入,创建一个空vnode
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        // oldElm为div
        const oldElm = oldVnode.elm
        // nodeOps.parentNode是查找传入元素的父元素,这里的parentElm为body
        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
        // 递归地更新父占位符节点元素,本次分析的节点是根节点,没有parent
        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
        // 本次分析的parentElm是body元素
        if (isDef(parentElm)) {
          // 移除旧元素
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    // 调用钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    将虚拟节点生成的真实元素返回
    return vnode.elm
  }

到这儿我们可以看出,patch的整个逻辑就是将vnode转换成真实的DOM,创建了一个新的DOM节点,并删除了旧的DOM节点,核心逻辑是调用了createElm()这个函数,我们继续分析createELm,源码如下:

createElm

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // 创建的是个空vnode,未定义elm
    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)
    }
    
    // 这是true
    vnode.isRootInsert = !nested // for transition enter check
    // 有组件节点进入这个逻辑,本次分析不涉及
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    // data为{ attrs:{ id: "app" } }
    const data = vnode.data
    // children为textNode
    const children = vnode.children
    // tag为'div
    const tag = vnode.tag
    // tag有定义
    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
          )
        }
      }
      
      // 创建真实DOM元素div
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      // __WEEX__平台逻辑,跳过,执行else逻辑
      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 {
        // 进入else逻辑,创建子节点,这里的children是文本节点textNode,这里面递归创建子节点最终最终插入到div#app元素上
        createChildren(vnode, children, insertedVnodeQueue)
        // 调用create钩子函数
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 执行插入操作,插入到body元素
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
          creatingElmInVPre--
      }
      
    } else if (isTrue(vnode.isComment)) {
      // tag未定义的情况下,如果是注释节点,创建注释节点
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      // tag未定义的情况下,如果不是注释节点,只可能是文本节点,创建文本节点,插入到div#app后面,本次分析递归执行了createElm函数,创建了子节点,并执行了插入操作
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

createChildren函数的逻辑:

createChildren

function createChildren (vnode, children, insertedVnodeQueue) {
    // 如果children是数组,遍历children,本次进入该逻辑:
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children);
      }
      // 递归调用createElm函数,创建子节点,并执行插入:
      for (var i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
      }
      // 如果是vnode的text是原生类型数据,进入该逻辑:
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
    }
  }

由以上逻辑可以看出,createElm函数在本次场景中,执行了两次:

1.首次进入时,tag有定义,会创建真实DOM元素div#app,再执行createChildren函数,遍历vnode.children数组,递归执行createElm函数;

2.这样就进入了第二次执行,这时候的children是文本节点,未定义tag,进入else逻辑,创建了文本节点,并将文本节点插入到div#app元素上面,完成文本节点的createElm;

3.继续进入第一次createElm函数剩下的逻辑,将插入文本节点的div#app插入到了body元素上面,这样页面上就存在了两个div#app的DOM元素了,从这里,我们也清楚了,DOM的创建是从子到父的一个过程;

4.完成DOM元素的创建并挂载后,再将之前的旧div#app元素给移除,这样,整个文本节点挂载到页面的流程我们就基本清楚了。

接下来做个简单的总结。

总结

对于以下代码:

<template>
  <div id="app">
    {{ message }}
  </div>
</template>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

渲染的流程大致如下:

第一步执行数据等流程初始化(init);

第二步执行$mount函数,将template模板进行编译,生成render函数(mount:template => render);

第三步编译完成后,调用mountComponent函数,生成updateComponent函数,实例化了一个渲染watcher,执行updateComponent函数(mount:new Watcher);

第四步执行render函数,内部是调用了createElement函数,最终生成vnode(render => vnode);

第五步将vnode作为参数,执行_update函数,主要核心是patch函数,patch函数中最重要的是createElm函数,将vnode转化成真实DOM节点并挂载到页面(patch);

graph TD
init -->  $mount --> compile(template => render) --> vnode --> patch --> DOM