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個全局變量,用來存放和定位追蹤的依賴。優秀的代碼呼之欲出。

let targetMap = new WeakMap();
let activeEffect;

然後優秀的打工仔一號出來了,那就是 effect,還記得該打工仔的api在vue3中是如何調用的嗎?

effect(() => {
  console.log('run cb')
})

該打工仔接收一個回調函數,然後會被送給 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;
}

然後出現的是另一個優秀的打工仔 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 的寫法。定義個 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 有好禮相送哦
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章