Vue 源码解析 - 组件挂载

[TOC]

前言

前文在对 Vue 整体流程进行分析时,我们已经知道对于 Runtime + Compiler 的编译版本来说,Vue 在实例化前总共会经历两轮mount过程,分别为:

  • 定义于src\platforms\web\runtime\index.js$mount函数,主要负责组件挂载功能。

  • 定义于src\platforms\web\entry-runtime-with-compiler.js$mount函数,主要负责模板编译 + 组件挂载(其会缓存src\platforms\web\runtime\index.js中定义的$mount函数,最后的组件挂载转交给该函数进行处理)功能。

组件挂载

以下我们对src\platforms\web\runtime\index.js$mount函数进行解析,主要分析 组件挂载 部分内容:

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this, el, hydrating)
}

$mount函数内部直接通过mountComponent进行组件挂载功能,其源码如下:

// src/core/instance/lifecycle.js
export function mountComponent(
    vm: Component,
    el: ?Element,
    hydrating?: boolean
): Component {
    vm.$el = el
    // 如果没有 render 函数,则进行默认设置,并给出警告
    if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        ...
        warn(...)
        ...
        }
    }
    callHook(vm, 'beforeMount')

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

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(vm, updateComponent, noop, {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */)
    hydrating = false

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
    return vm
}

mountComponent主要做了如下几件事:

  • 如果vm.$options没有定义render函数,则将其render设置为createEmptyVNode,一个用于产生空虚拟节点的函数,并给出警告:
    // src/core/vdom/vnode.js
    export const createEmptyVNode = (text: string = '') => {
        const node = new VNode()
        node.text = text
        node.isComment = true
        return node
    }
    
  • 立即触发beforeMount事件,表示即将进入挂载过程。
  • 实例化一个Watcher实例对象(渲染Watcher):
// src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
    before() {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)


// src/core/observer/watcher.js
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
    ...
    constructor(
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
    ) {
        ...
        this.getter = expOrFn
        ...
        this.value = this.lazy
            ? undefined
            : this.get()
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        ...
        value = this.getter.call(vm, vm)
        ...
    }
    ...
}

:在 Vue 中,Watcher有两种类别:用户Watcher 和 渲染Watcher。此处就是一个渲染Watcher,它会引起updateComponent过程,从而触发renderupdate过程,从而完成数据渲染到视图整个流程。
更多Watcher信息,请参考:Vue 源码解析 - 数据驱动与响应式原理

简单看下Watcher源码,可以看到,Watcher的构造函数内会直接调用参数expOrFn,对于mountComponent函数来说,即会直接回调updateComponent函数,而updateComponent其实主要就做了两件事:

// src/core/instance/lifecycle.js
updateComponent = () => {
    ...
    const vnode = vm._render()
    ...
    vm._update(vnode, hydrating)
    ...
}

首先通过vm._render函数创建一个虚拟节点vnode,然后将该虚拟节点交给vm._update函数进行渲染。

  • 我们先来看下vm._render函数。

我们先查阅一下Vue._render函数的定义链,首先回到主线流程src/core/instance/index.js文件中定义了Vue之后,会采用 Mixin 方式为Vue添加一些其他功能,其中就有renderMixin,源码如下:

// src/core/instance/index.js
import {renderMixin} from './render'
...
function Vue(options) {
    this._init(options)
}
...
renderMixin(Vue)

export default Vue

进入renderMixin函数,查看其源码:

// src/core/instance/render.js
export function renderMixin(Vue: Class<Component>) {
    // install runtime convenience helpers
    installRenderHelpers(Vue.prototype)

    Vue.prototype.$nextTick = function (fn: Function) {
        return nextTick(fn, this)
    }

    Vue.prototype._render = function (): VNode {
      ...
    }
}

到这里,我们就找到了vm._render的定义之处了,查看下_render函数源码如下:

// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const {render, _parentVnode} = vm.$options
    ...
    // render self
    let vnode
    ...
    vnode = render.call(vm._renderProxy, vm.$createElement)
    ...
    return vnode
}

