iT邦幫忙

2023 iThome 鐵人賽

DAY 6
1

「小時候父母教我要尊重別人,長大後我才發現不是每個人都有父母。」——歌德

轉眼間鐵人賽已來到了第六天,不知不覺已經混掉了1/6的日子,剩下幾天別說講完,能不能講到源碼最知名的diff算法都不好說……
總之上回書說道,我們在effect函數作用時給get被觸發的reactive對象做依賴收集,當reactive對象的屬性改變時觸發set,重新執行收集的所有依賴。這個觀念,可以完完整整地直接套用在ref上。

ref

首先新建文件/src/vue/ref.ts內容如下:

import { ReactiveEffect, trackEffects, triggerEffect } from "./effect";

class RefImpl {
  public dep: Set<ReactiveEffect> = new Set;
  public _value;
  constructor(public rawValue: any) {
    this._value = rawValue;
  }
  get value() {
    trackEffects(this.dep);
    return this._value;
  }
  set value(newValue: any) {
    if (newValue !== this.rawValue) {
      this._value = newValue;
      this.rawValue = newValue;
      triggerEffect(this.dep);
    }
  }
}

export const ref = (value: any) => {
  return new RefImpl(value);
};

然後因為引入了/src/vue/effect.ts的ReactiveEffect、trackEffects及triggerEffect,記得去effect.ts補上export:

export class ReactiveEffect // ......

export const trackEffects = // ......

export const triggerEffect = // ......

然後……就沒有然後了。
在/src/vue/index.ts暴露ref:

export * from './reactive';
export * from './ref';

然後去/src/index.ts試試看好不好用吧。

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

const msg = ref('nmsl');

effect(() => {
  document.body.innerHTML = `
    <div>
      ${msg.value}
    </div>
  `;
});

setInterval(() => {
  msg.value = 'wcnm';
}, 3000);


成功啦!

對比ref和reactive

有了我們先前在/src/vue/effect.ts打下的基礎,ref的實現和reactive其實沒有多大區別。真要說就是reactive的依賴收集存在effect.ts的targetMap,因為我們需要將對象、屬性及effect做雙層映射,但ref代理簡單數據類型,我們不需要如此複雜的映射關係,只要讓ref對象與effect一對一映射即可,因此我們可以直接在ref對象身上做一個屬性dep,在此記錄依賴它的effect。

public dep: Set<ReactiveEffect> = new Set;

然後當這個ref對象的value屬性被呼叫,它的get value方法會被觸發,便調用effect.ts暴露的trackEffects方法,將當前正在作用的effect收集至ref對象的dep屬性中。

get value() {
  trackEffects(this.dep);
  return this._value;
}

當ref對象的值被修改時,觸發set value方法,再調用effect.ts暴露的triggerEffect方法,遍歷調用ref對象的dep屬性中收集的所有依賴,驅動視圖。

set value(newValue: any) {
  if (newValue !== this.rawValue) {
    this._value = newValue;
    this.rawValue = newValue;
    triggerEffect(this.dep);
  }
}

一個基本的ref就完成啦~

優化ref

複雜數據類型

如上述所提及,由於ref代理簡單數據類型,我們不需要像reactive一樣,在effect.ts寫一個targetMap,將對象、屬性及effect雙層映射。但如果vue的開發者偏要傳一個複雜數據類型給ref呢?我們目前的ref能給value屬性做依賴收集,但當ref.value指向一個複雜數據類型的地址,只要那個地址不改變,便無法觸發ref的set value,也就是無法驅動視圖。

import { ref } from './vue'
const obj = ref({ a: 1 })
obj.value.a = 2 // 目前的ref無法驅動視圖

那我們該怎麼辦呢?其實答案也異常單純,直接把傳入ref的複雜數據再轉傳入reactive就行!
我們可以這樣修改/src/vue/ref.ts:

import { ReactiveEffect, trackEffects, triggerEffect } from "./effect";
import { reactive } from './reactive';

const toReactive = (value: any) => {
  return value && typeof value === 'object' ?
    reactive(value) :
    value;
};

class RefImpl {
  public dep: Set<ReactiveEffect> = new Set;
  public _value;
  constructor(public rawValue: any) {
    this._value = toReactive(rawValue);
  }
  get value() {
    trackEffects(this.dep);
    return this._value;
  }
  set value(newValue: any) {
    if (newValue !== this.rawValue) {
      this._value = toReactive(newValue);
      this.rawValue = newValue;
      triggerEffect(this.dep);
    }
  }
}

export const ref = (value: any) => {
  return new RefImpl(value);
};

當ref接收的參數是複雜數據類型,或ref的value屬性被賦值為複雜數據類型,就直接將其轉成reactive。

// ......
constructor(public rawValue: any) {
  this._value = toReactive(rawValue);
}
// ......
set value(newValue: any) {
  if (newValue !== this.rawValue) {
    this._value = toReactive(newValue);
    this.rawValue = newValue;
    triggerEffect(this.dep);
  }
}
// ......

其他和Ref相關的api

toRaw

眼尖的小夥伴可能會發現,在RefImpl類的constructor中,rawValue前面有一個public修飾符,這是ts中,將參數轉換為類的屬性的語法,也就是說我們特地在RefImpl的實例對象中存了一個叫rawValue的屬性去記錄當初被傳入ref的那個參數。

也就是說,vue的toRaw方法只要把這個rawValue返回出去就完成了!

export const toRaw = (ref: RefImpl) => {
  return ref?.rawValue ?? ref;
};

toRef

vue有個toRef的方法,能在解構reactive或響應式對象時,使解構出來的屬性不失響應式。
它的作法其實也意外簡單,就只是用get value和set value方法來代理原響應式對象的屬性,僅此而已。

class ObjectRefImpl {
  constructor(public object: any, public key: any) { }
  get value() {
    return this.object[this.key];
  }
  set value(newValue) {
    this.object[this.key] = newValue;
  }
}
export const toRef = (object: any, key: any) => {
  return new ObjectRefImpl(object, key);
};

可以看到它的get value就只是返回原對象的屬性,set value就只是給原對象的屬性賦值,到頭來操作的都是原對象,對吧。

toRefs

理解了toRef的原理,toRefs就簡單了。

export const toRefs = (object: any) => {
  const result: any = Array.isArray(object) ? [] : {};
  for (let key in object) {
    result[key] = toRef(object, key);
  }
  return result;
};

將一個響應式對象的多個屬性解構,說穿了就只是把屬性都toRef,然後解構出來而已。

以上就是ref相關的api,明天我們將進入watch的環節。

githubmain分支commit「[Day 06]ref」


上一篇
[Day 05]驅動視圖
下一篇
[Day 07]watch
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言