创建 VNode

    在这个例子中,我们注册了一个自定义组件 current-time,在 #app 中就有一个DOM元素和一个自定义组件。为什么要这样呢?因为 Vue 在创建 VNODE 的时候,对这两种处理是不一样的。

    我们依然从 _render 函数为入口开始看代码(依旧省略部分不影响我们理解的代码):

    core/instance/render.js

    1. Vue.prototype._render = function (): VNode {
    2. const vm: Component = this
    3. const { render, _parentVnode } = vm.$options
    4. // set parent vnode. this allows render functions to have access
    5. // to the data on the placeholder node.
    6. vm.$vnode = _parentVnode
    7. // render self
    8. let vnode
    9. try {
    10. vnode = render.call(vm._renderProxy, vm.$createElement)
    11. } catch (e) {
    12. // 省略
    13. vnode = vm._vnode
    14. }
    15. // set parent
    16. vnode.parent = _parentVnode
    17. return vnode
    18. }

    最核心的代码是下面这一句:

    1. vnode = render.call(vm._renderProxy, vm.$createElement)

    这里的 render 其实就是我们根据模板生成的 options.render 函数,两个参数分别是:

    • _renderProxy 是我们render 函数运行时的上下文
    • $createElement 作用是创建 vnode 节点

    对于我们的例子来说,我们的render函数编译出来是这个样子的:

    显然,这里的 this 就是 _renderProxy,在它上面就有 _c, v 等函数。这些函数就是一些 renderHelpers ,比如 _v 其实是创建文本节点的:

    1. target._v = createTextVNode

    仔细观察会发现 $createElement 其实没用到。为什么呢? 因为这是给我们自己写 render 的时候提供的,而这个函数其实就是 this._c,因此编译出来的 render 直接用了 _c 而不是用了 createElement

    我们知道 _c 就是 createElement, 而 createElement 其实会调用 _createElement 来创建 vnode,我们来看看 _createElement 的代码:

    core/vdom/create-element.js

    1. export function _createElement (
    2. context: Component,
    3. tag?: string | Class<Component> | Function | Object,
    4. data?: VNodeData,
    5. children?: any,
    6. normalizationType?: number
    7. ): VNode | Array<VNode> {
    8. // 省略大段
    9. if (typeof tag === 'string') {
    10. if (config.isReservedTag(tag)) { // 如果是保留的tag
    11. // platform built-in elements
    12. vnode = new VNode(
    13. config.parsePlatformTagName(tag), data, children,
    14. undefined, undefined, context
    15. )
    16. } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    17. // component
    18. vnode = createComponent(Ctor, data, context, children, tag);
    19. } else {
    20. // unknown or unlisted namespaced elements
    21. // check at runtime because it may get assigned a namespace when its
    22. // parent normalizes children
    23. vnode = new VNode(
    24. tag, data, children,
    25. undefined, undefined, context
    26. );
    27. //省略
    28. } else {
    29. // direct component options / constructor
    30. }
    31. if (Array.isArray(vnode)) {
    32. return vnode
    33. } else if (isDef(vnode)) {
    34. if (isDef(ns)) applyNS(vnode, ns)
    35. if (isDef(data)) registerDeepBindings(data)
    36. return vnode
    37. } else {
    38. return createEmptyVNode()
    39. }
    40. }

    首先我们来理解参数,假设我们现在是创建如下所示的最外层 div元素:

    那么这几个参数分别是:

    • context,这是vm 本身,因为有这个 context 的存在所以我们才能在模板中访问 vm 上的属性方法
    • tag 就是 div
    • data 是attributes被解析出来的配置 { staticClass: 'hello', on: {}
    • children, 其实就是 _c('span') 返回的 span 对应的 vnode,被数组包了一下

    我们在看函数体,几个条件判断有一点点绕,但是最终都是为了判断到底是需要创建一个 vnode 还是需要创建一个 component。我画了一个图来表示上面的条件判断:

    解释下 resolveAsset 其实就是看 tag 有没有在 components 中定义,如果已经定义了那么显然就是一个组件。

    对这段逻辑:比较常见的情况是:如果我们的 tag 名字是一个保留标签,那么就会调用 new VNode 直接创建一个 vnode 节点。如果是一个自定义组件,那么调用 createComponent创建一个组件。而保留标签其实就可以理解为 DOM 或者 SVG 标签。

    core/vdom/vnode.js

    1. export default class VNode {
    2. tag: string | void;
    3. data: VNodeData | void;
    4. children: ?Array<VNode>;
    5. text: string | void;
    6. elm: Node | void;
    7. // 省略很多属性
    8. constructor (
    9. tag?: string,
    10. data?: VNodeData,
    11. children?: ?Array<VNode>,
    12. text?: string,
    13. elm?: Node,
    14. context?: Component,
    15. componentOptions?: VNodeComponentOptions,
    16. asyncFactory?: Function
    17. ) {
    18. this.tag = tag
    19. this.data = data
    20. this.children = children
    21. // 省略很多属性
    22. }
    23. // DEPRECATED: alias for componentInstance for backwards compat.
    24. /* istanbul ignore next */
    25. get child (): Component | void {
    26. return this.componentInstance
    27. }
    28. }

    那么如果是第二种情况,我们创建的是一个自定义的组件要怎么办呢?我们看看 createComponent 的代码:

    core/vdom/create-component.js

    1. export function createComponent (
    2. Ctor: Class<Component> | Function | Object | void,
    3. data: ?VNodeData,
    4. context: Component,
    5. children: ?Array<VNode>,
    6. tag?: string
    7. // 省略
    8. // resolve constructor options in case global mixins are applied after
    9. resolveConstructorOptions(Ctor) // 合并 options, 就是把我们自定义的 options 和 默认的 `options` 合并
    10. // transform component v-model data into props & events
    11. if (isDef(data.model)) {
    12. transformModel(Ctor.options, data)
    13. }
    14. // extract props
    15. const propsData = extractPropsFromVNodeData(data, Ctor, tag)
    16. // functional component
    17. if (isTrue(Ctor.options.functional)) {
    18. return createFunctionalComponent(Ctor, propsData, data, context, children)
    19. }
    20. // extract listeners, since these needs to be treated as
    21. // child component listeners instead of DOM listeners
    22. const listeners = data.on
    23. // replace with listeners with .native modifier
    24. // so it gets processed during parent component patch.
    25. data.on = data.nativeOn
    26. if (isTrue(Ctor.options.abstract)) {
    27. // abstract components do not keep anything
    28. // other than props & listeners & slot
    29. // work around flow
    30. const slot = data.slot
    31. data = {}
    32. if (slot) {
    33. data.slot = slot
    34. }
    35. }
    36. // install component management hooks onto the placeholder node
    37. installComponentHooks(data)
    38. // return a placeholder vnode
    39. const name = Ctor.options.name || tag
    40. const vnode = new VNode(
    41. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    42. data, undefined, undefined, undefined, context,
    43. { Ctor, propsData, listeners, tag, children },
    44. asyncFactory
    45. )
    46. // Weex specific: invoke recycle-list optimized @render function for
    47. // extracting cell-slot template.
    48. // https://github.com/Hanks10100/weex-native-directive/tree/master/component
    49. /* istanbul ignore if */
    50. if (__WEEX__ && isRecyclableComponent(vnode)) {
    51. return renderRecyclableComponentTemplate(vnode)
    52. }
    53. }

    最前面一大段都是对 options, model, on 等的处理,我们暂且跳过这些内容,直接看 vnode 的创建:

    也就是说,其实自定义组件current-time也是创建了一个 vnode ,那么和 span 这种原生标签肯定有区别的,最大的区别在 componentOptions 上,如果我们是自定义组件,那么会在 componentOptions 中保存我们的组件信息,而 span 这种原生标签就没有这个数据:

    显然,对于 spancurrent-time 的更新机制肯定是不同的。由于我们知道了 createComponent 最终也会创建一个 vnode,前面的一张图中我们可以增加一个箭头,改成这样:

    下一章: