體驗concent依賴收集,賦予react更多想象空間

concent v2版本的發佈了,在保留了和v1如出一轍的api使用方式上,內置了依賴收集系統,支持同時從狀態計算結果反作用3個維度收集依賴,創建其精確更新路徑,固然也保留了v1的依賴標記特性,支持用戶按需選擇是讓系統自動收集依賴仍是人工管理依賴,大多數場景,推薦使用自動收集依賴,除非很是在乎渲染期間自動收集和更新依賴的那一點微弱的額外計算以及很是清楚本身組件對狀態的依賴關係,那麼能夠降級爲人工標記依賴,固然了,若是是v1版本,那就沒得選了,只能是人工標記了。html

組件編程體驗統一

在正式瞭解依賴收集以前,咱們先會細聊一下組件編程體驗統一這個話題,本質來講concent並無刻意的要統一類組件和函數組件的編碼方式,只是基於爲組件實例注入標記了元數據的實例上下文ref ctx的核心運行機制,隨着迭代的進行,發現了組件的形態已再也不那麼重要,它們表達的都是react vdom,並最終會被react-dom轉換成的真實的html dom渲染到瀏覽器窗口裏,react開發者針對hook也說過,hook並無改變react的本質,只是換了一種編碼方式書寫組件而已,包括狀態的定義和生命週期的定義,均可以在類組件和函數組件的不一樣表達代碼裏一一映射。vue

定義初始狀態

class ClassComp extends React.Component{
    constructor(props, context){
        super(props, context);
        this.state = {tag:props.tag, name:''}
    }
}

function FnComp(props){
    const propsTag = props.tag;
    const [tag, setTag] = useState(propsTag);
    const [name, setName] = useState('');
}
複製代碼

初次掛載

class ClassComp extends React.Component{
    componentDidMount(){
        //組件初次掛載觸發
    }
}

function FnComp(){
    React.useEffect(()=>{
         //組件初次掛載觸發
    }, [])
}
複製代碼

存在期渲染完畢

class ClassComp extends React.Component{
    componentDidUpdate(){
        //組件存在期渲染完畢觸發
    }
}

function FnComp(){
    const efFlag = React.useRef(0);
    React.useEffect(()=>{
        efFlag.current++;
        if(efFlag.current>1){
            //組件存在期渲染完畢觸發
        }
    })
}
複製代碼

組件卸載前

class ClassComp extends React.Component{
    componentWillUnmount(){
        //組件卸載前觸發
    }
}

function FnComp(){
    React.useEffect(()=>{
        return ()=>{
            //組件卸載前觸發
        }
    }, [])
}
複製代碼

存在期組件收到新的屬性

class SomePage extends Component{
    static getDerivedStateFromProps (props, state) {
        if (props.tag !== state.tag) return {tag: props.tag}
        return null
    }
}

function FnComp(props){
    const propsTag = props.tag;
    const [tag, setTag] = useState(propsTag);
    
    React.useEffect(()=>{
        // 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新
        // 等價於上面組件類裏getDerivedStateFromProps裏的邏輯
        if(tag !== propsTag)setTag(tag);
    }, [propsTag, tag]);
}
複製代碼

編程統一實戰

既然他們本質上只是表達方式的不一樣,concent經過setup只在組件初次渲染前執行一次的特性,開闢另外一個空間,完美和諧的統一他們的表達方式,而且還順帶額外提供其餘可選的特性給開發者使用。react

這裏提早申明一下,下面的代碼演示setup特性以及相關生命週期統一的函數是都是可選的,並不是必定要這樣編碼才能接入concent,你依然能夠按照最傳統的方式組織代碼,使用setState就能夠了git

如下舉一個實戰例子:es6

const api = {
  async fetchProducts() {
    return {
      products: [
        {name:'name_'+Math.random(), author:'zzk_invoke'},
        {name:'name_'+Math.random(), author:'concent_invoke'},
      ]
    };
  }
};

