返回

专栏第八篇-数据响应式原理

上一篇中我们将初次渲染页面的流程大致的过了一遍。

当然,还有很多细节没有描述。

Vue中最核心的部分的就是数据驱动视图,所谓响应式就是当数据发生改变时,界面会同步更新。

一、数据代理的基础

数据代理,有时也被称作数据劫持,是一种技术手段,它能够截获对对象属性的访问和修改操作,并在这些操作发生时执行额外的逻辑或修改返回的结果。

在 Vue.js 中,响应式系统的核心机制正是基于数据代理。

通过代理,Vue 能够在数据被访问时收集依赖,在数据被修改时更新这些依赖,这是响应式系统运作的基本理念。

而这一切都依赖于 Vue 对数据进行的拦截和代理操作。

尽管响应式特性本身并非本节的讨论重点,我们将探索数据代理在其他场景下的应用。

在深入分析之前,重要的是要理解实现数据代理的两种主要方法:Object.definePropertyProxy

这两种方法为我们提供了强大的支持,以实现对数据访问和修改行为的精细控制。

1.1 Object.defineProperty

Vue是通过这个API对数据进行监听的。

Object.defineProperty() 方法用于在一个对象上直接定义一个新属性,或者修改一个对象的现有属性,并返回该对象。

基本使用如下:

1
Object.defineProperty(obj, prop, descriptor)

参数说明:

obj:必需。目标对象
props:必需。需定义或修改的属性的名字
descriptor:必需。目标属性所拥有的特性

Object.defineProperty() 允许我们精确地添加或修改对象的属性。

给对象的属性添加特性描述有两种形式:1.数据描述和2.存取描述 color:orange

数据描述和存取描述所对应的属性是不同的。

1.1.1 数据描述

当修改或定义对象的某个属性的时候,给这个属性添加一些特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj = {
test:'hello world'
}
//对已有的属性添加特性描述
Object.defineProperty(obj,"test",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
//对新添加的属性的特性描述
Object.defineProperty(obj,"newKey",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});

数据描述中有四个属性,都是可选的,来看一下每一个属性的作用。

1.1.1.1 value

属性对应的值,可以使用任意类型的值,默认为 undefined。

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {}
//第一种情况:不设置value属性
Object.defineProperty(obj,"newKey",{

});
console.log( obj.newKey ); //undefined
----------------------------------------
//第二种情况:设置value属性
Object.defineProperty(obj,"newKey",{
value:"hello"
});
console.log( obj.newKey ); //hello

1.1.1.2 writable

属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {}
//第一种情况:writable设置为false,不能重写。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //hello
------------------------------
//第二种情况:设置value属性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //change value

1.1.1.3 configurable

是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。

这个属性起到两个作用:

  1. 目标属性是否可以使用delete删除
  2. 目标属性是否可以再次设置特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//-----------------测试目标属性是否能被删除------------------------
let obj = {}
//第一种情况:configurable设置为false,不能被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //hello
//第二种情况:configurable设置为true,可以被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //undefined

//-----------------测试是否可以再次修改特性------------------------
let obj = {}
//第一种情况:configurable设置为false,不能再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//重新修改特性
//报错:Uncaught TypeError: Cannot redefine property: newKey
// 因为 writable和 configurable都为 false 即无法重新配置
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});


//第二种情况:configurable设置为true,可以再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//重新修改特性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});
console.log( obj.newKey ); //hello

1.1.1.4 enumerable

此属性是否可以被枚举(使用for…in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let obj = {}
//第一种情况:enumerable设置为false,不能被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false
});

//枚举对象的属性
for( var attr in obj ){
console.log( attr ); // 打印为空
}
//第二种情况:enumerable设置为true,可以被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:true
});

//枚举对象的属性
for( var attr in obj ){
console.log( attr ); //newKey
}
一旦使用Object.defineProperty给对象添加属性,那么如果不设置属性的特性,那么configurable、enumerable、writable这些值都为默认的false color:orange

1.1.2 存取器描述

存取器描述符也有四个属性:get、set、enumerable、configure。

1
2
3
4
5
6
7
let obj = {};
Object.defineProperty(obj,"newKey",{
get:function (){} | undefined,
set:function (value){} | undefined
configurable: true | false
enumerable: true | false
});

当使用了getter或setter方法,不允许使用writable和value这两个属性

1.1.2.1 getter/setter

当设置或获取对象的某个属性的值的时候,可以提供getter/setter方法。

  • getter 是一种获得属性值的方法
  • setter是一种设置属性值的方法。

在特性中使用get/set属性来定义对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {};
let initValue = 'hello';
Object.defineProperty(obj,"newKey",{
get:function (){
//当获取值的时候触发的函数
return initValue;
},
set:function (value){
//当设置值的时候触发的函数,设置的新值通过参数value拿到
initValue = value;
}
});
//获取值
console.log( obj.newKey ); //hello

//设置值
obj.newKey = 'change value';

