我居然把 Vue3 的原理用到了 React 上?

2021-03-02 前端真好玩
前言

vue-next是Vue3的源碼倉庫,Vue3採用lerna做package的劃分,而響應式能力@vue/reactivity被劃分到了單獨的一個package中。

如果我們想把它集成到React中,可行嗎?來試一試吧。

使用示例

話不多說,先看看怎麼用的解解饞吧。

// store.ts
import { reactive, computed, effect } from '@vue/reactivity';

export const state = reactive({
  count: 0,
});

const plusOne = computed(() => state.count + 1);

effect(() => {
  console.log('plusOne changed: ', plusOne);
});

const add = () => (state.count += 1);

export const mutations = {
  // mutation
  add,
};

export const store = {
  state,
  computed: {
    plusOne,
  },
};

export type Store = typeof store;

// Index.tsx
import { Provider, useStore } from 'rxv'
import { mutations, store, Store } from './store.ts'
function Count() {
  const countState = useStore((store: Store) => {
    const { state, computed } = store;
    const { count } = state;
    const { plusOne } = computed;

    return {
      count,
      plusOne,
    };
  });

  return (
    <Card hoverable style={{ marginBottom: 24 }}>
      <h1>計數器</h1>
      <div className="chunk">
        <div className="chunk">store中的count現在是 {countState.count}</div>
        <div className="chunk">computed值中的plusOne現在是 {countState.plusOne.value}</div>
         <Button onClick={mutations.add}>add</Button>
      </div>
    </Card>
  );
}

export default () => {
  return (
    <Provider value={store}>
       <Count />
    </Provider>
  );
};

可以看出,store的定義只用到了@vue/reactivity,而rxv只是在組件中做了一層橋接,連通了Vue3和React,然後我們就可以盡情的使用Vue3的響應式能力啦。

預覽gif

可以看到,完美的利用了reactive、computed的強大能力。

分析

從這個包提供的幾個核心api來分析:

effect(重點)

effect其實是響應式庫中一個通用的概念:觀察函數,就像Vue2中的Watcher,mobx中的autorun,observer一樣,它的作用是收集依賴。

它接受的是一個函數,它會幫你執行這個函數,並且開啟依賴收集,

這個函數內部對於響應式數據的訪問都可以收集依賴,那麼在響應式數據被修改後,就會觸發更新。

最簡單的用法

const data = reactive({ count: 0 })
effect(() => {
    // 就是這句話 訪問了data.count
    // 從而收集到了依賴
    console.log(data.count)
})

data.count = 1
// 控制臺列印出1

那麼如果把這個簡單例子中的

() => {
    // 就是這句話 訪問了data.count
    // 從而收集到了依賴
    console.log(data.count)
}

這個函數,替換成React的組件渲染,是不是就能達成響應式更新組件的目的了?

reactive(重點)

響應式數據的核心api,這個api返回的是一個proxy,對上面所有屬性的訪問都會被劫持,從而在get的時候收集依賴(也就是正在運行的effect),在set的時候觸發更新。

ref

對於簡單數據類型比如number,我們不可能像這樣去做:

let data = reactive(2)
// 😭oops
data = 5

這是不符合響應式的攔截規則的,沒有辦法能攔截到data本身的改變,只能攔截到data身上的屬性的改變,所以有了ref。

const data = ref(2)
// 💕ok
data.value= 5

computed

計算屬性,依賴值更新以後,它的值也會隨之自動更新。其實computed內部也是一個effect。

擁有在computed中觀察另一個computed數據、effect觀察computed改變之類的高級特性。

實現

從這幾個核心api來看,只要effect能接入到React系統中,那麼其他的api都沒什麼問題,因為它們只是去收集effect的依賴,去通知effect觸發更新。

effect接受的是一個函數,而且effect還支持通過傳入schedule參數來自定義依賴更新的時候需要觸發什麼函數,如果我們把這個schedule替換成對應組件的更新呢?要知道在hook的世界中,實現當前組件強制更新可是很簡單的:

useForceUpdate
export const useForceUpdate = () => {
  const [, forceUpdate] = useReducer(s => s + 1, 0);
  return forceUpdate;
};