export const setup = ctx => {
  //初始化props.tag到state裏,initState會自動作合併
  ctx.initState({ tag: ctx.props.tag });

  const fetchProducts = () => {
    const { type, sex, addr, keyword } = ctx.state;
    api.fetchProducts({ type, sex, addr, keyword })
      .then(({products}) => ctx.setState({ products }))
      .catch(err => alert(err.message));
  };

  ctx.effect(() => {
    fetchProducts();
  }, ["type", "sex", "addr", "keyword"]);
  /** 原函數組件內寫法: useEffect(() => { fetchProducts(type, sex, addr, keyword); }, [type, sex, addr, keyword]); */

  ctx.effect(() => {
    return () => {
      // 返回一個清理函數
      // 等價於componentWillUnmout, 這裏搞清理事情
    };
  }, []);
  /** 原函數組件內寫法: useEffect(()=>{ return ()=>{// 返回一個清理函數 // 等價於componentWillUnmout, 這裏搞清理事情 } }, []);//第二位參數傳空數組,次反作用只在初次渲染完畢後執行一次 */

  ctx.effectProps(() => {
    // 對props上的變動書寫反作用
    const curTag = ctx.props.tag;
    if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
  }, ["tag"]);
  /** 原函數組件內寫法: useEffect(()=>{ // 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新 // 等價於上面組件類裏getDerivedStateFromProps裏的邏輯 if(tag !== propTag)setTag(tag); }, [propTag, tag]); */

  return {
    // 返回結果收集在ctx.settings裏
    fetchProducts,
    fetchByInfoke: () => ctx.invoke(api.fetchProducts),
    //推薦使用此方式,把方法定義在settings裏
    changeType: ctx.sync("type")
  };
};

複製代碼

定義一個初始化狀態函數github

export const iState = () => ({
  products: [],
  type: "",
  sex: "",
  addr: "",
  keyword: "",
  tag: ""
});
複製代碼

如今咱們來看看組件長什麼樣子吧編程

  • 函數組件
import { useConcent } from 'concent';

const ConcentFnPage = React.memo(function(props) {
  // useConcent返回ctx,這裏直接解構ctx,拿想用的對象或方法
  const { state, settings, sync } = useConcent({ setup, state: iState, props });
  // 渲染須要的數據
  const { products, type, sex, addr, keyword, tag } = state;
  // 裝配好的方法
  const { fetchProducts, fetchByInfoke } = settings;

  return <div>... your ui ... </div>
});
複製代碼
  • 類組件
import { register } from 'concent';

@register({ setup, module:'product' })
class ConcentFnModuleClass extends React.Component{
  render(){
    const { state, settings, sync } = this.ctx;
    const { products, type, sex, addr, keyword, tag } = state;
    const { fetchProducts, fetchByInfoke } = settings;
  
    return <div>... your ui ... </div>
  }
}
複製代碼

點我查看上述示例api

帶來的額外優點

經過觀察發現,是否是長得如出一轍呢?惟一不一樣的是實例上下文在類組件裏經過this.ctx得到,在函數組件裏經過useConcent返回,並且setup相比傳統的函數組件帶來了幾大優點數組

  • 方法都一次性裝配在settings裏返回給用戶使用,沒有了每一輪渲染都生成臨時閉包函數的多餘消耗以及其餘值捕獲陷阱、useCallback進一步封裝等問題。
  • 依賴列表都傳遞key名稱就夠了,concent自動維護着一個上一刻狀態和當前狀態的引用,同構淺比較直接決定要不要觸發反作用函數

下面一個示例演示閉包陷阱和使用setup後如何避免此問題,且複用在類與函數組件之間瀏覽器

// 這是一個普通的函數組件
function NormalDemo() {
  const [count, setCount] = useState(0);
  const dom = useRef(null);
  useEffect(() => {
    const cur = dom.current;
    const add = () => setCount(count + 1);
    cur.addEventListener("click", add);

    return () => cur.removeEventListener("click", add);
  }, [count]);//須要顯示傳遞count值做爲依賴
  return <div ref={dom}>normal {count}</div>;
}