console.log( obj.newKey ); //change value

注意:get或set不是必须成对出现,任写其一就可以。如果不设置方法,则get和set的默认值为undefined

configurable和enumerable同上面的用法。

属性描述符和数据描述符都是相互独立的,如果你在第三个参数中既写了 value/writable,又写了 get/set 则会报错。

1.1.3 兼容性

兼容性如上图所示,需要注意的是:在ie8下只能在DOM对象上使用,尝试在原生的对象使用 Object.defineProperty()会报错。

1.1.4 困惑的问题

如上,在使用 get 方法后,无法直接看到对象上的属性,需要点击才可以。

1.2 Proxy

1.2.1 defineProperty的缺点

我们知道Vue2中的数据响应式处理使用的是 Object.defineProperty,而 Vue3中实现数据响应使用的是 Proxy。

Object.defineProperty虽然可以实现功能,但是无法对数组或者深层次对象进行监听处理。

1.2.1.1 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let arr = [1,2,3,4,5];
arr.forEach((item,index)=>{
Object.defineProperty(arr,index,{
get() {
console.log('数组被getter拦截')
return item
},
set(value) {
console.log('数组被setter拦截')
return item = value
}
})
})
arr[1] = 11;
console.log(arr[1]);
arr.push(6)
arr[10] = 10
// 结果
数组被setter拦截
数组被getter拦截
11

很显然,已知长度的数组可以通过索引进行属性的设置与访问 color:orange

然而,数组的添加操作却无法被直接拦截。

这很容易理解:无论是通过 arr.push() 方法还是直接赋值如 arr[10] = 10 来添加元素,新添加的索引并未被包含在预设的数据拦截机制中,因此无法实现拦截处理。

这是使用 Object.defineProperty 进行数据代理的一个局限性。

Vue 在其响应式系统中对数组方法进行了重写,从而间接地解决了此问题。

1.2.1.2 深层次对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj={
level1:{
level2:"第二层"
}
}
// 保存 level1 的原始值 避免循环引用的错误
let originalLevel1 = obj.level1;
Object.defineProperty(obj,'level1',{
get(){
console.log("对象被getter拦截")
return originalLevel1
},
set(value){
console.log("对象被setter拦截")
originalLevel1 = value;
}
})
// 结果
对象被setter拦截
对象被getter拦截
obj.level1 = {level3:"第三层"}
console.log(obj.level1)// {level3:"第三层"}

可以看到更新设置的那一层是没问题的。

但是如果我们更新更深层次的对象,就无法监听到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj={
level1:{
level2:"第二层"
}
}
// 保存 level1 的原始值 避免循环引用的错误
let originalLevel1 = obj.level1;
Object.defineProperty(obj,'level1',{
get(){
console.log("对象被getter拦截")
return originalLevel1
},
set(value){
console.log("对象被setter拦截")
originalLevel1 = value;
}
})
obj.level1.level2 = '第二层-复制'
//结果
对象被getter拦截

可以看出来并没有执行 set 方法,如果想要深层次的属性也被监听到的话,需要递归地给对象中的每一个属性添加属性描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function walk(obj){
// 如果不是对象 无需处理
if(!(obj instanceof Object)) return obj
for(let k in obj){
let val = obj[k];
walk(val);
Object.defineProperty(obj,k,{
enumerable:true,
configurable:true,
get(){
console.log(`访问obj.${k}属性触发`)
return val
},
set(newValue) {
console.log(`修改obj.${k}属性触发` , newValue)
if (val === newValue) return;
val = newValue
walk(newValue)
}
})
}
return obj;
}
// 递归监听所有层级对象
let obj = {a:{b:{c:'d'}}}
walk(obj)
obj.a
obj.a.b
obj.a.b.c

1.2.2 Proxy的介绍

Proxy就可以解决Object.defineProperty的这些问题。

代理是目标对象的抽象。

他可以用作目标对象的替身,但又完全独立于目标对象。

目标对象既可以直接被操作,也可以通过代理来操作。但是直接操作会绕过代理赋予的行为。

1.2.2.1 空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。

代理对象上执行的所有操作都会无障碍的传播到目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const target = {
id: 'target'
}

const handler = {}

const proxy = new Proxy(target, handler)
// id属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target

// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo

// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar

// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype

// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

1.2.2.2 定义捕获器

当然,正常使用不会给一个空代理,肯定是要通过代理对访问和修改进行捕获。

本节暂且介绍几个常用的捕获器。覆盖Vue2中使用的所有捕获器。

1.2.2.2.1 get 捕获器

get捕获器会在获取属性值的操作中被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const target = {
foo: 'bar'
};

const handler = {
// 捕获器在处理程序对象中以方法名为键
get(){
return 'handler override';
}
}

const proxy = new Proxy(target, handler)

// 只有在代理对象上执行这些操作才会触发捕获器 在目标对象上执行这些操作仍然会产生正常的行为
console.log(target.foo) // bar
console.log(proxy.foo) // handler override
  1. 返回值

