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 : '' }
响应流程图