//定義一個setup函數
const setup = ctx => {
  const addCount = () => {
    const count = ctx.state.count;
    ctx.setState({ count: count + 1 });
  };

  // 由於鎖住了count在ctx.state裏,這裏不須要重複綁定和去綁定click事件了
  ctx.effect(() => {
    const cur = ctx.refs.dom.current;
    cur.addEventListener("click", addCount);
    return () => cur.removeEventListener("click", addCount);
  }, []);

  return { addCount };
};

function ConcentFnDemo() {
  const { useRef, state, settings } = useConcent({
    setup,
    state: { count: 0 }
  });
  return (
    <div> <div ref={useRef("dom")}>click me fn {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
  );
}

// or @register({setup, state:{count:0}})
@register({ setup })
class ConcentClassDemo extends React.Component {
  state = { count: 0 };
  render() {
    const { useRef, state, settings } = this.ctx;
    // this.ctx.state === this.state
    return (
      <div> <div ref={useRef("dom")}>click me class {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
    );
  }
}
複製代碼

點我查看上述示例

經過上面的示例代碼咱們發現,協調類組件和函數組件的共享和複用業務邏輯的方式是如此的簡單與輕鬆,但這並非必需的,你依然能夠像傳統方式同樣爲類組件和函數組件組織代碼,不過僅僅是多了一種更棒的方式提供給你罷了。

依賴收集,助力精確更新

咱們已提到依賴收集,首先會想到vue框架,依賴收集做爲其核心驅動視圖精確的原理,讓很多react須要人工維護shouldComponentUpdateuseCallback等額外api才能寫出性能更好的react代碼眼饞,不論是vue2definePropertyvue3proxy,本質上都能隱式的收集視圖對數據的依賴關係來作到精確更新。

那麼concent又怎樣來實現依賴收集呢?仍是離不開咱們提到的實例上下文,它將做爲咱們收集到依賴的重要媒介,來幫助咱們毫無違和感的書寫具備依賴收集的react代碼。

爲何說毫無違和感?由於你書寫的代碼和原始react代碼並無區別,依然保持react的味道。

普通的Concent組件

咱們定義一個普通的Concent組件

run();

const iState = ()=>({firstName:'Jim', lastName:'Green'});
function NormalPerson(){
    const { state, sync } = useConcent({state:iState});
    return (
        <div className="box">
            <input value={state.firstName} onChange={sync('firstName')} />
            <input value={state.lastName} onChange={sync('lastName')} />
        </div>
    );
}
複製代碼

若是咱們渲染這兩個組件的話,它們的狀態是各自獨立的

export default function App() {
  return (
    <div className="App"> <NormalPerson /> <NormalPerson /> </div>
  );
}
複製代碼

共享狀態的Concent組件

咱們提高一下狀態,讓全部示例共享 定義一個模塊名爲login

const iState = ()=>({firstName:'Jim', lastName:'Green'});

run(
  {
    login: {// 定義login模塊
      state: iState, // 傳遞狀態初始化函數,固然了這裏也能夠傳對象
    }
  }
);
複製代碼

而後指定組件屬於login模塊

function SharedPerson(){
    const { state, sync } = useConcent('login');
    return (
        <div className="box">
            <input value={state.firstName} onChange={sync('firstName')} />
            <input value={state.lastName} onChange={sync('lastName')} />
        </div>
    );
}
複製代碼

渲染它們看看效果吧

點我查看此在線示例

是否是提高狀態是從沒有感受過如此輕鬆愜意,無Provider包裹根組件,僅僅只是標記模塊,就完成了狀態提高和共享,示例是爲了方便使用sync,若是咱們更傳統一點,應該是這樣的

const { state, setState } = useConcent('login');
const changeFirstName = (e)=> setState({firstName: e.target.value})

<input value={state.firstName} onChange={changeFirstName} />
複製代碼

固然對於類組件也是同樣的,並無任何改變你認知的react組件形態

@register('login')
class SharedPersonC extends React.Component{
    changeFirstName = (e)=> this.setState({firstName: e.target.value})
    render(){
        const { state, sync } = this.ctx;
        return (
            <div className="box">
                <input value={state.firstName} onChange={this.changeFirstName} />
                <input value={state.lastName} onChange={sync('lastName')} />
            </div>
        );
    }
}
複製代碼

事實上this.state上能夠定義額外的key做爲私有狀態

@register('login')
class SharedPersonC extends React.Component{
    // 由於privKey並非模塊裏的key,因此這個key的狀態變動僅影響當前實例,
    // 並不會派發到其餘同屬於login模塊的實例
    state = {privKey:'key1'}
    render(){
        // this.state
        // {firstName:'', lastName:'', privKey:'key1'}
    }
}
複製代碼

若是咱們不喜歡共享狀態狀態合併到this.state,那就使用connect就行了,connect支持傳遞數組意味着能夠跨多個模塊消費共享數據。

function SharedPerson(){
    const { connectedState, sync } = useConcent({connect:['login']});
    // connectedState.login.firstName
}

@register({connect:['login']})
class SharedPersonC extends React.Component{
    render(){
        const { connectedState, sync } = this.ctx;
        // connectedState.login.firstName
    }
}
複製代碼

探索狀態依賴收集

鋪墊了這麼久,咱們說的依賴收集在哪裏,體如今何處,不要慌,咱們給組件加個開關,控制firstNamelastName是否顯示

const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
  const { state, sync, syncBool } = useConcent({
    module: "login",
    state: spState
  });
  return (
    <div className="box">
      {state.showF ? (
        <input value={state.firstName} onChange={sync("firstName")} />
      ) : (
        ""
      )}
      {state.showL ? (
        <input value={state.lastName} onChange={sync("lastName")} />
      ) : (
        ""
      )}
      <br />
      <button onClick={syncBool('showF')}>toggle showF</button>
      <button onClick={syncBool('showL')}>toggle showL</button>
    </div>
  );
}
複製代碼

若是咱們實例化2個實例,將第一個showF值置爲false,意味着視圖裏再也不有讀取state.firstName的行爲,那麼當前組件的依賴列表裏僅有lastName一個字段了,咱們在另外一個組件實例裏對lastName輸入新內容時,會觸發第一個實例渲染,可是對firstName輸入新內容時不該該觸發第一個實例渲染,如今咱們看看效果吧。

點我查看此在線示例

固然了用戶必定會有一個疑問,實例1不觸發更新,那麼當我須要用這個firstName時,是否是已通過期了,的確,若是你切換實例1的showF爲true,stata.firstName會拿到最新的值渲染,可是若是你不切換,而是直接點擊實例1的某個按鈕直接用firstName做業務邏輯處理的話,從state.firstName取到的的確是舊值,你只需從ctx.moduleState上去取就解決了,取到的值必定是最新值,由於全部屬於login模塊的實例的moduleState指向的是同一個對象,固然就不存在值過時的問題,固然你能夠一開始在視圖裏使用模塊數據時,就從moduleState裏取(同樣能收集到依賴),而不是從合併後的state上取,就不會形成渲染邏輯從state取而業務邏輯從moduleState裏取同一個值的違和感了。

探索計算依賴收集

咱們知道concent是支持定義計算函數的,分爲實例級別的計算和模塊級別的計算,咱們一個個來講

  • 定義實例計算

首先咱們經過setup一次性定義好實例計算函數,而後交給useConcent

const setup = ctx=>{
  ctx.computed('fullName', (newState, oldState)=>{
    return `${newState.firstName}_${newState.lastName}`;
  })
}

const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
  // 從refComputed取實例計算結果
  const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
    module: "login",
    state: spState,
    setup,
  });
  console.log(`%c${ccUniqueKey}`, "color:green");
  return (
    <div className="box">
      {state.showF ? (
        <input value={state.firstName} onChange={sync("firstName")} />
      ) : (
        ""
      )}
      {state.showL ? (
        <input value={state.lastName} onChange={sync("lastName")} />
      ) : (
        ""
      )}
      <br />
      {/** 此處渲染實例計算結果 */}
      fullName: {refComputed.fullName}
      <br />
      <button onClick={syncBool('showF')}>toggle showF</button>
      <button onClick={syncBool('showL')}>toggle showL</button>
    </div>
  );
}
複製代碼

