创建 VNode

上面我们讲了mount整体流程,那么下面我们来看看 render 函数到底是如何工作的?为了能比较容易理解,我们来写一个简单的例子:

  1. Vue.component('current-time', {
  2. data () {
  3. return {
  4. time: new Date()
  5. }
  6. },
  7. template: `<span>{{time}}</span>`
  8. })
  9. var app = new Vue({
  10. el: '#app',
  11. template: `
  12. <div class="hello" @click="click">
  13. <span>{{message}}</span>
  14. <current-time></current-time>
  15. </div>
  16. `,
  17. data: {
  18. message: 'Hello Vue!'
  19. },
  20. methods: {
  21. click() {
  22. this.message += '1'
  23. }
  24. }
  25. })

在这个例子中,我们注册了一个自定义组件 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函数编译出来是这个样子的:

  1. (function anonymous() {
  2. with (this) {
  3. return _c('div', {
  4. staticClass: "hello",
  5. on: {
  6. "click": click
  7. }
  8. }, [_c('span', [_v(_s(message))]), _v(" "), _c('current-time')], 1)
  9. }
  10. }
  11. )

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

core/instance/render-helpers/index.js

  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. //省略
  29. } else {
  30. // direct component options / constructor
  31. vnode = createComponent(tag, data, context, children)
  32. }
  33. if (Array.isArray(vnode)) {
  34. return vnode
  35. } else if (isDef(vnode)) {
  36. if (isDef(ns)) applyNS(vnode, ns)
  37. if (isDef(data)) registerDeepBindings(data)
  38. return vnode
  39. } else {
  40. return createEmptyVNode()
  41. }
  42. }

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

  1. <div class="hello" @click="click">
  2. <span>{{message}}</span>
  3. </div>

那么这几个参数分别是:

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

我们在看函数体,几个条件判断有一点点绕,但是最终都是为了判断到底是需要创建一个 vnode 还是需要创建一个 component。我画了一个图来表示上面的条件判断:
Vue2.x源码解析系列九:vnode的生成与更新机制 - 图1
解释下 resolveAsset 其实就是看 tag 有没有在 components 中定义,如果已经定义了那么显然就是一个组件。

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

因此在我们的例子中 span 是一个保留标签,所以会调用 new VNode() 直接创建一个vnode 出来。VNode 类其实非常简单,他就是把传入的参数都记录了下来而已。因为代码比较长所以这里只贴出一部分代码,有兴趣的话可以去 **core/vdom/vnode.js` 里面看看:

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

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

  1. const name = Ctor.options.name || tag
  2. const vnode = new VNode(
  3. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  4. data, undefined, undefined, undefined, context,
  5. { Ctor, propsData, listeners, tag, children },
  6. asyncFactory
  7. )

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

Vue2.x源码解析系列九:vnode的生成与更新机制 - 图2

显然,对于 spancurrent-time 的更新机制肯定是不同的。由于我们知道了 createComponent 最终也会创建一个 vnode,前面的一张图中我们可以增加一个箭头,改成这样:
Vue2.x源码解析系列九:vnode的生成与更新机制 - 图3

回到最开头的 _render,我们知道它最终返回了一个 vnode 节点组成的虚拟DOM树,树中的每一颗节点都会存储渲染的时候需要的信息,比如 context, children 等。那么Vue是如何把 vnode 渲染成真实的DOM呢?我们在下一章讲解

下一章:Vue2.x源码解析系列十:Patch和Diff 算法