Vue 2.x源码学习:数据响应式改造
众所周知,Vue是以数据驱动视图展示的,即Vue会监听数据的变化,从而自动重新渲染页面的结构。
Vue主要通过三步走来实现这个功能:
第一步是对数据进行响应式改造,即对数据的读写操作进行劫持;
第二步是对模板依赖的数据进行收集;
第三步是在数据发生变化时,触发组件更新。
数据响应式改造
0. defineReactive
对数据进行响应式改造的核心代码
// core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 当前`Dep.target`不为空时,通常指向一个`watcher`实例
dep.depend() // 属性被收集到当前`watcher`实例的依赖数组中
if (childOb) {
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
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}})
}
通过Object.defineProperty
修改对象属性的属性描述符descriptor,来实现劫持对象属性的读写操作。
前置知识,对象的属性分为data型和accessor型。
data型的属性描述符包含value和writable;accessor型的属性描述符包含getter和setter函数(两者至少存在一个)。
由上述代码可以看出,所有属性被处理成了accessor型属性,即通过getter和setter来完成读写,比如当我们读取person
对象上的属性name
,实际得到的是name
的属性访问符中的getter函数执行后的返回值。
上述的响应式改造中,每个属性会对应一个dep实例:const dep = new Dep()
,假如属性值val是对象或数组,会被列入观察对象,他的属性会被递归进行响应式改造let childOb = !shallow && observe(val)
。
get函数被用于收集依赖:
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
该函数在对象属性被访问时会执行,如果Dep.target
不为空,即当下有一个监听器watcher
在收集依赖,就进行依赖的收集,dep实例会被收集到该watcher
的依赖数组newDeps中,同时dep也会将此watcher
记录到自己的subs订阅数组中,记录有谁订阅了自己的变更。
如果childOb
不为空(即属性值val为数组或对象,且可扩展),就对val的__ob__
属性也进行收集操作。
如果value是数组,对数组中的对象元素也进行依赖收集。
就是一层层的递归收集。
set函数被用于通知变更:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
如果属性的新值是属性或对象,就更新childOb
。
完成属性赋值操作后,调用dep.notify()
,通知所有订阅了自己的watcher
实例执行update
操作,即下面代码中的for循环操作。
// core/observer/dep.js
export default class Dep {
// ...
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()
}}
}
如果是同步this.sync
的watcher会立即被执行,否则会插入到watcher队列queueWatcher(this)
排队等待执行:
// core/observer/watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}}
1. initInjections
// core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)}
}
通过执行resolveInject
解析inject中的数据,解析结果赋值给result。result包含inject中所有的key,如果上级组件中没有对应inject数据的provide,就赋默认值,简单来说大致就是result[key] = inject[key] || default
。
再调用defineReactive(vm, key, result[key])
将这些key加到vm
实例上,即inject中的数据也会进行响应式处理。
假设存在一个inject:["person"]
,如果person
的值是个对象,它是一个被观察对象,当前子组件的watcher
会对该对象的__ob__
属性依赖收集,在上级组件中更改了原始person的某个属性,就会触发子组件的更新。
2. initProps
// core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}}
toggleObserving(true)
}
propsOptions
,接收的是vm.$options.props
,是声明接收的props的配置,;vm.$options.propsData
是实际接收到的props数据。
调用defineReactive(props, key, result[key])
将propsOptions上的key加到props
对象上,即vm._props
上,进行响应式处理,如果是在vm
上不存在的key,通过proxy(vm, '_props', key)
操作,使得可以通过vm
直接访问到_props
的属性,而不需要通过_props
对象来访问。
3. initData
// core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}}
// observe data
observe(data, true /* asRootData */)
}
data选项会被挂在vm._data
上,从上述代码中可以看出,data必须是一个对象,或者返回值为对象的函数。
通过proxy(vm, '_data', key)
操作,vm可以直接访问到_data
的属性,而不需要通过_data
对象来访问。
最后通过observe(data, true /* asRootData */)
来对数据做响应式改造,可以看到这个observe方法多传了一个参数值为true
,标记当前处理的数据是$options.data
对象。
observe
方法实际是创建一个新的ob实例,数据的__ob__
属性相信在控制台打印过vue中数据的同学都不陌生,都是指向ob实例。
// core/observe/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return}
let ob: Observer | void
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) {
ob = new Observer(value)}
if (asRootData && ob) {
ob.vmCount++}
return ob
}
// core/observe/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}}
}
从上面Observer
的构造函数中可以看出,创建ob
实例后,这个实例就挂载数据的__ob__
属性上了,因为在iniDats
时传递给构造函数的参数是个对象,所以会调用walk方法,继续看walk方法的定义,可以看出,是把这个对象的属性逐个取出,调用defineReactive(obj, keys[i])
进行响应式改造。
4. initComputed
// core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}}
}
获得每个计算属性对应watcher的初始值
从上述代码中可以看出,会遍历computed所有的属性,每个属性对应配置一个watcher实例,watcher实例在创建时,会调用每个computed对应的getter获取一遍初始值,放在watcher实例的value
属性上
// core/observer/watcher.js
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
this.value = this.lazy
? undefined
: this.get()}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value}
// ...
}
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
可以看到,执行watcher实例的get()
方法时,会进行一个pushTarget(this)
的操作,此操作修改了Dep.target
,使它指向了当前的watcher实例,如果某个computed属性依赖了data中的某个属性,需要读取data中的某个属性值,就会触发该data属性的getter函数,使得该data属性被收集到当前watcher实例的依赖数组中。
完成computed属性的取值后,执行popTarget()
,即下面的代码:
// core/observer/dep.js
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
会使Dep.target
指回上一个watcher实例。
最后清理依赖this.cleanupDeps()
,将不再关联的依赖dep其订阅数组中对应的watcher移除,将newDeps
赋值给deps
并清空newDeps
,代表该watcher
实例一次依赖收集完毕。
计算属性被读取时,其对应watcher依赖的数据会被当前watcher收集为自身的依赖
如果computed某个属性的标识符不在vm
实例上,就继续执行defineComputed(vm, key, userDef)
,会将给vm实例添加一个名为key
的属性,该属性的getter函数由下述代码定义:
// core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}}
}
即在这个计算属性被读取时,会拿到它所对应的watcher实例,如果当前Dep.target
不为null时,watcher会执行实例方法depend()
。
export default class Watcher {
// ...
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}}
// ...
}
可以看到此watcher实例的依赖deps会被一一取出,执行dep实例的depend方法:
// core/observerdep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}}
即此watcher实例的依赖都会被收集到当前Dep.target
指向的watcher实例的依赖数组中。
5. initWatch
// core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler}
if (typeof handler === 'string') {
handler = vm[handler]}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function ( expOrFn: string | Function,
cb: any,
options?: Object ): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}}
initWatch的内容比较简单,就是通过调用createWatcher(vm, key, handler)
,一一对应生成watcher
实例,并且给watcher
实例标记options.user = true
,代表这个watcher
是用户配置的。
每个watch通常是有一个对应的表达式(通常是vm的data数据)和一个对应的回调函数,使用场景通常是当vm中的某些数据发生改变时,用户需要做一些自定义的操作来做处理。
与computed中生成对应watcher实例类似,watcher实例在创建时,每个watch对应的表达式就会被求值一遍,即vm实例上的某些数据属性被读取,这些属性对应的dep会被收集到该watcher
实例的依赖数组中,求得的值会放在watcher
实例的value
属性上,如果某个watch配置了immediate
,就立即执行一遍watch对应的回调函数,入参为watcher
的value
属性值。
可以看到在执行回调函数前,执行了一个pushTarget()
,此时Dep.target会指向空,所以在回调函数执行过程中,如果vm的某些数据属性被访问,这些属性不会被收集依赖,因为属性的getter
函数中在属性被收集依赖前有个对Dep.target
的判空检查。
6. 一些说明
__ob__
是给对象加的属性,指向observer实例,ob和对象是一对一,代表这个对象被观察,该对象的属性的读写操作会被做响应式处理,即被劫持。
dep
是给属性配置的用于依赖收集的,通常对象的某个属性与dep是一对一,可以被多个watcher收集,即多个watcher实例在监听这个属性的变化;__ob__
是对象的特殊属性,它也有自己的dep,可以被watcher
收集。
一个watcher实例只会关联一个vm,一个vm实例可以关联多个watcher
,watcher实例会放在vm._watchers
数组中;渲染watcher
还会放在vm._watcher
上,渲染watcher
从字面上理解就是与组件渲染有关的watcher
。
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享