所以,_render函数内部是通过vm.$options.render函数渲染出一个虚拟节点vnode的。

:我们在主线流程中有讲过,Vue 构建完成后会生成两种 Vue.js 版本:Runtime OnlyRuntime + Compiler

Vue 源码解析 - 模板编译 中提过,Runtime + Compiler版本会含有两个$mount函数定义:

  • 定义于src\platforms\web\runtime\index.js$mount函数,主要负责组件挂载功能。
  • 定义于src\platforms\web\entry-runtime-with-compiler.js$mount函数,主要负责模板编译 + 组件挂载(其会缓存src\platforms\web\runtime\index.js中定义的$mount函数,最后的组件挂载转交给该函数进行处理)功能。

分析到这里,其实 Vue 中的这种实现意图就已经清楚了,之所以定义了两个$mount函数,原因就是无论对于哪个版本的 Vue.js,组件挂载都是必需的,而 Vue 创建虚拟节点始终都需要通过一个render函数进行创建。因此:

  • 对于 Runtime + Compiler 版本,$mount函数的第一步为模板编译,这一步最终就会生成一个渲染模板的render函数,然后才可进行组件挂载。
  • 而对于 Runtime Only 版本,render函数是用户自己手动提供的,因此只需直接进行组件挂载即可。

由于render函数要么是用户手动提供的,要么就是模板在线自动编译生成的,因此 Vue 源码内部没有对该函数的定义信息,所以如果要了解render函数的内部调用逻辑,就只能通过官网查询(即手动提供render函数)或查看模板自动编译生成的render函数具体内容。

通过官网查看下该函数使用方法如下:

// 比如对于如下模板
<h1>{{ blogTitle }}</h1>

// 用户自定义:以下函数相当于如上模板
render: function (createElement) {
    return createElement('h1', this.blogTitle)
}
// 模板在线编译:以下函数相当于如上模板
(function anonymous() {
    with (this) {
        return _c('h1', [_v(_s(blogTitle))])
    }
})

可以看到,render函数的内部调用逻辑有两种:

  • 对于 用户提供的render函数,最终会通过vm.$createElement进行虚拟节点的创建;
  • 对于 模板自动编译的render函数,其内部最终会通过vm._c进行虚拟节点的创建。

Vue.prototype._render函数内,可以查看到其源码:

// src/core/instance/render.js
export function initRender(vm: Component) {
    ...
    // bind the createElement fn to this instance
    // so that we get proper render context inside it.
    // args order: tag, data, children, normalizationType, alwaysNormalize
    // internal version is used by render functions compiled from templates
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    // normalization is always applied for the public version, used in
    // user-written render functions.
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
    ...
}

vm.cvm.$createElement函数的区别只在于最后一个参数上alwaysNormalize

  • 对于vm.c,由于其被使用的render函数是模板编译生成的,因此无须始终进行规范化。
  • 而对于vm.$createElement,由于其被使用的render函数是由用户手动编写的,因此需要进行规范化,让所有节点都符合VNode类型。

但无论是vm.c,还是vm.$createElement,它们的函数内部都是通过createElement函数生成虚拟节点,因此,我们查看下createElement函数源码:

// src/core/vdom/create-element.js
export function createElement(
    context: Component,
    tag: any,
    data: any,
    children: any,
    normalizationType: any,
    alwaysNormalize: boolean
): VNode | Array<VNode> {
    ...
    return _createElement(context, tag, data, children, normalizationType)
}

createElement函数内部通过_createElement来真正生成vnode,其源码如下:

// src/core/vdom/create-element.js
export function _createElement(
    context: Component,
    tag?: string | Class<Component> | Function | Object,
    data?: VNodeData,
    children?: any,
    normalizationType?: number
): VNode | Array<VNode> {
    ...
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
    let vnode, ns
    if (typeof tag === 'string') {
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
            // platform built-in elements
            ...
            vnode = new VNode(
                config.parsePlatformTagName(tag), data, children,
                undefined, undefined, context
            )
        } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
            // component
            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
        vnode = createComponent(tag, data, context, children)
    }
    ...
    return vnode
}

