从源码角度理解 Vue 生命周期和响应式原理

1
2
3
4
5
6
var vm = new Vue({
el: '#app',
data: {
test: 1
}
})

beforeCreate && created

在 new Vue() 创建实例时,会先执行 _init 方法进行初始化.

1
2
3
4
5
6
7
8
9
// src/core/instance/index.js
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)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this

// ...

// merge options
vm._self = vm
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')

// ...

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

上面列出了 Vue 初始化时做的工作,在 beforeCreate 之前,主要是合并配置,初始化生命周期,初始化事件中心,初始化渲染。beforeCreate之后主要是 initState,initState 主要是初始化 props、data、methods、watch、computed,methods 等这些属性,在初始化完之后调用 create 钩子。所以在 beforeCreate 钩子函数被调用时,与上述属性相关的内容都不能访问,created 之后才可以访问。

beforeMounted && mounted

在 created 之后,开始执行挂载 $mount。因为 $mount 和平台、构建方式都有关,Vue 原型上定义 $mount 主要有两处,一处是 Runtime 版(运行版) Vue 的入口文件。

1
2
3
4
5
6
7
8
9
// src/platforms/web/runtime/index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

通过 mountComponent 函数完成真正的挂载工作。
另一处是 Runtime + Compiler 版(完整版)的入口文件。

1
2
3
4
5
6
7
8
9
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
return mount.call(this, el, hydrating)
}

这两个版本的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 运行时+编译器
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})

// 运行时版本
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})

完整版比运行时版多了编译器的处理,我们平时写项目时,vue-loader 会帮我们把 template 转化成 render 函数,采用运行时构建即刻。
将省略的编译器的部分展开分析

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
  el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 处理 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} 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)
}
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

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}

首先挂载点不能是 body 或 html 元素,接着看是否有渲染函数 render,如果有则直接调用运行时版的 $mount,如果没有则先将 template 或 el 处理成字符串,再通过调用 compileToFunctions 成渲染函数 render。接着去执行真正的挂载函数 mountComponent。

mountComponent

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
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
}
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

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

render 函数必须存在,否则会报警告,如果 render 存在首先调用 beforeMount 钩子,接着根据不同的环境定义 updateComponent 方法

updateComponent

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

updateComponent 方法中 vm._render 方法生成虚拟 Node,最终调用 vm._update 将虚拟 Node 渲染成真正的 DOM,_update 在首次渲染和数据更新时都会调用,核心方法是 patch ,将新虚拟节点和旧虚拟节点进行对比。
那什么时候调用 updateComponent 方法呢? 实例化一个 Watcher,将 updateComponent 作为参数传递进去,
在之前实现简单的MVVM中分析,初始化 Watcher 时,会通过传入的表达式(观察目标)进行一次求值,从而触发属性的 getter 属性进行依赖收集。对应到这里看源码对 Watcher 定义

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
// src/core/observer/watcher.js
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get() // 注意这里

}
get () {
// ...
}

// ...
}

实例化 Watcher时,第一个参数是当前组件实例对象,第二个参数 expOrFn 是 updateComponent函数,第三个参数是回调 noop 空函数,但不代表数据更新时什么都不做,因为数据更新时,还会再去获取新的值(也就是对观察目标求值),同时触发回调。获取新值时也不会再去进行依赖收集,因为 Dep.target 已经设置为 null。 第四个参数 isRenderWatcher 为 true,表示是渲染 Watcher。初始化时,因为 updateComponent 是函数,将其赋值给 getter 属性,接着去执行 Watcher 的 get() 方法,并将 get 得到的 value 值赋值给该观察者实例对象的 value 属性,也就是说 this.value 里存的是观察目标的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
}
return value
}

之前分析的类似,执行 get() 方法时,Dep.target 指向当前的渲染 Watcher,使用 pushTarget 方法先将此 Watcher 添加进 Dep 的观察者对象列表中(也就是进行依赖收集),执行 value = this.getter.call(vm, vm) this.getter()对应的就是 updateComponent 方法,vm._update(vm._render(), hydrating) 渲染函数 render 在生成虚拟 DOM 的过程中会访问 vm 中的数据,从而触发 Observer 观察者中属性的 getter 方法,进行依赖收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Dep {
// ...
}

Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

export function popTarget () {
Dep.target = targetStack.pop()
}

每个属性都有一个对应的 dep 对象,在触发属性的 getter 时会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/core/observer/dep.js
class Dep {
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}

addDep 将观察者加入到该属性对应的 Dep 实例的 subs 数组中,用来收集观察者并避免添加重复依赖。

总结挂载过程

  • 根据平台不同,将 template 转化为 render 函数
  • 触发 beforeMount 钩子
  • 实例化渲染 Watcher,执行 vm._render 函数生成虚拟 DOM,执行 vm._update 将虚拟 DOM 转化为真实 DOM,在这过程中进行了依赖收集
  • 触发 mounted 钩子, 组件挂载成功

beforeUpdate && update

还是看这段代码

1
2
3
4
5
6
7
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

初始化挂载的时候,因为 _isMounted 为 false,所以不会触发 beforeUpdate。当挂载成功,某个响应式属性更新时,会调用对应属性 dep 实例的 notify 方法,通知所有的订阅者更新调用 update 方法。

1
2
3
4
5
6
7
8
9
10
11
// src/core/observer/dep.js
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

update 做了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/core/observer/watcher.js
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}

如果观察者是异步更新,调用 queueWatcher 方法

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
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

当 queueWatcher 函数被调用时,会将该观察者放入队列 queue 中,并维护一个映射关系避免重复添加观察者,在 nextTick 中执行 flushSchedulerQueue。flushing 是一个标志,当观察者加入时队列正在更新时,放到队尾等待更新。wating 也是一个标志位,避免同时调用 nextTick。关于 nextTick 解析 查看之前的这篇Vue.nextTick 源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function flushSchedulerQueue () {
// ...
// 获取到 updatedQueue
callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}

flushSchedulerQueue 函数的作用之一是将观察者统一执行更新,在这个过程中还会通过this.get()一次观察目标的值,触发 watcher 回调进行渲染。当获取到已经更新的 watcher 之后,调用 callUpdatedHooks 函数,对数组遍历,当 watcher 是当前 watcher 而且已经挂载成功,执行 updated 钩子表示更新完成。

总结更新过程

  • 调用 beforeUpdate 钩子
  • 触发属性 setter
  • 调用 dep.notify 方法
  • 通知 watcher 调用 update()
  • queueWatcher(nextTick(flushSchedulerQueue))
  • updated钩子

beforeDestroy & destroyed

组件销毁阶段,先调用 beforeDestroy 钩子,接着执行一系列销毁动作,包括删掉 watcher,vnode等,最后执行 destroyed

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
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}