vue是一个轻量级、数据驱动的渐进式框架,其核心就是数据驱动。一直以来,对vue响应式原理的理解还是停留在利用Object.defineProperty中的get和set进行数据劫持,至于内部是如何运转的,并没有一个全面的认知。本文就简要概述下,vue响应式原理具体的实现。
就一般而言,props、data、computed、watch四个模块构建成vue数据处理系统的骨架,绝大部分的数据声明、转换、传递,都是通过这几个模块进行。盗用vue.js官网的一张图,vue的数据流如下图所示:
在说数据流之前,我们先明确一点,Object.defineProperty(obj, prop, descriptor)主要作用是声明对象的属性,obj是对象名称,prop是对象的键值,descriptor是属性的数据描述符和存取描述符,其中属性的存取描述符就是上图中的getter/setter,getter触发条件是取,setter的触发条件是存。如下述代码,
let o ={} let bValue; Object.defineProperty(o, "b", { get : function(){ console.log('执行了取操作') return bValue; }, set : function(newValue){ console.log('执行了存操作') bValue = newValue; }, enumerable : true, configurable : true }); o.b = 1; // 存 console.log(o.b) // 取 console.log(bValue) // o.b === bValue
在vue中,声明响应式属性用的方法和上述大致类似,只是中间加了许多业务处理逻辑。先简要概述下vue数据流,主要分为以下两步:
1、通过getter收集依赖
什么是依赖?依赖可以理解为一个观察者对象,里面包含数据必须的属性(比如属性作用域对象)、方法(比如watch的回调)、字符串表达式(比如渲染函数)等,简单来说,观察者对象Watcher的主要作用就是数据与视图的粘合剂,通过发布-订阅模式,响应式的进行视图渲染。
为什么在getter中能够收集到依赖信息呢?首先,视图要想渲染数据,肯定要有对数据的取操作,取值时,会触发对数据的取操作,此时,可以将依赖的数据信息添加到属性的订阅者列表中,这样后续数据如果有变化,就可以从数据的订阅者列表中,依次执行对应的处理逻辑。
2、通过setter派发更新
setter是承接getter的作用的。在第一次数据渲染时,会通过getter,收集到属性的所有订阅者信息,在给属性赋值时,会触发属性的setter方法,在setter里,会遍历订阅者列表,根据订阅者的表达式,回调,处理对应的逻辑方法,实现数据的响应式更新。setter在响应式系统中,更多的可能是承接一个启后的角色。
在vue.js中,进行响应式数据声明的代码如下所示:
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 } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set // 处理obj的值 if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果val值存在Object,则需要侦听val值的变化 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 每次取值时,如果存在依赖的target对象,将watcher对象添加到订阅列表中,当数据发生改变时,通知watcher进行更新 if (Dep.target) { // 将watcher加入到订阅列表 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 派发更新 // 获取到value数据 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() } }) }
上述代码量比较多,以defineReactive方法响应式声明属性为中心,往前往后,分析下整个依赖收集与派发更新的过程。在开始之前,可以参考【vue实例化过程】,来熟悉下vue实例化的大致流程。
1、依赖收集
依赖收集中,需要重点关注的是dep.depend方法,在说dep.depend之前,需要关注上面有个判断条件,Dep.target,只有存在Dep.target时,才能将观察者对象添加到订阅列表中。以实例化Vue为例,在实例化组件时,会实例化一个watcher,watcher中包含组件挂载的相关信息。其中比较重要的一环就是会调用Watcher的get方法,get方法具体执行逻辑如下:
get () { // 设置当前处理的Watcher 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 }
其中,this.getter是指Watcher的一个表达式,在组件挂载中是指方法mountComponent,此方法主要作用是根据render函数生成vnode,根据vnode转换成真实的DOM节点;在watch中,this.getter是指根据属性名获取属性值的一个方法。在代码的第一句,有个pushTarget操作,这一步就是为后面get的依赖收集服务的,是将当前的Watcher实例,挂载到Dep上,表明当前正在处理的Watcher,在this.getter中,往往会伴随着对数据的取操作。在getter调用完毕后,会有一个popTarget,执行完当前的Watcher后,会清除对当前Watcher的依赖,pushTarget/popTarget的实现过程如下所示,在一个时间点,只能有一个被执行的Watcher:
// The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null const targetStack = [] // 进栈 export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } // 出栈 export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
2、派发更新
在我们对响应式数据更改时,会执行属性声明时的set方法,在set方法里,有一个dep.notify操作,主要就是遍历执行属性的订阅者列表Watcher的update方法,dep.notify执行的方法如下:
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++) { // 执行订阅对象批量更新???@todo // 每个subs都有update方法 subs[i].update() } }
更新执行操作比较多,但是需要注意的点是,视图和响应式回调更新的时机,当我们改变数据时,直接获取DOM中绑定的数据,可能还是原先的数据,视图并没有随之更新。如果想要在数据渲染完成后再进行回调处理,通常使用this.$nextTick。在派发更新时,会调用nextTick方法,传入更新数据的回调,nextTick在一般情况下是一个微任务,在支持promise的浏览器中是在promise中处理回调。我们都知道,在浏览器中,代码一般的执行顺序是 同步任务>微任务>宏任务。微任务常用的就是promise.then,宏任务常用的setTimeout。所以,捕获视图更新的回调,一般在setTimeout里也是可行的。微任务、宏任务与同步任务的处理流程可以参考下面代码:
console.log('开始执行测试代码') let p = Promise.resolve() p.then(function () { console.log('执行了微任务') }) setTimeout(function () { console.log('执行了宏任务') }) console.log('执行结束')
代码的执行顺序如下图所示:
派发更新操作,到最后都会执行到Watcher的run方法,最终都会执行到Watcher的getter,继续走页面的渲染流程。其实vue响应式原理核心就是利用Object.defineProperty进行数据劫持,对引用的数据进行依赖收集,数据改变时在进行派发更新消息。Watcher中存储的是数据依赖更新的消息。
附Watcher.js中代码:
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { debugger this.vm = vm // 渲染组件的watcher,通常computed 和 watch 也会实例化watcher if (isRenderWatcher) { vm._watcher = this } // 组件上的watcher队列 vm._watchers.push(this) // options 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() // updateComponents, watch, computed callback function this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter // expression是function的情况:computed,mountComponent if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // watch走这个分支,watch传入的expression是字符串 // 将字符串按照.进行字符串分割解析,exp: 'a.b.c' 获取到具体对象的值 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. * 执行更新组件的方法,先将当前要执行的watcher推入到执行队列 */ get () { // 设置当前处理的Watcher 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 } /** * Add a dependency to this directive. * 添加依赖 */ 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) } } } /** * Clean up for dependency collection. */ 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 } /** * Subscriber interface. * Will be called when a dependency changes. * 依赖改变,会触发watcher的update方法 */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value 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) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. * 移除事件订阅 */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }