美文网首页
Vue 源码解析 - 组件挂载

Vue 源码解析 - 组件挂载

作者: Whyn | 来源:发表于2020-04-12 11:11 被阅读0次

    [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

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

    组件挂载时序图

    参考

    相关文章

      网友评论

          本文标题:Vue 源码解析 - 组件挂载

          本文链接:https://www.haomeiwen.com/subject/osdvmhtx.html