返回值无限制。

  1. 拦截的操作
  • proxy.property
  • proxy[property]
  • Object.create(proxy)[property]
  • Reflect.get(proxy, property, receiver)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用目标对象上的字符串键属性
  • receiver: 代理对象或继承代理对象的对象

1.2.2.2.2 set捕获器

set()捕获器会在设置属性值的操作中被调用。

1
2
3
4
5
6
7
8
9
const myTarget = {};
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('触发了set');
return Reflect.set(...arguments)
}
});
proxy.foo = 'bar';
// 触发了set
  1. 返回值

返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。

  1. 拦截的操作
  • proxy.property = value
  • proxy.[property] = value
  • Object.create(proxy)[property] = value
  • Reflect.set(proxy, property, value, receiver)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用目标对象上的字符串键属性
  • value:要赋给属性的值。
  • receiver: 接收最初赋值的对象

1.2.2.2.3 has捕获器

has()捕获器会在 in 操作符中被调用。

1
2
3
4
5
6
7
8
9
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('触发了has函数');
return Reflect.has(...arguments)
}
});
'foo' in proxy;
// 触发了has函数
  1. 返回值

has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。

  1. 拦截的操作
  • property in proxy
  • property in Object.create(proxy)
  • with(proxy) {(property);}
  • Reflect.has(proxy, property)
  1. 捕获器处理程序参数
  • target: 目标对象
  • property: 引用目标对象上的字符串键属性

1.2.3 试试深层次对象和数组

上面我们说到Object.defineProperty对于数组格式和深层次对象处理不了。

那我们来试试 Proxy是否可以处理。

1.2.3.1 处理数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let target = [1,2,3,4] 

let proxy = new Proxy(target,{
get(){
console.log("触发了获取数组")
return Reflect.get(...arguments)
},
set(){
console.log("触发了设置数组")
return Reflect.set(...arguments)
}
})

proxy1.push(5);
console.log(proxy1[4]);

可以看到打印了多次。

但是这里有点令我困惑的是为什么会打印这么多次?

1.2.3.2 处理深层次对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let target = {a:{b:{c:'d'}}}

let proxy = new Proxy(target,{
get(){
console.log("触发了获取对象")
return Reflect.get(...arguments)
},
set(){
console.log("触发了设置对象")
return Reflect.set(...arguments)
}
})

proxy.a.b.c
proxy.a.b.c = 'e'

二、initProxy

虽然Vue2中的数据劫持并没有使用 Proxy,但是却在检测 data 时使用了Proxy。

还记得我们在前面章节专栏第四篇-初始化干了什么事中介绍了Vue实例化的时候会调用 initProxy,但是当时我们并没有对这个方法进行深度剖析,因为这个方法主要是使用 Proxy 来进行数据代理。

但是我们现在已经学习了 Proxy,所以我们这几来详细看下这块的逻辑。

1
2
3
4
5
6
7
Vue.prototype._init = function (options) {
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
}

在开发环境下,调用了 initProxy。

而在生产环境下并没有调用,而是将实例本身赋值到_renderProxy属性中。