_createElement函数内部主要做了两件事:

  • 规范化子节点:通过normalizeChildrensimpleNormalizeChildren对子节点进行规范化,即将子节点转成VNode类型。
    规范化的细节就不深入进行分析了,其主要作用就是遍历子节点,如果子节点为数组类型,就进行打平,使深度为 1,如果是基本类型,就通过createTextVNode将其转为VNode···

  • 生成虚拟节点:规范化子节点后,就会进行虚拟节点的创建,总共有如下两种情况:

    1. 如果标签tagstring类型,则:

      • 如果是内置标签,则直接创建一个VNode
      • 如果是本地注册组件标签名,则通过createComponent创建一个组件类型的VNode虚拟节点。
      • 否则创建一个未知标签的VNode
    2. 如果标签tag不是string类型,则直接通过createComponent创建一个组件类型的VNode虚拟节点。

VNode即虚拟节点,render函数的主要作用就是生成虚拟节点,虚拟节点是 Vue 中用来对真实 DOM 节点的映射,之所以采用虚拟节点这一层映射关系,主要是因为 DOM 的量级比较重,并且对真实 DOM 的操作可能会引起页面进行无谓的重新渲染,而页面渲染是很耗费性能的操作,因此,Vue 采用 虚拟DOM 的机制,通过新生成的VNode对象与旧的VNode对象间的一系列的赋值对比等操作(不会引起页面重新渲染),就可以准确地识别出需要进行更新渲染的位置,再映射到真实 DOM 上,这样就能大大提升性能。
实际上,虚拟节点就是一个普通的 Javascript 对象,在 Vue 中,其定义如下:

// src/core/vdom/vnode.js
export default class VNode {
    tag: string | void;
    data: VNodeData | void;
    children: ?Array<VNode>;
    text: string | void;
    elm: Node | void;
    ns: string | void;
    context: Component | void; // rendered in this component's scope
    key: string | number | void;
    componentOptions: VNodeComponentOptions | void;
    componentInstance: Component | void; // component instance
    parent: VNode | void; // component placeholder node

    // strictly internal
    raw: boolean; // contains raw HTML? (server only)
    isStatic: boolean; // hoisted static node
    isRootInsert: boolean; // necessary for enter transition check
    isComment: boolean; // empty comment placeholder?
    isCloned: boolean; // is a cloned node?
    isOnce: boolean; // is a v-once node?
    asyncFactory: Function | void; // async component factory function
    asyncMeta: Object | void;
    isAsyncPlaceholder: boolean;
    ssrContext: Object | void;
    fnContext: Component | void; // real context vm for functional nodes
    fnOptions: ?ComponentOptions; // for SSR caching
    devtoolsMeta: ?Object; // used to store functional render context for devtools
    fnScopeId: ?string; // functional scope id support

    constructor(
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
    ) {
        this.tag = tag                           // 标签
        this.data = data                         // 属性
        this.children = children                 // 子元素列表
        this.text = text                         // 文本内容
        this.elm = elm                           // 映射的真实 DOM 节点
        this.ns = undefined
        this.context = context
        this.fnContext = undefined
        this.fnOptions = undefined
        this.fnScopeId = undefined
        this.key = data && data.key
        this.componentOptions = componentOptions
        this.componentInstance = undefined
        this.parent = undefined
        this.raw = false
        this.isStatic = false                    // 静态节点标识
        this.isRootInsert = true
        this.isComment = false
        this.isCloned = false
        this.isOnce = false
        this.asyncFactory = asyncFactory
        this.asyncMeta = undefined
        this.isAsyncPlaceholder = false
    }

    // DEPRECATED: alias for componentInstance for backwards compat.
    /* istanbul ignore next */
    get child(): Component | void {
        return this.componentInstance
    }
}

到这里,vm.render函数生成虚拟节点的过程就分析完毕了。

接下来继续看vm._update如何将vm.render生成的虚拟节点渲染成一个真实的 DOM 节点。

