初识Javascript设计模式之发布-订阅模式到分析Vue之数据响应式原理
目录
环境搭建
在开始分析代码之前先了解一下JavaScript的设计模式之发布-订阅模式,观察者模式
什么是发布-订阅模式
代码分析大致分为三部分:让数据变成响应式、依赖收集 和 派发更新。
让数据变成响应式
什么是数据响应式?
Object.defineProperty()
中间调用层(调度中心):Observer类——递归侦测对象全部属性
observe是干什么的?
defineReactive函数
如何观测数组?—— 数组的响应式处理
依赖收集
此处涉及到了两个重要的部分——依赖到底是什么?依赖要存放在哪里?
Dep类
为什么depend()的时候,不直接把Dep.target加入dep.subs,而是调用了Dep.target.addDep呢?
派发更新
Wacher类
总结:
一开始学习的时候还得有JS高级语法:
- 函数上下文的了解
- 简单了解webpack和webpack-dev-server
- 会Vue(Vue2.x和Vue3.x均可)
环境搭建
npm init
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
配置webpack.config.js文件
// 从https://www.webpackjs.com/官网照着配置
const path = require('path');module.exports = {// 入口entry: './src/index.js',// 出口output: {// 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成publicPath: 'xuni',// 打包出来的文件名,不会真正的物理生成filename: 'bundle.js'},devServer: {// 端口号port: 8080,// 静态资源文件夹contentBase: 'www'}
};
项目结构

index.html(虚拟途径)
Document
在开始分析代码之前先了解一下JavaScript的设计模式之发布-订阅模式,观察者模式
什么是发布-订阅模式
1. 定义
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
2. 例子
比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。
人的日常生活离不开各种人际交涉,比如你的朋友有很多,这时候你要结婚了,要以你为发布者,打开你的通讯录,挨个打电话通知各个订阅者你要结婚的消息。抽象一下,实现发布-订阅模式需要:
- 发布者(你)
- 缓存列表(通讯录,你的朋友们相当于订阅了你的所有消息)
- 发布消息的时候遍历缓存列表,依次触发里面存放的订阅者的回调函数(挨个打电话)
- 另外,回调函数中还可以添加很多参数,,订阅者可以接收这些参数,比如你会告诉他们婚礼时间,地点等,订阅者收到消息后可以进行各自的处理。
代码分析大致分为三部分:让数据变成响应式、依赖收集 和 派发更新。
- 任何一个 Vue Component 都有一个与之对应的 Watcher 实例。
- Vue 的
data上的属性会被添加 getter 和 setter 属性。 - 当 Vue Component
render函数被执行的时候,data上会被触碰(touch), 即被读,getter方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有data。(这一过程被称为依赖收集) data被改动时(主要是用户操作), 即被写,setter方法会被调用, 此时 Vue 会去通知所有依赖于此data的组件去调用他们的 render 函数进行更新
让数据变成响应式
什么是数据响应式?
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。

Object.defineProperty()
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改 一个对象的现有属性,并返回此对象。为什么要用Object.defineProperty
可以为属性设置很多特性,例如 configurable,enumerable,但是现在不过多解释,重点只放在 get 和 set
export const def = function (obj, key, value, enumerable) {Object.defineProperty(obj, key, {value,enumerable,writable: true,configurable: true});
};
中间调用层(调度中心):Observer类——递归侦测对象全部属性
Observer类是将每个目标对象(即data)的键值转换成getter/setter形式(这个使用闭包,封装在defineReactive),用于进行依赖收集以及调度更新。

