前言
Vue最独特的特性之一,是非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。当你修改它们的时候,视图也会进行更新。——官方文档
当你在使用Vue.js开发的时候,你是否有想过,为什么我们一定要把数据定义在data选项里?Vue.js是怎么实现响应式数据?为什么当我们改变data里的数据,视图就会自动的重新渲染,让我们看到改变后的数据。相信看完这篇文章你会对Vue的响应式原理会有个深刻的理解。
响应式原理
翻开Vue的官网,我们会找到官方给出的原理解释,让我们来一起再看一遍。
流程如下:
当一个JavaScript对象作为Vue的data项被传入的时候,Vue会遍历该对象。并使用Object.defineProperty把每个属性转为getter/setter。就是把data里的数据转为图上紫色的部分。
Vue使用getter/setter,在内部追踪收集的依赖。在属性被访问或修改的时候,发出变更通知。
每个组件中都有一个watcher,在组件渲染的时候,watcher会把每个touch过的数据属性记录依赖,也就是会触发getter然后收集依赖(Collect as Dependency)。在属性被setter的时候,由依赖dep通知watcher,重新渲染关联的组件。
以上是响应式原理的主要过程,可能会暂时不理解。但是没关系,我们会接着去实现一个响应式系统,去感受它的原理。
实现一个简易的Vue
首先,定义一个Vue类。我们在使用Vue的时候,都会先将需要侦测的数据放到data选项里。所以我们传入一个options,获取里面的data数据。
class Vue {
constructor(options) {
this._data = options.data;
}
}
接着就是要将这些数据转为响应式的,也就是getter/settter。如何转换呢?上面官方文档告诉我们使用的是Object.defineProperty。由于ES6在浏览器支持度不是很理想,Vue一直使用的是ES5的API。但是在Vue3.0中将会使用ES6的Proxy。
在这里我们暂时不考虑数组的情况。因为数组的响应式的实现方式和对象的方式不同。
class Vue {
constructor(options) {
this._data = options.data;
// 新增
new Observer(this._data);
}
}
class Observer {
constructor(value) {
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
以上代码中,Vue类里新增了一段,new 了一个Observer实例。接着将data传入这个实例,它的作用就是为了遍历data里的所有属性,并且将属性转为getter/setter。转换方法将会写在defineReactive里。
function defineReactive(target, key, val) {
// 递归,子属性
if (typeof value === 'object') {
new Observe(val);
}
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
set: (value) => {
if (val === value) {
return;
}
val = value;
},
get: () => {
return val;
},
});
}
在上面代码中,我们先判断val是否是object类型,因为data里的属性的子属性可能是对象。然后使用Object.defineProperty劫持数据,这样我们就可以在数据被访问和修改的时候监听到这些操作。
注意:Object.defineProperty不能侦测到属性的添加和删除。Vue提供给我们两个方法Vue.$set和Vue.$delete来解决。
当然光做到监听操作还是不行的,我们还需要知道,这个数据被哪些组件或模板使用了。所以我们要收集到这些依赖,并在数据发生改变的时候通知这些依赖。这个过程是通过Dep来管理的。它的作用就是将wathcer添加到Dep中,在需要的时候调用watcher中的update方法更新视图。所以需要"添加"和"发送通知"的方法,下面来实现。
class Dep {
constructor() {
this.subs = [];
}
depend() {
// window.target用来存储当前的Watcher对象的实例
this.subs.push(window.target);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
接着我们在defineReactvie中把依赖收集到dep中。并且在setter的时候调用dep.notify()通知变更。
function defineReactive(target, key, val) {
// 新增
const dep = new Dep();
// 递归,子属性
if (typeof value === 'object') {
new Observe(val);
}
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
set: (value) => {
if (val === value) {
return;
}
val = value;
// 新增
dep.notify();
},
get: () => {
// 新增
dep.depend();
return val;
},
});
}
以上新增了3处,分别是new了一个Dep,来存储依赖。第二处在set的时候通知视图更新,第三处把当前window.target添加到依赖中。这个window.target到底是啥呢?其实它就是一个watcher。
wathcer的作用是响应数据的变化,然后提供更新视图的方法。
class Watcher {
constructor() {
// 存储当前Watcher对象的实例
window.target = this;
}
update() {
console.log('视图更新了');
}
}
再来完善我们的Vue类
class Vue {
constructor(options) {
this._data = options;
new Watcher();
new Observer(this._data);
}
}
以上代码在Vue类中实例化了一个Watcher方法,在实例化的时候window.traget就是当前的Watcher实例,在defineReactive方法里,会把当前的Watcher实例添加到Dep中。这样就完成了依赖收集。
最后来验证一下我们的简易Vue能否跑起来
const vm = new Vue({
num: 10,
});
vm._data.num; // 先调用一下get,完成依赖收集
vm._data.num = 20; // 被set监听到,Dep派发通知,通知watcher更新视图
// '视图更新了'
总结
到这里,一个简易的Vue类已经完成了,虽然代码很简单,但是这种思想我们要掌握。
我们再来回顾一下整个流程
首先我们定义了一个Vue类来接收options参数。然后在这个Vue类里通过实例化一个Observer去侦测data里的数据,。
在Observer类里接收data,并循环data的key,将每个属性传给defineReactive让它来实现getter/setter的转换。
在defineReactive中需要收集依赖,所以我们定义一个Dep类,然后将依赖存储在Dep类中。接着,我们通过Object.defineProperty劫持数据,然后在get操作的时候将依赖添加到Dep中,set操作的时候,通知Dep中的所有依赖更新视图。
所谓的依赖就是Watcher。Watcher的作用就是响应数据的变化然后更新视图。