Vue 采用声明式编程替代过去的类 Jquery 的命令式编程,并且能够侦测数据的变化,更新视图。这使得我们可以只关注数据本身,而不用手动处理数据到视图的渲染,避免了繁琐的 DOM 操作,提高了开发效率。不过理解其工作原理同样重要,这样可以回避一些常见的问题,下面我们来介绍一下 Vue 是如何侦测数据并响应视图的。
Object.defineProperty
Vue 数据响应核心就是使用了 Object.defineProperty
方法( IE9 + ) 。
var obj = {}; Object.defineProperty(obj, ‘msg‘, { get () { console.log(‘get‘); }, set (newVal) { console.log(‘set‘, newVal); } }); obj.msg // get obj.msg = ‘hello world‘ // set hello world
取 obj 对象中 msg 的值时会调用 get 方法,给 msg 赋值时会调用 set 方法,并接收新值作为其参数。
这里提一句,在 Vue 中我们调用数据是直接 this.xxx ,而数据其实是 this.data.xxx,原来 Vue 在初始化数据的时候会遍历 data 并代理这些数据。
Object.keys(this.data).forEach((key) => { this.proxyKeys(key); }); proxyKeys (key) { Object.defineProperty(this, key, { enumerable: false, configurable: true, get() { return this.data[key]; }, set(newVal) { this.data[key] = newVal; } }); }
上面可以看到,取 this.key 的值其实是取 this.data.key 的值,赋值同理。
现在,我们已经知道如何去检测数据的变化,并且做出一些响应了。
观察者模式 ( 发布者-订阅者模式 )
vue 的响应式系统依赖于三个重要的类:Dep 类、Watcher 类、Observer 类。
Dep 类作为发布者的角色,Watcher 类作为订阅者的角色,Observer 类则是连接发布者和订阅者的纽带,决定订阅和发布的时机。
我们先看下面的代码,来对发布者和订阅者有个初步的了解。
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach(watcher => { watcher.update(); }); } } class Watcher { constructor() { } update() { // 接收通知后的处理方法 } } const dep = new Dep(); // 发布者 dep const watcher1 = new Watcher(); // 订阅者1 watcher1 const watcher2 = new Watcher(); // 订阅者2 watcher2 dep.addSub(watcher1); // watcher1 订阅 dep dep.addSub(watcher2); // watcher2 订阅 dep dep.notify(); // dep 发送通知
上面我们定义了一个发布者 dep,两个订阅者 watcher1、watcher2。让 watcher1、watcher2 都订阅 dep,当 dep 发送通知时,watcher1、watcher2 都能做出各自的响应。
现在我们已经了解了发布者和订阅者的关系,那么剩下的就是订阅和发布的时机。什么时候订阅?什么时候发布?想到上面提到的 Object.defineProperty ,想必你已经有了答案。
我们来看 Observer 类的实现:
class Observer { constructor(data) { this.data = data; this.walk(); } walk() { Object.keys(this.data).forEach(key => { this.defineReactive(this.data, key, this.data[key]); }); } defineReactive(data, key, value) { const dep = new Dep(); if ( value && typeof value === ‘object‘ ) { new Observer(value); } Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { if (Dep.target) { dep.addSub(Dep.target); // 订阅者订阅 Dep.target 即当前 Watcher 类的实例(订阅者) } return value; }, set(newVal) { if (newVal === value) { return false; } value = newVal; dep.notify(); // 发布者发送通知 } }); } }
在 Observer 类中,为 data 的每个属性都实例化一个 Dep 类,即发布者。并且在取值时让订阅者(有多个,因为 data 中的每个属性都可以被应用在多个地方)订阅,在赋值时发布者发布通知,让订阅者做出各自的响应。
这里需要提的是 Dep.target,这其实是 Watcher 类的实例,我们可以看看 Watcher 的详细代码:
class Watcher { constructor(vm, exp, cb) { this.vm = vm; this.exp = exp; // data 属性名 this.cb = cb; // 回调函数 // 将自己添加到订阅器 this.value = this.getValue(); } update() { const value = this.vm.data[this.exp]; const oldValue = this.value; if (value !== oldValue) { this.value = value; this.cb.call(this.vm, value, oldValue); // 执行回调函数 } } getValue() { Dep.target = this; // 将自己赋值给 Dep.target const value = this.vm.data[this.exp]; // 取值操作触发订阅者订阅 Dep.target = null; return value; } }
Watcher 类在构造函数中执行了一个 getValue 方法,将自己赋值给 Dep.target ,并且执行了取值操作,这样就成功的完成了订阅操作。一旦数据发生变化,即有了赋值操作,发布者就会发送通知,订阅者就会执行自己的 update 方法来响应这次数据变化。
数据的双向绑定
数据的双向绑定即数据和视图之间的同步,视图随着数据变化而变化,反之亦然。我们知道 Vue 是支持数据的双向绑定的,主要应用于表单,是通过 v-model 指令来实现的。而通过上面介绍的知识我们是可以知道如何实现视图随着数据变化的,那么如何让数据也随着视图变化而变化呢?其实也很简单,只要给有 v-model 指令的节点监听相应的事件即可,在事件回调中来改变相应的数据。这一切都 Compile 类中完成,假设有一个 input 标签应用了 v-model 指令,在开始编译模板时,遇到 v-model 指令时会执行:更新 dom 节点的值,订阅者订阅,事件监听。
compileModel (node, vm, exp) { let val = vm[exp]; // 更新内容 this.modelUpdater(node, val); // 添加订阅 new Watcher(vm, exp, (value) => { // 数据改变时的回调函数 this.modelUpdater(node, value); }); // 事件监听 node.addEventListener(‘input‘, (e) => { const newValue = e.target.value; if (val === newValue) { return false; } vm[exp] = newValue; val = newValue; }); }
当我们在文本框中输入数据时,会给原有 data 中的某个属性 a 赋值,这时候会触发发布者发起通知,那么所有属性 a 的订阅者都能够同步到最新的数据。
最后,附上一个小 demo