vue执行过程源码分析

vue执行过程

大致看作:

  • 完善Vue构造函数
  • 创建vue实例,调用vue._init()初始化
  • vue初始化后,$mount挂载组件
    • compile编译(模板->AST树->render函数)
    • 生成虚拟dom(v-node -> v-dom)
    • 挂载更新真实dom(__patch__ -> dom)

vue.js文件被引入开始执行

在vue实例化之前,会根据所处环境等因素扩展完善vue构造函数

  1. 扩展$mount方法
    两种实例化方式,中间过程稍有不同

    • web方式

      1
      2
      3
      new Vue({
      el: '#app'
      })
    • render方式(webpack的vue-lodaer或babel的jsx)

      1
      2
      3
      4
      import App from 'App.vue'
      new Vue({
      render: h => h(App)
      }).$mount('#app')

      两者区别在于:

    1. web方式没有render选项,在vue._init之前,web方式会扩展$mount方法,实现能够编译template或el指定的模板

    2. $mount(‘#app’)为手动挂载,所以如果不调用,将阻断页面加载,停止在created周期后;web方式会在初始化(vue._init())后自动调用$mount

      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
      // 源码部分
      const mount = Vue.prototype.$mount // 缓存一份$mount

      Vue.prototype.$mount = function ( // 扩展$mount
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      el = el && query(el)
      const options = this.$options
      // 整个扩展是针对没有render选项时
      if (!options.render) {
      let template = options.template
      // 获取template
      if (template) {
      if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
      template = idToTemplate(template)
      }
      } else if (template.nodeType) {
      template = template.innerHTML
      } else {
      if (process.env.NODE_ENV !== 'production') {
      warn('invalid template option:' + template, this)
      }
      return this
      }
      } else if (el) {
      template = getOuterHTML(el)
      }
      // 解析tmplate,核心是compileToFunctions方法,最终返回render函数
      if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
      outputSourceRange: process.env.NODE_ENV !== 'production',
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      }
      }
      return mount.call(this, el, hydrating)
      }
  2. initGlobalAPI

    • Vue.set

    • Vue.delete

    • Vue.nextTick

    • initUse(Vue)

    • initMixin(Vue)

    • initExtend(Vue)

      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
      export function initGlobalAPI (Vue: GlobalAPI) {
      // config
      const configDef = {}
      configDef.get = () => config
      Object.defineProperty(Vue, 'config', configDef)

      Vue.util = {
      warn,
      extend,
      mergeOptions,
      defineReactive
      }

      Vue.set = set
      Vue.delete = del
      Vue.nextTick = nextTick

      // 2.6 explicit observable API
      Vue.observable = <T>(obj: T): T => {
      observe(obj)
      return obj
      }

      Vue.options = Object.create(null)
      ASSET_TYPES.forEach(type => {
      Vue.options[type + 's'] = Object.create(null)
      })

      Vue.options._base = Vue

      extend(Vue.options.components, builtInComponents)

      initUse(Vue)
      initMixin(Vue)
      initExtend(Vue)
      initAssetRegisters(Vue)
      }
  3. 实现若干实例方法和属性

    • initMixin(Vue) // _init 实现vue初始化函数_init

      • initLifecycle(vm)
      • initEvents(vm)
      • initRender(vm)
      • callHook(vm, ‘beforeCreate’)
      • initInjections(vm) // resolve injections before data/props
      • initState(vm)
      • initProvide(vm) // resolve provide after data/props
      • callHook(vm, ‘created’)
    • stateMixin(Vue) // 组件状态相关api如$set,$delete,$watch实现

    • eventsMixin(Vue) // 事件相关api如$on,$emit,$off,$once实现

    • lifecycleMixin(Vue) // 组件声明周期api如_update,$forceUpdate,$destroy实现

    • renderMixin(Vue) // 实现组件渲染函数_render, $nextTick

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
      !(this instanceof Vue)
      ) {
      warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
      }

      initMixin(Vue)
      stateMixin(Vue)
      eventsMixin(Vue)
      lifecycleMixin(Vue)
      renderMixin(Vue)

vue实例创建,初始化

在 new Vue() 时会调用_init()进行初始化,会初始化各种实例方法、全局方法、执行一些生命周期、初始化 props、data等状态。其中最重要的是data的「响应化」处理。
整个过程大概是new Vue() 到 created生命周期结束.此过程视为初始化过程,当然web方式$mount的调用也在init中.


