vue作为前端使用广泛的三大框架(react、vue、Angular)之一,vue2.x的双向数据绑定是基于Object.defineProperty实现。
vue2.x双向数据绑定解析
vue2.x是利用Object.defineProperty劫持对象或对象的属性的访问器,在属性值发生变化时获取属性值变化, 从而进行后续操作。
1、Object.defineProperty在js中的描述:
Object.defineProperty(obj, prop, descriptor) 直接在一个对象上定义一个属性,或者修改一个对象的现有 属性,并返回这个对象。
参数:obj 要在其上定义属性的对象;prop 要定义或修改的属性的名称;descriptor 将被定义或修改的属性描述符。
返回值: 传递给函数的对象obj
// 定义一个对象 const data={name:'peak',age:10} // 遍历对象 实现对对象的属性进行劫持 Object.keys(data).forEach((key) => { Object.defineProperty(data, key, { // 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中 enumerable: true, // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除 configurable: true, get: ()=>{ // 一个给属性提供 getter 的方法 console.info(`get ${key}-${val}`) return val; }, set: (newVal)=>{ // 一个给属性提供 setter 的方法 // 当属性值发生变化时我们可以进行额外操作 如调用监听器 if(newVal === val ){ // 如果未发生变化 不做其他操作 return; } console.log(`触发视图更新函数${newVal}`); }, }); }); data.age=25 // 触发set方法
2、基于Object.defineProperty的数据劫持优势以及实现方式
Object.defineProperty的对象以及对象属性的劫持有以下优势:
(1)无需显示调用,如Vue2.x使用Object.defineProperty对象以及对象属性的劫持+发布订阅模式,只要数据发生变化直接通知变化 并驱动视图更新。
(2)可在set函数中精确得知变化数据而不用逐个遍历属性获取变化值,减少性能损耗。
实现思路:
(1)利用Object.defineProperty重新定义一遍目标对象,完成对目标对象的劫持,在属性值变化后即触发set方法 后通知订阅者,告诉该对象的某个属性值发生了变化。
(2)解析器Compile解析模板中的指令,收集指令所依赖的方法和数据,等待数据变化然后进行渲染。
(3)Watcher在收到属性值发生变化后,根据解析器Compile提供的指令进行视图渲染。
为更好的说明vue2.x的响应式原理,下面vue2.x的源码引用了Vue源码解读
监听数据变化
对data进行改造,所有属性设置set&get,用于在属性获取或者设置时,添加逻辑
// Dep用于订阅者的存储和收集,将在下面实现 import Dep from 'Dep' // Observer类用于给data属性添加set&get方法 export default class Observer{ constructor(value){ this.value = value this.walk(value) } walk(value){ Object.keys(value).forEach(key => this.convert(key, value[key])) } convert(key, val){ defineReactive(this.value, key, val) } } export function defineReactive(obj, key, val){ var dep = new Dep() // 给当前属性的值添加监听 var chlidOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=> { console.log('get value') // 如果Dep类存在target属性,将其添加到dep实例的subs数组中 // target指向一个Watcher实例,每个Watcher都是一个订阅者 // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法 // 此处的问题是:并不是每次Dep.target有值时都需要添加到订阅者管理员中去管理,需要对订阅者去重,不影响整体思路,不去管它 if(Dep.target){ dep.addSub(Dep.target) } return val }, set: (newVal) => { console.log('new value seted') if(val === newVal) return val = newVal // 对新值进行监听 chlidOb = observe(newVal) // 通知所有订阅者,数值被改变了 dep.notify() } }) } export function observe(value){ // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听 if(!value || typeof value !== 'object'){ return } return new Observer(value) }
管理订阅者
对订阅者进行收集、存储和通知
export default class Dep{ constructor(){ this.subs = [] } addSub(sub){ this.subs.push(sub) } notify(){ // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理 this.subs.forEach((sub) => sub.update()) } }
订阅者
每个订阅者都是对某条数据的订阅,订阅者维护着每一次更新之前的数据,将其和更新之后的数据进行对比,如果发生了变化,则执行相应的业务逻辑,并更新订阅者中维护的数据的值
import Dep from 'Dep' export default class Watcher{ constructor(vm, expOrFn, cb){ this.vm = vm // 被订阅的数据一定来自于当前Vue实例 this.cb = cb // 当数据更新时想要做的事情 this.expOrFn = expOrFn // 被订阅的数据 this.val = this.get() // 维护更新之前的数据 } // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用 update(){ this.run() } run(){ const val = this.get() if(val !== this.val){ this.val = val; this.cb.call(this.vm) } } get(){ // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者 Dep.target = this const val = this.vm._data[this.expOrFn] // 置空,用于下一个Watcher使用 Dep.target = null return val; } }
Vue
将数据代理到Vue实例上,真实数据存储于实例的_data属性中
import Observer, {observe} from 'Observer' import Watcher from 'Watcher' export default class Vue{ constructor(options = {}){ // 简化了$options的处理 this.$options = options // 简化了对data的处理 let data = this._data = this.$options.data // 将所有data最外层属性代理到Vue实例上 Object.keys(data).forEach(key => this._proxy(key)) // 监听数据 observe(data) } // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者 $watch(expOrFn, cb){ new Watcher(this, expOrFn, cb) } _proxy(key){ Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: (val) => { this._data[key] = val } }) } }
调用这个极简版演示数据双向绑定原理的Vue
import Vue from './Vue'; let demo = new Vue({ data: { 'a': { 'ab': { 'c': 'C' } }, 'b': { 'bb': 'BB' }, 'c': 'C' } }); demo.$watch('c', () => console.log('c is changed')) // get value demo.c = 'CCC' // new value seted // get value // c is changed demo.c = 'DDD' // new value seted // get value // c is changed demo.a // get value demo.a.ab = { 'd': 'D' } // get value // get value // new value seted console.log(demo.a.ab) // get value // get value // {get d: (), set d: ()} demo.a.ab.d = 'DD' // get value // get value // new value seted console.log(demo.a.ab); // get value // get value // {get d: (), set d: ()}
总结:
在一些技术博客上,有人指出Object.defineProperty存在缺陷,只能监听到非数组对象的变化,而监听不到数组的变化,实际上这是错误的理解,Object.defineProperty是可以监听数组变化的,只是从性能/体验的性价比考虑,放弃了这个特性,vue设置
7个变异数组(push
、pop
、shift
、unshift
、splice
、sort
、reverse
)改用hack的方式解决数组变化的问题。
参考资料