vue响应式原理的实现
程序员文章站
2024-01-31 12:58:22
...
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。----官方文档
引言
Vue的数据双向绑定,响应式原理,其实就是通过Object.defineProperty()结合发布者订阅者模式来实现的。
- Observer 通过Object.definePropty进行数据劫持
- Dep 发布者,添加订阅者以及在数据发生改变的时候通知所有的订阅者
- Watcher 订阅者,对数据进行观察以及保存数据修改需要触发的回调
- Compiler 模板编译器,对HTML模板进行编译,提取其中的变量并转化为数据(绑定更新Watcher 订阅者)。
本文整理的较为粗糙,大体的说明了一下响应式的实现过程,很多地方直接使用模拟数据,不过整体的流程还是比较清晰。
数据劫持
听起来这个词挺唬人的,换句话讲就是如何对监听一个对象的改变?
其实有两种办法:使用Object.defineProperty和ES6的Proxy ,这里就用第一种方式来实现的
class Vue {
constructor(options) {
this.data = options.data;
observe(this.data);
// 模拟 {{name}} {{age}}
new Watcher(this.data,'name');
new Watcher(this.data,'name');
new Watcher(this.data,'age');
new Watcher(this.data,'age');
}
}
function observe(obj) {
// 判断类型
if (!obj || typeof obj != 'object') {
return
}
Object.keys(obj).forEach(item => {
defineReactive(obj, item, obj[item])
})
// 监听对象属性的的变化
function defineReactive(obj, key, value) {
// 递归子属性
observe(value);
let dp = new Dep();
// 监听对象属性
Object.defineProperty(obj, key, {
enumerable: true, //属性可枚举(可遍历)
configurable: true, //属性可配置(比如可以删除)
// 获取属性时 会调用get
get() {
if (Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
},
// 设置属性时 会调用set
set(newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal != value) {
value = newVal;
dp.notify() //新增
}
}
})
}
}
- Object.keys(obj).forEach 对对象里的每个属性进行遍历监听。
- Object.defineProperty(obj, key,{}) 对属性进行getter/setter的监听
注意! 无法检测到对象属性的添加或删除 这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性(可以使用Vue.set(location, a, 1))。 -
Dep.target是什么?
每次对一个属性进行监听,都需要创建对应的Dep发布者,来存储对应属性的所有watcher订阅者,watcher订阅者调用的时候设置Dep.target指向自己,get()每次调用的时候只需要push Dep.target即可 因为指向的就是watcher本身,Dep.target主要的作用就是代指某个watcher,当添加完之后设置Dep.target = null。
Dep发布者
收集所有订阅者以及触发订阅者的更新,其实它是订阅者和数据之间的一个调度中心,用于集中处理一些事情。
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
// 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
// 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
Watcher订阅者
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。
然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
比如说{{message}}这个变量 在头部、尾部、导航中都有用到,那{{message}}改变的时候如何同时所有调用它的地方呢? watcher是每个{{message}} Dep包含所有watcher的集合。
class Watcher {
constructor(obj, key) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this;
this.obj = obj;
this.key = key;
this.value = obj[key];
Dep.target = null;
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 后期定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
console.log('watcher订阅者更新的内容 this.value',this.value);
}
}
总结
这是细化后的响应式流程
上面完整的代码
/**
* 订阅者 Dep
* 收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
* 于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
* */
// Dep的简单实现
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
// 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
// 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
// Watcher
/**
* Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher
* 当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。
* 然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
* */
class Watcher {
constructor(obj, key) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this;
this.obj = obj;
this.key = key;
this.value = obj[key];
Dep.target = null;
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
console.log('watcher订阅者更新的内容 this.value',this.value);
}
}
class Vue {
constructor(options) {
this.data = options.data;
observe(this.data);
// 模拟 {{name}} {{age}}
new Watcher(this.data,'name');
new Watcher(this.data,'name');
new Watcher(this.data,'age');
new Watcher(this.data,'age');
}
}
function observe(obj) {
// 判断类型
if (!obj || typeof obj != 'object') {
return
}
Object.keys(obj).forEach(item => {
defineReactive(obj, item, obj[item])
})
// 监听对象属性的的变化
function defineReactive(obj, key, value) {
// 递归子属性
observe(value);
let dp = new Dep();
// 监听对象属性
Object.defineProperty(obj, key, {
enumerable: true, //属性可枚举(可遍历)
configurable: true, //属性可配置(比如可以删除)
// 获取属性时 会调用get
get() {
if (Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
},
// 设置属性时 会调用set
set(newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal != value) {
value = newVal;
dp.notify() //新增
}
}
})
}
}
const app = new Vue({
data: {
name: 'qfl',
age:'26',
location: {
x: 100,
y: 100
},
arr: ['1', '2']
}
})
上一篇: Docker练习及在CTFd平台上放题目
下一篇: Swing