数据的双向绑定(数据观测)
上一章我们在学习initState
的时候发现初始化的时候,initProps
和initData
都涉及到observer
文件夹中的一些方法(defineReactive
和observe
),以及initComputed
和initWatch
也涉及到Watcher
对象和$watch
方法等等,这些都有是和数据的双向绑定(也叫数据观察)有关,这一章我们就来学习一下vue关于数据双向绑定的代码。核心代码主要是在src/core/observer
文件夹中。
一些概念
vue是由render
函数来生成虚拟dom,再映射到页面上。Observer
将遍历此对象所有的属性,并使用Object.defineProperty
把这些属性全部转为 getter/setter
,通过Dep
达到追踪依赖的效果。当我们访问属性时会添加依赖,当我们修改属性时将会通知Watcher
重新计算,从而致使它关联的组件得以更新。
-
Observer: 观察者,对实例对象的所有属性进行监听
- 思维路径
walk() => defineReactive() => Object.defineProperty() => Dep...
- 思维路径
-
Watcher: 当数据变化时执行回调函数cb,还有一些与dep操作相关的函数
- 思维路径
update() => sync ? (run() => cb.call()) : queueWatcher()
- 思维路径
Dep: dependence依赖的缩写,依赖收集器,Observer和Watcher之间的桥梁,存放watch数组,绑定数据getter时添加依赖,更改数据setter时通知watcher更新
observe
还记得initData
是通过observe()
作为入口进行数据绑定的吗,我们就从这里开始看起吧~先看一下这个函数:
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 (
observerState.shouldConvert && // 为真时,改变后的新值也会进行双向绑定
!isServerRendering() && // 非服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 是一个数组
Object.isExtensible(value) && // 或者是一个单纯的可拓展的对象
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++ // 以此对象作为根节点$data的vue实例数量加一
}
return ob
}
第一个参数是一个data对象,假如它拥有__ob__
属性的话证明该对象已经进行绑定了一个observer,就不用再次创建了;假如没有__ob__
属性,而且满足以上条件(注释中有标明)的时候,我们将为它创建一个Observer对象。ob = new Observer(value)
,并且执行vmCount++
。我们转到Observer
看看:
Observer
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 定义__ob__属性
def(value, '__ob__', this)
// 若是数组对象则另作处理(关于数组原型方法的数据监听)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 其他途径走walk()
this.walk(value)
}
}
...
}
每一个Observer对应一个Dep依赖,并且为该数据对象添加__ob__
属性,值为这个Observer,之前在observe函数中可以看到假如已经存在__ob__
属性就不需要再次创建Observer对象了。
接下来要判断传入的值是否是一个数组,假如value不是一个数组而是一个普通对象,那么执行walk()
函数。
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
walk()
中遍历对象对每个属性执行defineReactive
,我们又看看defineReactive
defineReactive (划重点)
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
}
const getter = property && property.get
const setter = property && property.set
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 (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
该方法先是为每个属性创建一个Dep,这样做的原因是每个属性对应自己的依赖集,当有属性发生变化时只触发相应属性的更新而不用更新全部属性,导致资源的浪费。
vue允许用户自己定义getter和setter,假如用户自己设置了的话,vue就使用用户所定义的getter和setter。
现在重头戏来了,vue的数据双向绑定就是通过Object.defineProperty
这个原生的方法实现的,Object.defineProperty
是 ES5.1 规范中提供的方法,用来修改对象属性的属性描述对象,通过 Object.defineProperty
函数,我们可以通过定义对象属性的存取器(getter/setter)来劫持数据的读取,实现数据变动时的通知功能。
回到源码中来,通过Object.defineProperty
我们重写了对象的属性描述符,在getter中添加dep.depend()
,让数据和watcher建立关系,并且对子对象的observer也进行依赖绑定childOb.dep.depend()
,如果数组元素也是对象,那么它们的observe过程也生成了__ob__
实例,那么就让__ob__
也收集依赖。
在setter中若新老值有变化的话就添加dep.notify
来通知watcher更新数据,并监听新的值childOb = !shallow && observe(newVal)
,customSetter
在开发过程中用于输出错误。
注意,defineReactive
中的dep
和Observer
构造函数的dep
是不同的,原因是getter和setter只能监听到属性的更改,不能监听到属性的删除与添加,所以Observer
构造函数的dep
会在vue的set
和delete
操作中会利用上。
看回Observer
构造函数中的第二种情况,假如value是一个数组对象,会做另外的处理。由于传进来的数组对象只是一个引用地址,对于数组一些原有的操作(push、splice、sort等)是无法进行数据监听的,所以vue中有两种操作:1,在环境支持__proto__
时,执行protoAugment,改写value的__proto__
;2,不支持__proto__
时,执行copyAugment,直接在value上重新定义这些方法。
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
重点是在于来自同级文件array.js的arrayMethods
,它是一个改写了数组方法的对象
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 添加数据更新
ob.dep.notify()
return result
})
})
特别对push、unshift、splice参数大于2时的三种情况进行调用ob.observeArray
,因为他们都添加了新元素需要进行监听,然后给所有的原型方法都添加了数据更新ob.dep.notify()
。然后对原型方法进行重新定义后,遍历这个数组为每个元素执行observe
,监听新插入的值。
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
Dep
前面提及了好几次dep中的方法,而且我们也提到了dep的基本概念,就是联系Observer和Watcher的依赖收集器,我们现在看一下Dep对象
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
dep中有个静态属性target
指向目标Watcher,id
作为Dep的唯一标识,subs
是用于存放watcher的数组。addSub
向数组添加watcher,removeSub
从数组中删除watcher,depend
是在getter中为目标Watcher添加依赖,调用了watcher的addDep(稍后分析),notify
在setter时触发watcher的更新。
Watcher
let uid = 0
export default class Watcher {
constructor (
) {
this.vm = vm
vm._watchers.push(this)
this.id = ++uid
this.cb = cb
this.getter = expOrFn
this.value = this.get()
}
get ()
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
update () {
this.run()
}
run () {
const value = this.get()
this.cb.call(this.vm, value, oldValue)
}
}
emmm...我们直接看一下超级无敌简化版的watcher,走一下最常见的流程,例如最简单的:vm.$watch("mydata",()=>console.log("我是回调"))
。主要是看传来的监听表达式/函数expOrFn
(mydata)和响应回调函数cb
(()=>console.log("我是回调")),大概流程就是observer监听到属性值更改后(setter)调用dep.notify
通知watcher更新update
,update
中调用了函数run
,run
通过get
获取到监听的值expOrFn
,最后调用回调函数cb进行响应式更新(大概思路:update => run => get => cb)
现在我们再具体看看那几个函数
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
}
来个小插曲,什么是dep.target呢?它是Watcher实例,为什么要放在Dep.target里呢?是因为在getter中会执行dep.depend()
,执行Dep.target.addDep(this)
,而在addDep
中通过 dep.addSub(this)
又把Watcher实例添加到dep的观察集中。
// Dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// Watcher.js
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)
}
}
}
// Dep.js
addSub (sub: Watcher) {
this.subs.push(sub)
}
看回get()
函数,先是执行pushTarget(this)
,将这个watcher设为target,对传来的表达式expOrFn进行一定的运算得到value,这个过程触发target相关依赖的getter(例如表达式为‘a+b’的话就会触发a和b的getter),也就是说会把target加到各自的dep.subs中,也就是成功地绑定了所有相关的依赖,完成操作后再执行popTarget()
将target又恢复为null。每次进行数据监听都会设定当前watcher为新的target,完成更新后又会将target设为空值,这样数据之间就不会互相污染。
update && run
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 异步队列
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
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)
}
}
}
}
当属性值发生变化时触发setter,就会执行dep.notify
,然后调用了watcher的update
,在update
中有两种情况,同步的话就执行run
,异步的话执行queueWatcher
。后者我们稍后再讨论,先看run
,若新旧值不同的话就执行回调函数this.cb.call(this.vm, value, oldValue)
。只有在用户设定了sync:true
时才会直接执行数据更新,否则的话都会走另外一条路:异步更新队列。
sync && immediate
说到sync,再说说immediate,设置immediate:true
也能直接执行回调,但是和sync是不一样的。代码实现在src/core/instance/state.js中有提及:
if (options.immediate) {
cb.call(vm, watcher.value)
}
区别在于,当指定immediate:true
时,会立即以表达式的当前值触发回调函数,不会等到数据发生变化时才进行回调;而指定sync:true
时是数据发生变化时不将watcher放进异步队列中而是立即执行回调。
queueWatcher
简单来说异步更新队列就是开启一个队列,将一个事件循环内的全部数据变动缓冲在其中,并对watcher
做去重操作。在下一个tick
中我们再将队列中的watcher
依次取出并执行更新。这对避免不必要的计算和DOM操作非常重要。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
flushing
变量是正在刷新,当页面不在刷新时,把watcher
push到一个队列尾部,若正在刷新的话则把watcher放到队列相应的位置中(根据id);waiting
变量是正在等待,假如没有处于等待状态的话则等到下一个nexttick
中执行flushSchedulerQueue
。
flushSchedulerQueue
function flushSchedulerQueue () {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
此函数将队列中的watcher
根据它们的id从小到大排列依次执行它们的run
方法,完成后resetSchedulerState
重置flushing
,waiting
等状态,再
callUpdatedHooks(updatedQueue)
callUpdatedHooks
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')
}
}
}
vm._watcher保存的是渲染模板时创建的watcher,如果队列中有该watcher,则说明模板有变化,调用updated
生命钩子。
在vue官网中对异步更新队列的解释是
只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
set && del
前面提到了使用defineReactive
进行数据监听有个缺陷,就是当属性进行删除和新增时,getter和setter并不会触发,所以vue为了弥补这个缺陷,提供Vue.set和Vue.delete方法让我们设置和删除对象的属性。
export function set (target: Array | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// Observer构造函数中定义的dep
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== `production` && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
export function del (target: Array | Object, key: any) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
// Observer构造函数中定义的dep
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
还记得Observer
构造函数中定义了dep
吗,这是为每个数组和对象都创建另一个依赖收集器,挂在它们的__ob__
属性上。在依赖收集阶段,watcher
将被同时注册在这两个 dep
上,准备接收响应。setter
操作,通知 set/get
中的 dep
;非 setter
操作,如对象 key
添加删除、数组变异方法调用,通知__ob__
中的 dep
。
deep
当我们设置deep: true
时即进行深度监听。举个例子,有这么个对象{a:{b:{c:{d:1}}}}
,当我们访问a.b.c
的值时,依次触发了a.b
和a.b.c
的getter
,将 watcher
注册进对应的 dep
,所以当我们修改a.b.c
上游的值时a.b.c
会监测到发生改变;但是相反,当我们修改a.b.c
的值时a.b.c
上游是不会监测到改变的,因为setter
只会检测到引用变化,不会监测到对象内部的变化。所以这个时候我们可以设置deep: true
,在代码中的实现是traverse(value)
,当发现依赖目标为一个对象时,递归进去遍历每一个子属性,这就会主动触发了深度属性的getter
。实现深度监听的功能~
这就是vue中实现数据双向绑定的整个过程,如有描述不妥或不清晰之处请随时提出,谢谢~