❤ star me if you like concent ^_^vue
導讀react
上期寫完文章concent 騷操做之組件建立&狀態更新後,末尾留下了下面兩期的文章預告,按照原來的預告內容,這一次文章題目應該是【探究setup帶來的變革】了,可是由於本文會實打實的將
vue3
裏的setup
特性提出來和Concent
作對比,因此臨時改了題目爲【應戰Vue3 setup,Concent攜手React出招了!】,以便體現出有了setup
特性的加持,你的react應用將變得犀利無比,代碼組織方式將具備更大的想象空間,固然這裏要認可一點,大概是在6月份左右在某乎看到了Vue Function-based API RFC這篇文章,給了我極大的靈感,在這以前我一直有一個想法,想統一函數組件和類組件的裝配工做,須要定義一個入口api,可是命名彷佛一直感受定不下來,直到此文中說起setup
後,我如醍醐灌頂,它所作的工做和我想要達到的效果本質上是如出一轍的呀!因而乎Concent
裏的setup
特性就這樣誕生了。git
正文開始以前,先預覽一個生產環境的setup 示例,以示這是一個生產環境可用的標準特性。 進入在線IDE體驗github
在Function-based API文章裏說得很清楚了,setup API 受 React Hooks 的啓發,提供了一個全新的邏輯複用方案,可以更好的組織邏輯,更好的在多個組件之間抽取和複用邏輯, 且將不存在如下問題。typescript
使用基於函數的 API,咱們能夠將相關聯的代碼抽取到一個 "composition function"(組合函數)中 —— 該函數封裝了相關聯的邏輯,並將須要暴露給組件的狀態以響應式的數據源的方式返回出來api
import { reactive, computed, watch, onMounted } from 'vue'
const App = {
template: ` <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> `,
setup() {
// reactive state
const count = reactive(0)
// computed state
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
複製代碼
說起Concent
的setup
的設計動機以前,咱們再來複盤下官方給出的hook
設計動機數組
這裏面提到的複用狀態邏輯很難,是兩大框架都達成了一致的共識點,社區也一致在經過各類嘗試解決此問題,到了最後,你們發現一個有趣的現象,咱們寫UI的時候,基本上用不到繼承,並且官方也是極力推薦組合大於繼承的思想,試想一下,誰會寫個BasicModal
,而後漫天的各類***Modal
繼承自BasicModal
來寫業務實現呢?基本上基礎組件設計者都是BasicModal
留幾個接口和插槽,而後你引入BasicModal
本身再封裝一個***Modal
就完事了對吧?bash
因此在react基於Fiber的鏈表式樹結構能夠模擬出函數調用棧後,hook
的誕生就至關因而順勢而爲了,可是hook
只是給函數組件撕開了一個放置傳送門的口子,這個傳送門很是神奇,能夠定義狀態,能夠定義生命週期函數等,可是原始的hook和業務開發友好體驗度上仍是有些間隙,因此你們開始在傳送門上開始大作文章,有勤勤懇懇的專一於讓你更輕鬆的使用hook的全家桶react-use
,也有專一於某個方向的hook如最近開始大紅大紫的專一於fetch data
體驗的useSWR
,固然也有很多開發開始慢慢沉澱本身的業務hook 包。閉包
可是基於hook
組織業務邏輯有以下侷限性框架
特別注意的陷阱是,閉包函數內部千萬不要引入外部的變量,而是要放在依賴列表裏
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// When passing a function, SWR will use the
// return value as `key`. If the function throws,
// SWR will know that some dependencies are not
// ready. In this case it is `user`.
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
複製代碼
以上面useSWR的官方示例代碼爲例,看起來第二個useSWR是必定會報錯的,可是它內部會try catch住undefined錯誤,推導user還未準備好,從而巧妙的躲過渲染報錯,可是本質上hook不是異步的,咱們的實際業務邏輯複雜的時候,請求多且相互依賴多的時候,它內部的處理會有更多的額外消耗。
基於這些問題的存在,Concent
的setup
誕生了,巧妙的利用hook這個傳送門,讓組件初次渲染時執行setup,從而開闢了另外一個空間,斡旋在function組件
和class組件
之間,讓二者的業務邏輯能夠互相共享,從而達成了function組件
和class組件
完美的和諧共存局面,實現了Concent
的核心目標,不管是function組件
和class組件
,它們都只是ui的載體,真正的業務邏輯處於model
裏。
本文要說的主角是setup
,爲何這裏要提useConcent
呢?由於setup
須要傳送門呀,在Concent
裏useConcent
就扮演着這個重要的傳送門角色,咱們接下來經過代碼一步一步的分析,最後引入setup
來作出對比。
瞭解更多能夠查看往期文章
聊一聊狀態管理&Concent設計理念
或進入在線IDE體驗(如點擊圖片無效可點擊左側文字連接)
按照約定,使用任何Concent
接口前必定要先配置模型定義
/** ------ code in runConcent.js ------ */
import { run } from 'concent';
import { foo, bar, baz } from 'models';
run({foo, bar, baz});
/** ------ code in models/foo/state.js ------ */
export default {
loading: false,
name: '',
age: 12,
}
/** ------ code in models/foo/reducer.js ------ */
export async function updateAge(payload, moduleState, actionCtx){
const { data } = await api.serverCall();
// 各類複雜業務邏輯略
return {age: payload};
}
export async function updateName(payload, moduleState, actionCtx){
const { data } = await api.serverCall();
// 各類複雜業務邏輯略
return {name: payload};
}
export async function updateAgeAndName({name, age}, moduleState, actionCtx){
// actionCtx.setState({loading:true});
// 任意組合調用其餘reducer
await actionCtx.dispatch(updateAge, age);
await actionCtx.dispatch(updateName, name);
// return {loading: false}; // 當前這個reducer自己也能夠選擇返回新的狀態
}
複製代碼
注意model並不是必定要在run裏集中配置,也能夠跟着組件就近配置,一個標準的代碼組織結構示意以下圖
利用configure
就近配置page model
下面咱們經過useConcent
定義一個Concent函數組件
function Foo(){
useConcent();
return (
<div>hello</div>
)
}
複製代碼
這就是一個Concent函數組件,固然這樣定義是無心義的,由於什麼都沒有幹,因此咱們爲此函數組件加個私有狀態吧先
function Foo(){
// ctx是Concent爲組件注的實例上下文對象
const ctx = useConcent({state:{tip:'I am private', src:'D'}});
const { state } = ctx;
// ...
}
複製代碼
儘管Concent會保證此狀態只會在組件初次渲染時在賦值給ctx.state做爲初始值,可是每次組件重渲染這裏都會臨時建立一次state對象,因此更優的寫法是咱們將其提到函數外面
const iState = {tip:'I am private', src:'D'}; //initialState
function Foo(){
const ctx = useConcent({state:iState});
const { state } = ctx;
// ...
}
複製代碼
若是此組件會同時建立多個,建議將iState寫爲函數,以保證狀態隔離
const iState = ()=> {tip:'I am private'}; //initialState
複製代碼
定義完組件,能夠讀取狀態了,下一步咱們固然是要修改狀態了,同時咱們也定義一些生命週期函數吧
function Foo(){
const ctx = useConcent({state:iState});
const { state, setState } = ctx;
cosnt changeTip = (e)=> setState({tip:e.currentTarget.value});
cosnt changeSrc = (e)=> setState({src:e.currentTarget.value});
React.useEffect(()=>{
console.log('首次渲染完畢觸發');
return ()=> console.log('組件卸載時觸發');
},[]);
// ...
}
複製代碼
這裏看起來是否是有點奇怪,只是將React.setState
句柄調用替換成了useConcent
返回的ctx
提供的setState
句柄,可是若是我想定義當tip發生變化時就觸發反作用函數,那麼React.useEffect
裏第二爲參數列表該怎麼寫呢,看起來直接傳入state.tip就能夠了,可是咱們提供更優的寫法。
是時候接入setup
了,setup
的精髓就是隻會在組件初次渲染前執行一次,利用setup
開闢的新空間完成組件的功能裝配工做吧!
咱們定義當tip
或者src
發生改變時執行的反作用函數吧
// Concent會將實例ctx透傳給setup函數
const setup = ctx=>{
ctx.effect(()=>{
console.log('tip發生改變時執行');
return ()=> console.log('組件卸載時觸發');
}, ['tip']);
ctx.effect(()=>{
console.log('tip和src任意一個發生改變時執行');
return ()=> console.log('組件卸載時觸發');
}, ['tip', 'src'])
}
function Foo(){
// useConcent裏傳入setup
const ctx = useConcent({state:iState, setup});
const { state, setState } = ctx;
// ...
}
複製代碼
注意到沒有!ctx.effect
和React.useEffect
使用方式如出一轍,除了第二爲參數依賴列表的寫法,React.useEffect
須要傳入具體的值,而ctx.effect
之須要傳入stateKey名稱,由於Concent
老是會記錄組件最新狀態的前一箇舊狀態,經過二者對比就知道需不須要觸發反作用函數了!
由於ctx.effect
已經存在於另外一個空間內,不受hook
語法規則限制了,因此若是你想,你甚至能夠這樣寫(固然了,實際業務在不瞭解規則的狀況下不推薦這樣寫)
const setup = ctx=>{
ctx.watch('tip', (tipVal)=>{// 觀察到tip值變化時,觸發的回調
if(tipVal === 'xxx' ){//當tip的值爲'xxx'時,就定義一個新的反作用函數
ctx.effect(()=>{
return ()=> console.log('tip改變');
}, ['tip']);
}
});
}
複製代碼
咱們經過上面的示例,完成了狀態的定義,和反作用函數的遷移,可是狀態的修改仍是處於函數組件內部,如今咱們將它們挪到setup
空間內,利用setup
返回的對象能夠在ctx.settings
裏取到這一特色,將這寫方法提高爲靜態的api定義,而不是每次組件重複渲染期間都須要臨時再定義了。
const setup = ctx=>{
ctx.effect(()=>{ /** code */ }, ['tip']);
cosnt changeTip = (e)=> setState({tip:e.currentTarget.value});
cosnt changeSrc = (e)=> setState({src:e.currentTarget.value});
return {changeTip, changeSrc};
}
function Foo(){
const ctx = useConcent({state:iState, setup});
const { state, setState, settings } = ctx;
// 如今能夠綁定settings.changeTip , settings.changeSrc 到具體的ui上了
}
複製代碼
上面示例裏組件始終操做的是本身的狀態,若是須要讀取model的數據和操做model的方法怎麼辦呢?你僅須要標註鏈接的模塊名稱就行了,注意的是此時state是私有狀態和模塊狀態合成而來,若是你的私有狀態裏有key和模塊狀態同名了,那麼它其實就自動的被模塊狀態的值覆蓋了。
function Foo(){
// 鏈接到foo模塊
const ctx = useConcent({module:'foo', state:iState, setup});
const { state, setState, settings } = ctx;
// 此時state是私有狀態和模塊狀態合成而來
// {tip:'', src:'', loading:false, name:'', age:12}
}
複製代碼
若是你討厭state被合成出來,污染了你的ctx.state
,你也可使用connect
參數來鏈接模塊,同時connect
還容許你鏈接多個模塊
function Foo(){
// 經過connect鏈接到foo, bar, baz模塊
const ctx = useConcent({connect:['foo', 'bar', 'baz'], state:iState, setup});
const { state, setState, settings, connectedState } = ctx;
const { foo, bar, baz} = connectedState;
// 經過ctx.connectedState讀取到各個模塊的狀態
}
複製代碼
還記得咱們上面定義的foo模塊的reducer函數嗎?如今咱們能夠經過dispatch直接調用reducer函數,因此咱們能夠在setup
裏完成這些橋接函數的裝配工做。
const setup = ctx=>{
cosnt updateAgeAndName = e=> ctx.dispatch('updateAgeAndName', e.currentTarget.value);
cosnt updateAge = e=> ctx.dispatch('updateAge', e.currentTarget.value);
cosnt updateName = e=> ctx.dispatch('updateName', e.currentTarget.value);
return {updateAgeAndName, updateAge, updateName};
}
複製代碼
固然,上面的寫法是在註冊Concent組件時指定了明確的module
值,若是是使用connect
參數鏈接的模塊,則須要加明確的模塊前綴
const setup = ctx=>{
// 調用的是foo模塊updateAge方法
cosnt updateAge = e=> ctx.dispatch('foo/updateAge', e.currentTarget.value);
}
複製代碼
等等!你說討厭字符串調用的形式,由於你已經在上面foo模塊的reducer文件裏看到函數之間能夠直接基於函數引用來組合邏輯了,這裏還要寫名字很不爽,Concent
知足你直接基於函數應用調用的需求
import * as fooReducer from 'models/foo/reducer';
const setup = ctx=>{
// dispatch fooReducer函數
cosnt updateAge = e=> ctx.dispatch(fooReducer.updateAge, e.currentTarget.value);
}
複製代碼
嗯?什麼,這樣寫也以爲不舒服,想直接調用,固然能夠!
const setup = ctx=>{
// 直接調用fooReducer
cosnt updateAge = e=> ctx.reducer.foo.updateAge(e.currentTarget.value);
}
複製代碼
由於class組件也支持setup,也擁有實例上下文對象,那麼和function組件間共享業務邏輯天然是水到渠成的事情了
import { register } from 'concent';
register('foo')
class FooClazzComp extends React.Component{
$$setup(ctx){
// 模擬componentDidMount
ctx.effect(()=>{
/** code */
return ()=>{console.log('模擬componentWillUnmount');}
}, []);
ctx.effect(()=>{
console.log('模擬componentDidUpdate');
}, null, false);
// 第二位參數depKeys寫null表示每一輪都執行
// 第三位參數immediate寫false,表示首次渲染不執行
// 二者一結合,即模擬出了componentDidUpdate
cosnt updateAge = e=> ctx.dispatch('updateAge', e.currentTarget.value);
return { updateAge }
}
render(){
const { state, setState, settings } = this.ctx;
// 這裏其實this.state 和 this.ctx.state 指向的是同一個對象
}
}
複製代碼
上文裏,其實讀者有注意的話,咱們一直提到了一個關鍵詞實例上下文,它是Concent管控全部組件和加強組件能力的重要入口。
例如setup在ctx上提供給用戶的effect接口,底層會自動去適配函數組件的useEffect
和類組件的componentDidMount
、componentDidUpdate
、componentWillUnmount
,從而抹平了函數組件和類組件之間的生命週期函數的差別。
例如ctx上提供的emit&on接口,讓組件之間除了數據驅動ui的模式,仍是更鬆耦合的經過事件來驅動目標組件完成一些其餘動做。
下圖完整了的解釋了整個Concent組件在建立期、存在期和銷燬期各個階段的工做細節。
最後的最後,咱們使用Concent提供的registerHookComp
接口來寫一個組件和Vue3 setup
作個對比,指望此次出招可以打動做爲react開發者的你的心,相信基於不可變原則也能寫出優雅的組合api型函數組件。
registerHookComp
本質上是基於useConcent
淺封裝的,自動將返回的函數組件包裹了一層React.memo
^_^
import { registerHookComp } from "concent";
const state = {
visible: false,
activeKeys: [],
name: '',
};
const setup = ctx => {
ctx.on("openMenu", (eventParam) => { /** code here */ });
ctx.computed("visible", (newVal, oldVal) => { /** code here */ });
ctx.watch("visible", (newVal, oldVal) => { /** code here */ });
ctx.effect( () => { /** code here */ }, []);
const doFoo = param => ctx.dispatch('doFoo', param);
const doBar = param => ctx.dispatch('doBar', param);
const syncName = ctx.sync('name');
return { doFoo, doBar, syncName };
};
const render = ctx => {
const {state, settings} = ctx;
return (
<div className="ccMenu"> <input value={state.name} onChange={settings.syncName} /> <button onClick={settings.doFoo}>doFoo</button> <button onClick={settings.doBar}>doBar</button> </div> ); }; export default registerHookComp({ state, setup, module:'foo', render }); 複製代碼
❤ star me if you like concent ^_^,Concent的發展離不開你們的精神鼓勵與支持,也期待你們瞭解更多和提供相關反饋,讓咱們一塊兒構建更有樂趣,更加健壯和更高性能的react應用吧。
下期預告【concent love typescript】,由於Concent整套api都是面向函數式的,和ts結合是天生一對的好基友,因此基於ts書寫concent將是很是的簡答和舒服😀,各位敬請期待。
強烈建議有興趣的你進入在線IDE fork代碼修改哦(如點擊圖片無效可點擊文字連接)
若是有關於concent的疑問,能夠掃碼加羣諮詢,我會盡力答疑解惑,幫助你瞭解更多。