前言
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}>addButton>div>Card>
??);
}
export?default?()?=>?{
??return?(
????<Provider?value={store}><Count?/>Provider>
??);
};
可以看出,store
的定義只用到了@vue/reactivity
,而rxv
只是在組件中做了一層橋接,連通了Vue3和React,然后我們就可以盡情的使用Vue3的響應式能力啦。
預覽

可以看到,完美的利用了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?=?(store:?T)?=>?S;const?StoreContext?=?React.createContext(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?',
????);
??}return?contextValue;
};/**
?*?在組件中讀取全局狀態
?*?需要通過傳入的函數收集依賴
?*/export?const?useStore?=?(selector:?Selector):?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的effect
api,
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();if?(!effectionRef.current)?{
????effectionRef.current?=?effect(...effectArgs);
??}//?卸載組件后取消effectconst?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-next倉庫內部完整的測試用例。
- 完善的TypeScript類型支持。
- 完全復用@vue/reacivity實現超強的全局狀態管理能力。
- 狀態管理中組件級別的精確更新。
- Vue3總是要學的嘛,提前學習防止失業!
缺點:
- 由于需要精確的收集依賴全靠
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