手记

vue3 深入浅出(连载三)手摸手实现 vue3

大家好,我是Mokou,最近一直在做 vue3 相关内容,比如源码解析和mini-vue3的开发。

回顾下前几章的内容,在前几章中主要讲述了以下内容。

  1. 新构建工具 vite 的原理和从零开始实现

  2. vue3 使用新姿势

  3. 新api:reactive 使用和源码解析

  4. 追踪收集 track 实现和源码解析

  5. 追踪触发器 trigger 实现和源码解析

  6. 响应式核心 effecttrack、trigger 工作原理和源码解析

好的,这章的目标:从零开始完成一个 Vue3 !

必须要知道的前置知识 effecttrack、trigger 工作原理,具体详情请看公众号 -> 前端进阶课,一个有温度且没有广告的前端技术公众号。

在这里还是简单解析下这3个函数的作用吧

  1. track: 收集依赖,存入 targetMap

  2. trigger:触发依赖,使用 targetMap

  3. effect:副作用处理

本章源码请看 uuz 急需 star 维持生计。

手摸手实现 Vue3

首先。我们2个全局变量,用来存放和定位追踪的依赖,也就是给 tracktrigger 使用的仓库。


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

  1. 定义一个内部函数 _effect,并执行。

  2. 返回一个闭包

而内部 _effect 也做了两件事

  1. 将自身赋值给 activeEffect

  2. 执行 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 来调用 tracktrigger,劫持 gettersetter 完成响应式设计


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 的写法。定义个 setuprender


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的响应式。但这就结束了吗?

还有以下问题

  1. Proxy 一定需要传入对象

  2. render 函数 和 h 函数并正确(Vue3的h函数现在是2个不是以前的createElement了)

  3. 虚拟 dom 的递归

  4. 别再说了- -!,我不听。

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

那么通过前面几章实现的 tracktrigger 可以轻松实现 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

首先:参考 vue3computed 使用方式


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 可以这么写

  1. 内部执行 effect(fn, {lazy: true}) 保证 computed 执行的时候不触发回调。

  2. 通过对象的 getter 属性,在 computed 被使用的时候执行回调。

  3. 通过 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;

}

总结

  1. reactive 的核心是 track + trigger + Proxy

  2. ref 是通过对象自有的 gettersetter 配合 track + trigger 实现的

  3. computed 其实是一个在 effect 基础上的改进

下章内容:vue3 该怎么结合 jsx

最后

原创不易,给个三连安慰下弟弟吧。

  1. 源码请看 uuz

  2. 本文内容出自 https://github.com/zhongmeizhi/FED-note

  3. 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。回复 全栈Vue 有好礼相送哦

1人推荐
随时随地看视频
慕课网APP