Vue响应式原理 在 Vue 2.x 版本中,实现数据双向绑定的主要原理就是通过数据劫持的方式,即Object.defineProperty的getter和setter方法,配合发布-订阅模式,来监听到数据的赋值与变化,从而通知相关的视图进行更新。
那么,具体是怎么实现的呢?
Object.defineProperty首先,我们先了解一下最核心的Object.defineProperty的基本用法:
1 Object.defineProperty(obj, prop, descriptor)
obj:要定义属性的对象
prop:要定义或修改的属性的名称或Symbol
descriptor:要定义或修改的属性描述符,包含以下属性:
configurable:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false 。
enumerable:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false 。
value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为undefined 。
writable:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值。默认为false 。
get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为undefined 。
set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为undefined 。
注意点 : get和set不能和writable及value共存,否则浏览器会报错
1 2 3 Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> at Function.defineProperty (<anonymous>) at <anonymous>:1:8
▼简单示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let obj = {}let value = null Object .defineProperty(obj, 'property1' , { set : function (val) { console .log('set value' ) value = val }, get : function () { console .log('get value' ) return value } }) obj.property1 = 1 obj.property1
实例化一个Vue对象 以下面一个简单的Vue实例作为分析参考:
1 2 3 4 5 <div id ="app" > <div @click ="changeMsg" > {{ message }} </div > </div >
1 2 3 4 5 6 7 8 9 10 11 var app = new Vue({ el: '#app' , data: { message: 'Hello Vue!' }, methods: { changeMsg () { this .message = 'Hello World!' } } })
数据代理 在Vue中,我们定义在data的数据可以通过this.xxx来获取,主要原因在于Vue对data中的数据做了一层数据代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const sharedPropertyDefinition = { enumerable: true , configurable: true , get : noop, set : noop } function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter ( ) { return this [sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val ) { this [sourceKey][key] = val } Object .defineProperty(target, key, sharedPropertyDefinition) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function initData (vm ) { let data = vm.$options.data vm._data = data if (!isPlainObject(data)) { data = {} } const keys = Object .keys(data) let i = keys.length while (i--) { const key = keys[i] proxy(vm, `_data` , key) } observe(data, true ) }
代理的实现方式并不难,通过Object.defineProperty把target[sourceKey][key]的读写变成了对target[key]的读写。
为什么这样处理呢?
方便读取 :比起用this._data.xxx来读取,这样的方式更加直接方便
保证数据统一 :为了实现this.xxx的方式获取,当然也可以通过for循环把数据一个一个赋值到实例上(methods的处理方式),但是这样做就会导致维护了两份数据,增加了维护的成本
不影响依赖的收集与更新 :当对this.xxx进行读取的时候,就会触发_data.xxx中的get与set方法,不会影响到data中数据的依赖收集与更新
数据劫持 因为我们需要在data数据更新的时候,通知视图的更新,因此我们对data中的每一个数据都生成对应的响应式对象,给对象的每一个属性都加上getter与setter,在getter与setter中插入消息绑定与发布的动作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function observe (value, asRootData ) { if (!isObject(value)) { return } let ob if ( hasOwn(value, '__ob__' ) && value.__ob__ instanceof Observer ) { ob = value.__ob__ } else { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export class Observer { constructor (value) { this .value = value this .dep = new Dep() this .vmCount = 0 def(value, '__ob__' , this ) this .walk(value) } walk (obj) { const keys = Object .keys(obj) for (let i = 0 ; i < keys.length; i++) { defineReactive(obj, keys[i]) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export function defineReactive (obj, key, val ) { const dep = new Dep() const property = Object .getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false ) { return } if (arguments .length === 2 ) { val = obj[key] } let childOb = observe(val) Object .defineProperty(obj, key, { enumerable: true , configurable: true , get : function reactiveGetter () { const value = val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set : function reactiveSetter (newVal) { const value = val if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal childOb = observe(newVal) dep.notify() } }) }
通过defineReactive初始化Dep对象实例,接着拿到obj的属性描述符,然后对子对象进行递归调用ovserve方法,这样就保证了无论obj的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改obj中一个嵌套较深的属性,也能触发getter与setter。
依赖收集 在defineReactive中,我们实例化了一个Dep对象,Dep扮演的对象实际上就是发布-订阅模式中的订阅器,或者说是调度中心 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 let uid = 0 export default class Dep { constructor () { this .id = uid++ this .subs = [] } addSub (sub) { this .subs.push(sub) } removeSub (sub) { 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 = null const targetStack = []export function pushTarget (_target ) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget ( ) { Dep.target = targetStack.pop() }
在Dep类中,定义了一个静态属性target,是为了存储全局唯一的Watcher。因为我们在操作数据的时候,必然会涉及到数据的读取,但是我们只需要收集到需要Watcher的对象的依赖就好了,因此需要用Dep.target来判断是否要进行依赖收集,当我们运行Watcher.get()的时候,Dep.target才会被赋值。并且在同一段时间内,只能处理一个Watcher。
而Watcher所扮演的角色就是观察者,它的主要作用就是为我们需要观察的属性提供回调与收集依赖,当被观察的值发生变化时,就会受到来着dep的通知,从而触发回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 let uid = 0 export default class Watcher { constructor ( vm, expOrFn, cb ) { this .vm = vm this .cb = cb this .id = ++uid this .deps = [] this .depIds = new Set () this .expression = expOrFn.toString() if (typeof expOrFn === 'function' ) { this .getter = expOrFn } else { this .getter = parsePath(expOrFn) if (!this .getter) { this .getter = function ( ) {} } } this .value = this .get() } get () { pushTarget(this ) const vm = this .vm let value = this .getter.call(vm, vm) popTarget() return value } addDep (dep) { const id = dep.id if (!this .depIds.has(id)) { this .depIds.add(dep.id) this .deps.push(dep) dep.addSub(this ) } } update () { const value = this .get() if (value !== this .value || isObject(value)) { const oldValue = this .value this .value = value this .cb.call(this .vm, value, oldValue) } } }
当执行new Watcher生成一个新的观察者时,就会执行Watcher类中的get方法,从而触发数据接触中的getter方法,将自身加入到Dep的subs队列中,以此完成依赖收集。
派发更新 同理,当在代码中出现this.xxx = xxx的赋值行为时,就会触发数据劫持的setter方法,此时Dep会通知subs队列中的每一个观察者都执行自身的update,以此触发new Watcher时所绑定的callback函数。
模板编译 在Vue中,render的实现也是一大核心,这里先略过,用最简单的DOM操作来完成一些简单指令的实现。举例模板语法,通过正则匹配到message,然后生成对应的Watcher:
1 2 3 4 5 6 7 8 9 createWatcher: function (node, vm, exp, dir, ext ) { const updaterFn = updater[dir + 'Updater' ] updaterFn && updaterFn(node, this ._getVMVal(vm, exp), undefined , ext) new Watcher(vm, exp, (value, oldValue) => { updaterFn && updaterFn(node, value, oldValue, ext) }) }
1 2 3 4 [DIRECTIVE.TEXT + 'Updater' ]: function (node, value ) { node.textContent = isDef(value) ? value : '' }
响应流程图