关于vue.js:vue3-深入浅出连载三手摸手实现-vue3

2次阅读

共计 5412 个字符,预计需要花费 14 分钟才能阅读完成。

大家好,我是 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 有好礼相送哦

正文完
 0