iT邦幫忙

2023 iThome 鐵人賽

DAY 5
1
Vue.js

淺談vue3源碼,很淺的那種系列 第 5

[Day 05]驅動視圖

  • 分享至 

  • xImage
  •  

「一個人的一生會經歷三次成長:
第一次,是明白事情的對與錯。
第二次,是明白有些事不只對與錯。
第三次,是明白有些事沒有對錯之後,仍然堅定地去做自己相信的事情,並為之負起責任。」——米哈遊

書接上回,我們在/src/vue/baseHandler.ts寫了reactive返回的代理對象的get方法,其中調用/src/vue/effect.ts暴露的track方法,將對象本身、觸發get的屬性及當前正在作用的effect做雙層映射,以記錄每個reactive對象的key分別在哪些effect函數作用時被觸發,也就是所謂依賴收集。

讓我們先回顧一下/src/vue/effect.ts依賴收集相關的程式碼吧。

type Dep = Set<ReactiveEffect>;
type DepsMap = Map<string | symbol, Dep>;
type TargetMap = WeakMap<object, DepsMap>;
// type TargetMap = WeakMap<
//   object,
//   Map<
//     string | symbol, Set<ReactiveEffect>
//   >
// >
const targetMap: TargetMap = new WeakMap();
const trackEffects = (dep: Dep) => {
  if (activeEffect && !dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
};
export const track = (target: object, key: string | symbol) => {
  if (!activeEffect) return;

  let depsMap: DepsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, depsMap = new Map());

  let dep: Dep = depsMap.get(key);
  if (!dep) depsMap.set(key, dep = new Set());

  trackEffects(dep);
};

既然我們能將一個reactive的key的所有依賴都收集到TargetMap的target的key所指向的這個Set中,我們只要遍歷這個Set,並依序執行這些ReactiveEffect對象的run方法,便能將這個reactive的key所收集的依賴,也就是跟它有關的組件全部重新渲染。
也就是說,接下來我們只要在effect.ts再暴露一個方法,去遍歷調用reactive的key映射的ReactiveEffect對象,並依序調用他們的run方法,最後再給reactive的set調用這個方法,數據驅動視圖就完成了!

trigger

先給/src/vue/effect.ts加上以下程式碼:

const triggerEffect = (effects: Dep) => {
  effects.forEach(effect => {
    if (effect !== activeEffect) {
      effect.run();
    }
  });
};
export const trigger = (target: object, key: string | symbol) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const effects = depsMap.get(key);
  if (effects) {
    triggerEffect(effects);
  }
};

先從暴露的trigger方法看起,首先trigger方法接收一個target跟一個key,分別代表觸發set的對象及被賦值導致set觸發的key。
拿到了target跟key,就檢查看看這個target是否有被targetMap收集:

const depsMap = targetMap.get(target);
if (!depsMap) return;

如果有被收集到,就檢查這個depsMap是否有收集到被更新的key,若有,便調用triggerEffect方法:

const effects = depsMap.get(key);
if (effects) {
  triggerEffect(effects);
}

而這個triggerEffect方法就簡單了,只要收集到的effect不是當前的activeEffect,就調用它的run方法,可以理解成重新渲染它所對應的組件。
(畢竟activeEffect的run方法本來就會被調用,沒必要調兩次。)

最後再到/src/vue/baseHandler.ts把這個trigger方法在set中調用:

import { track, trigger } from './effect';
// ......
export default {
  // ......
  set: (target: Record<string, any>, key: string, value: any) => {
    target[key] = value;
    trigger(target, key);
    return true;
  }
};

就大功告成啦!趕緊到/src/index.ts隨便寫點甚麼測試看看好不好用。

import './assets/css/style.less';
import { reactive } from './vue';
import effect from './vue/effect';

const person = reactive({
  name: '自尤雨溪望',
  msg: '我想準時下班'
});

effect(() => {
  document.body.innerHTML = `
    <div>
      <h5>${person.name}</h5>
      <p>${person.msg}</p>
    </div>
  `;
});

setInterval(() => {
  person.msg += '!';
}, 3000);


過啦~

當然目前的寫法仍是效能巨差,因為我們直接把整個body裡面的內容都重新渲染了。
按照計畫,之後我們會講ast抽象語法樹將template解析為虛擬dom,再用diff算法比對新舊虛擬dom,以實現渲染最少量的dom元素來更新視圖,不過這都是講完ref、computed、watch那些之後的事了。
而且也要30天內能講得完才行ㄏ

githubmain分支commit「[Day 05]驅動視圖」


上一篇
[Day 04]依賴收集 - 2——利用響應式變數的get收集當前effect
下一篇
[Day 06]ref
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言