vue2源码解析(四):依赖收集
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
你好呀,我是小九,很高兴见到你。
摘要
vue2源码学习之路。
查看之前的文章
vue2源码解析(三):从html模板到真实dom呈现全过程分析
在上一篇文章中,实现了从html模板到真实dom渲染的过程,简单回顾一下。
首先进行模板编译,将模板编译成ast语法树,然后将ast生成render函数。
调用render函数生成虚拟dom,最后再将虚拟dom转换成真实的dom。
整个过程中,有两个关键的函数,一是_render
函数用于创建虚拟dom,_update
函数负责将虚拟dom转成真实dom。
如果数据发生变化,视图也要随之变化,就要重新进行渲染。
此时就需要重新调用_render
和_update
函数。
通过手动调用_render
函数和_update
函数能够实现更新视图的效果
<div id="app" class="test" style="color: red">
<div class="number">{{a}}</div>
<div>name: {{obj.name}}</div>
<div>age: {{obj.age}}</div>
</div>
const app = new Vue({
el: "#app",
data: {
a: 100,
obj: {
name: "tom",
age: 10,
},
},
});
setTimeout(() => {
app.a = 20;
app.obj.name = "小张";
app.obj.age =12;
app._update(app._render())
}, 1000);
结果
但实际上,不可能每次更新都手动调用_render
和_update
函数。
此时就需要实现数据的依赖收集,数据变化自动更新视图。
接下来就介绍一下如何实现自动更新。
项目结构
vue更新策略
先来了解一下vue的更新策略。
vue更新策略是以组件为单位的,每个组件都有一个watcher,这个watcher也叫做渲染watcher。
组件中的属性变化后,就会调用当前组件对应的watcher,重新进行渲染。
而watcher的本质就是将渲染的逻辑封装起来,也就是_render
和_update
函数。
这里顺便说一句,为什么vue要组件化,面试中可能会问到。
为什么要组件化,组件化的好处是什么?
一是复用,二是方便维护,三是局部更新
封装Watcher
1.封装_render
和_update
在之前的代码中,_render
和_update
在挂载函数mountComponent
中调用。
为了能够自动更新数据,这里,使用Watcher对这两个函数进行包装。
export function mountComponent(vm, el) {
const updateComponent = () => {
vm._update(vm._render());
};
new Watcher(vm, updateComponent, () => {}, {}, true);
}
此时就需要一个Watcher类。
创建文件,路径src-->observer-->watcher.js,创建一个Watcher类。
2. 基本参数
一个页面可能有很多组件,每一个组件都有一个watcher,因此每个watcher都需要添加一个唯一标识。
Watcher接收几个参数
vm
:当前vue实例
expOrFn
:表达式或函数
cb
:回调函数,用于更新的函数
options
:选项
isRenderWatcher
:是不是渲染watcher,只是起了一个名字。
如果expOrFn是一个函数,就把这个函数放到getter上。
创建一个get函数,get函数用来执行getter。
get会默认执行一次,让页面渲染。
import { observer } from ".";
import { isFunction } from "../shared/until";
// 每个watcher都添加一个唯一值
let uid = 0;
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.id = ++uid;
this.vm = vm;
this.cb = cb;
if (isFunction(expOrFn)) {
this.getter = expOrFn;
}
this.get();
}
get() {
this.getter();
}
}
export default Watcher;
封装Dep
一个页面有可能有多个组件,每个组件都有自己的数据,每个组件对应一个watcher,因此要将组件的数据和watcher绑定在一起。
有些数据在页面中使用了,有些数据没有使用,所以需要知道模板中使用了哪些属性。
可以给模板中使用的每一个属性,增加一个收集器dep,目的就是收集watcher。
1. dep和watcher的对应关系
一个组件对应一个watcher,一个组件中有多个属性,一个属性对应一个dep,所以可能存在多个dep对应一个watcher。
一个属性可以在多个组件中使用,一个属性对应多个组件,所以可能存在一个dep对应多个watcher。
dep和watcher之间是多对多的关系。
2. 创建Dep类
创建文件,路径src-->observer-->dep.js
dep可能有很多个,所以和watcher一样,需要一个唯一标识。
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
}
}
Dep.target = null;
export default Dep;
vue源码中借鉴了js的单线程模型。
在Dep类上有一个target属性,用于保存当前渲染的watcher,渲染哪个watcher,就保存哪个,渲染完成之后,就把当前的target清空。
注意,target不需要定义在Dep原型上,不是给实例用的,只是相当于定义了一个全局变量。
3.给属性添加dep
在之前的代码中,有一个defineReactive
函数,这个函数用来实现数据响应式,也就是数据劫持。
本质上是使用Object.defineProperty
给每一个属性添加get和set方法。
获取数据就调用get方法,修改数据就调用set方法。
因此,可以在这个函数中给每个属性添加一个收集器dep
,用于收集watcher。
使用get方法取值时,进行依赖收集,使用set方法修改时,进行依赖更新。
简单来说,当页面取值时,说明这个值用来渲染了,此时可以把watcher和属性对应起来。
当页面更新时,就调用watcher封装起来的_render
和_update
函数,重新渲染。
export function defineReactive(data, key, value) {
observer(value);
// 给每个属性都添加一个dep
let dep = new Dep();
Object.defineProperty(data, key, {
// 取值时依赖收集
get() {
if (Dep.target) {
dep.depend();
}
return value;
},
// 修改时依赖更新
set(newValue) {
if (newValue === value) return;
observer(newValue);
value = newValue;
dep.notify();
},
});
}
4. watcher和dep建立双向记忆
上面的代码,如果Dep.target
变量中,说明当前有一个渲染的watcher,那么就让dep记住当前的watcher。
if (Dep.target) {
dep.depend();
}
调用set后,数据更新,页面重新渲染。
dep.notify();
因此,需要在合适的时候,将当前的wathcer保存到全局变量Dep.target上。
前面封装watcher类时,watcher中有一个get方法,本质是调用渲染函数。
所以,需要在渲染之前,将watcher保存在全局变量Dep.target
上,渲染完成后将这个变量清空。
class Watcher {
...
get() {
// 将当前的watcher放到全局变量上
// this是当前watcher实例
pushTarget(this);
// 首先调用一次,让页面渲染
this.getter();
// 视图渲染完成后,清空这个值
popTarget();
}
...
}
在src-->observer-->dep.js 中添加pushTarget
函数和popTarget
函数
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget() {
Dep.target = null;
}
难点来了,如何让dep和watcher记住对方?
源码中的思路是,在Dep和Watcher类中分别创建一个保存的函数,调用对方的函数,同时将自己的实例传进去,并借助Dep.target
这个中间变量。
如图所示
Dep类中添加addSub
方法用于添加watcher
addSub(sub) {
this.subs.push(sub);
}
添加depend
方法让watcher也记住dep。
depend() {
Dep.target.addDep(this);
}
Watcher类中添加addDep
方法,记住dep。
这里有一点需要注意,如果页面中对于一个属性,使用了多次,每次使用,都让当前的watcher保存一次这个属性的dep,就会出现重复。
因此,就需要对dep去重。
源码中使用的是Set
,先判断dep是否存在,再决定是否保存dep。
constructor{
this.deps = [];
this.depIds = new Set();
}
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
this.deps.push(dep);
this.depIds.add(id);
dep.addSub(this);
}
}
至此,watcher和dep建立双向记忆完成。
5, 重新渲染
defineReactive
这个函数中,get时调用了dep.depend()
记住watcher,set时调用了dep.notify()
重新渲染。
dep.notify()
的逻辑就是遍历subs中保存的所有watcher,调用watcher中的渲染方法。
在Dep类中添加notify
函数
notify() {
this.subs.forEach((watcher) => watcher.update());
}
Watcher类中添加update
函数,调用自身的get方法,重新渲染。
update() {
this.get();
}
测试
还是刚才的代码,这次可以实现自动更新
<div id="app" class="test" style="color: red">
<div class="number">{{a}}</div>
<div>name: {{obj.name}}</div>
<div>age: {{obj.age}}</div>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
a: 100,
obj: {
name: "小明",
age: 10,
},
},
});
setTimeout(() => {
app.a = 20;
app.obj.name = "自动更新的name";
app.obj.age = "自动更新的age";
}, 1000);
结果
总结
每一个属性都有一个dep,每一个组件都有一个watcher,watcher和dep是多对多的关系。
当创建渲染watcher的时候,会把当前的渲染watcher放到Dep的target上,调用_render方法取值,就会执行到属性的get方法,将当前渲染的watcher保存到全局属性上,然后让dep保存watcher,同时也将dep保存到这个watcher上。
每个属性有一个dep,属性就是被观察者,watcher就是观察者,属性变化了会通知观察者更新,这就是观察者模式。
watcher和dep相互记忆的过程很绕,需要好好理解,希望我们一起加油。
完整代码,请移步gihub vue2-source
文章就到这里,下次再见!
转载自:https://juejin.cn/post/7143870723768418341