一,前言
上篇,主要介绍了 Vue 依赖收集的过程分析;
- 介绍了 Vue 的响应式特性
- 介绍了 Vue 的依赖收集过程
- 介绍了 dep 和 watcher 以及观察者模式;
本篇,Vue 依赖收集的实现
二,Watcher 部分
1,watcher 的本质
根据之前的分析:
vm._render方法:调用render方法,生成虚拟节点;vm._update方法:将虚拟节点更新到页面上;
所以,从本质上来说,通过执行vm._update(vm._render())就能够触发视图的更新:
断点测试vm._update(vm._render())调用前后的视图更新情况:
// dist/index.html
<div id="app">
<li>{{name}}</li>
<li>{{age}}</li>
</div>
let vm = new Vue({
el: '#app',
data() {
return { name: "Brave" , age: 123 }
}
});
// 主动调用测试视图更新
vm.name = "Brave Wang"; // 数据改变
debugger; // 点断:查看更新前后的页面数据变化
vm._update(vm._render()); // 视图更新
断点查看,更新前:
断点查看,更新后:
在Vue中,数据更新的原理如下:
- 每个数据有一个
dep属性:记录使用该数据的组件或页面的视图渲染函数watcher; - 当数据发生变化时,
dep属性中存放的多个watcher将会被通知(观察者模式)
这里的
watcher就相当于vm._update(vm._render())
因此,需要将视图渲染逻辑vm._update(vm._render()),抽取为一个可单独被调用的函数;
2,抽取视图更新逻辑 watcher
将视图渲染逻辑抽取成为可调用函数,包装为function:
export function mountComponent(vm) {
// 抽取成为一个可被调用的函数
let updateComponent = ()=>{
vm._update(vm._render());
}
// 调用视图渲染逻辑
updateComponent();
}
接下来,只要能够通过watcher来调用执行updateComponent方法,就能够触发视图更新了;
3,创建 Watcher 类
“数据改变,视图更新”,所以
Watcher类应从属于响应式模块;
创建watcher类:src/observe/watcher.js
// src/observe/watcher.js
class Watcher {
constructor(vm, fn, cb, options){
this.vm = vm;
this.fn = fn;
this.cb = cb;
this.options = options;
this.getter = fn; // fn 为页面渲染逻辑
this.get(); // Watcher初始化时调用页面渲染逻辑
}
get(){
this.getter();
}
}
export default Watcher;
Watcher为什么使用类的方式来实现而不使用prototype实现:
- 如果是一个整体的功能,那么优先采用类来实现;
- 如果希望将功能拆分到不同文件中,使用
prototype来实现;
将页面的更新逻辑updateComponent注入到Watcher类中,
再考虑如何通过watcher调用页面更新方法updateComponent;
// src/lifecycle.js
export function mountComponent(vm) {
// 包装更新方法
let updateComponent = ()=>{
vm._update(vm._render());
}
// 注入更新方法-渲染 watcher(每个组件都有一个渲染 watcher)
new Watcher(vm, updateComponent, ()=>{
console.log('Watcher-update')
}, true)
}
4,依赖收集的必要性
提出问题:
- 由数据响应式原理可知,当响应式数据发生变化时,就会进入
Object.defineProperty中的set方法,- 那么,此时在
set方法中调用视图更新逻辑vm._update(vm._render())就能触发视图的更新操作;这样做,就实现了“数据变化,视图更新”;那么,是否还存在其他问题呢?
代码示例:
// src/observe/index.js#defineReactive
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return
// 当响应式数据发生变化时,触发视图更新操作
vm._update(vm._render());
observe(newValue);
value = newValue;
}
})
这样做,虽然能够实现“数据变化,视图更新”,但同时也带来了一个严重问题:
- 由于所有的响应式数据被修改时都会进入到
set方法,这就将导致未被视图使用的数据发生变化时也会触发页面的更新;- 也就是说,这种做法将会触发不必要的视图更新,造成多余的性能开销;
要想避免这种问题:就需要在视图渲染的过程中,将被使用到的数据记录下来;后续仅针对这些收集到的数据变化才触发视图更新操作;
这里,就需要进行依赖收集操作,为数据创建dep用来收集渲染watcher;
三,Dep 部分
1,创建 Dep 类
前面提到:
- 每一个数据都有一个
dep属性,用于存放对应的渲染watcher; - 在每一个
watcher中,也可能存在多个dep;
所以:
- 在
Dep类中,需要具有一个添加watcher的方法; - 在
Watcher类中,也需要有一个添加dep的方法;
当数据发生变化时,通知当前数据dep属性中的所有watcher 执行视图更新操作(这里应用了观察者模式);
备注:为了标识
Dep的唯一性,每次new Dep时添加一个唯一id;
// src/observe/dep.js
// dep 对象的唯一 id
let id = 0;
class Dep {
constructor(){
this.id = id++;
this.subs = [];
}
// 保存数据的渲染 watcher
depend(){
this.subs.push(Dep.target)
}
}
// 静态属性,用于记录当前 watcher
Dep.target = null;
export default Dep
2,为 data 中的属性添加 dep
在数据初始化过程中,通过Object.defineProperty为每个数据添加属性时,为当前属性key创建一个dep实例:
// src/observe/index.js
function defineReactive(obj, key, value) {
observe(value);
let dep = new Dep(); // 为每个属性添加一个 dep
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return
observe(newValue);
value = newValue;
}
})
}
当视图渲染时,就会执行Watcher中的get方法,即执行了vm._update(vm._render());
这里,利用了JS的单线程特性,在即将执行页面的渲染逻辑前,先将当前watcher保存到Dep类静态属性中,即Dep.target = this:
// src/observe/watcher.js
class Watcher {
constructor(vm, fn, cb, options){
this.vm = vm;
this.fn = fn;
this.cb = cb;
this.options = options;
this.getter = fn;
this.get();
}
get(){
Dep.target = this; // 在触发视图渲染前,将 watcher 记录到 Dep.target 上
this.getter(); // 调用页面渲染逻辑
Dep.target = null; // 渲染完成后,清除 Watcher 记录
}
}
export default Watcher
在视图渲染的过程中,将会触发数据的取值操作,如:vm.name;
此时,便会进入Object.defineProperty中get方法中;
如果get方法中Dep.target有值(即为当前watcher),就使用当前数据的dep对象记住这个渲染 watcher:
在数据渲染时,如果当前数据被视图所使用,当进入
Object.defineProperty的get方法时,Dep.target有值且为当前watcher对象,使用当前数据的dep对象记住此渲染watcher;
// src/observe/index.js
function defineReactive(obj, key, value) {
observe(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 如果 Dep.target 有值,将当前 watcher 保存到 dep
if(Dep.target){
dep.depend();
}
return value;
},
set(newValue) {
if (newValue === value) return
observe(newValue);
value = newValue;
}
})
}
这样,dep就“记住”了“自己”参与渲染的全部watcher;当未参与视图渲染的数据更新时,由于dep中并没有记录该watcher,所以不会触发多余的视图更新操作;
四,结尾
本篇, dep 和 watcher 关联
- 介绍了依赖收集的必要性;
- 介绍了 Watcher 和 Dep 的作用;
- 实现了 Watcher 类和 Dep 类;
- Watcher 和 Dep 如何产生关联;
下一篇,视图更新部分
维护日志:
- 20210801:修改目录结构,将 Watcher 和 Dep 部分分离;更新文章摘要;
- 20230203:将必要过程进行拆解并添加图片说明;添加内容中的代码高亮;
- 20230207:添加部分注释,调整部分内容描述,使表达更加清晰、易懂;调整图片显示;