什么是响应式?
响应式 是Vue 最独特的特性之一,是非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。我们也叫他双向绑定。
如果想要更改视图,只要直接更改对应属性的值即可,不需要调用 Vue 提供的 API 接口,而像 React 和小程序则需要调用相应的接口才能使视图进行更新。举个例子:
// Vue
this.a++;
// 小程序
this.setData({
a: this.data.a++
})
响应式系统其实套用了观察者模式。
什么是观察者模式?
观察者模式(Observer):通常又被称作为发布-订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。但是观察者模式不等于发布-订阅者模式。详见附录观察者模式与发布订阅者模式。
观察者模式的适用性
当一个抽象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中可以让它们可以各自独立的改变和复用。(高内聚)
当一个对象必须通知其它对象,但是却不知道具体对象到底是谁。换句话说,你不希望这些对象是紧密耦合的。(低耦合)
当一个对象的改变的时候,需要同时改变其它对象,被改变的对象不知道具体多少对象有待改变
数据初始化(VUE响应式系统初始化)的观察者模式流程示意图
流程解析:
1、 获取用户的 data 值
在 Vue 中,用户会传进来 options 参数,首先我们要获取用户的 data 对象的值,因为可能用户传的是对象,也可能是函数,所以要进行判断,然后将 data 挂载到 vm 实例上,这是为了后续方便获取 data 上的值。
vm.$data = data = (type === 'function' ? data.call(vm) : data);
看不懂call的用法?可以看之前这篇=>
2、代理 $data 数据
我们拿data 里的数据都是直接通过 this.xxx 来访问某个属性,并且能拿到相关数据,其实是 Vue 做了一层代理,实际上还是访问的 this.$data.xxx 来获取属性的值的。
这其实是通过 Object.defineProperty 来做代理,代理的对象是 this,如果用户想访问 this.xxx ,那么就通过 get 方法 return this.$data.xxx 来解决代理。
通过 Object.defineProperty 来做代理,代理的对象是 this,如果用户想访问 this.xxx ,那么就通过 get 方法 return this.$data.xxx 来解决代理。
// 实现的思路就是获取到 vm.$data 后遍历每个 key 后进行代理处理,代码如下:
function proxyData(vm) {
// 代理$data,能通过this.xxx直接访问属性
const $data = vm.$data;
for (let key in $data) {
Object.defineProperty(vm, key, {
get() {
return $data[key];
},
set(newVal) {
$data[key] = newVal;
}
})
}
}
这里不需要递归代理每一个数据,因为我们只要代理第一层数据,让代码能访问到第一层的数据即可,比如访问 this.a.b ,因为 this 对象下并没有 a 属性,所以要代理,代理后能访问到 this.a ,因为对象 a 中本来就有 b 属性,所以不进行代理还是能获取到的。
3、对data里面的数据进行数据劫持
源码:
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历将其变成 vue 的访问器属性
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
从上面对象的遍历我们看到了 defineReactive ,那么劫持最关键的点也在于这个函数,该函数里面封装了 getter 和 setter 函数,使用观察者模式,互相监听。
// 设置为访问器属性,并在其 getter 和 setter 函数中,使用发布/订阅模式,互相监听。
export function defineReactive (
obj: Object,
key: string,
val: any
) {
// 这里用到了观察者(发布/订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个订阅者监听一个发布对象,这个发布对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。
// 实例化一个发布对象,对象中有空的订阅者列表
const dep = new Dep()
// 获取属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 收集依赖,建立一对多的的关系,让多个观察者监听当前主题对象
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
// 这里是对数组进行劫持
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// 劫持到数据变更,并发布消息进行通知
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}
什么是observe实例?
大概工作内容如下:
function observe(value) {
// 不对基础类型进行观察
if (typeof value !== 'object' || value === null) return;
new Observe(value);
}
// 观察者类,用于观测数据使其成为响应式数据
function Observe(value) {
if (Array.isArray(value)) {
// 如果是数组
......
} else {
// 观测对象
this.walk(value);
}
}
observe 函数来观测待劫持的对象,最开始的也就是用户传的 data 对象,注意一下 observe 函数只观测对象类型的数据,也就是 Object 或者 Array,如果不是这两个类型的直接返回,那么可能就有疑问,那普通数据怎么劫持呢,更改普通数据不也能实现响应式嘛?这是因为普通数据在对象里被观察了,因为用户传的 data 也是一个对象,所以如果基本数据类型的话,肯定是在 data 这个最大的对象下存在的,所以肯定被观测过了。
其次 Observe 类主要就是对对象和数组进行观测,并实施不同的策略,如果是 Object 的话,那就调用 walk 方法,遍历当前对象的每一个 key 值,然后利用 defineReactive 函数对其进行劫持。数组会在后续讨论
而 defineReactive 函数会一开始就调用 observe 函数,因为如果当传进来的 value 值还是 Object 就继续递归,直到为基本数据类型,就会被直接 return 回来,然后执行下面的 Object.defineProperty 方法,因为这样过后 Object 里的所有基本数据类型的值都被劫持了,深层的对象中的数据也被劫持了,目前数组里的基本类型数据先不讨论。
而我们可以看到defineReactive里也有observe函数,因为当data里的键值是对象或数组时,我们需要递归层层进行数据劫持
封装中转站Dep与Watcher
我们在劫持到数据变更的时候,并进行数据变更通知的时候,如果不做一个"中转站"的话,我们根本不知道到底谁订阅了消息,具体有多少对象订阅了消息。其中Dep是发布者,Watcher是订阅者。
Dep-发布者
Dep,全名 Dependency, Dep 类是用来做依赖收集的。收集订阅者Watcher清单。
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
// 用来给每个订阅者 Watcher 做唯一标识符,防止重复收集
this.id = uid++
// 定义subs数组,用来做依赖收集(收集所有的订阅者 Watcher)
this.subs = []
}
// 收集订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
作用:
定义subs数组,用来收集订阅者Watcher
当劫持到数据变更的时候,通知订阅者Watcher进行update操作
绑定发布者的目标对象。(target -> Dep(中转站) ->Watcher)
Wacther -订阅者
它负责做的事情就是订阅 Dep ,当Dep 发出消息传递(notify)的时候,所以订阅着 Dep 的 Watchers 会进行自己的 update 操作。
精简源码:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
this.cb = cb
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 解析表达式
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.get()
}
get () {
// 将目标收集到目标栈
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
// 删除目标
popTarget()
return value
}
// 订阅 Dep,同时让 Dep 知道自己订阅着它
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 收集订阅者
dep.addSub(this)
}
}
}
// 订阅者'消费'动作,当接收到变更时则会执行
update () {
this.run()
}
run () {
const value = this.get()
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
Dep 负责收集所有的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要通过 target 指向的计算去收集订阅其消息的 Watcher 即可,然后只需要做好消息发布 notify 即可。
Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。
两者看似相互依赖,实则却保证了其独立性,保证了模块的单一性。
JS手写观察者模式
例子:
页面有两个输入框,当我们改变鞋子/猪肉的价格时,猪肉/鞋子的价格也将随着改变。
这里我们延伸出两对观察订阅关系。
鞋子(dep发布者)-猪肉(watcher观察者或订阅者)
猪肉(dep发布者)-鞋子(watcher观察者或订阅者)
发布者代码:
class Dep { //发布者(商店)
constructor(goodValue, goodName) {
this.goodValue = goodValue;
this.goodName = goodName;
// 观察者数组
this.observers = [];
}
setValue(state) {
// 状态改变,通知订阅者
this.goodValue = state;
this.noticy();
}
addObserver(ob) {
//添加观察
this.observers.push({
name: ob.name,
constructor: ob
});
}
noticy() {
//状态改变,发布
this.observers.forEach(ob => {
ob.constructor.update(`${ob.name}你好,${this.shopName}价格更新了,价格为${this.shopMoney} `)
})
}
}
这里是发布者构造函数,包含商品初价,商品名称,订阅者列表,之后的订阅者创造实例时会用到addObserver方法,noticy方法用于通知每一个订阅者(observers)
观察者代码:
class Observer { //订阅者(顾客)
constructor(name, dep) {
this.name = name;
this.dep = dep;
// 一个订阅者可以订阅多个发布者
this.dep.forEach(d => {
d.addObserver(this);
})
}
update(val) { //改变通知
console.log(val)
}
}
这里订阅者可以同时订阅多个dep,并且在实例化时调用dep实例的addObserver方法。
这样就形成dep和observer的关联性。
完整源码:
<html lang="en">
<body>
<span>鞋子价格</span><input type="text" oninput="changeMoney(event, 1)" value="500">
<span>猪肉价格</span><input type="text" oninput="changeMoney(event, 2)" value="30">
</body>
<script>
class Dep { //发布者(商店)
constructor(shopMoney, shopName) {
this.shopMoney = shopMoney;
this.shopName = shopName;
this.observers = [];
}
setState(state) {
//状态改变,通知订阅
this.shopMoney = state;
this.noticy();
}
addObserver(ob) {
//添加订阅者
this.observers.push({
name: ob.name,
constructor: ob
});
}
noticy() {
//状态改变,发布
this.observers.forEach(ob => {
ob.constructor.update(`${ob.name}你好,${this.shopName}价格更新了,价格为${this.shopMoney}`)
})
}
}
class Observer { //订阅者(顾客)
constructor(name, dep) {
this.name = name;
this.dep = dep;
this.dep.forEach(d => {
d.addObserver(this);
})
}
update(val) { //改变通知
console.log(val)
}
}
const dep1 = new Dep(500, '鞋子');
const dep2 = new Dep(30, '猪肉');
const ob1 = new Observer('顾客1', [dep1, dep2]);
const ob2 = new Observer('顾客2', [dep1]);
function changeMoney(e, id) {
// 通知发布者
id === 1
?
dep1.setState(e.target.value)
:
dep2.setState(e.target.value)
}
</script>
</html>
其他应用:
vue的$on 以及 $emit 的设计也使用了观察者模式。
$emit 负责发布消息,$on 是订阅者 。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(vm, args)
}
}
return vm
}
总结:
目标和观察者间的抽象耦合:一个目标只知道他有一系列的观察者(目标进行依赖收集),却不知道其中任意一个观察者属于哪一个具体的类,这样目标与观察者之间的耦合是抽象的和最小的。
支持广播通信:观察者里面的通信,不像其它通常的一些请求需要指定它的接受者。通知将会自动广播给所有已订阅该目标对象的相关对象,即上文中的 dep.notify() 。当然,目标对象并不关心到底有多少对象对自己感兴趣,它唯一的职责就是通知它的各位观察者,处理还是忽略一个通知取决于观察者本身。
一些意外的更新:因为一个观察者它自己并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。如果观察者直接在目标上做操作的话,可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新,所以一般我们会把一些操作放在目标内部,防止出现上述的问题。
附录:观察者模式与发布订阅模式有什么区别
发布订阅模式:
在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。
那他们之间如何交流?
答案是,通过第三者,也就是在消息队列里面,我们常说的经纪人Broker。
发布者只需告诉Broker,我要发的消息,topic是AAA;
订阅者只需告诉Broker,我要订阅topic是AAA的消息;
于是,当Broker收到发布者发过来消息,并且topic是AAA时,就会把消息推送给订阅了topic是AAA的订阅者。当然也有可能是订阅者自己过来拉取,看具体实现。
也就是说,发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的。
从表面上看:
观察者模式里,只有两个角色 —— 观察者 + 被观察者
而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人Broker
往更深层次讲:
观察者和被观察者,是松耦合的关系
发布者和订阅者,则完全不存在耦合
从使用层面上讲:
观察者模式,多用于单个应用内部
发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件