import { def } from './utils.js';
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';export default class Observer {constructor(value) {// 每一个Observer的实例身上,都有一个depthis.dep = new Dep();// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例def(value, '__ob__', this, false);// console.log('我是Observer构造器', value);// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object// 检查它是数组还是对象if (Array.isArray(value)) {// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethodsObject.setPrototypeOf(value, arrayMethods);// 让这个数组变的observethis.observeArray(value);} else {this.walk(value);}}// 遍历walk(value) {for (let k in value) {defineReactive(value, k);}}// 数组的特殊遍历observeArray(arr) {for (let i = 0, l = arr.length; i < l; i++) {// 逐项进行observeobserve(arr[i]);}}
}; observe是干什么的?
只为对象/数组 实例一个Observer类的实例,而且就只会实例化一次,并且需要数据是可配置的时候才会实例化Observer类实例。
import Observer from './Observer.js';
export default function (value) {// 如果value不是对象,什么都不做if (typeof value != 'object') return;// 定义obvar ob;if (typeof value.__ob__ !== 'undefined') {ob = value.__ob__;} else {ob = new Observer(value);}return ob;
} defineReactive函数
defineReactive 是真正为数据添加 get 和 set 属性方法的方法,它将 data 中的数据定义一个响应式对象,并给该对象设置 get 和 set 属性方法,其中 get 方法是对依赖进行收集, set 方法是当数据改变时通知 Watcher 派发更新。
这里采用闭包的写法,那么闭包的妙用:上述代码里Object.defineProperty()里的get/set方法相对于var dep = new Dep()形成了闭包,从而很巧妙地保存了dep实例
那么 get 和 set 方法有什么用?
- get 值是一个函数,当属性被访问时,会触发 get 函数
在get方法中:
- 先为每个data声明一个 Dep 实例对象,被用于getter时执行dep.depend()进行收集相关的依赖;
- 根据Dep.target来判断是否收集依赖,还是普通取值。Dep.target是在什么时候,如何收集的后面再说明,先简单了解它的作用,
- set 值同样是一个函数,当属性被赋值时,会触发 set 函数
在set方法中:
- 获取新的值并且进行observe,保证数据响应式;
- 通过dep对象通知所有观察者去更新数据,从而达到响应式效果。
import observe from './observe.js';
import Dep from './Dep.js';export default function defineReactive(data, key, val) {const dep = new Dep();// console.log('我是defineReactive', key);if (arguments.length == 2) {val = data[key];}// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用let childOb = observe(val);Object.defineProperty(data, key, {// 可枚举enumerable: true,// 可以被配置,比如可以被deleteconfigurable: true,// getterget() {console.log('你试图访问' + key + '属性');// 如果现在处于依赖收集阶段if (Dep.target) {dep.depend();if (childOb) {childOb.dep.depend();}}return val;},// setterset(newValue) {console.log('你试图改变' + key + '属性', newValue);if (val === newValue) {return;}val = newValue;// 当设置了新值,这个新值也要被observechildOb = observe(newValue);// 发布订阅模式,通知depdep.notify();}});
}; 如何观测数组?—— 数组的响应式处理

