技术咨询、项目合作、广告投放、简历咨询、技术文档下载
点击这里 联系博主
# Vue3响应式原理
声明:文章大部分内容转载自:Vue3 源码入门,实现简易版 reactivity (opens new window);其中加上了部分个人的理解
# 前言
Vue 3.0 中的 reactive
和 Vue 2.6 中提供的一个全局 API Vue.observable
相同,都是用于让一个对象可响应,首先来对比一下他们之间的差异:
reactive | Vue.observable |
---|---|
基于 Proxy 实现 | 基于 Object.defineProperty 实现 |
对 代理对象 进行操作 | 直接操作 源对象 |
返回一个可响应的 代理对象 | 返回一个可响应的 源对象 |
# reactivity 工作流程
在 Vue 3.0 中, reactivity
被独立出来,没有任何依赖,可以用于任何想做响应式数据的地方
先抛开源码中实现的复杂判断,来看一下他的主要工作流程
# 关键方法实现
本文主要实现了其中几个关键的方法
# reactive
- 创建响应式对象,在
Proxy
中定义get
及set
捕获器,对传入的 源对象 的 代理对象 进行拦截处理 get
捕获到当前对象的属性也是对象,要进行递归- 定义基于
WeakMap
的reactiveMap
管理代理对象,如果传入的object
已经有记录,直接返回 此对象 的 代理对象,如果没有,按照正常流程走
/**
* 处理器对象,定义捕获器
*/
const baseHandlers = {
set(target, key) {
Reflect.set(...arguments);
// 依赖触发
trigger(target, key);
},
get(target, key) {
// 依赖收集
track(target, key);
return typeof target[key] === "object"
? reactive(target[key])
: Reflect.get(...arguments);
},
};
/**
* 定义响应式对象,返回proxy代理对象
* @param {*} object
*/
function reactive(object) {
if (reactiveMap.has(object)) return reactiveMap.get(object);
const proxy = new Proxy(
object,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
reactiveMap.set(object, proxy);
return proxy;
}
# handles 的类型
在对象类型中,将Object
和Array
与Map
,Set
, WeakMap
,WeakSet
区分开来了。它们调用的是不同的Proxy Handle
。
baseHandlers.ts
:Object
&Array
会调用此文件下的mutableHandlers
对象作为Proxy Handle
。collectionHandlers.ts
:Map
,Set
,WeakMap
,WeakSet
会调用此文件下的mutableCollectionHandlers
对象作为Proxy Handle
。
/**
* 对象类型判断
* @lineNumber 41
*/
function targetTypeMap(rawType: string) {
switch (rawType) {
case "Object":
case "Array":
return TargetType.COMMON;
case "Map":
case "Set":
case "WeakMap":
case "WeakSet":
return TargetType.COLLECTION;
default:
return TargetType.INVALID;
}
}
会在new Proxy
的根据返回的targetType
判断。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
# effect
- 副作用,创建用于管理
effect
的栈effectStack
,将effect
先入栈用于依赖收集,执行一次该effect
,进入get
捕获阶段,捕获完毕之后进入finally
将其在栈中移出
vue 源码: effect.ts (opens new window)
- computed (opens new window) 是使用effect实现的
- watch (opens new window) 是使用effect实现的
- 各类声明周期 (opens new window)并不会使用effect;
/**
* 副作用函数
* @param {*} fn
*/
function effect(fn) {
try {
// 将需要执行的effect入栈,用于依赖收集过程中与key的关系对应
effectStack.push(fn);
// 执行该effect,进入proxy的get拦截
return fn();
} finally {
// 依赖收集完毕及所有get流程走完,当前effect出栈
effectStack.pop();
}
}
# track
effect
执行后数据触发get
捕获器, 在此过程中调用track
进行依赖收集- 定义
targetMap
,以WeakMap
的方式收集依赖,管理目标对象target
及其对应的key
- 第二层用于管理
key
及其对应的effect
,上面流程图可以看到数据的结构和层次划分
vue 源码: effect.ts (opens new window)
/**
* 依赖收集
* @param {*} target
* @param {*} key
*/
function track(target, key) {
// 初始化依赖Map
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 第二层依赖使用Set存放key对应的effect
let dep = depsMap.get(key);
if (!dep) {
targetMap.get(target).set(key, (dep = new Set()));
}
// 取当前栈中的effect存入第二层依赖中;
const activeEffect = effectStack[effectStack.length - 1];
activeEffect && dep.add(activeEffect);
}
为什么取
effectStack[effectStack.length - 1]
就可以保证当前对象一定在effect
函数中执行呢?
原因:
- 因为代码按照顺序执行,当执行到
effect(()=>{})
函数的时候,effect
会把函数压入堆栈末尾,并执行对应的函数; - 当执行到
effect
函数中的代码,回去对应的值的时候会触发proxy
的get
,并进行依赖收集 - 此时
effectStack
末尾的 effect 函数就是依赖于当前对象的
# trigger
- 修改在
effect
中指定过的内容时会触发set
捕获器,在此过程中trigger
负责执行当前target
下key
对应的effect
,完成响应式的过程
Vue3源码: trigger (opens new window)
/**
* 触发响应,执行effect
* @param {*} target
* @param {*} key
*/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (depsMap) {
const effects = depsMap.get(key);
effects && effects.forEach((run) => run());
}
}
# computed
- 这里采用简单粗暴的方式,直接返回一个
effect
/**
* 计算属性
* @param {*} fn
*/
function computed(fn) {
return {
get value() {
return effect(fn);
},
};
}
# 效果展示
# 对象属性响应式,多层嵌套
const object = {
o: {
a: 1,
},
};
const proxy = reactive(object);
effect(() => {
console.log(`proxy.o.a: ${proxy.o.a}`);
});
- 首次调用打印一次,重新赋值后再次响应,调用
effect
# 响应式调色器
- 配置响应式对象,指定其 rgb 属性,顺便测一下
computed
const object = {
r: 0,
g: 0,
b: 0,
};
const proxy = reactive(object);
const computedObj = computed(() => proxy.r * 2);
effect(() => {
const { r, g, b } = proxy;
document.getElementById("r").value = r;
document.getElementById("b").value = b;
document.getElementById("g").value = g;
document.getElementById(
"color"
).style.backgroundColor = `rgb(${r},${g},${b})`;
document.getElementById("color_text").innerText = `rgb:${r},${g},${b}`;
const { value } = computedObj;
document.getElementById(
"computed_text"
).innerText = `computed_text: r*2=${value}`;
});
- 拖动 rgb 3 个 range 时各自的变化会体现在颜色块上
- 对象依赖关系
targetMap
结构如下
- reactiveMap 结构如下
# 总结
- 通过上述内容可以了解到 Vue 3.0 中的响应式原理
reactive
创建响应式对象effect
副作用,调用自身收集依赖,数据变更后重新调用该函数track
依赖收集trigger
触发依赖中对应的effect
computed
计算属性,对应属性值变更调用其effect
- 过程中还能更加熟悉一些前置基础知识
- 代理与反射:
Proxy
Reflect
- js 标准内置对象:
WeakMap
Map
Set
- 语句执行使用技巧:
try
finally
- 代理与反射:
# 参考资料
- 完整代码传送门 (opens new window)
- Vue 3.0 源码 (opens new window)
- Proxy (opens new window)
- Reflect (opens new window)
- WeakMap (opens new window)
- Map (opens new window)
- Set (opens new window)
- 语句执行 (opens new window)
声明:文章大部分内容转载自:Vue3 源码入门,实现简易版 reactivity (opens new window);其中加上了部分个人的理解
- 本文链接: https://mrgaogang.github.io/vue/deep/Vue3%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!