把数据渲染到页面,并且能够追踪数据的改变并自动更新页面的显示,是现代前端框架做的重要工作之一,下面是一段常见的Vue
代码片段。
{{msg}}
运行起来可以看到页面由空白变为展示相应字符,中间到底发生了什么?
为了便于理解需要了解的前置知识
- 实现于
ES5
的Object.defineProperty
(MDN)
/*
* obj: 目标对象
* prop: 需要操作的目标对象的属性名
* descriptor: 描述符
* return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)
重点关注的描述符对象
- enumerable,属性是否可枚举,默认 false。
- configurable,属性是否可以被修改或者删除,默认 false。
- get,获取属性的方法,值被读取时调用。
- set,设置属性的方法,值被更改时调用。
- 发布订阅模式
在Vue
的数据响应系统设计里,采用了发布-订阅
设计模式,这是一种经常与观察者
模式比较的模式。
观察者
模式中有两个角色,首先 观察者(Observers) 告诉 被观察者(Subject) 关注的事件,然后当事件发生后,被观察者(Subject) 对 观察者(Observers) 进行通知。
这种模式对于复杂高的系统可能不太合适,因为两个角色的关系耦合性很高,对于跨组件跨系统的事件的处理增加了复杂度,使代码变得难以维护。
发布-订阅模式
模式其实是
观察者
模式的应用,有的资料里干脆说是别名,在这个模式里,多了一个中间人的角色,中间人充当了
消息中介。
- 对于 发布者(Publisher),它不需关心是谁在关注哪些事件,只需要在事件发生时通知 消息中介,
- 对于 订阅者(Subscriber) ,同样不需要关注事件在哪里发生,只需要告诉 消息中介 关注哪些事件然后坐等通知即可。
接下来是Vue
响应系统的简单示例:
function observer (value) {
if (!value || (typeof value !== 'object')) {
return;
}
Object.keys(value).forEach((key) => {
// 将对象的每个属性进行响应式处理
defineReactive(value, key, value[key]);
});
}
function cb (val) {
console.log("视图更新啦~", val);
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
val = newVal;
// 在对响应式对象的属性赋值时,触发视图更新
cb(newVal);
}
});
}
class Vue {
constructor(options) {
this.$data = options.data;
observer(this.$data);
}
}
let o = new Vue({
data: {
test: "I am test."
}
});
o.$data.test = "hello,test.";
以上简单实现了在数据更新后,对视图进行更新的功能。
下面是Vue
的 观察者 Watcher 的简单实现,对应上图叫做 订阅者(Subscriber)。
class Watcher {
constructor () {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
/* 更新视图的方法 */
update () {
console.log("视图更新啦~");
}
}
Dep.target = null;
接下来实现Vue
的 订阅者 Dep,我的理解是相当于上图的 消息中介 的角色,一方面收集 订阅(),一方面通知 订阅者 更新视图。Vue
称依赖
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
function defineReactive (obj, key, val) {
/* 一个Dep类对象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dep.notify();
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
/* 在这里模拟render的过程,为了触发test属性的get函数 */
console.log('render~', this._data.test);
}
}
之前说到发布-订阅
模式有三种角色,现在就剩下 发布者 了。
实际上在一个Vue
实例化时,除了会初始化一些属性和方法以外,还会进行模板的渲染,也就是render()
的过程,此时会调用getter()
,将data
的每个属性与相关的 依赖 建立关系,所以我认为data
的属性就扮演了 发布者 的角色,每当通过任何方式修改data
属性,就会触发对 消息中介 进行通知,再由 消息中介 通知到相应的 订阅者。
横向对比 React
和 Angular
- Angular数据响应
不像Vue
的机制,Angular
没有进行精准的观察数据的变动,而是在某些用户的操作或者某些关键时间点时,对所有的数据递归检查,对于有变动的数据进行视图更新,检查可能会重复多次,因为单次的变动是可能引发连锁反应的。有时候还需要手动触发检查。 - React数据响应
区分于Vue
的mutable
模式,data
属性值的修改只会作用域使用该属性的地方,React
采用的是immutable
设计,state
不能直接修改,而是需要通过setState()
来实现修改,并且React
会重新渲染当前组件,当然,经过一系列优化,不会对未变动的值这么做。
即将到来的 Vue
3.0 数据响应
Vue
3.0即将到来,除了到来很多功能上的改进以外,对于项目结构也将降低开源参与者的门槛,会将许多功能按照模块化的思想进行解耦,可能到时源码内的数据响应这块的代码也将更清晰易懂。
此外将采用ES6
引入的元编程
语言特性Proxy
,来重写数据响应,由于目前是不能很好的检测数组和对象的变动,所以很多地方要使用$set
来实现数据响应,这个问题将在3.0版彻底解决,但也因此将不支持IE11
,因为这也是个不能polyfill
的特性。
本文代码参考和来源自剖析 Vue.js 内部运行机制