Vue2源码 - 响应式系统1
前言
本文手把手,换位到读者的视角,一字一句的带你阅读源码。尽量不说多余的废话,也不讲什么高深的术语,用线性的方式带你学习。如果本文对你有帮助,也请大家点点赞,收藏,推荐一下。
响应式系统的概念
- 响应式的意思就是,根据一个东西,从而变成什么东西(这我自己领悟的)。比如,我现在在照镜子,我做了一个动作,镜子里面的我也跟着我做了同样的动作。
- 那么把这个概念放到Vue中呢。就是,我的数据是什么,页面中所用到这个数据的地方,也应该是什么。
体验响应式
先体验一下,如果现在没有响应式,我们的编码是一个什么样子。
用代码举个例子
<div id="myDiv"><span id="str">haha</span>!</div>
<button onclick="change()">Change</button>
<script>
function change() {
// 获取要修改的元素
const nameElement = document.getElementById('str');
// 修改元素的文本内容
nameElement.textContent = 'xixi';
}
</script>
从这段代码中,我们可以看到。没有响应式是一件多么痛苦的事情,只是想把haha变成xixi,我们就需要先获取html对应的元素,然后在写一大些名字很长的属性,费时费力。
接着,我们在体验一下简单响应式。
上代码
<div id="myDiv"><span id="str">haha</span>!</div>
<button onclick="change()">Change</button>
<script>
// 定义一个响应式对象
const data = {
str: 'haha'
};
// 使用 Object.defineProperty 定义 str 属性
Object.defineProperty(data, 'str', {
get() {
return data._str;
},
set(newValue) {
data._str = newValue;
// 更新相关的 HTML 元素
const nameElement = document.getElementById('str');
nameElement.textContent = newValue;
}
});
// 修改 str 属性的方法
function change() {
data.str = 'xixi';
}
</script>
上述代码,带你体验了简单的响应式。也就是Vue2.X的响应式雏形,怎么说呢?还不如上面那段代码。还是我们手动的获取了DOM元素,并且代码量还增加了。
那么重头戏来了,体验一下Vue的响应式。
上代码
<div id="app">
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
const app = Vue.createApp({
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
})
app.mount('#app')
这段代码,我们换个例子,用计数的方式体现。当点击按钮时,count 的值会自动更新,并且在页面上显示出来。你能感觉到吗?这段代码中,我们没有操作DOM元素! 没有写很长很长的属性。 就做到了页面随着数据的改变而改变。其实,这些脏活累活,不是没有做,只是Vue帮我们做了,并且做的很好。
剖析Vue的响应式系统
首先,学习Vue的响应式绕不开3个类,下面我们一一说明一下这3个类。
Observer
Observer 是 Vue 中的一个核心概念,它的主要任务是负责将一个普通的 JavaScript 对象转换为一个响应式对象。Observer 通过递归的方式遍历对象的所有属性,并使用 Object.defineProperty 方法将(就是上面我们举例子用到的那个方法)它们转换为 getter/setter,从而使得这个对象的每个属性都具有响应性。当我们访问或修改这个对象的属性时,getter/setter 会被触发,从而实现对数据变化的监听和响应。
比方: 假如你现在装修一个新房子,这个新房子可以比作一个对象(数据),Observer可以比作一个成,你安装在这个新房子的摄像头。当这个房子的装修工人发生变化时(刷漆,铺瓷砖...),你都会立即知道。因为你通过安装摄像头,赋予了这个房子随时了解变化的能力。
Dep
Dep(依赖)是 Vue 中用来管理依赖关系的类。每个响应式属性都有一个与之关联的 Dep 实例,用来存储所有依赖于该属性的 Watcher 实例。当一个属性被访问时,当前的 Watcher 实例会被添加到这个属性的 Dep 中。当一个属性被修改时,Dep 会通知所有依赖于该属性的 Watcher 实例,让它们执行相应的更新操作。
比方: 你可以把Dep当做一个手机APP,一个房子会有好几个房间,我们在好几个房间都安装了摄像头。然后我们通过一个APP来管理这些摄像头,当某一个房间的工人发生变化的时候,APP就会向你推送一条信息(xx房间的工人,正在xx)。
Watcher
Watcher 是 Vue 中的观察者类,它用来观察一个表达式或计算属性的变化。当我们创建一个 Watcher 实例时,需要提供一个回调函数,这个函数会在 Watcher 所观察的表达式或计算属性的值发生变化时被调用。Watcher 会在初始化时读取一次它所观察的表达式或计算属性的值,以便收集依赖。在后续的更新过程中,如果 Watcher 所观察的表达式或计算属性的值发生了变化,Watcher 会再次读取新的值,并调用提供的回调函数。
比方:Watcher可以比作是你本人,或者你的家人们。你或你的家人们,都很关注装修这个事情。一直盯着手机。一旦APP向你发了条推送,(xx房间的工人,正在刷漆)。你打开APP一看,我X,我要刷白漆,怎么给我刷成黑的了。然后赶紧,通过这个摄像头告诉工人。你刷成黑的了,换成白的!
源码中的响应式系统
- 此篇文章针对于获取操作,关于修改操作,会在第二篇中讲解。
observer/index.ts - Observer
class Observer {
dep: Dep
vmCount: number // number of vms that have this object as root $data
constructor(public value: any, public shallow = false, public mock = false) {
// this.value = value
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (isArray(value)) {
if (!mock) {
if (hasProto) {
/* eslint-disable no-proto */
;(value as any).__proto__ = arrayMethods
/* eslint-enable no-proto */
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}
上述代码就是 Vue源码中,定义的Observer类,先不要慌,结合上面举的例子,给他传入一个对象,把这个对象中的属性,变成响应式的。比如,程序运行的第一次先传入data对象,然后首先先把data中的属性都变成响应式的。处理数组有处理数组的方法,对象有对象的方法,暂且先不开考虑数组,只考虑对象的情况。然后通过调用
defineReactive
方法赋予数据响应式的能力。
observer/index.ts - defineReactive
function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
// 创建一个Dep实例,这个实例,所有的响应式数据,都会有
const dep = new Dep()
// 获取对象的
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ( (!getter || setter) && (val === NO_INITIAL_VALUE || arguments.length === 2) ) {
val = obj[key]
}
// 默认深度观察,除了$attrs这种明确写了不深度检测的情况除外,
// 执行了observe,就想到当于默认执行,深度检测了。
let childOb = !shallow && observe(val, false, mock)
// 赋予响应式能力
Object.defineProperty(obj, key, {
enumerable: true, // 属性描述符
configurable: true, // 属性描述符
get: function reactiveGetter() {
// 这里是获取数据,首先先看一下这个属性有没有一个get方法。
// 如果有,那么就调用这个get方法
// 如果没有,就返回当前属性的值
const value = getter ? getter.call(obj) : val
// 这个判断,在程序第一次进入此函数的时候是不会执行的,
// 因为在创建阶段,还没有需要计算的Watcher实例
if (Dep.target) {
// 这个判断的意思是分为两个模式,如果是true那么是开发者模式
// false则是生产模式
// 开发者模式,你可以想象成,主要是给开发者工具服务的即可
if (__DEV__) {
// 这里是开发模式调用的dep中的depend方法,用来搜集 watcher依赖
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
// 这里是生产模式调用的dep中的depend方法,用来搜集 watcher依赖
dep.depend()
}
if (childOb) {
// 在观察的属性上调用Dep类中的depend方法。
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
// 这段主要是为了兼容v3,暂且不谈,就知道这里是在返回当前属性的值即可
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}
上述是源码。defineReactive方法是真正的安装那个摄像头的地方,也就是将数据赋予响应式能力的地方。ok,我们先看
function reactiveGetter() {}
这个函数,这个函数就是响应式数据的get方法的实现,直接看源码即可(代码中有注释)。
通过上述源码,我们了解到了,一个数据被赋予了响应式的能力以后,会有这个get方法,然后每当我们去使用的这个属性的时候,这个get方法就会被触发。我在举个例子。
<div id="app">
<p>Count: {{ count }}</p>
</div>
const app = Vue.createApp({
data() {
return {
count: 0
}
}
})
app.mount('#app')
拿这个代码进行举例,我在data中定义了,count这个属性,程序开始执行,首先Observer先遍历data对象,把需要赋予响应式能力的属性传递给defineReactive进行响应式的改造。改造完了以后,count就变成了一个响应式的属性了。然后,程序继续运行发现,在模板中使用到了
<p>Count: {{ count }}</p>
。因为count被获取了,那么get方法就会被调用。这样,源码中的Dep.target
设置为这个Watcher
实例。if (Dep.target)
就会变为true
。这样程序就会执行条件判断的内部。我们以生产模式为例,就会执行dep.depend()
这个方法。ok,到了这里,我们接着往下看源码的分析
observer/dep.ts - Dep
class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
// pending subs cleanup
_pending = false
constructor() {
this.id = uid++
this.subs = []
}
// 搜集订阅当前Dep实例的Watcher实例。
addSub(sub: DepTarget) {
this.subs.push(sub)
}
// 删除订阅当前Dep实例的Watcher实例。
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
// 这个参数针对于开发者模式,我们暂且忽略。
depend(info?: DebuggerEventExtraInfo) {
// 先看一下这个watcher依赖函数是否有元素,
if (Dep.target) {
// 调用addDep方法,这个方法是在Watcher类中所实现,
// 用于将这个Dep实例存放到Watcher的订阅者列表中
Dep.target.addDep(this)
// 在开发模式下调用,主要是针对于调试,开发者工具有用处。
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
// 发布
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update()
}
}
}
上述源码中,定义了Dep类,结合上面的例子,我们继续。 首先看到这个类中定义的
depend
方法,if (Dep.target)
这个判断是true
,因为不是true
,我们也看不到这个方法的执行了。继续往下走,到了Dep.target.addDep(this)
这句代码。先看一下这个方法的定义
observer/dep.ts - DepTarget
// 定义一个接口DepTarget,并继承了DebuggerOptions这个类,
interface DepTarget extends DebuggerOptions {
id: number
addDep(dep: Dep): void
update(): void
}
这个addDep方法,虽然是在Dep中定义的,但是,这个函数的实现是在Watcher类中实现的,是的。其实从这能体现出,这两个类你中有我,我中有你。既然是在Watcher中实现的,那接着往下看,
observer/watcher.ts - addDep
// 目前展示的代码是Watcher类中的addDep相关的代码片段,不是全部代码。
newDeps: Array<Dep> // 定义newDeps
this.newDeps = [] // 初始化newDeps
// 传入的是一个Dep实例,此方法是在Dep中定义,在Watcher类中实现。
addDep(dep: Dep) {
// 获取一下Dep实例的ID值
const id = dep.id
// 如果这个实例的 ID 不存在 newDepIds 集合中
if (!this.newDepIds.has(id)) {
// 那么就把这个实例的ID添加进去
this.newDepIds.add(id)
// 根据上面的代码可得知,** **
// newDeps是一个Dep类型的数组,所以只能存放Dep类型的数据
this.newDeps.push(dep)
// 首次调用次方法,这个if是true,因为deIds中还没有任何的数据
if (!this.depIds.has(id)) {
// 把当前的Watcher实例添加进addSub这个数组中
dep.addSub(this)
}
}
}
从上述源码的注释中看到,
dep.addSub(this)
这句代码。将当前的这个Dep实例添加到addSub
数组中。addSub
这个数组就是存放订阅这个Dep实例的观察者数组,里面会存放一个或者多个Watcher实例。其实到这里就可以大致的感受到了在代码中,这三者的关系了。
observer/watcher.ts - Watcher
// 在这里,只为了讲解其原理,值贴出了需要用到的get方法
class Watcher implements DepTarget {
//可以理解为Watcher实例的第一个调用的方法
get() {
// 调用Dep中的 pushTarget,把watcher实例,放进Dep的依赖收集函数中
// 并将这个 Watcher 实例设置为当前的目标观察者(Dep.target)
pushTarget(this)
let value
const vm = this.vm // 存放Vue实例
try {
// 以 vm 为上下文(this 值),
// 调用 getter 函数。getter 函数的参数也是 vm
// 但是这个参数的vm一般用不到。就是,我可以不用,但你不能没有
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 看一下侦听属性有没有深度监听的选项
if (this.deep) {
// 遍历所有属性,从而实现深度监听
traverse(value)
}
// 这个方法一旦调用,那么Dep.target这个数组中也会随之删除一位元素。
popTarget()
this.cleanupDeps()
}
return value
}
}
上述代码中,是Watcher类的执行代码。还是回到一开始的那个例子,页面中使用到了
<p>Count: {{ count }}</p>
,那么这一刻Watcher也就开始工作了,首先get
方法来获取数据的初始值,并在这个过程中收集依赖,记录哪些数据(Dep实例)被这个 Watcher 所依赖。在get
方法中,Watcher 会调用pushTarget(this)
将自己设置为当前的目标观察者,然后调用 getter 函数来获取数据的值。在调用 getter 函数时,如果访问了其他的响应式数据,这些数据的 Dep 实例就会将当前的目标观察者(也就是这个 Watcher)添加到它们的依赖列表中。这样,当这些数据的值发生变化时,就知道需要通知哪些 Watcher 进行更新。
总结
在 Vue 的响应式系统中,当我们访问一个响应式数据时,会触发 getter,这时 Vue 会将当前的 Watcher 添加到这个数据的 Dep 中。当数据发生变化时,会触发 setter,这时 Dep 会通知所有依赖于这个数据的 Watcher,然后 Watcher 会重新计算或重新渲染,从而更新视图。这个系统使得 Vue 能够自动追踪数据的变化并更新视图,无需我们手动操作 DOM,简化了开发的复杂性。
转载自:https://juejin.cn/post/7269385060612243471