一般仅存在于开发环境中的逻辑都是一些报错提示信息,所以我们盲猜initProxy也是添加了开发时报错的提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const hasHandler = {
has(target, key) {
const has = key in target
const isAllowed =
allowedGlobals(key) ||
(typeof key === 'string' &&
key.charAt(0) === '_' &&
!(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}

const getHandler = {
get(target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}

initProxy = function initProxy(vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers =
options.render && options.render._withStripped ? getHandler : hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
}

2.1 hasProxy

首先判断当前宿主环境中是否存在Proxy,通过Proxy是否定义和是原生属性来判断。

1
2
3
4
5
export function isNative(Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)

2.2 _renderProxy

在开发环境下给 vm 设置代理后,将_renderProxy设置为代理对象。代理目标为 vue实例 vm。这意味着当访问 vm时,会触发代理捕获器。

在生产环境下直接将_renderProxy设置为实例 vm。

所以可以看出来不论是在开发环境或者生产环境如何都是给 vm._renderProxy设置值。

那么这里的_renderProxy是在何时使用的呢?

答案是在执行_render方法生成 vnode的时候,将 vm._renderProxy 当做第一个参数传入render方法中。

1
vnode = render.call(vm._renderProxy, vm.$createElement)

比如我们在.vue文件的模版中编写一段代码:

1
<div>{{ msg }}</div>

上面这段代码会被转化成一个render函数。

1
2
3
4
5
6
7
8
9
10
render = function(){
// 获取 this
const _vm = this;
// _vm._self 是 vue 实例
// _c是创建 vnode的方法
const _c = _vm._self._c
// _s是转成字符串的方法
const _s = _vm._self._s
return _c('div',_s(_vm.msg))
}

可以看出来 render函数的 this指向的就是vm._renderProxy,也就是 vue实例。

然后通过 vm.xxx 访问实例上的属性时就可以触发代理上的定义的捕获器逻辑。

2.3 模板渲染的几种情况

在我们了解捕获器中的详细逻辑前,我们需要了解编写Vue渲染中的三种情况。

知道对应的代码处理的是哪些场景有助于我们更好的了解源码。

2.3.1 用户自定义 template 模板

1
2
3
4
5
6
7
8
new Vue({
template:`<div>{{ msg }}</div>`,
data(){
return {
msg:'Hello World'
}
}
}).$mount("#app");

对template选项进行模版编译是发生在实例的挂载阶段。

所以在初始化时,选项中只有 template标签。

经过选项合并后,vm.$options上只有 template选项并没有 render方法。

所以这种情况在执行initProxy方法对目标对象实例 vm 进行代理时,注册的是hasHandler方法。

hasHandler方法中使用的是has捕获器,has捕获器用于处理属性的存在性检查。

vue在对template选项进行模板编译时,会将其转化为 render函数,且会被 with包裹。

而前面我们说过has捕获器可以拦截 with语句下的作用。在执行 with语句的过程中,该作用域下的变量都会触发 has钩子。这也是模版渲染时之所以会触发代理拦截的原因。

2.3.2 使用.vue文件来编写页面

这也是我们编写 vue项目中最常用的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Test.vue
<template>
<div>
{{ msg }}
</div>
</template>

<script>

export default {
data(){
return {
msg: "Hello World"
}
}
};
</script>

.vue文件在webpack预编译阶段会被 vue-loader 解析成一个对象,其中模板部分会被解析成render函数。

并且在会在render方法中挂载_withStripped为true。

所以这个变量标识着当前的 render函数是使用.vue文件编写编译而来的。

所以这种情况在执行initProxy方法对目标对象实例 vm 进行代理时,注册的是getHandler方法。

getHandler方法中使用的是get捕获器,get捕获器用于处理对 Vue 实例属性的读取操作。

webpack + vue-loader对.vue文件进行编译时,会转化成下面这种形式。

可以看到直接使用 vm.xxx 访问的属性。所以可以使用 get捕获器进行捕获。

2.3.3 用户自定义 render方法来编写页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.js
import Fun from "./Fun.js";

new Vue(Fun).$mount("#app");

// fun.js
let Component = {
render:function(h){
return h('div',this.msg)
},
data(){
return {
msg:'Hello World'
}
}
}

export default Component;

虽然这种情况在也存在 render方法,但是并没有 _withStripped 属性

所以这种情况在执行initProxy方法对目标对象实例 vm 进行代理时,注册的是hasHandler方法。

但是自定义 render的情况并没有使用 in或者 with的情况。

所以用户自定义的方法不会有报错提示。

2.4 捕获器里面的具体逻辑

前面我们说到了三种情况都有对应的捕获方法。

那么这个捕获方法内部究竟做了些什么操作呢?

主要是在开发者开发时对属性和方法的定义做提示。

  1. 当开发者在渲染内容中使用了_作为变量名的前缀时,提示不可以使用这种命名方式。因为这种方式可能会与内部的变量存在冲突。
  2. 当开发者在渲染内容中使用了未定义的变量时,提示该变量并没有在 vue实例上找到。

而前面我们说到了用户自定义的方法不会有报错提示。

我们可以实验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.js
import Fun from "./Fun.js";

new Vue(Fun).$mount("#app");

// fun.js
let Component = {
render:function(h){
// 使用了未定义的变量
return h('div',this.msg1)
},
data(){
return {
msg:'Hello World'
}
}
}

export default Component;

我们在render方法中使用了一个未使用的变量 msg1。

然后打开控制台并没有报错提示。

因为此时代理使用的是has捕获器,而我们既没有使用 with,也没使用 in。

那么有没有解决办法呢?

有,那就是在 render上挂载一个_withStripped变量,让他在 initProxy时注册 get捕获器。

1
2
3
4
5
6
7
8
9
10
11
12
13
let Component = {
render:function(h){
return h('div',this.msg1)
},
data(){
return {
msg:'Hello World'
}
}
}
// 在 render方法中挂载_withStripped属性
Component.render._withStripped = true;
export default Component;

可以看到控制台出现了报错信息。

三、实现简易版vue依赖收集

因为Vue中数据响应式的代码比较多,涉及多个文件。

但是我们知道数据响应式的原理就是页面渲染访问数据时收集依赖,在数据更新时更新对应依赖的渲染内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// =================== 新增点击事件更改 ===================
const btnElement = document.createElement('button');
document.body.appendChild(btnElement);
btnElement.innerHTML = '点击';
btnElement.onclick = function(){
// 点击事件
PageA.msg = '我修改了'
}

// =================== 增加属性监听 ===================
// 存储页面的依赖
let Dep = undefined;
// A页面
let PageA = {
msg:'Hello World',
render(){
const appElement = document.getElementById('app');
// 当访问 msg变量时 会触发数据代理的 get方法 进行依赖收集
appElement.innerHTML = this.msg;
}
}
let pageMsg = PageA.msg;
Object.defineProperty(PageA,'msg',{
get(){
//访问msg变量时执行
console.log("访问了 msg变量")
// 将依赖设置为页面 A
Dep = PageA;
return pageMsg;
},
set(newVal){
//设置msg变量时执行
console.log("设置了 msg变量")
pageMsg = newVal;
// 执行依赖的 render方法
Dep.render();
}
})


// 执行渲染函数
PageA.render();

把上面的代码拷贝到浏览器中。

这些代码非常好理解,也非常简单,这就是 Vue中最核心的原理响应式。

如果你完全看懂并能理解以后,可以说你就掌握了 Vue的响应式核心原理。

四、初始化时增加侦听器的逻辑

4.1 initData

上一节中我们简单的实现了数据响应。

我们知道数据响应的前提是数据被代理了,也就是说数据被 Object.defineProperty 设置了侦听。

这样在数据被访问和设置的时候才能知晓从而触发对应的操作。

Vue选项中的 data就是在 vue实例化初始化的时候在initData中设置监听的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function initData(vm){ 
let data = vm.$options.data;
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
// 如果不是一个对象,进行提示
if (!isPlainObject(data)) {
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
const ob = observe(data)
ob && ob.vmCount++
}

initData在 mergeOptions后面执行,所以此时可以在 vm.$options上获取到 data。

4.1.1 编写data的 2 种形式

我们知道通常我们编写 data有 2 种形式:

1
2
3
4
5
6
7
8
9
10
11
// 直接编写对象
data:{
msg:'Hello World'
}

// 编写一个函数
data(){
return {
msg:"Hello World"
}
}

所以这里判断是否是函数用来执行不同的逻辑获取 data:

  1. 如果是函数,执行getData来获取 data
  2. 如果不是函数,这直接获取 data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let data = vm.$options.data; 
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};

// 将当前依赖变成 undefined,在收集依赖时就可以判断是否是空的 如果是空的就不进行依赖收集
export function getData(data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e: any) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}

4.1.2 判断 data是否是对象

1
2
3
4
5
6
7
8
9
if (!isPlainObject(data)) {
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}

isPlainObject函数用于判断是否是一个对象。

如果 data不是一个对象,则提示应该返回一个对象。

4.1.3 判断不能与methods/props重名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}

首先使用 Object.keys获取所有data的数量。

然后使用 while循环将所有 data中的key值和 props/methods做比较。

如果有重名的则给予提示。

4.1.4 给 data中每一项都设置代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 如果不是关键字,则对 vm._data进行保留
if (!isReserved(key)) {
proxy(vm, `_data`, key)
}

const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}


export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

前面我们将data赋值给了vm._data。

所以这里代理的意思就是,每当你使用 this.xxx来访问data中的属性时,会直接在 vm._data上面进行查找返回。

这样就不用将data中的每一项都挂载到实例上了。

4.1.5 对data进行侦听

接下来就是重头戏了,前面我们说到vue在初始化时会对 data中的每一项进行侦听。

就是使用 observe方法进行侦听的。

1
const ob = observe(data); 

observe 是一个函数,用于将一个普通的 JavaScript 对象或数组转换为响应式对象。它会递归地遍历对象的所有属性,并将它们转换为 getter/setter,以便 Vue 能够跟踪属性的变化。

可以看到observe就是实例化了一个 Observer的类。

4.2 Observer类中设置侦听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function observe(
value,
shallow,
ssrMockReactivity
){
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
return new Observer(value, shallow, ssrMockReactivity)
}
}

