solid.js 的響應核心分析

零、參考資料

1. SolidJS 是如何實現響應式的?

一、雙劍之 createSignal

用法:

// js
const [value, setValue] = createSignal(1);

// html-template
<div>值:{ value() }</div>
從語法格式上來看,非常像 React,但是和 React 不同的是, value 並不是一個具體的值,而是一個  getter 函數,所以在寫模版的時候,需要加個 括號 來完成函數的調用,從而獲取到具體值

簡單實現:

const createSignal = (value) => {
    const getter = () => {
        return value;
    };
    const setter = (newValue) => {
        value = newValue;
    };

    return [getter, setter];
};
就是這麼簡單,但是還沒有達到能夠實現響應式的程度,需要與另外一劍 createEffect  來互相配合才能實現

二、雙劍之 createEffect

用法:

// js
const [getValue, setValue] = createSignal(1);

createEffect(() => {
    console.log('effect 執行了', getValue());
})
從語法格式上來看,這裏又像 V3 了

大致實現,包括對  createSignal  的改造:

// 緩存橋變量
const observers = [];

const getCurrentObserver = () => observers[observers.length - 1];

const createSignal = (value) => {
  const subscribers = []; // 注意,這裏目前用的是 數組

  const getter = () => {
    const currentObserver = getCurrentObserver();

    currentObserver && subscribers.push(currentObserver);

    return value;
  };

  const setter = (newValue) => {
    value = newValue;

    subscribers.forEach(subscriber => subscriber());
  };

  return [getter, setter];
};

const createEffect = (effect) => {
  const execute = () => {
    observers.push(effect);
  
    try {
      effect();
    } finally {
      observers.pop();
    }
  }

  execute();
}
最核心的是 effect() 這句。在這一句執行之前,我們已經把自定義的要執行的 effect 函數緩存進了 observers 數組(此時還沒有執行到  observers.pop(); ,因此  observers  數組長度至少爲一),接着執行 effect() 這句,而在這行這個函數的過程中,會觸發  getValue() 這個  getter  函數的執行,所以 effect 這個函數會被放入函數的執行棧,優先執行 getter 函數,在  getter 函數中就能拿到被放入 observers 數組中的 effect 函數,這樣就能將 effect 函數放入當前 signal 的訂閱者隊伍( subscribers )中去,一旦執行 setValue() ,就能通知所有訂閱者(依賴此變量的所有函數)去執行相應邏輯,從而完整實現了響應式(當然,這是之後的邏輯執行)。而在 getter 函數執行完畢之後,從函數的執行棧中接着執行 effect() 這句之後的邏輯,即  finally { observers.pop(); } ,清理緩存橋中的緩存,並最終完成當前其他任務的執行

三、派生之 createMemo

用法:

const [count, setCount] = createSignal(10);
const double = () => count() * 2;

從代碼上看, double 是一個 signal 的派生值,頁面上只要有一個地方用到了  double ,那麼就要執行一遍過程計算函數 ` () => count() * 2 `,如果頁面上有 n 個地方都用到了  double ,那麼是不是要執行 n 遍呢?回答:是的。所以就引入了 createMemo 

這個函數,這個函數的功能和 vue 的 computed 一致,緩存計算結果,只要依賴不變,那麼過程計算函數只執行一邊,其他的都是直接取緩存值就行

實現:

const createMemo = (memo) => {
  const [_value, _setValue] = createSignal();

  createEffect(() => {
    _setValue(memo());
  });

  return _value;
}
對,沒錯,就是這麼簡單。
當然也有不完美的地方:在實際運行中會發現一個問題:即使 signal 的 value 沒有任何變化,僅僅只是調用了 setter , Memos 也會重新執行一次。顯然,這個行爲是不符合預期的,所以我們可以對  setter 進行進一步的優化:
const setter = (newValue) => {
  if (value === newValue) return; // 就加了這一句

  value = newValue;

  subscribers.forEach(subscriber => subscriber());
};

 

四、總結

核心的實現和 Vue 3 的實現基本沒什麼差別,只不過在創建響應式數據的時候,V3 選擇的是使用 Proxy 對象,而 solid 選擇的是閉包緩存數據

全部代碼:

const observers = [];

const getCurrentObserver = () => observers[observers.length - 1];

const createSignal = (value) => {
  const subscribers = new Set(); // 注意,這裏要用 Set

  // 獲取 Signal value
  const getter = () => {
    const currentObserver = getCurrentObserver();

    currentObserver && subscribers.add(currentObserver);

    return value;
  };

  // 修改 Signal value
  const setter = (newValue) => {
    if (value === newValue) return;

    value = newValue;

    subscribers.forEach(subscriber => subscriber());
  };

  return [getter, setter];
};

const createEffect = (effect) => {
  const execute = () => {
    observers.push(effect);
  
    try {
      effect();
    } finally {
      observers.pop();
    }
  }

  execute();
}

const createMemo = (memo) => {
  const [_value, _setValue] = createSignal();

  createEffect(() => {
    _setValue(memo());
  });

  return _value;
}
值得注意的是,我們在創建 subscribers 的時候用的是 Set 結構,而 observers 則是普通的數組,爲什麼呢?
因爲在每次執行 setter 時,我們會循環通知 subscribers 裏面的所有訂閱者函數(在示例中就是  () => {console.log('effect 執行了', getValue());} )的執行,那麼訂閱者函數執行的過程又會觸發一遍 getter 函數的執行,在  getter 中很可能又會添加一個相同的訂閱者函數至 subscribers 中,所以 subscribers 需要用 Set 結構去進行去重,防止不必要的影響
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章