带你一行一行手写Vue3.0源码系列(一) -响应式数据原理
“TM的现在也太卷了,上来就问小明:你能说下reactive的实现原理吗?”
小明哭丧着脸!Vue2还没搞明白呢,现在给我整vue3,有没有搞错哦!
现在面试原理很正常啊,都2023年了不会还没关注源码系列内容吧?
小明又挠了挠头,关注是关注了,一个文件几千行的代码表示看不懂了。

正文
1. Composition API
大家都知道Vue2.0
的响应式是通过Object.defineProperty()
,但是只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历,并且无法检测数组的变化。而进入3时代尤大大居然想到了使用Es6
语法中的Proxy
,从而大大优化了上述问题。
import { reactive } from 'Vue';
const state = reactive({
name: '小明',
age: 108, // 虽然是90后,但是心智已过百 卷...
})
使用过Vue3
的这段代码想必是非常熟悉了,不在是老套的将响应式数据写在data
中了,类似React
中的Hooks
,使用更加灵活。并且同时它还给我们提供了其他几个带有优化功能的Api。
名称 | 功能 |
---|---|
reactive | 定义响应式变量,仅支持对象、数组、Map、Set等集合类型有效。对String、number、boolean、等原始类型无效 |
shallowReactive | 与reactive的区别就是该Api只对对象第一层数据进行响应式 |
readonly | 入参和reactive相同,整个对象只读无法进行修改 |
shallowReadonly | 只是第一层是只读的 |
2. 暴露出响应式API
// reactivity/reactive.js
export function reactive(target) { // target为目标对象
return createReactiveObj(target, false, reactiveHandlers)
}
export function shallowReactive(target) { // target为目标对象
return createReactiveObj(target, false, shallowReactiveHandlers)
}
export function readonly(target) { // target为目标对象
return createReactiveObj(target, true, readonlyHandlers)
}
export function shallowReadonly(target) { // target为目标对象
return createReactiveObj(target, true, shallowReadonlyHandlers)
}
在源码中采用的是高阶函数柯里化,因为这四个API
的功能实现上大差不差,柯里化可以通过不同的参数来进行不同的处理,提供公共的方法。所以我这里我们只需要考虑两点:
- 是不是只读的(readonly)
- 是不是浅层响应式数据(shallow)
// reactivity/reactive.js
// 用来存储已经响应式的数据,防止重复代理
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();
// target目标对象 isReadonly是不是只读的 baseHandlers为proxy的配置属性参数
function createReactiveObj(target, isReadonly, baseHandlers) {
// target必须是一个对象
if (!isObject(target)) {
return target;
}
// 优化已经被代理的对象
const proxymap = isReadonly ? readonlyMap : reactiveMap;
const proxyEs = proxymap[target];
// 已存在直接返回代理数据
if (proxyEs) {
return proxyEs;
}
// 使用proxy对目标对象进行代理
const proxy = new Proxy(target, baseHandlers);
proxymap.set(target, proxy);
return proxy;
}
小明很开心😀,这段代码我看懂了:“ 这里是通过isReadonly
参数的不同来创建代理对象proxy
。如果传入的目标对象已经被代理过就会被缓存 ”
面试官: ” 那为什么这里要使用WeakMap
而不是用Map
“
小明又暗自窃喜🤭,之前刚好看过这个。因为WeakMap
数据结构的key
可以是一个引用类型的对象,并且可以被自动垃圾回收。
// reactivity/baseHandlers.js
// 这里是上文中各个不同API的proxy的配置对象
export const reactiveHandlers = {
get: get,
set: set
}
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet
}
export const readonlyHandlers = { // readonly的不可进行修改,所以这里直接给出警告
get: readonlyGet,
set: (target, key) => {
console.warn('is readonly');
return true;
}
}
export const shallowReadonlyHandlers = { // readonly的不可进行修改,所以这里直接给出警告
get: shallowReadonlyGet,
set: (target, key) => {
console.warn('is readonly');
return true;
}
}
// reactive/baseHandlers.js
const get = createGetter(); // 不是只读也不是前层次的
const shallowGet = createGetter(false, true); // 不是只读 是浅层次的
const readonlyGet = createGetter(true); // 只读 深的
const shallowReadonlyGet = createGetter(true, true); // 只读 浅层次的
const set = createSet();
const shallowSet = createSet(true); // 浅层次的
小明:“ 哦!这里又和上面一样吧,又是区分参数来实现函数柯里化吧! 通过是不是只读的和是不是浅层次的来创建不同的get函数
和set函数
”
// reactivity/baseHandlers.js
// 创建get函数来进行依赖来进行依赖收集
// isReadonly只读的 shallow是不是浅层次
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
if (shallow) { // 浅层次的直接返回,不做深层次的递归
return res;
}
if (isObject(res)) { // vue3性能优化 懒代理 只有访问了才进行递归深层次的代理
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
// 收集依赖
Track(target, TrackOpTypes.GET, key);
}
return res;
}
}
// 拦截设置功能
function createSet(shallow = false) {
return function set(target, key, value, receiver) { // target目标对象 key属性 value修改的新值
// 获取到老值
const oldValue = target[key];
// 1.数组还是对象 2.添加值 还是 修改值
const hadKey = isArray(target) && (isIntergetKey(key) ? Number(key) < target.length : hasOwn(target, key));
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) { // 新增
// 触发更新
trigger(target, TriggerOpTypes.ADD, key, value);
} else if (hasChange(value, oldValue)) { // 修改
// 触发更新
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return result;
}
}
createGetter
: 函数主要是完成了对浅层次的剔除,不做深一层的递归代理。同时如果是只读类型的,函数就递归又调用了一边readonly
Api,对响应式的数据使用reactive
递归代理。
面试官问小明:“ 这段代码里面有什么优化的操作吗? ”
小明: “ 额.....没看出来😅 ”
面试官:“ 让我来告诉你吧!这里就是proxy
比Object.defineProperty
好的地方。之前在Vue2.0中一上来就对data中属性进行递归遍历,不管是用到的还是没用到的。而proxy
是对对象进行代理,如果我们没有用到深层次的数据,他就不会进行递归代理,只有我们用到了才进行递归,明白了吗? ”
小明:“ 那是不是就是Object.freeze()
的意思啊? ”
面试官:“ 666 ”
3. Effect观察者
在讲依赖收集之前,大家要先知道一个effect
概念。effect
有点类似vue2.0
中的watcher
监听者,为了解决vue2的问题,依赖收集(即添加观察者/通知观察者)模块单独出来,就是现在的effect
。我们先使用一下effect。
import { effect } from 'Vue';
<script>
export default{
setup(){
// effect接受两个参数 一个是函数,一个配置对象
effect(() => {
console.log('小明今年高寿啊!') // 哈哈哈...
})
}
}
</script>
我们发现页面一进入就打印出了“ 小明今年高寿啊! ”。当然effect也可以不立即执行,只需要添加配置参数lazy: true
。
// reactivity/effect.js
// effect函数接口一个函数fn和配置对象options
export function effect(fn, options = {}) {
const effect = createReactiveEffect(fn, options);
if (!options.lazy) { // 如果lazy不为true就立即执行
effect();
}
return effect;
}
let uid = 0; // 创建effect的自增uid,类似于vue2.0中的每个watcher都有一个id
let activeEffect; // 保存当前的effect,类似于vue2.0中的Dep.target
const effectStack = []; // effect存储栈
function createReactiveEffect(fn, options = {}) {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) {
try {
activeEffect = effect; // 当前依赖收集的effect
effectStack.push(activeEffect); // 入栈
return fn(); // 执行用户的方法并返回值
} finally {
// 出栈
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]; // 重置当前effect
}
}
}
effect.id = uid++; // 用于区别effect
effect._isEffect = true; // 用户区分我们effect是不是响应式的
effect.raw = fn; // 保存用户的方法
effect.options = options; // 保存用户的属性
return effect;
}
effect
: 函数主要用来生成/处理/追踪reactiveEffect数据,主要是收集数据依赖(观察者),通知收集的依赖(观察者)。
小明:“ 我去官网看了下,好像说是这个effect
不是给开发者用的 ”
面试官::“ 是的呢!这个函数主要是给作者用的。 ”
4. Track依赖收集
上文中在proxy
的get
方法中使用了Track
进行依赖收集,依赖收集的数据一个是key
为target
目标对象,value是一个key
为目标对象的属性value为收集到的effect的Set
。
数据结构:
WeakMap(target, Map(key, Set(effect)))
// reactivity/effect.js
// 收集effect
let targetMap = new WeakMap(); // 用于存放effect的map
export function Track(target, type, key) {
if (!activeEffect) return // 如果当前没有effect直接结束
let depMap = targetMap.get(target);
if (!depMap) {
targetMap.set(target, (depMap = new Map())); // 根据当前target获取depMap,如果没有就新增一个Map
}
let dep = depMap.get(key);
if (!dep) {
depMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect); // 收集effect
}
}
5. trigger派发更新
当我们修改一个对象中的key时,就去刚刚的targetMap
中去查找依赖的effect
并执行。trigger
主要功能是通知target[key](将观察者队列函数一一取出来执行)。
// reactivity/effect.js
// 触发更新
// target 目标对象 type可以为SET ADD DELETE key:属性key
export function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let effects = new Set(); // 触发的effect 需要进行去除
const add = (effectAdd) => {
if (effectAdd) {
effectAdd.forEach(effect => {
effects.add(effect);
})
}
}
add(depsMap.get(key)); // 获取当前属性的effect
if (key === 'length' && isArray(target)) { // 如果修改数组的长度length,需要做处理
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newValue) {
add(dep)
}
})
} else {
if (key !== undefined) {
add(depsMap.get(key));
}
switch (type) {
case TriggerOpTypes.ADD: {
if (isArray(target) && isIntergetKey(key)) { // 如果数组新增索引需要处理数组length
add(depsMap.get('length'))
}
break;
}
}
}
effects.forEach(effect => { // 遍历执行effect栈
if (effect.options.scheduler) {
effect.options.scheduler(effect);
} else {
effect(); // 执行effect
}
})
}
小明:“ 相比之下这种里面Map
来存储关联的effect
比vue2.0
中在目标对象上添加一个_ob_
来实现容易理解的多。 ”
6. 使用effect
面试官:“ 到此为止,我们的Vue3.0的响应式源码已经完毕,让我们来测试下其功能。 ”
小明: “哇,”
<div id="app"></div>
<button id="age">长大了</button>
import { effect, reactive } from './reactivity/index';
let state = reactive({
name: '小明',
age: 108,
});
effect(() => { // 这里代替页面上使用了state.age值
document.getElementById("app").innerHTML = state.age;
})
document.getElementById("age").onclick = function () {
state.age++
}
7. 目录结构
这里时我写源码的基本目录结构:
- reactivity
-
- reactive.js // 这里列出了常用api(relative, shallowRealtive, readonly)
-
- effect.js // 这里列出了依赖收集和派发更新方法
-
- baseHandlers.js // baseHandlers 中主要包含四种proxy的配置对象
-
- operations.js // 这里列出了一项枚举值
- shared
-
- index.js // 这里时工具方法,比如:isObject, isArray
小结
至此Vue3.0
的响应式数据原理已经完结 大家可以试着自己动手写一遍核心代码哈,本文主要列出了一个核心功能点,其中不乏出错的地方,望请见谅!至于修改数据如何更改试图,后续我还会更新,目前只是用了个小案例测试一下。
如果觉得本文有帮助 记得点赞三连哦 十分感谢!
转载自:https://juejin.cn/post/7206670501199986747