主线流程中,文件core/instance/index.js定义了Vue之后,有如下初始化操作:

// src/core/instance/index.js
...
import {lifecycleMixin} from './lifecycle'

function Vue(options) {
    ...
}
...
lifecycleMixin(Vue)
...

vm._update函数就定义于lifecycleMixin函数内:

// src/core/instance/lifecycle.js
export function lifecycleMixin(Vue: Class<Component>) {
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        ...
        const prevVnode = vm._vnode
        ...
        vm._vnode = 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)
        }
        ...
    }
    ...
}

Vue.prototype._update源码中可以看到,_update函数内部主要通过vm.__patch__进行数据渲染,且总共存在两种数据渲染:

  • 首次渲染:首次将虚拟节点渲染到一个真实的 DOM 中。
  • 数据更新:对虚拟节点绑定的真实 DOM 节点上的数据进行更新。

这里我们着重介绍 首次渲染 流程,数据更新 流程请参考:Vue 源码解析 - 数据驱动与响应式原理

__patch__操作是一个平台相关的操作,如下图所示:

vm.__patch__

这里我们只对 Web 平台进行分析,其源码为:

// src/core/util/env.js
export const inBrowser = typeof window !== 'undefined'

// src/platforms/web/runtime/index.js
...
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
...

Web 平台分为浏览器端和服务器端,由于服务器端无须将VNode渲染为真实 DOM,因此我们这里只分析浏览器端VNode渲染流程。

可以看到,在浏览器端,负责对VNode进行渲染的函数为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'

// 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函数执行的结果,而createPatchFunction的参数为{nodeOps,modules},在解析createPatchFunction函数之前,我们先来查看下其参数对象内容:

  • 首先看下nodeOps参数内容:
// src/platforms/web/runtime/node-ops.js
import { namespaceMap } from 'web/util/index'

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
}

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

export function parentNode (node: Node): ?Node {
  return node.parentNode
}

export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

export function tagName (node: Element): string {
  return node.tagName
}

export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

从源码中可以看到,nodeOps就是代理了浏览器真实 DOM 的一系列操作。
所以看到这里其实就挺明显了,Vue 最后就是通过nodeOps进行真实 DOM 的创建/修改...,所以vm._update函数的最重要的一个功能(即VNode转换成真实 DOM 节点)就在此处完成了。

  • 接下来看下modules参数具体内容:
// src/platforms/web/runtime/patch.js
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

参数modulesplatformModulesbaseModules拼接而成,其中:

  • platformModules源码如下:
// src/platforms/web/runtime/modules/index.js
// platformModules
export default [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
]

// src/platforms/web/runtime/modules/attrs.js
export default {
    create: updateAttrs,
    update: updateAttrs
}

// src/platforms/web/runtime/modules/class.js
export default {
    create: updateClass,
    update: updateClass
}

// src/platforms/web/runtime/modules/events.js
export default {
    create: updateDOMListeners,
    update: updateDOMListeners
}

// src/platforms/web/runtime/modules/dom-props.js
export default {
    create: updateDOMProps,
    update: updateDOMProps
}

// src/platforms/web/runtime/modules/style.js
export default {
    create: updateStyle,
    update: updateStyle
}

// src/platforms/web/runtime/modules/transition.js
export default inBrowser ? {
    create: _enter,
    activate: _enter,
    remove(vnode: VNode, rm: Function) {
        /* istanbul ignore else */
        if (vnode.data.show !== true) {
            leave(vnode, rm)
        } else {
            rm()
        }
    }
} : {}

所以platformModules其实就是提供了对VNode的属性attrs,类klass,事件监听eventdomProps,样式style和变换transition的一系列创建和更新等操作。

  • 再来看下baseModules源码:
// src/core/vdom/modules/index.js
// baseModules
export default [
    ref,
    directives
]

// src/core/vdom/modules/ref.js
// ref
export default {
    create(_: any, vnode: VNodeWithData) {...},
    update(oldVnode: VNodeWithData, vnode: VNodeWithData) {...},
    destroy(vnode: VNodeWithData) {...}
}