上节我们说到在initData中会调用observe方法给data设置侦听。

如上所示,observe中实际上就是实例化了 Observer类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export class Observer {
// 一个 Dep 实例,用于依赖收集,即跟踪哪些 watcher 依赖于该对象的数据变化。
dep: Dep
// 记录有多少个 Vue 实例将这个对象作为它们的根 $data。
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)
}
}
}

这个 Observer 类是 Vue.js 响应式系统的一部分,用于将普通的 JavaScript 对象或数组转换为响应式对象。

4.2.1 三个参数

  1. value:它必须是一个数组或者对象。Observe类会将这个值转换为响应式对象,使得 Vue能够跟踪其属性的变化并更新视图。
  2. shallow:这是一个布尔值,表示是否进行浅层响应式转换。如果 shallow 为 true,则 Observer 只会对对象的直接属性进行响应式转换,而不会递归地对其嵌套对象的属性进行响应式转换。
  3. mock:这个参数也是布尔类型,用于指示是否在服务端渲染(SSR)环境下模拟响应式行为。当 mock 为 true 时,Observer 会使用一个特殊的 Dep 实例(mockDep),以便在服务端渲染时不会实际触发视图更新。

4.2.2 类成员

  1. dep: Dep:每个Observer实例都有一个与之关联的Dep实例。Dep代表“Dependency”,用于存储所有依赖于该Observer实例的观察者(如计算属性或渲染函数)。
  2. vmCount: number:表示有多少个Vue实例将此对象作为根$data对象。这有助于Vue内部管理数据的生命周期。