接下來咱們要說此處有趣的事了,咱們依然渲染兩個實例,當咱們點擊第一個實例toggle showF按鈕設置showF爲false,可是注意哦,實例1的讀取了refComputed.fullName,而這個值是經過${newState.firstName}_${newState.lastName}計算出來的數據,因此儘管視圖不顯示firstName了,可是當前實例的依賴列表依然爲firstName, lastName,因此咱們在實例2裏輸入firstName,依然能觸發實例1渲染

點我查看此在線示例

  • 定義模塊計算
    咱們發現兩個實例對一樣的模塊狀態計算輸出是同樣的,因此顯然每一個實例都來一次計算就形成了浪費,更好的處理是將其提高到模塊裏,這樣只用算一次,而後讓全部實例共享
run({
  login: {
    // 定義login模塊
    state: iState, // 傳遞狀態初始化函數,固然了這裏也能夠傳對象
    computed:{
      fullName(newState, oldState){
        return `${newState.firstName}_${newState.lastName}`;
      }
    }
  }
});
複製代碼

如今咱們的組件代碼僅需將refComputed.fullName改成moduleComputed.fullName便可

// const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
  // 改成從moduleComputed取實例計算結果
  const { state, sync, ccUniqueKey, syncBool, moduleComputed } = useConcent({
    module: "login",
    state: spState,
    setup,
  });
