Vue是当前最流行的框架之一,现在很多项目都或多或少都会用到Vue。所以了解Vue的响应式原理对我们意义非凡,有利于…
我们直接开始吧
Vue对数据进行响应式的处理的入口在src/core/instance/state.js
文件下的initState
函数
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)
}
}
函数参数vm指的是Vue实例,我们可以把它看作是我们平常写vue代码是经常使用的this
。
函数首先给vm实例定义_watchers
属性,这个什么作用我们暂时不用管,接着往下。
然后获取vm的 o p t i o n s 配 置 。 这 里 的 options配置。这里的 options配置。这里的options配置包括我们Vue预先定义的配置、mixins传入的配置,和我们自己实例话Vue时传入的配置。
比如我们经常这样实例化Vue
new Vue({
data() {
return {
a: 1
}
}
})
通过$options就能拿到我们传入的配置。
后面的代码我们很容易就能理解了,Vue拿到我们传入的props、methods、data、computed、watch属性分别进行初始化。我们在这里主要关注data的初始化,所以接下来我们看看initData做了什么
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 */)
}
initData中首先拿到data的数据,并将数据赋值为vm._data, 如果时data是个函数的话,就调用getData获取数据
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
然后判断data是否是个对象,接着判断data中属性的名字是否和props和methods中属性命名冲突,是否时Vue的保留字(_isReserve
)
如果data中的数据的属性不和props和method命名冲突,也不是以$
和_
开头(Vue自己定义的属性以$
和_
开头),那么久调用proxy
函数对data中数据进行代理
proxy函数
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy函数非常简单,主要通过 Object.defineProperty 函数在实例对象 vm 上定义与 data 数据字段同名的访问器属性,并且这些属性代理的值是 vm._data
上对应属性的值,所以我们平常使用的this.a
其实是this._data.a
中的值
接下来调用observe函数对data中的数据进行响应式处理,所以说,从这里开始才是Vue响应式系统的开始,接下来我们开看看observe函数做了什么
observe定义在src/core/observer/index.js文件中
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
//...
ob = new Observer(value)
//..
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
为了方便理解,代码删除了一些条件判断。从上面的代码很容易就可以看出,observe函数首先判断传入的value是不是对象,如果是对象才进行响应式处理,毫无疑问,我们传入的data是个对象,接着定义了一个Oberser对象并返回,接下来我们看看Observer
是怎样的
Observer定义在通过一个目录下,是一个类(构造函数)
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)
}
}
// ...
}
在connstructor方法中,首先将传入的value(也就是data)赋值为this.value,实例对象的 dep 属性,保存了一个新创建的 Dep实例对象。然后初始化vmCount,之后给我们的data定义一个__ob__
属性,指向创建的实例。 也就是说,如果我们传入的data是
data = {
a: 1
}
这样的话,经过def(value, '__ob__', this)
就变成了
data = {
a: 1
__ob__: {
vmCount: 0,
dep: Dep实例,
...
}
}
接下来就进入了分支判断,判断value是否是数据,因为Vue对普通对象和数组的响应式策略不同,所以需要进行判断,在这里我们主要关注Vue对普通对象的响应式处理
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
walk函数非常简单,遍历obj的每一个属性,然后调用defineReactive
函数
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 为每个属性定义一个dep实例,作为依赖收集器
const dep = new Dep()
// 判断属性是否可以被配置
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取getter和setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 如果val是对象,先递归对其属性进行响应式处理
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.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// 拦截赋值操作,并通知以来数据更新
set: function reactiveSetter (newVal) {
// 获取原来的值
const value = getter ? getter.call(obj) : val
// 判断是否需要更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 如果属性不可修改,直接返回
if (getter && !setter) return
// 修改数据
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对新的值进行响应式处处理
childOb = !shallow && observe(newVal)
// 通知所有的依赖,数据进行了更新
dep.notify()
}
})
}
defineReactive
函数有点复杂,不过主要值有三点
this.a = xxx
对对数据进行更新,从而使得视图得到更新通过前面的讨论,我们已经知道了Vue会在获取data的值的会调用getter,进行执行dep.depend()
时候进行依赖收集,那么依赖收集又是什么实现的呢?而且dep
具体又是什么,接下来我们就讨论一下这部分内容
我们下来了解一下数据响应系统中另一个很重要的部分——Watcher
。其实Watcher
就是我们前面所说的依赖,dep
收集依赖就是收集watcher
,当data更新是,会通知它所收集的watcher数据得到了更新,进而watcher就会执行相应的逻辑,更新视图。
每一个Vue实例都对应一个Watcher
,所以每次data数据得到更新时,Vue实例对应的Watcher
都会得到通知,进而触发视图更新
那么Vue实例对应的Watcher时什么时候创建的呢?
wacher的创建在Vue挂在模板时进行创建。我们可以在src/core/instance/lifecycly.js中的mountComponent函数中看到Watcher的创建
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//...
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
const vnode = vm._render()
// ...
vm._update(vnode, hydrating)
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...
}
在面代码中,定义了updateComponent函数,从名字可以看出,这个是用来更新视图的,开始创建和数据更新时都会调用这个函数来更新视图。updateComponent中调用了vm._render
函数,这个函数的作用就是将我们的返回一个VNode
(虚拟DOM),也就是在这个函数中我们得到data的值,触发getter,进行依赖收集。我们可以通过一个例子来看一下,如果我们自己定义render函数的话:
new Vue({
data() {
return {
a: 1
}
},
render: function(createElemnet) {
let a = this.a
return createElement('div', a);
}
})
vm._render
会调用到这里的render
函数,在render函数中,我们获取a的值,触发了a的getter,随之进行依赖收集
接下来我们看看Watcher的定义,在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)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// ...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
// ..
}
// ...
}
在构造函数中定义了一系列属性。因为options没有传入deep、user、lasy、sync
,所以this.deep
等的值都为false
,this.deps
和this.depIds
时用来保存收集器的,也就是该watcher
被哪些收集器所收集,而newDepIds
和newDeps
是为了解决重复依赖的问题的。将我们传入的expOrFn(这里是updateComponent函数)赋值给gettter(这里很重要)。最后调用get函数。
下面我们就看看get函数做了什么
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 {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
该函数首先调用了pushTarget函数,最后调用了popTarget,这两个函数就定义在core/observer/dep.js下
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
这里就很关键了,就是在这里Vue将Dep.target
设为当前的Watcher
,因为Vue中会存在多个Watcher(computed属性会对应一个Watcher
,我们自定义的watch也会对应Watcher
),所以在pushTarget函数中会先将和原来的Dep.target
放入targetStack
,在popTarget
函数再将Dep.Target
设为原来的Dep.Target
。
之后get函数就执行
value = this.getter.call(vm, vm)
就是刚刚定义updateComponent
函数,updateComponent
调用vm._render
函数,在vm._render
函数中,我们获取data中的数据,触发其getter
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
不为空,所以就进入dep.depend
,然后就可以收集依赖了
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
depend函数很简单,调用了Watcher的addDep函数
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)
}
}
}
addDep函数首先获取dep的id,然后判断依赖是否被收集了,如果没有这里的判断,下面的情况会出现重复收集依赖。
render: function(createElement) {
let a1 = this.a
let a2 = this.a
}
然后就将当前Watcher,加入到dep的依赖收集器中
addSub (sub: Watcher) {
this.subs.push(sub)
}
我们回到Watcher的get函数中,在执行了popState函数后,然后执行this.cleanupDeps()
函数, 我们来看看这个函数的作用
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
函数一开始会将dep中不依赖当前Watcher从sub中剔除, 然后就是简单的交换。将deps和depIds设置为最新的newDeps和newDepIds
依赖收集就到这里了,依赖收集了解之后,依赖更新就很容易理解了
我们使用this.a = Xxx后,会触发setter,然后触发依赖进行更新,触发依赖调用了Dep
的notify
,我们看看这个函数做了什么,函数定义在core/observer/dep.js下
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()
}
}
notify的功能很简单,就是遍历所有的watcher,然后调用它们的update函数进行更新进行更新
至此,Vue响应式的原理就看完了,Vue源码博大精深,还有更广阔的天地等着大家探索,少年们加油吧