# render流程

根组件在实例化后, 调用$mount来挂载组件, 从而开始组件的render, patch

# _render()

Vue内部使用vm._render()来生成vnode, 下面是是部分关键代码

关于virtual dom, 尤雨溪在知乎的一个回答很值得一看

网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么? - 尤雨溪的回答 - 知乎

src/core/instance/render.js





























 




















export function initRender (vm: Component) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
  //....
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // 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 {
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
    // ...
        vnode = vm._vnode
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

可以看到_render的主要逻辑是调用vnode = render.call(vm._renderProxy, vm.$createElement)

TIP

vm.$createElement也就是常说的h函数

TODO:render_Proxy

render有3个来源

  • 用户写的render函数
  • .vue文件中template编译而来, 它内部会使用vm._c而非render(h)函数的h参数(即vm.$createElement)
  • 运行时编译template参数

TIP

运行时编译出来的render函数类似这种, with(this){return [_c('div',[[_c('span',[_v("1")]),_v(" "),_c('span',[_v("2")])]],2)]}

vue-loader则会去除with, 生成的render函数类似如下

function () {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _vm.a ? _c("div") : _vm._e()
}
1
2
3
4
5
6

如何看待Vue.js 2.0 的模板编译使用了with(this)的语法? - 尤雨溪的回答 - 知乎

export default {
  render (h) {
    return
  }
}
1
2
3
4
5

如果render函数返回的不是VNode的实例, 则会创建emptyVNode

其等价在.vue中template内没有任何内容

new Vue({
  el: "#app",
  data () {
    return {
      show: false
    }
  },
  template: `<div v-if="show">t</div>`
})
1
2
3
4
5
6
7
8
9

经过模板编辑器, 上述template会被编译成如下形式

(function anonymous(
) {
with(this){return (show)?_c('div',[_v("t")]):_e()}
})
1
2
3
4

同样也是生成emptyVNode

# createElement()

render函数通过调用h函数来生成vnode, h函数是调用了createElement

  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
1

src/core/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
 // 允许不传入data, 直接传入children(函数重载)
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

对于用户定义的render函数, 需要进行normailize, 并且设置normalizationType, 因此在内部调用的_createElement的参数tag,data,children是由开发者传入的, 而normalizationType开发者传入也是会被覆盖的, 其是在alwaysNormalizefalse使用的(作为编译出的render函数的参数)

# _createElement()

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // object syntax in v-bind
  // is属性
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // 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, ns
  // DOM tag/ 局部注册的组件
  if (typeof tag === 'string') {
  //...
  } 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()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

当tag满足!tag === true, 则会创建emptyVNode

_createElement首先对children进行格式化. 需要格式化成Array<VNode>,

比如, 手写的render函数, 对于text VNodes, 传入的是string, 需要要根据string创建text VNodes.

new Vue({
  el: "#app",
  data () {
  	return {}
  },
  render (h) {
    return h('div',{}, [
      'test'
    ])
  }
})
1
2
3
4
5
6
7
8
9
10
11

又比如

<template>
  <div id="app">
    <span v-for="i in 2" :key=`span1_${i}`>1</span>
    <span v-for="i in 2" :key=`span2_${i}`>2</span>
  </div>
 </template>
1
2
3
4
5
6

在创建div对应的vnode的时候, 其children为[[vnodespan1_1, vnodespan1_2], [vnodespan2_1, vnodespan2_2](v-for指令生成), 所以需要将其flatten成长度为4的数组

TIP

v-for内部由函数renderList(src/core/instance/render-helpers/render-list.js)实现, 其返回一个数组

还有其他情况需要格式化, 这里先不讨论.

格式化之后, 代码主要对tag为string和非string的情况做对应的处理.

string

  • 创建DOM元素对应的vnode
  • 内部注册的组件

非string

  • component option即Vue组件的配置对象
  • 组件构造函数(比如来自Vue.extend)

因此vnode除了对应DOM之外, 也可以对应Vue组件, 前者直接来源于new VNode, 后者来源createComponent(最后也会调用new VNode)

  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)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

对于string, 先用config.isReservedTag判断是否是reserverdTag, 在浏览器端就是判断是否是原生DOM元素对应的tag. 显然这个函数是与平台相关的.