// src/core/vdom/modules/directives.js
// directives
export default {
    create: updateDirectives,
    update: updateDirectives,
    destroy: function unbindDirectives(vnode: VNodeWithData) {
        updateDirectives(vnode, emptyNode)
    }
}

所以baseModules功能就是提供了对VNode引用和指令directives的创建,更新和销毁操作。

综上,modules是一个数组,其内部子元素具备对VNode进行操作的能力,而nodeOps具备对真实 DOM 进行操作的能力。

到这里,我们再回过头来看下createPatchFunction函数:


// src/core/vdom/patch.js
...
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
...
export function createPatchFunction(backend) {
    let i, j
    const cbs = {}

    const {modules, nodeOps} = backend

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

    function emptyNodeAt(elm) {...}

    function createRmCb(childElm, listeners) {...}

    function removeNode(el) {...}

    function isUnknownElement(vnode, inVPre) {...}

    let creatingElmInVPre = 0

    function createElm(vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {...}

    function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {...}

    function initComponent(vnode, insertedVnodeQueue) {...}

    function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {...}

    function insert(parent, elm, ref) {...}

    function createChildren(vnode, children, insertedVnodeQueue) {...}

    function isPatchable(vnode) {...}

    function invokeCreateHooks(vnode, insertedVnodeQueue) {...}

    // set scope id attribute for scoped CSS.
    // this is implemented as a special case to avoid the overhead
    // of going through the normal attribute patching process.
    function setScope(vnode) {...}

    function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {...}

    function invokeDestroyHook(vnode) {...}

    function removeVnodes(vnodes, startIdx, endIdx) {...}

    function removeAndInvokeRemoveHook(vnode, rm) {...}

    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {...}

    function checkDuplicateKeys(children) {...}

    function findIdxInOld(node, oldCh, start, end) {...}

    function patchVnode(... {...}

    function invokeInsertHook(vnode, queue, initial) {...}

    let hydrationBailed = false
    // list of modules that can skip create hook during hydration because they
    // are already rendered on the client or has no need for initialization
    // Note: style is excluded because it relies on initial clone for future
    // deep updates (#7063).
    const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')

    // Note: this is a browser-only function so we can assume elms are DOM nodes.
    function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {...}

    function assertNodeMatch(node, vnode, inVPre) {...}

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

createPatchFunction总体上主要做了三件事:

  • 创建VNode操作对象cbs
    依据前面的分析,modules其实就具备了对VNode进行操作的能力,但是cbs这里对modules进行了一些转换:

    • modules内部是每个子元素都具备不同的对VNode进行操作的能力,分别为如下:
      modules[0] = {create: updateAttrs , update: updateAttrs}:具备对属性attrs的创建和更新操作
      modules[1] = {create: updateClass , update: updateClass}:具备对类class的更新操作
      modules[2] = {create: updateDOMListeners , update: updateDOMListeners}:具备对事件event的创建和更新操作
      modules[3] = {create: updateDOMProps , update: updateDOMProps}:具备对domProps的创建和更新操作
      modules[4] = {create: updateStyle , update: updateStyle}:具备对样式style的创建和更新操作
      modules[5] = {create: _enter , activate: _enter , remove}:具备对transition的创建,激活和移除操作
      modules[6] = {create , update , destroy}:具备对虚拟节点引用VNode的创建,更新和销毁操作
      modules[7] = {create: updateDirectives , update: updateDirectives , destroy: unbindDirectives}:具备对指令directives的创建,更新和销毁操作

    • cbsmodules进行了转换,将对VNode的各个操作以动作进行分类(modules是以对VNode的操作对象进行分类),比如,将创建操作归类到一起,将更新操作归类到一起···,如下图所示:

    cbs
  • 定义了很多 DOM 操作辅助函数,包含VNode和真实 DOM 之间相互转换的函数

  • 返回一个patch函数:这个patch函数就是我们需要的那个函数:

// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

到这里,我们就终于得到patch函数,现在让我们回到vm._update的主线流程:

:这里,我们主要对vm._update首次渲染 进行分析:

" src/core/instance/lifecycle.js
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

结合我们上面的渲染函数例子一起进行分析:

<body>
    <div id="app">
    </div>

    <script>
        new Vue({
            el: "#app",
            render: function (createElement) {
                return createElement("h1", this.blogTitle);
            },
            data: {
                blogTitle: 'Hello Vue'
            },
        });
    </script>
</body>

:上述例子的render函数相当于模板<h1>{{ blogTitle }}</h1>

前面讲到,vm._update内部通过调用vm.__patch__,而vm.__patch__在 Web 平台下就对应patch函数,
所以我们接下来看下patch函数的源码:

// src/core/vdom/patch.js
return function patch(oldVnode, vnode, hydrating, removeOnly) {
    ...
    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)) {
            // patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            if (isRealElement) {
                ...
                // either not server-rendered, or hydration failed.
                // create an empty node and replace it
                oldVnode = emptyNodeAt(oldVnode)
            }

            // replacing existing element
            const oldElm = oldVnode.elm
            // 获取挂载 DOM 的父节点
            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
            if (isDef(vnode.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
}

对应到我们上面的例子中,patch函数此时接收到的参数为:

patch(oldVnode, vnode, hydrating, removeOnly)

其中:

  • oldVnode = vm.$el:就是 DOM 节点:div#app
  • vnode:就是render函数渲染生成的节点:<h1>{{ blogTitle }}</h1>
  • hydrating:表示服务端渲染,故此处为false
  • removeOnly:为false

因此,当进入patch函数后,对于 首次渲染oldVnode对应div#app,是一个真实 DOM 节点,因此,patch函数主要做了如下几件事:

  • 将真实 DOM 节点包装为一个VNode
// src/core/vdom/patch.js
oldVnode = emptyNodeAt(oldVnode)

function emptyNodeAt(elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
  • render函数渲染生成的虚拟节点vnode映射到一个真实 DOM 节点:
// src/core/vdom/patch.js
// 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)
)

createElm的第四个参数为虚拟节点进行映射的参考位置,从具体的实参nodeOps.nextSibling(oldElm)可以知道,模板对应的真实 DOM 节点位于挂载节点div#app的后一个兄弟节点,如下图所示:

下面查看下createElm的源码,看下VNode是怎样转换为真实 DOM 节点的:

// src/core/vdom/patch.js
function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
) {
    ...
    // 尝试先进行组件创建
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
        ...
        vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode)
        setScope(vnode)

        /* istanbul ignore if */
        if (__WEEX__) {
            ...
        } else {
            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)
    }
}

