Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lihongxun945 opened this issue Aug 2, 2018 · 1 comment
Open

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Aug 2, 2018

创建 VNode

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

Vue.component('current-time', {
      data () {
        return {
          time: new Date()
        }
      },
      template: `<span>{{time}}</span>`
    })
    var app = new Vue({
      el: '#app',
      template: `
      <div class="hello" @click="click">
        <span>{{message}}</span>
        <current-time></current-time>
      </div>
      `,
      data: {
        message: 'Hello Vue!'
      },
      methods: {
        click() {
          this.message += '1'
        }
      }
    })

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

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

core/instance/render.js

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
        // 省略
        vnode = vm._vnode
    }

    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

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

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

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

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

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

(function anonymous() {
    with (this) {
        return _c('div', {
            staticClass: "hello",
            on: {
                "click": click
            }
        }, [_c('span', [_v(_s(message))]), _v(" "), _c('current-time')], 1)
    }
}
)

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

core/instance/render-helpers/index.js

target._v = createTextVNode

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

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

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 (typeof tag === 'string') {
    if (config.isReservedTag(tag)) { // 如果是保留的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 (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

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

<div class="hello" @click="click">
  <span>{{message}}</span>
</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 标签。

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

core/vdom/vnode.js

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  // 省略很多属性

  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
    // 省略很多属性
  }

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

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

core/vdom/create-component.js

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // 省略
  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor) // 合并 options, 就是把我们自定义的 options 和 默认的 `options` 合并

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

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

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // 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

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

最前面一大段都是对 options, model, on 等的处理,我们暂且跳过这些内容,直接看 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
  )

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants