使用Vue开发也快半年了,在使用过程中越发的觉得是真的好用,上手简单,看一遍官方文档就可以直接上手了。但是在做了两个项目之后,乘着项目的休息期,觉得自己需要提高一下对于Vue原理的一些了解,其中最感兴趣的就是响应式原理了,于是开始着手进行学习,并把自己学习所收获的和大家分享一下,也希望能和大家一起探讨研究下,共同学习,共同进步!
对象属性
在了解响应式原理之前,必不可少的是要理解对象的属性和setter、getter,这是响应式原理实现的基石。ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。ECMScript中将对象的属性分为了两类:数据属性和访问器属性。那么何为属性?可参考下图:
属性值可以由一个或两个方法替代,这两个方法就是getter和setter,由getter和setter定义的属性称之为“存取器属性”。存取器属性不具有value和writable特性,被getter和setter所取代。如下demo:
let test = {
//普通的数据属性
w: 0,
x: 1,
y: 2,
//存取器属性定义为一个或两个和属性同名的函数
get z() {
return this.x + this.y;
},
set z(newVal) {
const oldVal = this.x + this.y;
const ratio = newVal / oldVal;
this.x *= ratio;
this.y *= ratio;
}
};
console.log(test.z); // 3
test.z = 6;
console.log(test.x, test.y); // 2 4
test.y = 5;
console.log(test.x, test.z); // 2 7
//{value:0,configurable:true,enumerable:true,writable:true}
Object.getOwnPropertyDescriptor(test, "w");
//{get:/*func*/,set:/*func*/,configurable:true,enumerable:true,writable:true}
Object.getOwnPropertyDescriptor(test, "w");
//操作失败但是不会报错,在严格模式中则抛出类型异常错误
//Uncaught TypeError: Cannot assign to read only property 'w' of object '#<Object>'
Object.defineProperty(test, "w", { writable: false });
test.x = 3;
//属性依然是可以配置的,可通过直接修改value属性进行修改
Object.defineProperty(test,"w",{value:3});
console.log(test.w); //3
// 将x从数据属性修改为存取器属性
Object.defineProperty(test,"w",{get:function(){return Math.random()}});
console.log(test.w);
对象属性的setter和getter是Vue响应式原理的基础,当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
发布-订阅模式
响应式原理的代码实现中,其核心设计模式就是发布-订阅模式了。发布-订阅模式又叫做观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。引用一下《JavaScript设计模式与开发实践》一书中对于发布订阅模式的举例:
不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常之广泛。我们先看一个
现实中的例子。
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼
MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。
但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除
了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决
定辞职,因为厌倦了每天回答 1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在
了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一
样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,
遍历上面的电话号码,依次发送一条短信来通知他们。
其模式关系如下图所示:
发布-订阅模式的实现流程:
Vue的属性变化追踪
当我们理解了对象的属性和发布-订阅者模式后,我们可以开始来看一下在Vue中是如何对属性变化进行追踪的了。多的不说,先上官网的示意图:
官网的注解也是很简单
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
相信你看到这些时仍然是一脸懵逼,因为我也是如此。在看了源码之后我画了一张Vue实例化过程中实例data流程图:
在上图中,我们可以看到最关键最终的三个模块是Obsever、Watcher、Dep,这三个模块间互相关联,那么他们具体有什么作用呢?对照之前我们讲的对象的属性和发布-订阅者模式,我们可以这样理解:
1、Obsever的核心作用有:将data的每个属性通过setter跟getter变成响应式数据,每个属性调用Dep将其变成一个发布者;
2、编译html时为每个与数据绑定的相关节点生成一个订阅者Watcher,每个订阅者初始化时需要获取对应属性的值,此时会触发getter,getter中会将Watcher添加到Dep.subs中,其实也就是建立了花名册;
3、属性setter时会调用notify方法通知所有watcher,也就是所有订阅者,watcher从而执行update方法进行视图更新。
大概的逻辑和流程就是这些,接下来就直接深入对源码进行一些了解。
核心代码解析
首先我们来看下如何Observe这块的核心功能
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果对象原本用用getter方法则执行
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
console.log(Dep.target);
//如果有依赖则进行依赖收集,是否有依赖取决于当前数据是否在页面上被使用到。
dep.depend();
if (childOb) {
// 对子对象进行依赖收集
childOb.dep.depend();
if (Array.isArray(value)) {
//如果是数组则递归数组并进行依赖收集
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
//通过getter获取新值,如果与当前值相同则return
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if ("development" !== "production" && customSetter) {
customSetter();
}
//若setter已存在则直接执行
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//若是新的只则重新进行observe,保证数据响应式
childOb = !shallow && observe(newVal);
//dep对象发出消息通知所有watcher
dep.notify();
}
});
关于Dep模块,自己参考源码写了一个简易版的Dep类,以方便大家理解阅读
//发布者,保存订阅者信息,并通知订阅者进行相关操作
class Dep {
constructor() {
//存放所有的订阅者
this.subs = [];
}
//添加一个订阅者
addSub(watcher) {
this.subs.push(watcher);
}
//收集订阅者
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
//通知触发相关依赖者更新
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Watch模块同样自己手写了一个简易版,以方便大家理解阅读
//订阅者,发布者notify时调用订阅者update方法,
//update跟据值是否改变来判定执行回调
class Watcher {
constructor(vm, expOrFn, cb) {
// 回调函数
this.cb = cb;
//传进来的对象
this.vm = vm;
//收集发布者,用于移除监听
this.newDeps = [];
//表达式
this.getter = expOrFn;
this.value = this.get();
}
update() {
this.run();
}
run() {
var value = this.get();
var oldValue = this.value;
this.cb.call(this.vm, value, oldValue);
}
get() {
pushTarget(this);
const vm = this.vm;
var value;
try {
value = this.vm.data[this.getter];
} catch (error) {}
return value;
}
addDep(dep) {
this.newDeps.push(dep);
dep.addSub(this);
}
}
function pushTarget(watcher) {
Dep.target = watcher;
}
看了这些源码,大家可以再参考下上面所画的流程图,就大概了解其原理了。
小结:要想真正理解Vue的深入响应式原理,还是在大概理解其实现原理的基础上再深入源码,方才有所收获。自己水平有限,文中不免有错误之处,还望各位能指正教导,共同学习!