其实VNode转真实 DOM 的操作还是挺简单的,主要就是做了如下判断:
▪ 首先尝试将VNode转为组件,成功则直接返回;
▪ 判断VNode有无标签,有则创建其真实 DOM 节点及其子子节点,并插入到 DOM 文档中;
▪ 判断VNode有无注释,有则创建对应的注释 DOM 节点,并插入到 DOM 文档中;
▪ 其他情况则将VNode作为一个文本节点进行创建并插入到 DOM 文档中;

  • 销毁旧节点,这里其实就是将div#app节点进行移除:removeVnodes([oldVnode], 0, 0),此时,挂载节点就只剩下模板了,这个效果其实相当于模板节点替换了原生挂载节点div#app,如下图所示:

到这里,我们从render函数渲染得到的VNode到其映射为真实 DOM 节点的整个过程都分析完毕,组件挂载 这个过程也就完成了。

总结

简单来说,组件挂载 就是将render函数渲染出来的虚拟节点VNode映射成对应真实节点并挂载到 DOM 文档中,其过程大致包含几个重要步骤:挂载mount -> 渲染render -> 得到虚拟节点VNode -> 更新update -> patch -> 真实 DOM

组件挂载 的整个时序图如下所示:

组件挂载时序图

参考

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

推荐阅读更多精彩内容