複製代碼

讓咱們看看效果吧

點我查看此在線示例

探索反作用依賴收集

還記得開文裏咱們說組件編程體驗統一里提到的ctx.effect嗎,埋了這麼久的伏筆,在這裏終於要排上用場了,ctx.effect的執行時機是組件渲染完畢,檢查依賴列表裏是否有變化從而決定是否要觸發反作用函數。

在這裏咱們簡單定義一個反作用即firstName發生變化時打印一句話。

const setup2 = ctx=>{
  ctx.effect(()=>{
    console.log('firstName changed');
  }, ['firstName']);
}
複製代碼

嘿嘿,接下來咱們聲明一個空組件並將其傳給它

function EmptyPerson() {
  console.log('render EmptyPerson');
  useConcent({module:'login', setup:setup2});
  return <h1>EmptyPerson</h1>
}
複製代碼

這個組件僅僅是標記它屬於login模塊,可是咱們並無讀取任何模塊狀態用於渲染,只不過在setup裏定義了一個反作用,依賴列表裏有firstName,因此當咱們把EmptyPersonSharedPerson放一塊兒實例化後,當咱們在SharedPerson實例裏輸入firstName新內容時,會觸發EmptyPerson渲染並觸發它的反作用函數。

點我查看在線示例

結語

隨着再也不考慮古老的瀏覽器支持,擁抱es6新特性後,v2的concent已攜帶一整套完整的方案,可以漸進式的開發react組件,即不干擾react自己的開發哲學和組件形態,同時也可以得到巨大的性能收益,這意味着咱們能夠至下而上的增量式的迭代,狀態模塊的劃分,派生數據的管理,事件模型的分類,業務代碼的分隔均可以逐步在開發過程勾勒和剝離出來,其過程是絲滑柔順的,也容許咱們至上而下統籌式的開發,一開始吧全部的領域模型和業務模塊抽象的清清楚楚,同時在迭代過程當中也能很是快速的靈活調整而影響整個項目架構,指望讀到此文的你可以瞭解到concent在依賴收集到所作的努力並有興趣開始瞭解和試用。

彩蛋

某一個夜晚,我作了個夢,發現基於現有的concent運行機制,加以適當的約束,好像可讓reactvue之間相互轉譯彷佛有那麼一點點可能.....

❤ star me if you like concent ^_^

Edit on CodeSandbox

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

Edit on StackBlitz

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

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

相關文章
相關標籤/搜索