在初始化Vue全局api的时候, 将Vue.config和上面的config绑定在一起

src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)
  //...

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后在Vue web平台代码的入口文件中(此时initGlobalAPI已经执行), 设置成具体平台所对应的函数(平台相关的函数代码在src/platforms/web/util/index.js)

src/platforms/web/runtime/index.js

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
1
2
3
4
5
6

若是DOM tag, 则创建对应vnode

      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
1
2
3
4

TIP

对于web平台config.parsePlatformTagName(tag)返回tag

VNode constructor

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  )
1
2
3
4
5
6
7
8
9
10

# createComponent

创建组件对应的vnode

src/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 {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
  //..
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
  //...
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // https://github.com/vuejs/vue/issues/3957
  resolveConstructorOptions(Ctor)

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

  // extract props
  // 对props 和attrs做区分
  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
  )
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

首先需要获得组件的构造函数, Ctor可能是Vue的配置对象, 也可能是构造函数, 如果是配置对象, 则使用baseCtor.extend生成一个构造函数, 即使用Vue.extend来生成子组件的构造函数 baseCtor其实就是new Vue()中的Vue, 相关代码如下

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
1
2
3
4
5
6

src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
  Vue.options._base = Vue
}
1
2
3

实例化Vue的时候, 会将options merge到vm.$options中, 根组件是在new Vue(options)的时候, 子组件是在是实例化子组件构造函数的时候

src/core/instance/init.js








 


 








export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
        // 子组件
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
      // 根组件
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在得到组件的构造函数之后, 需要加入一些钩子

  // install component management hooks onto the placeholder node
  installComponentHooks(data)
1
2















 













const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  },

  insert (vnode: MountedComponentVNode) {
  },

  destroy (vnode: MountedComponentVNode) {
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

为什么需要这些钩子呢? 比如下面的栗子,

new Vue({
  el: "#app",
  render (h) {
    return h('div',{}, [
      ‘test',
      h(Child1),
      h(Child2)
    ])
  }
})
1
2
3
4
5
6
7
8
9
10

我们需要实例化根组件, 同时我们需要在适当的时机, 实例化其子组件, 而这些子组件vnode中的钩子, 可以提供这种能力.(比如init钩子能够挂载子组件)

记得有人说过类似的一句话, 框架中的virtual dom关键点在于其结合了一系列的钩子

在注入钩子之后, 就创建组件对应的vnode, 在componentOption中保存了创建子组件所需要的构造函数, 从父组件接受的propsData, 父组件监听的listener, slot相关的children

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

TIP

VNode constructor

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  )
1
2
3
4
5
6
7
8
9
10

我们可以将组件对应的vnode和DOM元素对应的vnode做个对比.

      // DOM tag
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
1
2
3
4
5

可以看出来, 组件vnode的children为undefined, 而componentOptions中有个children(用于slot).

刚接触Vue的时候, 我对这里感到奇怪, 我组件里明明写里很多html模板, 我错误地以为组件vnode的children会是组件template里写的那些东西. 尝试去理解Vue的组件设计, children为undefined恰恰是将整个页面隔离成一个个组件的关键.

# case study 分析个栗子

分析下面代码根组件执行render的过程

const Child1 =  {
  render (h) {
    return h('div', null, 'child1')
  }
}
const Child2 = {
  render (h) {
    return h('div', null, 'child2')
  }
}
new Vue({
  el: "#app",
  render (h) {
    return h('div',{}, [
      'test',
      h(Child1),
      h(Child2)
    ])
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

根组件render的过程

  1. 创建Child1 vnode
  2. 创建Child2 vnode
  3. 创建div vnode(会normalize children 创建textVnode) render_root

由于函数的调用顺序, 是先创建children的vnode, 再创建其外层vnode(可能会受normalize影响). 这里需要注意, 创建Child1 vnode和执行Child1的render()方法是不同的概念. 这也就是Vue组件化的本质, 子组件在父组件中的vnode可以理解为一个占位符placeholder, 为父子组件建立连接.

在父组件render之后, 就会进行其patch过程, 在patch之前, 子组件的render和其相关钩子函数都还没执行.