大家好,我是Mokou,最近一直在做 vue3 相关内容,比如源码解析和mini-vue3的开发。
回顾下前几章的内容,在前几章中主要讲述了以下内容。
vite
的原理和从零开始实现vue3
使用新姿势reactive
使用和源码解析track
实现和源码解析trigger
实现和源码解析effect
与 track、trigger
工作原理和源码解析好的,这章的目标:从零开始完成一个 Vue3 !
必须要知道的前置知识 effect
与 track、trigger
工作原理,具体详情请看公众号 -> 前端进阶课
,一个有温度且没有广告的前端技术公众号。
在这里还是简单解析下这3个函数的作用吧
targetMap
targetMap
本章源码请看 uuz 急需 star 维持生计。
首先。我们2个全局变量,用来存放和定位追踪的依赖,也就是给 track
和 trigger
使用的仓库。
let targetMap = new WeakMap(); let activeEffect;
所以第一个需要设计的方法就是 track
,还记得该track
在vue3是如何调用的吗?
track(obj, 'get', 'x');
track
会去找 obj.x
是否被追踪,如果没找到就将obj.x放入targetMap
(完成追踪任务),将 obj.x
作为 map 的 key 将 activeEffect 作为 map 的 value。
抛开取值异常处理之类的,track
只做了一件事,将activeEffect
塞入targetMap
;
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')
trigger
只会去 targetMap
中寻找obj.x
的追踪任务,如果找到了就去重,然后执行任务。
也就是说:抛开取值异常相关,trigger
也只做了一件事:从 targetMap
取值然后调用该函数值。
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()) }
最后就是 effect
,还记得该打工仔的api在vue3中是如何调用的吗?
effect(() => { console.log('run cb') })
effect
接收一个回调函数,然后会被送给 track
。所以我们可以这么完成 effect
_effect
,并执行。而内部 _effect
也做了两件事
activeEffect
effect
回调函数优秀的代码呼之欲出。
function effect(fn) { // 定义一个内部 _effect const _effect = function(...args) { // 在执行是将自身赋值给 activeEffect activeEffect = _effect; // 执行回调 return fn(...args); }; _effect(); // 返回闭包 return _effect; }
所有的前置项都完成了,现在开始完成一个 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 代理数据实现的。
这样我们就可以通过 Proxy
来调用 track
和 trigger
,劫持 getter
和 setter
完成响应式设计
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() }
测试一下。参照 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
了)- -!
,我不听。使用 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
?
首先:参考 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; }
track
+ trigger
+ Proxy
getter
和 setter
配合 track
+ trigger
实现的effect
基础上的改进下章内容:vue3
该怎么结合 jsx
?
原创不易,给个三连安慰下弟弟吧。
全栈
或 Vue
有好礼相送哦