4.2.3 构造函数内部逻辑

  1. 初始化依赖:如果处于 ssr渲染模式下,使用传入的 mockDep,否则创建一个新的 Dep实例。
  2. 定义观测标识:def是 defineProperty的封装函数,通过def方法给value对象添加一个不可枚举的属性__ob__,其值为当前Observer实例。这是Vue内部用来判断一个对象是否已经被观测过的方式。
  3. 处理数组:如果value是一个数组且不是模拟模式:
    • 如果环境支持__proto__,则将数组的方法指向arrayMethods,这是一个经过修改的数组原型方法集合,用于拦截数组的变异方法(如push, pop等)。
    • 如果不支持__proto__,则手动遍历arrayKeys,并用def方法覆盖原生数组方法,使其指向arrayMethods中的对应方法。
    • 如果不是浅层观测,则调用observeArray方法递归地观测数组中的每一个元素。
  4. 处理对象:如果value是一个对象,则遍历对象的所有键,并对每个键调用defineReactive方法来转换成getter/setter形式,从而实现响应式。

4.3 defineReactive定义响应式

调用 Observer 方法最终会调用 defineReactive方法对 data中的每一项进行添加侦听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
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]
}

let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
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 ? newVal && newVal.__ob__ : observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})

return dep
}

4.3.1 创建依赖

首先会创建一个新的 Dep实例 dep,用于存储所有依赖于该属性的观察者。

在Observer的构造函数中,会遍历对象中的每一个属性,并对每一个属性执行defineReactive。

所以说每一个定义在 data中的每一个属性都有一个自己的依赖列表,记录着当 data变化时需要更新的依赖。

1
2
3
4
5
6
7
8
9
10
11
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
const dep = new Dep()
}

4.3.2 检查属性配置

1
2
3
4
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

因为后面的逻辑是给这个对象设置代理(Object.defineProperty)。

getOwnPropertyDescriptor可以获取对象上的属性描述符。

如果该属性不可配置,就不可以再配置 Object.defineProperty了,所以如果 configurable为 false,直接返回,不做任何处理。

4.3.3 用于处理对象属性的 getter 和 setter

1
2
3
4
5
6
7
8
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
  1. 尝试从对象的属性描述符(property)中获取现有的 getter 和 setter 函数。如果 property 存在且有 get 方法,则 getter 会被设置为该方法;同理,如果 property 存在且有 set 方法,则 setter 会被设置为该方法。
  2. 如果满足下列两种情况,则将 val设置为对象上当前属性的值,即 obj[key]。这意味着如果属性已经有 getter/setter 或者没有提供初始值,Vue 将不会尝试覆盖这些属性,而是使用对象上已经存在的值。
  • 检查是否存在 setter或者不存在 getter。如果属性已经有 setter(或者没有 getter),这意味着属性可能已经被定义为具有自定义行为的属性,因此 Vue不会覆盖它。(前面我们使用过 proxy对 vm上的属性做过代理,而这里是对 data中的数据做代理,所以作用点不同)
  • 检查传入的 val 是否是 NO_INITIAL_VALUE(一个特殊的值,表示没有初始值)或者调用 defineReactive 时只提供了两个参数(即没有提供 setter 值)

这段代码的目的是处理那些可能已经有预定义 getter/setter 的属性。在 Vue 的响应式系统中,当你尝试将一个属性转换为响应式时,如果该属性已经有 getter/setter,Vue 通常不会覆盖它们,除非你明确提供了一个新的值。这样做是为了避免破坏已经存在的属性行为。

4.3.4 观测子属性

1
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  • shallow 是一个布尔值参数,表示是否进行浅层响应式转换。如果为 true,则只对对象的直接属性进行响应式转换,而不递归地对其嵌套对象的属性进行响应式转换。默认为false,即递归的对嵌套对象的属性进行响应式转换。

  • val是当前正在处理的对象属性的值。

  • val.__ob__是一个检查,用于确定 val 是否已经被 Vue 的响应式系统转换过。如果一个对象已经被转换为响应式对象,Vue 会在该对象上添加一个 ob 属性,其值为对应的 Observer 实例。

这行代码的作用是根据 shallow 参数的值来决定是否需要对对象的值进行递归的响应式转换。如果 shallow 为 true,则只使用现有的响应式对象(如果存在),否则会创建一个新的响应式对象。这种方式允许 Vue 在不同场景下灵活地处理对象的响应式转换,同时避免了不必要的性能开销。

4.3.5 定义 getter 和 setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
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 ? newVal && newVal.__ob__ : observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
function dependArray(value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
if (e && e.__ob__) {
e.__ob__.dep.depend()
}
if (isArray(e)) {
dependArray(e)
}
}
}

