vue3 深入浅出(连载三)手摸手实现 vue3
大家好,我是Mokou,最近一直在做 vue3 相关内容,比如源码解析和mini-vue3的开发。
回顾下前几章的内容,在前几章中主要讲述了以下内容。
- 新构建工具
vite的原理和从零开始实现 vue3使用新姿势- 新api:
reactive使用和源码解析 - 追踪收集
track实现和源码解析 - 追踪触发器
trigger实现和源码解析 - 响应式核心
effect与track、trigger工作原理和源码解析
好的,这章的目标:从零开始完成一个 Vue3 !
必须要知道的前置知识 effect 与 track、trigger 工作原理,具体详情请看公众号 -> 前端进阶课,一个有温度且没有广告的前端技术公众号。
在这里还是简单解析下这3个函数的作用吧
- track: 收集依赖,存入
targetMap - trigger:触发依赖,使用
targetMap - effect:副作用处理
本章源码请看 uuz 急需 star 维持生计。
手摸手实现 Vue3
首先。我们2个全局变量,用来存放和定位追踪的依赖。优秀的代码呼之欲出。
let targetMap = new WeakMap();
let activeEffect;
然后优秀的打工仔一号出来了,那就是 effect,还记得该打工仔的api在vue3中是如何调用的吗?
effect(() => {
console.log('run cb')
})
该打工仔接收一个回调函数,然后会被送给 track干苦力。所以我们可以这么完成 effect
- 定义一个内部函数
_effect,并执行。 - 返回一个闭包
而内部 _effect 也做了两件事
- 将自身赋值给
activeEffect - 执行
effect回调函数
优秀的代码呼之欲出。
function effect(fn) {
// 定义一个内部 _effect
const _effect = function(...args) {
// 在执行是将自身赋值给 activeEffect
activeEffect = _effect;
// 执行回调
return fn(...args);
};
_effect();
// 返回闭包
return _effect;
}
然后出现的是另一个优秀的打工仔 track,还记得该track在vue3是如何调用的吗?
track(obj, 'get', 'x');
该打工仔会去找 obj.x 是否被追踪,如果没找到就将obj.x放入targetMap(完成追踪任务),然后让当前最优秀的打工仔 activeEffect 制定工期(将obj.x作为map的key将activeEffect作为map的value)。
优秀的代码呼之欲出。
function track(target, key) {
// 首先找 obj 是否有被追踪
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果没有被追踪,那么添加一个
targetMap.set(target, (depsMap = new Map()));
}
// 然后寻找 obj.x 是否被追踪
let dep = depsMap.get(key);
if (!dep) {
// 如果没有被追踪,那么添加一个
depsMap.set(key, (dep = new Set()));
}
// 如果没有添加 activeEffect 那么添加一个
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}
然后出现的是最后一个优秀的打工仔 trigger,还记得trigger在vue是如何调用的吗?
trigger(obj, 'set', 'x')
其实该打工仔只会去 targetMap 中寻找obj.x的追踪任务,如果找到了就去重,然后执行任务。
优秀的代码呼之欲出。
function trigger(target, key) {
// 寻找追踪项
const depsMap = targetMap.get(target);
// 没找到就什么都不干
if (!depsMap) return;
// 去重
const effects = new Set()
depsMap.get(key).forEach(e => effects.add(e))
// 执行
effects.forEach(e => e())
}
打工仔都就绪了。然后出现了优秀的包工头 reactive,也就是对象式响应式的api。还记得vue3中如何使用 reactive 吗?
<template>
<button @click="appendName">{{author.name}}</button>
</template>
setup() {
const author = reactive({
name: 'mokou',
})
const appendName = () => author.name += '优秀';
return { author, appendName };
}
通过上面的的优秀代码,很轻易的实现了vue3的响应式操作。通过回顾前几章的内容,我们知道 reactive 是通过 Proxy 代理数据实现的,而优秀的包工头只需要看看工地,然后让优秀的打工仔干活就可以了。
优秀的代码呼之欲出。
export function reactive(target) {
// 代理数据
return new Proxy(target, {
get(target, prop) {
// 执行追踪
track(target, prop);
return Reflect.get(target, prop);
},
set(target, prop, newVal) {
Reflect.set(target, prop, newVal);
// 触发effect
trigger(target, prop);
return true;
}
})
}
好了。一切就绪,那么我们挂载下我们的 fake vue3 吧
export function mount(instance, el) {
effect(function() {
instance.$data && update(el, instance);
})
instance.$data = instance.setup();
update(el, instance);
}
function update(el, instance) {
el.innerHTML = instance.render()
}
用 mini-vue3 写一个 demo
测试一下。参照 vue3 的写法。定义个 setup 和 render。
const App = {
$data: null,
setup () {
let count = reactive({ num: 0 })
setInterval(() => {
count.num += 1;
}, 1000);
return {
count
};
},
render() {
return `<button>${this.$data.count.num}</button>`
}
}
mount(App, document.body)
执行一下,果然是优秀的代码。响应式正常执行,每次 setInterval 执行后,页面都重写刷新了 count.num 的数据。
源码请看 uuz,ps:7月23日该源码已经支持 jsx 了。
以上通过 50+行代码,轻轻松松的实现了 vue3的响应式。但这就结束了吗?
还有以下问题
Proxy一定需要传入对象render函数 和h函数并正确(Vue3的h函数现在是2个不是以前的createElement了)- 虚拟 dom 的递归
- 别再说了
- -!,我不听。
ref
使用 reactive 会有一个缺点,那就是,Proxy 只能代理对象,但不能代理基础类型。
如果你调用这段代码 new Proxy(0, {}),浏览器会反馈你 Uncaught TypeError: Cannot create proxy with a non-object as target or handler
所以,对于基础类型的代理。我们需要一个新的方式,而在 vue3 中,对于基础类型的新 api 是 ref
<button >{{count}}</button>
export default {
setup() {
const count = ref(0);
return { count };
}
}
实现 ref 其实非常简单:利用 js 对象自带的 getter 就可以实现
举个栗子:
let v = 0;
let ref = {
get value() {
console.log('get')
return v;
},
set value(val) {
console.log('set', val)
v= val;
}
}
ref.value; // 打印 get
ref.value = 3; // 打印 set
那么通过前面几章实现的 track 和 trigger 可以轻松实现 ref
直接上完成的代码
function ref(target) {
let value = target
const obj = {
get value() {
track(obj, 'value');
return value;
},
set value(newVal) {
if (newVal !== value) {
value = newVal;
trigger(obj, 'value');
}
}
}
return obj;
}
computed
那么该怎么实现 computed?
首先:参考 vue3 的 computed 使用方式
let sum = computed(() => {
return count.num + num.value + '!'
})
盲猜可以得到一个想法,通过改造下 effect 可以实现,即在 effect 调用的那一刻不执行 run 方法。所以我们可以加一个 lazy 参数。
function effect(fn, options = {}) {
const _effect = function(...args) {
activeEffect = _effect;
return fn(...args);
};
// 添加这段代码
if (!options.lazy) {
_effect();
}
return _effect;
}
那么 computed 可以这么写
- 内部执行
effect(fn, {lazy: true})保证computed执行的时候不触发回调。 - 通过对象的
getter属性,在computed被使用的时候执行回调。 - 通过
dirty防止出现内存溢出。
优秀的代码呼之欲出:
function computed(fn) {
let dirty = true;
let value;
let _computed;
const runner = effect(fn, {
lazy: true
});
_computed = {
get value() {
if (dirty) {
value = runner();
dirty = false;
}
return value;
}
}
return _computed;
}
那么问题来了 dirty 在第一次执行后就被设置为 false 如何重置?
此时 vue3 的解决方法是,给 effect 添加一个 scheduler 用来处理副作用。
function effect(fn, options = {}) {
const _effect = function(...args) {
activeEffect = _effect;
return fn(...args);
};
if (!options.lazy) {
_effect();
}
// 添加这行
_effect.options = options;
return _effect;
}
既然有了 scheduler 那就需要更改 trigger 来处理新的 scheduler。
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = new Set()
depsMap.get(key).forEach(e => effects.add(e))
// 更改这一行
effects.forEach(e => scheduleRun(e))
}
// 添加一个方法
function scheduleRun(effect) {
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect);
} else {
effect();
}
}
然后,把上面代码合并一下,computed 就完成了
function computed(fn) {
let dirty = true;
let value;
let _computed;
const runner = effect(fn, {
lazy: true,
scheduler: (e) => {
if (!dirty) {
dirty = true;
trigger(_computed, 'value');
}
}
});
_computed = {
get value() {
if (dirty) {
value = runner();
dirty = false;
}
track(_computed, 'value');
return value;
}
}
return _computed;
}
总结
- reactive 的核心是
track+trigger+Proxy - ref 是通过对象自有的
getter和setter配合track+trigger实现的 - computed 其实是一个在
effect基础上的改进
下章内容:vue3 该怎么结合 jsx?
最后
原创不易,给个三连安慰下弟弟吧。
- 源码请看 uuz
- 本文内容出自 https://github.com/zhongmeizhi/FED-note
- 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。回复
全栈或Vue有好礼相送哦

浙公网安备 33010602011771号