前言

心态爆炸的几天,为什么新年会这么心态爆炸QAQ

不过就算心态爆炸爆炸还是要学一下的,这篇介绍一下Vue3是怎么实现数据响应式的,Vue2的实现的方法我也写过,可以看这里:https://www.sakura-snow.com/archives/491

这篇来介绍一下Vue3的数据响应式,我们会使用我们自己手写的effectreactive来模拟(因为源码不是那么好看,不过也会放一下源码)

文档:https://github.com/vuejs/docs-next

vue3:https://github.com/vuejs/vue-next

reactive和effect的实现

我们用下面的代码来进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let { reactive, effect, computed } = VueReactivity;
let obj = { name: 'Sakura', age: 16, address: { a: 1 }, arr: [1, 2, 3,4,5] }
let state = reactive(obj);
effect(function fn1(){
app.innerHTML =
`
name:${state.name}
age:${state.age}
arr:${JSON.stringify(state.arr)}
`
});
setTimeout(() => {
state.age++;
state.name = "Sena";
state.arr[10] = "10"
}, 1000);

使用下面是reactive

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
import {isObject} from "../shared";
import {mutableHandlers} from "./baseHandlers";

export const reactive = (target: object) => {
// 把对象变成响应式对象
return createReactiveObject(target, mutableHandlers);
}
// 这个map用于缓存已经变成了响应式的对象
const reactiveMap = new WeakMap();
function createReactiveObject(target, baseHandler) {
// 如果这个target 是一个对象
if (!isObject(target)) {
// 不是对象直接返回即可
return target;
}
// 如果对象已经被代理过了,就不要再次代理了
let existProxy = reactiveMap.get(target);
if (existProxy) {
// 直接返回上一次的代理
return existProxy;
}
const proxy = new Proxy(target, baseHandler); // 创建代理对象
reactiveMap.set(target, proxy); // 缓存代理对象
return proxy;
}

这里的mutableHandlers是从外部导入的

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
import {trigger} from "./effect";

export const mutableHandlers = {
// 目标原对象 属性代理后的对象
get(target, key, receiver) { // 内置的 proxy中get和set参数是固定的
let res = Reflect.get(target, key, receiver);
if (typeof key == 'symbol') { // 内置的symbol 不进行依赖收集
return res;
}
// 依赖收集
track(target, key); // 属性和 effect之间做一个关联
if (res && res.__v_isRef) {
return res.value;
}
return isObject(res) ? reactive(res) : res; // target[key]
}, // 当取值的时候 应该将effect 存储起来
set(target, key, value, receiver) {
const oldValue = target[key]; // 上一次的结果
const hadKey = isArray(target) && (parseInt(key, 10) == key) ?
Number(key) < target.length :
hasOwn(target, key);
let result = Reflect.set(target, key, value, receiver);
// 调用push方法 会先进行添加属性 在去更新长度(这次长度更新是没有意义的)
if (!hadKey) {
trigger(target, 'add', key, value); // 触发新增操作
} else if (hasChange(oldValue, value)) {
trigger(target, 'set', key, value);
}
// 设置一般分为两种一种是添加新的属性,还有种是修改属性
return result
} // 当设置值的时候 应该通知对应的effect来更新
}

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
import {trigger} from "./effect";

export const mutableHandlers = {
// 目标原对象 属性代理后的对象
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
// 内置的symbol不进行依赖收集
// 因为对数组进行操作时会触发很多内置的Symbol
if (typeof key == 'symbol') {
return res;
}
// 依赖收集
track(target, key); // 让属性和effect之间做一个关联
// 检测结果是不是是ref()函数创建的
if (res && res.__v_isRef) {
return res.value;
}
// 如果结果是一个对象,对这个对象进行观测
return isObject(res) ? reactive(res) : res;
},
// 当取值的时候 应该将effect 存储起来
set(target, key, value, receiver) {
const oldValue = target[key]; // 上一次的结果
const hadKey = isArray(target) && (parseInt(key, 10) == key) ?
Number(key) < target.length :
hasOwn(target, key);
// 设置值
let result = Reflect.set(target, key, value, receiver);
// 触发保存的effect
if (!hadKey) {
trigger(target, 'add', key, value); // 触发新增操作
} else if (hasChange(oldValue, value)) {
trigger(target, 'set', key, value);
}
return result
}
}

Vue3的大致思路和Vue2是一样的,核心就是

  • getter中收集依赖
  • setter中触发依赖

effect设置的函数就是依赖,

然后是track函数,这个函数可以用于将effectproxy对象的属性关联起来

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
export const effect = (fn: Function, options: any = {}) => {
// 需要让传递来的fn 变成响应式的effect,数据有变化 这个fn就能重新执行
// fn是用户传递的函数
const effect = createReactiveEffect(fn, options);

// 计算属性会用到这个属性
if (!options.lazy) {
effect();
}

return effect;
}

export let effectStack = [];
export let activeEffect = null;
let id = 0;

// 创建effect
function createReactiveEffect(fn: Function, options): () => void {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect);
activeEffect = effect;
return fn(); // 让函数执行, 会执行取值逻辑. 在取值逻辑中可以和effect做关联
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]
}
}

}
effect.id = id++;
effect.options = options;
return effect;
}