vue在初始化时具体都做了什么?

  1. 合并options属性
  2. 初始化proxy拦截器(vm._renderProxy)
  3. initLifecycle(vm) // 初始化组件生命周期标志位;
    • $parent,$root,$children,$refs,_watcher,_isMounted,_isDestroyed…
  4. initEvents(vm) // 初始化组件事件侦听;
    • $on,$off,$emit
  5. initRender(vm) // 初始化渲染方法
    • $createElement,$nextTick
  6. callHook(vm, ‘beforeCreate’) // beforeCreate生命周期
  7. initInjections(vm) // 初始化依赖注入内容,在初始化data、props之前
  8. initState(vm) // 初始化props/data/method/watch/methods
    • 数据响应化
  9. initProvide(vm) // 为子组件提供数据,在初始化data、props之后
  10. callHook(vm, ‘created’)
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
// vue构造函数
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&   !(this instanceof Vue) ){
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 实例调用init方法.注意此方法在下面initMixin(Vue) 中添加到vue原型上
}
initMixin(Vue)

// initMixin
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++

let startTag, endTag

vm._isVue = true
// 合并属性
if (options && options._isComponent) {
// 组件合并属性
initInternalComponent(vm, options)
} else {
// 合并vue实例属性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 初始化proxy拦截器
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
initLifecycle(vm) // 初始化组件生命周期标志位
initEvents(vm) // 初始化组件事件侦听
initRender(vm) // 初始化渲染方法
callHook(vm, 'beforeCreate') // beforeCreate生命周期
initInjections(vm) // 初始化依赖注入内容,在初始化data、props之前
initState(vm) // 初始化props/data/method/watch/methods
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

// web方式自动$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

$mount挂载组件

此$mount就是初始化时扩展的$mount,它的目的是将模板变成render函数,进而实现挂载渲染
其中模板变成render函数的过程即compile编译
render函数的执行会将编译后的模板形成v-dom
通过patch方法实现真实dom挂载更新

compile编译
模板编译的目的是将模板转成render函数,其中核心方法是compileToFunctions;
在web情况下是通过扩展$mount来实现模板解析的,在cli脚手架中通过vue-loader(解析.vue文件),vue-template-compiler(模板预编译为渲染函数(template => ast => render))
compile包括三部分:

  • parse 使用正则解析template中的vue的指令(v-xxx) 变量等等 形成抽象语法树AST
  • optimize 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
  • generate AST 转化为渲染函数 render function

解析完成后会render方法挂载到options选项上,再之后会再次调用$mount,
此$mount是未扩展之前的,即上边提到的提前缓存的mount.call(this, el, hydrating),其中有mountComponent渲染组件方法并执行.


mountComponent方法执行内部:

  • beforeMount;
  • 定义updateComponent方法,是渲染DOM的入口方法
    • updateComponent方法主要执行在vue初始化时声明的_render,_update方法,其中,_render的作用主要是生成vnode,_update主要功能是调用__patch__,将vnode转换为真实DOM,并且更新到页面中
    • const vnode = vm._render(); 调用vue初始化时的_render函数,_render函数来自于组件的option(上边说到解析完成后会render方法挂载到options选项上),生成v-dom
    • vm._update(); __patch__的入口函数,对比新老v-node,挂载更新dom;
  • new Watcher; 添加组件监听,有变化时走更新流程,再次调用updateComponent方法
    • beforeUpdate
    • updated
  • mounted
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
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) { // 没有render函数则渲染空
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')

let updateComponent // 定义updateComponent方法,是渲染DOM的入口方法
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, { // 添加组件监听,有更新时会回调updateComponent
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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
}

生成虚拟dom
render函数的作用就是生成v-node,其中核心是依靠creatElement函数实现,在vue内部通常用_c表示,在外部用h表示;

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
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
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 {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
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)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}

vnode和ast类似,相当于dom节点的描述对象

【render详解】
挂载更新真实dom
通过vue.__patch__将虚拟dom变成真实dom,其中涉及到【diff算法】

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
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置当前激活的作用域
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 执行具体的挂载逻辑
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}

在挂载DOM的过程中,是先添加新数据生成的节点,然后再移除老的节点。


组件初始化&实例过程

组件初始化&实例过程和vue初始化&实例过程类似,可以看作vue为根组件,组件为子组件.

组件初始化

vue实例过程中,调用render方法生成vnode时,会判断tag是否是HTML标签,如果是就继续后续步骤生成vnode,如果不是,说明是组件,则调用组件初始化方法(createComponent),和vue实例初始化类似,其中包括合并配置、构造方法属性,初始化组件的钩子函数,创建并返回vnode.至此,组件初始化完成,注意是初始化不是实例化,此时还没有到实例步骤,因此不涉及属性,数据,方法,钩子等.

组件实例化

初始化完成后,返回vnode,与render方法中生成的其他vnode合并.此时vue实例过程中render方法完成,下一步是调用update方法,__patch__方法;patch方法中调用createElm根据vnode生成真实dom.
createElm中执行**createComponent**判断是否是组件,是,则实例化组件.




参考:

Vue初始化过程
vue实例化过程
vue组件初始化过程