4.3.5.1 getter函数

在Vue执行渲染时,会访问 data中的属性,从而执行这里定义的 reactiveGetter方法。

如果存在原始的 getter(数据从 Object.defineProperty定义),则调用它。否则,返回属性的值 val。

Dep.Target是一个全局属性,因为每次只能同时执行一次渲染,每次渲染时都会对应一个 Watcher实例。

在每次渲染前都会将 Dep.Target设置为当前的这个 Watcher实例,然后在渲染后会将 Dep.Target设置为空。

所以这个 Dep.Target指的就是当前渲染对应的那个 Watcher实例。

此时调用dep.depend收集依赖。

如果有子属性,则调用子属性的 dep.depend方法。

如果属性值是数组,递归地依赖数组中的每个元素。

最后,如果属性值是一个 ref 对象(Vue 3 引入的响应式引用类型),并且不是浅层响应式,则返回其内部值;否则返回原始值。

4.3.5.2 setter函数

当属性被重新赋值时,如 this.xxxx = xxx。会执行 reactiveSetter 方法。

如果新值与旧值相同,则不进行任何操作。

在开发模式下,如果提供了自定义的 setter,则调用它。

如果存在原始的 setter,则调用它。

如果没有 setter 但有 getter,不进行任何操作,这可能是一个只读属性。

如果属性值是 ref 对象,并且不是浅层响应式,则更新其内部值。

否则,直接更新属性的值。

因为值发生了变化,所以需要再次设置新值为响应式。

最后通知所有依赖于该属性的 watcher,属性值已改变。

然后再次执行渲染操作。

4.4 总结

在前面的内容中,我们带大家看了 vue 是如何设置侦听的。

主要是通过 observe来将数据设置为响应式。

五、数据响应式的核心模块

经过前面的学习,我们知道:

  1. Vue中每一个 data中都绑定了一个对应的依赖列表。
  2. Vue中每一次 渲染都对应着一个 Watcher实例,这个 Watcher实例上存储着可以执行这次渲染的方法。

Vue实现数据响应式就是通过数据访问时在数据对应的依赖列表中增加当前渲染的 Watcher实例,然后在数据修改时通知依赖中存储的 Watcher实例需要再次执行对应的渲染方法

其中的依赖和Watcher是单独的模块,我们这几会对这 2 个主要模块进行分析讲解。

5.1 Dep模块

Dep表示依赖类,每一个 data中都会绑定一个 Dep实例 dep。

在访问数据时,会调用dep上的 depend方法进行依赖收集。

5.1.1 Dep.Target

1
2
3
4
5
6
7
8
9
10
11
12
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}

export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
每一次渲染都对应着一个 Watcher实例,实际上渲染是 Watcher实例中的一部分Dep.target 是一个静态属性,用于存储当前正在执行渲染的 Watcher 实例

每次在渲染前都会调用 pushTarget 将 Dep.Target设置为当前执行渲染的 Watcher实例。

然后在渲染完成后会调用 popTarget 将 Dep.target 设置为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Watcher{
constructor(){
this.initRender();
}
initRender(){
// 渲染前设置Dep.target为当前 Watcher实例
pushTarget(this);
// 渲染
render()
// 渲染后设置Dep.target为空
popTarget();
}
}

所以在渲染时触发数据的getter逻辑收集依赖时可以通过 Dep.target获取当前渲染对应的 Watcher实例并添加到依赖数组中。

5.1.2 subs集合

Dep类维护了一个 subs集合,用于存储所有依赖于该属性的 Watcher实例。

每当用户访问数据时,因为可以通过 Dep.Target获取到本次渲染对应的 Watcher实例。

所以就可以将Watcher实例添加进 subs集合上,以便后续的使用。

5.1.3 depend方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//======================= Dep类====================
depend(info?: DebuggerEventExtraInfo) {
// 当前渲染对应的 watcher
if (Dep.target) {
Dep.target.addDep(this);
}
}

addSub(sub: DepTarget) {
this.subs.push(sub)
}
//======================= Watcher类================
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

在数据访问时,会调用数据对应的 Dep实例上的 depend方法,进而执行 Watcher实例上的 addDep方法。

而在Watcher实例上的 addDep方法中最终又会调用 Dep实例上的 addSub方法。

然后再在Dep实例上的subs集合上添加Watcher实例。

你可能会有疑问,为什么不直接地将当前 Watcher实例添加到 subs中呢,而是要绕这么一大圈呢?

dep中不会存在重复的 watcher,同时 watcher中不会存在相同的 dep

5.1.3.1 去重机制

通过 Watcher 的 addDep 方法,可以在 Watcher 级别进行去重。这样可以确保同一个 Dep 不会多次添加相同的 Watcher,从而避免不必要的重复依赖。

