vue数据绑定源码
思路分析
数据的双向绑定,就是数据变化了自动更新视图,视图变化了自动更新数据,实际上视图变化更新数据只要通过事件监听就可以实现了,并不是数据双向绑定的关键点。关键还是数据变化了驱动视图自动更新。
所有接下来,我们详细了解下数据如何驱动视图更新的。
数据驱动视图更新的重点就是,如何知道数据更新了,或者说数据更新了要如何主动的告诉我们。可能大家都听过,vue的数据双向绑定原理是Object.defineProperty( )对属性设置一个set/get,是这样的没错,其实get/set只是可以做到对数据的读取进行劫持,就可以让我们知道数据更新了。但是你详细的了解整个过程吗?
先来看张大家都不陌生的图:
- Observe 类劫持监听所有属性,主要给响应式对象的属性添加 getter/setter 用于依赖收集与派发更新
- Dep 类用于收集当前响应式对象的依赖关系
- Watcher 类是观察者,实例分为渲染 watcher、计算属性 watcher、侦听器 watcher三种
介绍数据驱动更新之前,先介绍下面4个类和方法,然后从数据的入口initState开始按顺序介绍,以下类和方法是如何协作,达到数据驱动更新的。
defineReactive
这个方法,用处可就大了。
我们看到他是给对象的键值添加get/set
方法,也就是对属性的取值和赋值都加了拦截,同时用闭包给每个属性都保存了一个Dep
对象。
当读取该值的时候,就把当前这个watcher
(Dep.target
)添加进他的dep里的观察者列表,这个watcher
也会把这个dep
添加进他的依赖列表。
当给设置值的时候,就让这个闭包保存的dep
去通知他的观察者列表的每一个watcher
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
if (!getter && arguments.length === 2) {
val = obj[key]
}
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
Observer
什么是可观察者对象呢?
简单来说:就是数据变更时可以通知所有观察他的观察者。
1、取值的时候,能把要取值的watcher(观察者对象)加入它的dep(依赖,也可叫观察者管理器)管理的subs列表里(即观察者列表);
2、设置值的时候,有了变化,所有依赖于它的对象(即它的dep里收集到的观察者watcher)都得到通知。
这个类功能就是把数据转化成可观察对象。针对Object类型就调用defineReactive方法循环把每一个键值都转化。针对Array,首先是对Array经过特殊处理,使它可以监控到数组发生了变化,然后对数组的每一项递归调用Observer进行转化。
对于Array是如何处理的呢?这个放在下面单独说。
export class Observer {
/**
*如果是对象就循环把对象的每一个键值都转化成可观察者对象
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 如果是数组就对数组的每一项做转化
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Dep
这个类功能简单来说就是管理数据的观察者的。当有观察者读取数据时,保存观察者到subs,以便当数据变化了的时候,可以通知所有的观察者去update,也可以删除subs里的某个观察者。
export default class Dep {
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 这个方法非常绕,Dep.target就是一个Watcher对象,Watcher把这个依赖加进他的依赖列表里,然后调用dep.addSub再把这个Watcher加入到他的观察者列表里。
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省去了初始化各种属性和option
this.dirty = this.lazy // for lazy watchers
// 解析expOrFn,赋值给this.getter
// expOrFn也要明白他是什么?
// 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render
// 当是计算watcher时,expOrFn是计算属性的计算方法
// 当是侦听器watcher时,expOrFn是watch属性的取值表达式,可以去读取要watch的数据,this.cb就是watch的handler属性
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 执行this.getter,同时重新进行依赖收集
*/
get () {
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
if (this.deep) {
// 对于deep的watch属性,处理的很巧妙,traverse就是去递归读取value的值,
// 就会调用他们的get方法,进行了依赖收集
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
/**
* 不重复的把当前watcher添加进依赖的观察者列表里
*/
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)
}
}
}
/**
* 清理依赖列表:当前的依赖列表和新的依赖列表比对,存在于this.deps里面,
* 却不存在于this.newDeps里面,说明这个watcher已经不再观察这个依赖了,所以
* 要让个依赖从他的观察者列表里删除自己,以免造成不必要的watcher更新。然后
* 把this.newDeps的值赋给this.deps,再把this.newDeps清空
*/
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
}
/**
* 当一个依赖改变的时候,通知它update
*/
update () {
if (this.lazy) {
// 对于计算watcher时,不需要立即执行计算方法,只要设置dirty,意味着
// 数据不是最新的了,使用时需要重新计算
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 调度watcher执行计算。
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
this.cb.call(this.vm, value, oldValue)
}
}
}
/**
* 对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
* 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* 把这个watcher所观察的所有依赖都传给Dep.target,即给Dep.target收集
* 这些依赖。
* 举个例子:具体可以看state.js里的createComputedGetter这个方法
* 当render里依赖了计算属性a,当渲染watcher在执行render时就会去
* 读取a,而a会去重新计算,计算完了渲染watcher出栈,赋值给Dep.target
* 然后执行watcher.depend,就是把这个计算watcher的所有依赖也加入给渲染watcher
* 这样,即使data.b没有被直接用在render上,也通过计算属性a被间接的是用了
* 当data.b发生改变时,也就可以触发渲染更新了
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
综上所述,就是vue数据驱动更新的方法了,下面是对整个过程的简单概述:
每个vue实例组件都有相应的watcher对象,这个watcher是负责更新渲染的。他会在组件渲染过程中,把属性记录为依赖,也就是说,她在渲染的时候就把所有渲染用到的prop和data都添加进watcher的依赖列表里,只有用到的才加入。同时把这个watcher加入进data的依赖的订阅者列表里。也就是watcher保存了它都依赖了谁,data的依赖里保存了都谁订阅了它。这样data在改变时,就可以通知他的所有观察者进行更新了。渲染的watcher触发的更新就是重新渲染,后续的事情就是render生成虚拟DOM树,进行diff比对,将不同反应到真实的DOM中。
queueWatcher
下面是Watcher的update方法,可以看的除了是计算属性和标记了是同步的情况以外,全部都是推入观察者队列中,下一个tick时调用。也就是数据变化不是立即就去更新的,而是异步批量去更新的。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
下面来看看queueWatcher方法
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)
}
}
}
这里使用了一个 has 的哈希map用来检查是否当前watcher的id是否存在,若已存在则跳过,不存在则就push到queue,队列中并标记哈希表has,用于下次检验,防止重复添加。因为执行更新队列时,是每个watcher都被执行run,如果是相同的watcher没必要重复执行,这样就算同步修改了一百次视图中用到的data,异步更新计算的时候也只会更新最后一次修改。
nextTick(flushSchedulerQueue)
把回调方法flushSchedulerQueue传递给nextTick,一次异步更新,只要传递一次异步回调函数就可以了,在这个异步回调里统一批量的处理queue中的watcher,进行更新。
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()
}
resetSchedulerState()
}
每次执行异步回调更新,就是循环执行队列里的watcher.run方法。
在循环队列之前对队列进行了一次排序:
- 组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
- 一个组件的user watchers(侦听器watcher)比render watcher先运行,因为user watchers往往比render watcher更早创建
- 如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过
nextTick
export function nextTick (cb?: Function, ctx?: Object) {
// 这个方法里,我把关于不写回调,使用promise的情况处理去掉了,把trycatch都去掉了。
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
setTimeout(flushCallbacks, 0) // 异步任务进行了简化
}
}
下面是异步的回调方法flushCallbacks,遍历执行callbacks里的方法,也就是遍历执行调用nextTick时传入的回调方法。
你可能就要问了,queueWatcher的时候不是控制了只会调用一次nextTick吗,为啥要用callbacks数组来存储呢。举个例子:
你写了一堆同步语句,改变了data等,然后又调用了一个this.$nextTick来做个异步回调,这个时候不就又会向callbacks数组里push了一个回调方法吗。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
如何把数组处理成可观察对象
不考虑兼容处理
本质就是改写数组的原型方法。当数组调用methodsToPatch这些方法时,就意味者数组发生了变化,需要通知所有观察者update。
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.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)
// notify change
ob.dep.notify()
return result
})
})
后记
关于从数据入口initState开始解析的部分,写在一篇里篇幅太大,我放在下一篇文章了,记得去读哦,可以加深理解。
参考文章