然后是track函数,effect只会把用户传入的函数设置在effectStackactiveEffect上,而track函数可以把activeEffect上的函数与proxy对象建立联系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 保存 某个对象中的  某个属性 依赖了 哪些effect
const targetMap = new WeakMap();

// 建立属性 和 effect之间的关联
export function track(target, key) {
if (activeEffect == undefined) {
return;
}
// effect映射表
// 对象 -> effect
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 属性 -> effect
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}

trigger函数用于触发proxy对象上的依赖函数

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

const run = (effects) => {
if (effects) effects.forEach(effect => {
if (effect.options.scheduler) { // 计算属性
effect.options.scheduler(effect);
} else {
// 正常情况
effect();
}
});
}

export function trigger(target, type, key, value?) {
const depsMap = targetMap.get(target);
if (!depsMap) {// 属性变化了 但是没有依赖 直接跳过即可
return;
}
// 修改
if (key == 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
// 只要改了length就触发
// 如果改变了`数组长度`那么一定要更新小于改变的长度取值的长度
// 如果你修改的是长度 正好内部也对长度进行了收集 长度也要触发(情况1)
if (key == 'length' || key >= value) {
run(dep)
}
});
} else {
if (key !== undefined) { // 如果有收集过就触发
let effects = depsMap.get(key);
run(effects)
}

switch (type) {
case 'add': // 数组添加属性时需要触发length(情况2)
if (isArray(target)) {
if (parseInt(key) == key) {
run(depsMap.get('length'));
}
}
break;
default:
break;
}
}
}

情况1和情况2对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 情况1
const state = reactive([1,2,3]);
effect(() => {
app.innerHTML = `
${state[2]}
`
});
setTimeout(() => {
state.length = 1;
}, 1000)

// 情况2
const state = reactive([1,2,3]);
effect(() => {
app.innerHTML = `
${state}
`
});
setTimeout(() => {
state[10] = 1;
}, 1000)

计算属性的实现

实现和Vue2大致相同,Vue3把计算属性当成一个依赖,然后在渲染时收集到计算属性的effect,在计算属性的值更新时就会触发对应的effect

另外计算属性本身也依赖一些属性,这些属性会在计算值的时候进行收集

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
import {effect, track, trigger} from "./effect";
import {isFunction} from "../shared";

class ComputedRefImpl {
public effect;
public __v_isReadonly = true;
public readonly __v_isRef = true;
public _dirty = true;
private _value;
public setter;

constructor(getter, setter) {
this.setter = setter;
// 默认getter执行的时候会依赖于一个effect, 而计算属性默认就是一个effect
this.effect = effect(getter, {
lazy: true, // 标识lazy为true的时候不会执行
scheduler: () => { // 依赖的值变化会执行scheduler方法
this._dirty = true; // 依赖的值变化了 变成脏值
trigger(this, 'set', 'value');
}
});
}

get value() { // 类属性描述器
if (this._dirty) { // 缓存
// 收集计算属性依赖于哪些项
// this.effect会把当前计算属性的getter执行一遍
// 然后依赖项就可以收集到计算属性的effect
// 当依赖项更新时,就会触发scheduler方法
// scheduler方法会通过trigger触发依赖了计算属性的effect
this._value = this.effect();
// 收集哪些effect依赖了计算属性
track(this, 'value'); // 取值时收集依赖 {this:{'value':[effect]}
this._dirty = false;
}
return this._value
}

set value(newValue) {
// 调用用户的set即可
this.setter(newValue)
}
}

export function computed(getterOrOptions) {
// 分别拿到get和set
let getter;
let setter;

// 规范化
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {
console.log('computed not set value')
}
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}

ref实现

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
import {reactive} from "./reactive";
import {hasChange, isArray, isObject} from "../shared";
import {track, trigger} from "./effect";

const convert = (val) => isObject(val) ? reactive(val) : val

class RefImpl {
public _rawValue;
public readonly __v_isRef = true;
public _value;

constructor(public rawValue) { // rawValue类型
this._rawValue = rawValue;
this._value = convert(rawValue)
}

get value() { // 属性访问器 这里新增了value属性
track(this, 'value'); // {this:{value:[effect]}} depend
return this._value;
}

set value(newValue) {
if (hasChange(newValue, this._rawValue)) { // 如果值有变化再去触发更新,如果值没发生变化 就不要再次触发更新了
this._rawValue = newValue
this._value = convert(newValue);
trigger(this, 'set', 'value'); // notify
}
}
}

export function ref(rawValue) {
return new RefImpl(rawValue)
}

class ObjectRefImpl {
constructor(public _object, public _key) {

}

// 代理操作
// 和vue2把this._data代理到vm上一个意思
get value() {
return this._object[this._key]; // 读取的是原值 (原来的值都不是响应式 那不能是响应的)
}

set value(newValue) {
this._object[this._key] = newValue
}
}

export function toRefs(object) { // object可能是数组

const result = isArray(object) ? new Array(object.length) : {}

for (let key in object) {
result[key] = new ObjectRefImpl(object, key);
}

return result
}

详细的注释过两天再写了orz

后记

还没写完)有空再补