思路仍然是一样的:
- 保留数组原来的操作
push、unshift、splice这些方法,会带来新的数据元素,而新带来的数据元素,我们是有办法得知的(即为传入的参数)- 那么新增的元素也是需要被配置为可观测数据的,这样子后续数据的变更才能得以处理。所以要对新增的元素调用
observer实例上的observeArray方法进行一遍观测处理 - 由于数组变更了,那么就需要通知
观察者Observer类,所以通过ob.dep.notify()对数组的观察者watchers进行通知 - 针对当前的数据对象新建一个订阅器;
-
为每个数据的 value 都添加一个__ob__属性,该属性不可枚举并指向自身;
-
针对数组类型的数据进行单独处理(包括赋予其数组的属性和方法,以及 observeArray 进行的数组类型数据的响应式);
-
this.walk(value),遍历对象的 key 调用 defineReactive 方法;
import { def } from './utils.js';// 得到Array.prototype
const arrayPrototype = Array.prototype;// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);// 要被改写的7个数组方法
const methodsNeedChange = ['push','pop','shift','unshift','splice','sort','reverse'
];methodsNeedChange.forEach(methodName => {// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺const original = arrayPrototype[methodName];// 定义新的方法def(arrayMethods, methodName, function () {// 恢复原来的功能const result = original.apply(this, arguments);// 把类数组对象变为数组const args = [...arguments];// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。const ob = this.__ob__;// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的let inserted = [];switch (methodName) {case 'push':case 'unshift':inserted = args;break;case 'splice':// splice格式是splice(下标, 数量, 插入的新项)inserted = args.slice(2);break;}// 判断有没有要插入的新项,让新项也变为响应的if (inserted) {ob.observeArray(inserted);}console.log('啦啦啦');ob.dep.notify();return result;}, false);
}); 总结起来,就是:
- 将
Observer类的实例挂载在__ob__属性上,提供后续观测数据使用,以及避免被重复实例化。然后,实例化Dep类实例,并且将对象/数组作为value属性保存下来 - 如果value是个对象,就执行
walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理) - 如果value是个数组,就执行
observeArray()过程,递归地对数组元素调用observe(),以便能够对元素还是数组的情况进行处理
依赖收集
依赖收集的原理是:当视图被渲染时,会触发渲染中所使用到的数据的 get 属性方法,通过 get 方法进行依赖收集。
此处涉及到了两个重要的部分——依赖到底是什么?依赖要存放在哪里?
- 这两部分刚好对应 Vue 中两个类,一个是 Watcher 类,而依赖就是 Watcher 类的实例;
数据对象中的 get 方法主要使用 depend 方法进行依赖收集(收集依赖实例Watcher),depend 是 Dep 类中的属性方法
- 另一个是 Dep 类,而依赖就是存放在 Dep 实例的 subs 属性(数组类型)中进行管理的。
Dep.target 对象是一个 Watcher 类的实例,调用 Dep 类的 addSub方法
Dep类
- Dep 类中有三个属性:target、uid 和 subs,分别表示当前全局唯一的静态数据依赖的监听器 Watcher、该属性的 uid 以及订阅这个属性数据的订阅者列表[a*],其中 subs 其实就是存放了所有订阅了该数据的订阅者们。另外还提供了将订阅者添加到订阅者列表的 add 方法、从订阅者列表删除订阅者的 remove方法(我没写)。
- Dep.target 是当前全局唯一的订阅者,这是因为同一时间只允许一个订阅者被处理。
- addDep 这个属性方法做了什么?可以看到入参是一个 Dep 类实例,这个实例实际上是当前全局唯一的订阅者,这个方法主要的逻辑就是调用当前数据依赖 dep 的类方法 addSub,而这个方法在上面 Dep 类方法中可以看到,就是将当前全局唯一的 watcher 实例放入这个数据依赖的订阅者列表中。
为什么depend()的时候,不直接把Dep.target加入dep.subs,而是调用了Dep.target.addDep呢?
这是因为,我们不能无脑地直接把当前watcher塞入dep.subs里,我们要保证dep.subs里的每个watcher都是唯一的。
var uid = 0;
export default class Dep {constructor() {console.log('我是DEP类的构造器');this.id = uid++;// 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。// 这个数组里面放的是Watcher的实例this.subs = [];}// 添加订阅addSub(sub) {this.subs.push(sub);}// 添加依赖depend() {// Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行if (Dep.target) {this.addSub(Dep.target);}}// 通知更新notify() {console.log('我是notify');// 浅克隆一份const subs = this.subs.slice();// 遍历for (let i = 0, l = subs.length; i < l; i++) {subs[i].update();}}
};
派发更新
Wacher类
Watcher 是一个中介,数据发生变化时通过 Watcher 中转,通知组件import Dep from "./Dep";var uid = 0;
export default class Watcher {constructor(target, expression, callback) {console.log('我是Watcher类的构造器');this.id = uid++;this.target = target;this.getter = parsePath(expression);this.callback = callback;this.value = this.get();}update() {this.run();}get() {// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段Dep.target = this;const obj = this.target;var value;// 只要能找,就一直找try {value = this.getter(obj);} finally {Dep.target = null;}return value;}run() {this.getAndInvoke(this.callback);}getAndInvoke(cb) {const value = this.get();if (value !== this.value || typeof value == 'object') {const oldValue = this.value;this.value = value;cb.call(this.target, value, oldValue);}}
};function parsePath(str) {var segments = str.split('.');return (obj) => {for (let i = 0; i < segments.length; i++) {if (!obj) return;obj = obj[segments[i]]}return obj;};
}
总结:
其实在 Vue 中初始化渲染时,视图上绑定的数据就会实例化一个 Watcher,依赖收集就是是通过属性的 getter 函数完成的,文章一开始讲到的 Observer 、Watcher 、Dep 都与依赖收集相关。其中 Observer 与 Dep 是一对一的关系, Dep 与 Watcher 是多对多的关系,Dep 则是 Observer 和 Watcher 之间的纽带。依赖收集完成后,当属性变化会执行被 Observer 对象的 dep.notify() 方法,这个方法会遍历订阅者(Watcher)列表向其发送消息, Watcher 会执行 run 方法去更新视图,我们再来看一张图总结一下:


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
