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开始解析的部分,写在一篇里篇幅太大,我放在下一篇文章了,记得去读哦,可以加深理解。
参考文章