應戰Vue3 setup,Concent攜手React出招了!

❤ 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

Vue3 setup 設計動機

在Function-based API文章裏說得很清楚了,setup API 受 React Hooks 的啓發,提供了一個全新的邏輯複用方案,可以更好的組織邏輯,更好的在多個組件之間抽取和複用邏輯, 且將不存在如下問題。typescript

  • 模版中的數據來源不清晰。舉例來講,當一個組件中使用了多個 mixin 的時候,光看模版會很難分清一個屬性究竟是來自哪個 mixin。HOC 也有相似的問題。
  • 命名空間衝突。由不一樣開發者開發的 mixin 沒法保證不會正好用到同樣的屬性或是方法名。HOC 在注入的 props 中也存在相似問題。
  • 性能。HOC 和 Renderless Components 都須要額外的組件實例嵌套來封裝邏輯,致使無謂的性能開銷。

使用基於函數的 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 設計動機

說起Concentsetup的設計動機以前,咱們再來複盤下官方給出的hook設計動機數組

  • 在組件之間複用狀態邏輯很難
  • 複雜組件變得難以理解
  • 難以理解的 class

這裏面提到的複用狀態邏輯很難,是兩大框架都達成了一致的共識點,社區也一致在經過各類嘗試解決此問題,到了最後,你們發現一個有趣的現象,咱們寫UI的時候,基本上用不到繼承,並且官方也是極力推薦組合大於繼承的思想,試想一下,誰會寫個BasicModal,而後漫天的各類***Modal繼承自BasicModal來寫業務實現呢?基本上基礎組件設計者都是BasicModal留幾個接口和插槽,而後你引入BasicModal本身再封裝一個***Modal就完事了對吧?bash

因此在react基於Fiber的鏈表式樹結構能夠模擬出函數調用棧後,hook的誕生就至關因而順勢而爲了,可是hook只是給函數組件撕開了一個放置傳送門的口子,這個傳送門很是神奇,能夠定義狀態,能夠定義生命週期函數等,可是原始的hook和業務開發友好體驗度上仍是有些間隙,因此你們開始在傳送門上開始大作文章,有勤勤懇懇的專一於讓你更輕鬆的使用hook的全家桶react-use,也有專一於某個方向的hook如最近開始大紅大紫的專一於fetch data體驗的useSWR,固然也有很多開發開始慢慢沉澱本身的業務hook 包。閉包

可是基於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不是異步的,咱們的實際業務邏輯複雜的時候,請求多且相互依賴多的時候,它內部的處理會有更多的額外消耗。

  • hook和class的開發流程是不同的,二者之間互相共用邏輯已經不可能

基於這些問題的存在,Concentsetup誕生了,巧妙的利用hook這個傳送門,讓組件初次渲染時執行setup,從而開闢了另外一個空間,斡旋在function組件class組件之間,讓二者的業務邏輯能夠互相共享,從而達成了function組件class組件完美的和諧共存局面,實現了Concent的核心目標,不管是function組件class組件,它們都只是ui的載體,真正的業務邏輯處於model裏。

初探useConcent

本文要說的主角是setup,爲何這裏要提useConcent呢?由於setup須要傳送門呀,在ConcentuseConcent就扮演着這個重要的傳送門角色,咱們接下來經過代碼一步一步的分析,最後引入setup來作出對比。

瞭解更多能夠查看往期文章
聊一聊狀態管理&Concent設計理念
進入在線IDE體驗(如點擊圖片無效可點擊左側文字連接)

https://codesandbox.io/s/concent-guide-xvcej

定義model

按照約定,使用任何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

定義Concent函數組件

下面咱們經過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的精髓就是隻會在組件初次渲染前執行一次,利用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.effectReact.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的數據和操做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共享業務邏輯

由於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和類組件的componentDidMountcomponentDidUpdatecomponentWillUnmount,從而抹平了函數組件和類組件之間的生命週期函數的差別。

例如ctx上提供的emit&on接口,讓組件之間除了數據驅動ui的模式,仍是更鬆耦合的經過事件來驅動目標組件完成一些其餘動做。

下圖完整了的解釋了整個Concent組件在建立期、存在期和銷燬期各個階段的工做細節。

對比Vue3 setup

最後的最後,咱們使用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代碼修改哦(如點擊圖片無效可點擊文字連接)

Edit on CodeSandbox

https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz

https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

若是有關於concent的疑問,能夠掃碼加羣諮詢,我會盡力答疑解惑,幫助你瞭解更多。

相關文章
相關標籤/搜索