這是一個很經典的自定義hook,通過不斷的把狀態+1來強行讓組件渲染。

而rxv的核心api: useStore接受的也是一個函數selector,它會讓用戶自己選擇在組件中需要訪問的數據。

那麼思路就顯而易見了:

把selector包裝在effect中執行,去收集依賴。指定依賴發生更新時,需要調用的函數是當前正在使用useStore的這個組件的forceUpdate強制渲染函數。

這樣不就實現了數據變化,組件自動更新嗎?

簡單的看一下核心實現

useStore和Provider
import React, { useContext } from 'react';
import { useForceUpdate, useEffection } from './share';

type Selector<T, S> = (store: T) => S;

const StoreContext = React.createContext<any>(null);

const useStoreContext = () => {
  const contextValue = useContext(StoreContext);
  if (!contextValue) {
    throw new Error(
      'could not find store context value; please ensure the component is wrapped in a <Provider>',
    );
  }
  return contextValue;
};

/**
 * 在組件中讀取全局狀態
 * 需要通過傳入的函數收集依賴
 */
export const useStore = <T, S>(selector: Selector<T, S>): S => {
  const forceUpdate = useForceUpdate();
  const store = useStoreContext();

  const effection = useEffection(() => selector(store), {
    scheduler: forceUpdate,
    lazy: true,
  });

  const value = effection();
  return value;
};

export const Provider = StoreContext.Provider;

這個option是傳遞給Vue3的effectapi,

scheduler規定響應式數據更新以後應該做什麼操作,這裡我們使用forceUpdate去讓組件重新渲染。

lazy表示延遲執行,後面我們手動調用effection來執行


{
  scheduler: forceUpdate,
  lazy: true,
}

再來看下useEffection和useForceUpdate

import { useEffect, useReducer, useRef } from 'react';
import { effect, stop, ReactiveEffect } from '@vue/reactivity';

export const useEffection = (...effectArgs: Parameters<typeof effect>) => {
  // 用一個ref存儲effection
  // effect函數只需要初始化執行一遍
  const effectionRef = useRef<ReactiveEffect>();
  if (!effectionRef.current) {
    effectionRef.current = effect(...effectArgs);
  }

  // 卸載組件後取消effect
  const stopEffect = () => {
    stop(effectionRef.current!);
  };
  useEffect(() => stopEffect, []);

  return effectionRef.current
};

export const useForceUpdate = () => {
  const [, forceUpdate] = useReducer(s => s + 1, 0);
  return forceUpdate;
};

也很簡單,就是把傳入的函數交給effect,並且在組件銷毀的時候停止effect而已。

流程先通過useForceUpdate在當前組件中註冊一個強制更新的函數。通過useContext讀取用戶從Provider中傳入的store。再通過Vue的effect去幫我們執行selector(store),並且指定scheduler為forceUpdate,這樣就完成了依賴收集。那麼在store裡的值更新了以後,觸發了scheduler也就是forceUpdate,我們的React組件就自動更新啦。

就簡單的幾行代碼,就實現了在React中使用@vue/reactivity中的所有能力。

優點:直接引入@vue/reacivity,完全使用Vue3的reactivity能力,擁有computed, effect等各種能力,並且對於Set和Map也提供了響應式的能力。後續也會隨著這個庫的更新變得更加完善的和強大。完全復用@vue/reacivity實現超強的全局狀態管理能力。缺點:由於需要精確的收集依賴全靠useStore,所以selector函數一定要精確的訪問到你關心的數據。甚至如果你需要觸發數組內部某個值的更新,那你在useStore中就不能只返回這個數組本身。

舉一個例子:

function Logger() {
  const logs = useStore((store: Store) => {
    return store.state.logs.map((log, idx) => (
      <p className="log" key={idx}>
        {log}
      </p>
    ));
  });

  return (
    <Card hoverable>
      <h1>控制臺</h1>
      <div className="logs">{logs}</div>
    </Card>
  );
}

這段代碼直接在useStore中返回了整段jsx,是因為map的過程中回去訪問數組的每一項來收集依賴,只有這樣才能達到響應式的目的。

源碼地址

https://github.com/sl1673495/react-composition-api

