前文中,已经分析了在vuejs源码中是如何定义Vue类,以及如何添加实例属性和静态方法:大数据进阶-读懂vuejs源码1。
Vue实例化时调用_init,本文将深入该方法内部做了哪些事情及vuejs如何实现数据响应式。
Vue初始化
在core/instance/index.js
文件中定义了Vue的构造函数:
function Vue (options) {
// 执行_init方法,此方法在initMixin中定义
this._init(options)
}
_init方法定义在core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
// 。。。
// 1. 合并options
if (options && options._isComponent) {
// 此处有重要的事情做。
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2. 初始化属性
// 初始化$root,$parent,$children
initLifecycle(vm)
// 初始化_events
initEvents(vm)
// 初始化$slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// 执行生命周期钩子
callHook(vm, 'beforeCreate')
// 注册inject成员到vue实例上
initInjections(vm) // resolve injections before data/props
// 初始化_props/methods/_data/computed/watch
initState(vm)
// 初始化_provided
initProvide(vm) // resolve provide after data/props
// 执行生命周期钩子
callHook(vm, 'created')
// 3. 调用$mount方法
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在合并options的时候,如果options表示一个组件(_isComponent)则调用了initInternalComponent
函数:
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
// 此处保留组件之间的父子关系,
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
//...
}
此方法中设置了组件之间的父子关系,在后续的注册及渲染组件的时候会用到。
initProvide
定义在core/instance/inject.js
文件中。
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
在上面的代码中可以看出,如果provide是一个函数,那么会调用这个函数,并将this指向vm实例。由于initProvide在_init方法中最后被调用,因此能够访问到实例的属性。
initInjections
定义在core/instance/inject.js
文件中。
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
// 遍历result属性,利用Object.defineProperty将其添加到vue实例上
// ...
}
}
此方法调用resolveInject方法获取所有inject值。
export function resolveInject(inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
// ....
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// ...
}
return result
}
}
在resolveInject方法中会从当前实例出发,延着parent一直向上找,直到找到_provided中存在。
总结
此时整个Vue定义和初始化流程可以总结为如下:
数据响应式
vuejs框架的整个数据响应式实现过程比较复杂,代码散落在各个文件中。我们都知道,在定义组件的时候,组件会自动将data属性中的数据添加上响应式监听,因此我们从_init方法中调用initState
函数开始。
启动监听
在initState函数中:
export function initState (vm: Component) {
// ...
if (opts.data) {
// 处理data数据
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// ...
}
options中的data数据会交由initData方法处理:
function initData(vm: Component) {
// ... 1. 获取data数据,如果data是一个函数,但没有返回值,会提示错误。
// ... 2. 遍历data所有的属性,首先判断在props和methods是否同名,然后将其代理到vue实例上。
// 3. 添加响应式数据监听
observe(data, true /* asRootData */)
}
添加监听
observe函数
定义在core/observer/index.js
文件中:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 通过__ob__属性判断该属性是否添加过响应式监听
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 如果添加过,不做处理,直接返回
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建Observer实例,其为响应式的核心
ob = new Observer(value)
}
// 通过vmCount可以判断某个响应式数据是否是根数据,可以理解为data属性返回的对象是根数据,如果data对象的某个属性也是一个对象,那么就不再是根数据。
// vmCount属性后续会被用到
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
该方法的核心就是为data数据创建Observer实例ob, ob对象会为data添加getter/setter方法,其可以用来收集依赖并在变化的时候触发dom更新。
Observer类
定义在core/observer/index.js
文件中,在其构造函数中,根据传入data的类型(Array/Object),分别进行处理。
Object
constructor(value: any) {
this.value = value
// Observer实例上包含dep属性,这个属性后续会有很大作用,有些无法监听的数据变化可以由此属性完成
this.dep = new Dep()
this.vmCount = 0
// 为data添加__ob__属性
def(value, '__ob__', this)
if (Array.isArray(value)) {
// ... 处理数组
} else {
// 处理对象
this.walk(value)
}
}
- walk
遍历data的所有属性,调用defineReactive
函数添加getter/setter。
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 添加数据拦截
defineReactive(obj, keys[i])
}
}
- defineReactive
数据响应式实现的核心方法,原理是通过Object.defineProperty为data添加getter/setter拦截,在拦截中实现依赖收集和触发更新。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 1. 创建闭包作用域内的Dep对象,用于收集观察者,当数据发生变化的时候,会触发观察者进行update
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 2. 获取对象描述中原有的get和set方法
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 3. 添加getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 静态属性target存储的是当前观察者。
if (Dep.target) {
dep.depend()
if (childOb) {
// 将观察者添加到Obsetver实例属性dep中。
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// ... 一些判断,省略
// 当赋值的时候,如果值为对象,需要为新赋值的对象添加响应式
childOb = !shallow && observe(newVal)
// 调用set就是为属性赋值,赋值说明有新的变化,所以要触发更新
dep.notify()
}
})
}
整个defineReactive有两个地方比较难以理解:
- 通过Dep.target获取依赖
由于这个地方涉及到后面的编译部分,所以我们把这部分逻辑单独拿出来,用一段简短的代码来描述整个过程,如下:
// 模拟Dep
let Dep = {}
Dep.target = null
// 模拟变化数据
let data = {
foo: 'foo'
}
Object.defineProperty(data, 'foo', {
get() {
if (Dep.target) {
console.log(Dep.target)
}
}
})
// 模拟编译 {{foo}}
// 1. 解析到template中需要foo属性的值
const key = 'foo'
// 2. 在foo属性对应的值渲染到页面之前,为Dep.target赋值
Dep.target = () => {
console.log('观察foo的变化')
}
// 3. 获取foo属性的值,此时会触发get拦截
const value = data[key]
// 4. 获取完成后,需要将Dep.target的值重新赋值null,这样下一轮解析的时候,能够存储新的观察者
Dep.target = null
- 在闭包作用域内已经包含了Dep对象,在set中通过此对象的notify方法触发更新,为什么还需要在get方法中,将依赖添加到Observer对象的实例属性dep中。
其实,这是为了方便在其他手动触发更新,由于defineReactive方法内部的dep对象是闭包作用域,在外部无法直接访问,只能通过赋值方式触发。
如果在Observer对象上保存一份,那么就可以通过data.__ob__.dep
的方式访问到,直接手动调用notify方法就可以触发更新,在Vue.set方法内部实现就可以这种触发更新方式。
Array
众所周知,Object.defineProperty是无法监控到通过push,pop等方法改变数组,此时,vuejs通过另外一种方式实现了数组响应式。该方式修改了数组原生的push,pop等方法,在新定义的方法中,通过调用数组对象的__ob__
属性的notify方法,手动触发更新。
Observer构造函数中:
if (Array.isArray(value)) {
if (hasProto) {
// 支持__proto__,那么就通过obj.__proto__的方式修改原型
protoAugment(value, arrayMethods)
} else {
// 不支持,就将新定义的方法遍历添加到数组对象上,这样可以覆盖原型链上的原生方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历数组项,如果某项是对象,那么为该对象添加响应式
this.observeArray(value)
}
其中arrayMethods就是重新定义的数组操作方法。
- arrayMethods
定义在core/Observer/array.js
文件中,该文件主要作了两件事情:
- 创建新的集成自
Array.prototype
的原型对象arrayMethods。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
- 在新的原型对象上,添加自定义方法覆盖原生方法。
// 定义所有会触发更新的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 获取Array中原生的同名方法
const original = arrayProto[method]
// 通过Object.defineProperty为方法调用添加拦截
def(arrayMethods, method, function mutator(...args) {
// 调用原生方法获取本该得到的结果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// push,unshift,splice三个方法会向数组中插入新值,此处根据情况获取新插入的值
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果新插入的值是对象,那么需要为对象添加响应式,处理逻辑和data处理逻辑相似
if (inserted) ob.observeArray(inserted)
// 手动触发更新
ob.dep.notify()
return result
})
})
从上面的处理逻辑可以看出,下面的数组操作可以触发自动更新:
// 修改数组项
[].push(1)
[].pop()
[].unshift(1)
[].shift()
[].splice()
// 修改数组项顺序
[].sort()
[].reverse()
而下面的操作不能触发:
// 修改数组项
[1, 2][0] = 3
[1, 2].length = 0
Dep
在添加数据监听的过程中用到了Dep类,Dep类相当于观察者模式中的目标,用于存储所有的观察者和发生变化时调用观察者的update方进行更新。
export default class Dep {
// 当前需要添加的观察者
static target: ?Watcher;
// id,唯一标识
id: number;
// 存储所有的观察者
subs: Array;
constructor() {
this.id = uid++
this.subs = []
}
// 添加观察者
addSub(sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
// 调用观察者的addDep方法,将目标添加到每一个观察者中,观察者会调用addSub方法
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 将观察者排序,然后依次调用update
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
Watcher类是观察者模式中的观察者,当Dep触发变化的时候,会调用内部存储的所有Watcher实例的update方法进行更新操作。
在vuejs中,Watcher可大致分为三种:Computed Watcher, 用户Watcher(侦听器)和渲染Watcher(触发Dom更新)。
Watcher类包含大量的实例成员,在构造函数中,主要逻辑如下:
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options ?: ? Object,
isRenderWatcher ?: boolean
) {
// ... 根据参数为实例成员赋值
// 调用get方法
this.value = this.lazy
? undefined
: this.get()
}
在get方法中,获取初始值并将自身添加到Dep.target。
get() {
// 1. 和下面的popTarget相对应,这里主要是为Dep.target赋值
// 由于存在组件之间的父子关系,所以在pushTarget中还会将当前对象存放到队列中,方便处理完成子组件后继续处理父组件
pushTarget(this)
let value
const vm = this.vm
try {
// 2. 获取初始值,并触发get监听,Dep会收集该Watcher
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 实现deep深度监听
if (this.deep) {
traverse(value)
}
// 3. 将Dep.target值变为null
popTarget()
this.cleanupDeps()
}
return value
}
addDep
addDep方法用于将当前Watcher实例添加到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方法,将Watcher实例添加到Dep中
dep.addSub(this)
}
}
}
update
update主要处理两种情况:
- 如果是用户添加的监听器,在变化的时候会执行run方法。
- 如果是渲染Dom时添加的,在变化的时候会执行queueWatcher函数,在queueWatcher函数中,通过队列的方式批量执行更新。
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 用户添加的监听器会执行run方法
this.run()
} else {
// 触发dom更新会执行此方法, 以队列方式执行update更新
queueWatcher(this)
}
}
run
run方法主要用于在数据变化后,执行用户传入的回调函数。
run() {
if (this.active) {
// 1. 通过get方法获取变化后的值
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 2. 获取初始化时保存的值作为旧值
const oldValue = this.value
this.value = value
if (this.user) {
try {
// 3. 调用用户定义的回调函数
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
生成渲染Watcher
在查找编译入口那部分讲到了platforms/web/runtime/index.js
文件定义了$mount方法,此方法用于首次渲染Dom。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
其内部执行了mountComponent函数。
mountComponent
定义在core/instance/lifecycle.js
文件中,该函数主要执行三块内容:
- 触发
beforeMount
,beforeUpdate
和mounted
生命周期钩子函数。 - 定义
updateComponent
方法。 - 生成Watcher实例,传入updateComponent方法,此方法会在首次渲染和数据变化的时候被调用。
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ... 1. 触发生命周期钩子
// 2. 定义updateComponent方法
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
vm._update(vnode, hydrating)
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 生成watcher实例
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// ... 触发生命周期钩子
return vm
}
_update, _render
_update, _render是Vue的实例方法, _render方法用于根据用户定义的render或者模板生成的render生成虚拟Dom。_update方法根据传入的虚拟Dom,执行patch,进行Dom对比更新。
总结
至此,响应式处理的整个闭环脉络已经摸清。