1
2
3
4
5
6
7
8
9
10
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
  • newDepIds 和 newDeps:这两个集合用于记录当前收集周期中新添加的依赖。newDepIds 是一个 Set,用于快速查找是否已经添加过某个 Dep;newDeps 是一个数组,用于存储新添加的 Dep 实例。
  • depIds:这是一个 Set,用于记录已经存在的依赖。如果 depId 不在 depIds 中,说明这是第一次添加该依赖,因此调用 dep.addSub(this) 将 Watcher 添加到 Dep 的 subs 集合中。

5.1.3.2 有哪些需要去重的场景

5.1.3.2.1 去重场景一:多次访问同一个响应式属性

当一个组件在同一个渲染周期内多次访问同一个响应式属性时,可能会导致重复添加 Watcher。例如,在模板中多次引用同一个数据属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{ message }}
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
}
};
</script>

在这个例子中,message 被引用了两次。如果不进行去重处理,每次访问 message 都会尝试添加一个新的 Watcher。

5.1.3.2.2 去重场景二:计算属性和侦听器的多重依赖

计算属性和侦听器(watch)可能会依赖多个响应式属性,如果这些属性在同一个渲染周期内被多次访问,也可能导致重复添加 Watcher。

1
2
3
4
5
6
7
8
9
10
11
12
13
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
watch: {
firstName() {
console.log('firstName changed');
},
lastName() {
console.log('lastName changed');
}
}

如果 fullName 计算属性在同一个渲染周期内被多次访问,firstName 和 lastName 会被多次依赖,如果没有去重处理,可能会导致重复的 Watcher。

5.1.3.2.3 去重场景三:组件的生命周期钩子

在组件的生命周期钩子中,如果多次访问同一个响应式属性,也可能会导致重复添加 Watcher。

1
2
3
4
mounted() {
console.log(this.message);
console.log(this.message);
}
5.1.3.2.4 去重场景四:动态组件和条件渲染

在动态组件和条件渲染中,如果同一个响应式属性在不同的条件分支中被多次访问,也可能导致重复添加 Watcher。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<component :is="currentComponent" />
<p v-if="showMessage">{{ message }}</p>
<p v-else>{{ message }}</p>
</div>
</template>

<script>
export default {
data() {
return {
currentComponent: 'ComponentA',
showMessage: true,
message: 'Hello, Vue!'
};
}
};
</script>

5.1.4 notify方法

notify 方法是 Dep 类中的一个重要方法,用于在数据变化时通知所有依赖于该数据的 Watcher 实例。

这个方法确保所有相关的 Watcher 都能接收到数据变化的通知,并触发相应的更新操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
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()
}
}

5.1.4.1 去掉空值

1
const subs = this.subs.filter(s => s) as DepTarget[]

这一步是为了确保 subs 列表中没有 null 或 undefined 的值。filter(s => s) 会过滤掉所有非真值的项。

5.1.4.2 排序订阅者(仅在开发模式下且未异步运行时)

1
2
3
4
5
6
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
  • DEV 是一个环境变量,表示当前是否处于开发模式。
  • config.async 是一个配置选项,表示是否异步执行更新。
  • 如果处于开发模式且未异步运行,subs 列表需要按 id 排序,以确保 Watcher 按正确的顺序触发更新。这是因为异步调度器会自动处理排序,而在同步模式下需要手动排序。

5.1.4.3 通知每个订阅者

1
2
3
4
5
6
7
8
9
10
11
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()
}
  • 遍历 subs 列表,逐个通知每个 Watcher。
  • 如果处于开发模式且提供了 info,调用 sub.onTrigger 方法,传递调试信息。
  • 最终调用 sub.update() 方法,触发 Watcher 的更新操作。

5.2 Watcher模块

5.2.1 将渲染函数挂载到实例上

在上一章节中,我们初次渲染时会实例化一个 watcher实例。

然后将渲染的方法传入 watcher实例中。

然后就会执行对应的渲染方法。

所以可以想到渲染的逻辑应该是写在了 Watcher类的构造器中。

1
2
3
4
5
6
7
8
9
10
11
// 初次渲染时 
new Watcher(渲染函数)


class Watcher{
constructor(渲染函数){
this.渲染函数 = 渲染函数;
// 执行渲染
this.渲染函数();
}
}

当然源码中实际上不仅仅这么简单。

1
2
3
4
5
6
7
8
9
10
11
// 初次渲染时 
new Watcher(渲染函数)


class Watcher{
constructor(渲染函数){
this.渲染函数 = 渲染函数;
// 执行渲染
this.渲染函数();
}
}

源码中将渲染函数挂载到了 getter上。

1
this.getter = expOrFn

然后在 get方法中调用 getter方法。

在 get方法中设置 Dep.target为当前的 watcher。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

5.2.2 update方法

在数据被访问时,会调用 dep.notify通知依赖需要更新渲染,然后调用 watcher.update。

然后会调用 run方法,最后调用 get方法重新执行渲染方法。

六、总结

这一篇中我们详细解释了数据响应式相关的原理。

一句话就是在数据访问时收集依赖,在数据被修改时更新依赖


本站总访问量