从 `render()` 函数到 VNode

上文介绍了在编译阶段,从 HTML 字符串到 render() 函数的生成过程。 让我们回到运行阶段的 $mount() 方法:

// src/platforms/web/runtime/index.js

Vue.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating);
};

抛开依赖收集和生命钩子的执行,实际调用的是 _render() 而非我们之前挂的 vm.$options.render()

export function mountComponent(vm, el, hydrating) {
    vm.$el = el;

    callHook(vm, 'beforeMount');

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

    // 依赖收集
    vm._watcher = new Watcher(vm, updateComponent, noop);
    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;
}

_render() 是在 Vue 初始化阶段挂在原型对象上的,可以看到内部调用了真正的 render(),并传入了 $createElement

// src/core/instance/render.js

Vue.prototype._render = function () {
    const vm = this;
    const {render, $parentVnode} = vm.$options;
    let vnode;
    try {
        vnode = render.call(vm._renderProxy, vm.$createElement);
    }
    catch (e) {
        // 省略错误处理
    }

    // set parent
    vnode.parent = $parentVnode;
    return vnode;
};

还是在初始化阶段,_c 的来源也找到了:

// src/core/instance/render.js

import {createElement} from '../vdom/create-element';
export function initRender(vm) {
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
}

顺便介绍一下之前在 render 函数中使用到的其他快捷方法,例如 _v_s,其实就是创建文本类型 VNode 和 toString()

// src/core/instance/render-helpers/index.js

export function installRenderHelpers(target) {
    target._s = toString;
    target._v = createTextVNode;
}

已经出现了很多创建 VNode 的方法了,让我们看看吧。

创建 VNode

关于 VNode 的结构,这次我们看下 ts 中的定义

export interface VNode {
    tag?: string;
    data?: VNodeData;
    children?: VNode[];
    text?: string;
    elm?: Node;
    ns?: string;
    context?: Vue;
    key?: string | number;
    componentOptions?: VNodeComponentOptions;
    componentInstance?: Vue;
    parent?: VNode;
    raw?: boolean;
    isStatic?: boolean;
    isRootInsert: boolean;
    isComment: boolean;
}

创建文本类型 VNode,也就是之前的 _v

export function createTextVNode(val) {
    return new VNode(undefined, undefined, undefined, String(val));
}

在创建元素时,需要支持多种 参数类型。 首先第一个参数标签名可以是字符串或者异步函数。当标签名是字符串时,又分成 HTML 保留标签和自定义标签两种情况。对于前者,直接创建 VNode 就好,而对于后者,首先需要检查 components 属性是否定义了组件构造函数,如果有再调用 createComponent

export function $createElement(
    context,
    tag,
    data,
    children,
    normalizationType
) {
    // support single function children as default scoped slot
    if (Array.isArray(children) && typeof children[0] === 'function') {
        data = data || {};
        data.scopedSlots = {
            'default': children[0]
        };
        children.length = 0;
    }

    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children);
    }
    else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children);
    }

    let vnode;
    let ns;
    // 处理标签名
    if (typeof tag === 'string') {
        let Ctor;
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
        // HTML 保留元素
        if (config.isReservedTag(tag)) {
            // platform built-in elements
            vnode = new VNode(
                config.parsePlatformTagName(tag), data, children,
                undefined, undefined, context
            );
        }
        // 自定义组件
        else if (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);
    }
    if (isDef(vnode)) {
        if (ns) {
            applyNS(vnode, ns);
        }

        return vnode;
    }
    return createEmptyVNode();
}

创建组件

这里省略了配置对象和异步函数这两种情况。其中主要处理了这么几件事:

  1. v-model 的处理,转成 data.on
  2. data.on 调整到 listeners
  3. 生成 data.hook 对象,这涉及到后续的 patch 阶段,调用 init prepatch 等钩子
export function createComponent(
    Ctor,
    data,
    context,
    children,
    tag
) {
    data = data || {};
    // transform component v-model data into props & events
    if (isDef(data.model)) {
        transformModel(Ctor.options, data);
    }

    // extract props
    const propsData = extractPropsFromVNodeData(data, Ctor, tag);

    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    const listeners = data.on;
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn;

    // merge component management hooks onto the placeholder node
    mergeHooks(data);

    // return a placeholder vnode
    const name = Ctor.options.name || tag;
    const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        {Ctor, propsData, listeners, tag, children},
        asyncFactory
    );
    return vnode;
}

总结

生成 VNode 这种中间的表现形式,说到底还是为了后续操作方便。