如果你喜歡這個庫,歡迎給出你的star✨,你的支持就是我最大的動力~

相關焦點

  • 前端技術:React&Vue對比
    React和vue的業務邏輯是差不多,vue在react上封裝了更簡潔的方法,使用起來更加的便捷,如:提供了便捷的指令(v-for,v-if,v-model),還提供了更多的屬性(computed,watch),我還是比較喜歡用react的,更接近js原生,更容易於理解它。
  • React Hooks 與Vue3.0 Function based API的對比?
    目錄:•React Hooks•React Hooks是什麼•useState Hook•useEffect Hook•React Hooks 引入的原因以及設計原則•React Hooks 使用原則及其背後的原理•Vue3.0 Function-based API•基本用法•引入的原因以及解決的問題•React Hooks 與 Vue3.0 Function-based
  • 三年React 開發經驗的我,遷移到 Vue 的心路歷程
    前幾年我一直在使用 React。最初僅有 React,後來使用 Redux 和 React 的其他庫(react-router、react-redux、prop-types 等)配合使用。我喜歡 React 的簡單和方便,使用 React 的時光一直都很快樂。我喜歡這個時代,有太多的好工具幫助我們更快更好地開發應用。
  • 手寫ReactHook核心原理,再也不怕面試官問我ReactHook原理
    下面是一個簡單的例子, 會在頁面上渲染count的值,點擊setCount的按鈕會更新count的值。這是因為你看似addClick方法沒改變,其實舊的和新的addClick是不一樣的,如圖所示在這裡插入圖片描述這時,如果想要,傳入的都是同一個方法,就要用到useCallBack。
  • Vue、React、Angular之三國殺,web前端入坑第六篇(上)
    vue、react、angular對比和選擇 這個話題我在vue1.x 時代 2016年 就想寫了,可時光如梭,懶癌侵身,一個擱淺便是這麼多天。vue都2.5了,angular 都變成了另外一種框架了,不敢想,不敢想, JavaScript 開發框架發展的是如此之快。如果有不知道mvvm概念的同學,請先回顧我 入坑第五篇: 秒懂前端框架歷史和MVVM框架原理!
  • 快速在你的vue/react應用中實現ssr(服務端渲染)
    摘要ssr(服務端渲染)技術實現方案接下來筆者將列舉幾個常用的基於vue/react的服務端渲染方案,如下:使用next.js/nuxt.js的服務端渲染方案>使用node+vue-server-renderer實現vue項目的服務端渲染使用node+React renderToStaticMarkup實現react項目的服務端渲染傳統網站通過模板引擎來實現ssr(比如ejs, jade, pug等)
  • 關於 Vue 和 React 區別的一些筆記
    因為涉及到的內容很多,可能下面的每一個點都能寫成一篇文章,這次先簡單做一個概要,等我有空做一個詳細的專題出來。監聽數據變化的實現原理不同為什麼 React 不精確監聽數據變化呢?這是因為 Vue 和 React 設計理念上的區別,Vue 使用的是可變數據,而React更強調數據的不可變。
  • 適合Vue用戶的React教程,你值得擁有(二)
    上周小編我寫了 適合Vue用戶的React教程,你值得擁有,得到了小夥伴們的一致好評,今天這篇文章是這一系列的第二篇文章。今年的9月18日是九一八事變89周年,同時在這一天,Vue3.0正式版發布了。相信很多小夥伴已經看過了Vue3.0相關的很多文章了。今天這篇文章將會對Vue2,Vue3,React的一些用法進行對比,方便小夥伴們學習。
  • 手把手教你用JS/Vue/React實現幸運水果機(80後情懷之作)
    當小燈停下時,如果停在玩家押注的目標上則可贏取相應的遊戲幣。物品:遊戲中的物品有八種,分別為:蘋果、西瓜、木瓜、橙子、鈴鐺、77、雙星,這些物品又分為大小兩種。還有BAR圖標,分為2種。賠率:以下為所有固定賠率物品的賠率。
  • 前端諸神大戰,Vue、React 依舊笑傲江湖
    默認語言採用 JavaScript,正在開發中的 Vue 3 計劃支持 TypeScript。Vue 的優勢:增強 HTML,在這方面 Vue 與 Angular 很相似;文檔完備;適應性強。由於語法與 Angular 和 React 很相似,所以從其他框架遷移至 Vue 相對容易;可擴展性強。Vue 可用於開發大型應用程式;尺寸小。
  • 框架React vs Vue
    當然,Vue裡面還有更多高級語法,在我看來,作者參考了很多現代語言,比如攔截器,計算屬性等等如果你的應用需要儘可能的小和快,請使用Vue廢話不說,上圖;但是不能作為一個標準,開發過程中各種場景都有,並不一定Vue就比React渲染快,但是Vue體積比React小一半這倒是事實。
  • 什麼是MVVM,MVC和MVVM的區別,MVVM框架VUE實現原理
    比如:angular、react、vue。瀏覽器的兼容性問題已經不再是前端的阻礙。前端的項目越來越大,項目的可維護性和擴展性、安全性等成了主要問題。當年為了解決瀏覽器兼容性問題,出現了很多類庫,其中最典型的就是jquery。但是這類庫沒有實現對業務邏輯的分成,所以維護性和擴展性極差。
  • 精通React/Vue系列之帶你實現一個功能強大的通知提醒框
    比如Modal組件,我們一般這樣來調用:<Modal title="xui基礎彈窗" centered mask={false} visible={false}><p>我是彈窗內容</p><p>我是彈窗內容</p>
  • vue高級進階系列——用typescript玩轉vue和vuex
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫用過vue的朋友大概對vuex也不陌生,vuex的官方解釋是專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
  • 深入認識 vue-cli:能做的不僅僅是初始化 vue 工程
    藉助 vue-cli,我們通過非常簡單的問答形式,方便地初始化一個vue工程,完全不需要擔心繁複的webpack、eslint配置等等。但是,仍然有許多同學沒有搞清楚 vue-cli和 vue工程之間的關係,導致沒有充分發揮 vue-cli的功能。在這篇文章中,我將從底層原理開始並結合幾個例子,告訴大家 vue-cli還能這樣用。
  • kpc v0.7.8 發布,同時支持 Vue/React/Intact 的前端組件庫
    眾所周知, 前端單頁應用的開發無非基於3大框架:React/Vue/Angular。如果不同框架維護一套自己的組件庫,一方面 維護成本非常高,存在大量重複勞動力;另一方面,即使大家都按統一的互動設計稿開發組件庫,也很難保證 各個組件庫交互和設計的完全統一。
  • 手寫React-Router源碼,深入理解其原理
    React-Router應用,感覺跟以前寫過的react-redux的Provider類似,我猜是用來注入context之類的。React-Router架構思路 我之前另一篇文章講Vue-Router的原理提到過,前端路由實現無非這幾個關鍵點:其實React-Router的思路也是類似的,只是React-Router將這些功能拆分得更散,監聽URL變化獨立成了history庫,vue-router裡面的current
  • Vue和React的區別,你在用哪個呢?
    這些都是vue創建組件實例的時候隱式幹的事。由於vue默默幫我們做了這麼多事,所以我們自己如果直接把組件的聲明包裝一下,返回一個高階組件,那麼這個被包裝的組件就無法正常工作了。四、組件通信的區別 在深層上,模板的原理不同,這才是他們的本質區別:React是在組件JS代碼中,通過 原生JS實現 模板中的常見語法,比如插值,條件,循環等,都是通過JS語法實現的Vue是在和組件JS代碼分離的單獨的模板中, 通過指令來實現的 ,比如 條件語句就需要 v-if 來實現react中 render函數是支持閉包特性的,所以我們import的組件在render
  • 精通react/vue組件設計教你實現一個極具創意的加載(Loading)組件
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先看看實現後的組件效果:因為動圖體積太大,就不給大家傳gif了,接下來我們具體分析一下該組件的特點.1. 組件設計思路按照之前筆者總結的組件設計原則,我們第一步是要確認需求.
  • 如何用純css打造類materialUI的按鈕點擊動畫並封裝成react組件
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言作為一個前端框架的重度使用者,在技術選型上也會非常注意其生態和完整性.筆者先後開發過基於vue,react,angular等框架的項目,碧如vue生態的elementUI