精讀《React PowerPlug 源碼》

1. 引言

React PowerPlug 是利用 render props 進行更好狀態管理的工具庫。前端

React 項目中,通常一個文件就是一個類,狀態最細粒度就是文件的粒度。然而文件粒度並不是狀態管理最合適的粒度,因此有了 Redux 之類的全局狀態庫。react

一樣,文件粒度也並不是狀態管理的最細粒度,更細的粒度或許更合適,所以有了 React PowerPlug。git

好比你會在項目中看到這種眼花繚亂的 state:github

class App extends React.PureComponent {
  state = {
    name = 1
    isLoading = false
    isFetchUser = false
    data = {}
    disableInput = false
    validate = false
    monacoInputValue = ''
    value = ''
  }

  render () { /**/ }
}
複製代碼

其實真正 App 級別的狀態並無那麼多,不少 諸如受控組件 onChange 臨時保存的無心義 Value 找不到合適的地方存儲。typescript

這時候能夠用 Value 管理局部狀態:數組

<Value initial="React">
  {({ value, set, reset }) => (
    <>
      <Select
        label="Choose one"
        options={["React", "Preact", "Vue"]}
        value={value}
        onChange={set}
      />
      <Button onClick={reset}>Reset to initial</Button>
    </>
  )}
</Value>
複製代碼

能夠看到,這個問題本質上應該拆成新的 React 類解決,但這也許會致使項目結構更混亂,所以 RenderProps 仍是必不可少的。babel

今天咱們就來解讀一下 React PowerPlug 的源碼。數據結構

2. 精讀

2.1. Value

這是一個值操做的工具,功能與 Hooks 中 useState 相似,不過多了一個 reset 功能(Hooks 其實也何嘗不能有,但 Hooks 確實沒有 Reset)。函數

用法

<Value initial="React">
  {({ value, set, reset }) => (
    <>
      <Select
        label="Choose one"
        options={["React", "Preact", "Vue"]}
        value={value}
        onChange={set}
      />
      <Button onClick={reset}>Reset to initial</Button>
    </>
  )}
</Value>
複製代碼

源碼

State 只存儲一個屬性 value,並賦初始值爲 initial:工具

export default {
  state = {
    value: this.props.initial
  };
}
複製代碼

方法有 set reset

set 回調函數觸發後調用 setState 更新 value

reset 就是調用 set 並傳入 this.props.initial 便可。

2.2. Toggle

Toggle 是最直接利用 Value 便可實現的功能,所以放在 Value 以後說。Toggle 值是 boolean 類型,特別適合配合 Switch 等組件。

既然 Toggle 功能弱於 Value,爲何不用 Value 替代 Toggle 呢?這是個好問題,若是你不擔憂本身代碼可讀性的話,的確能夠永遠不用 Toggle。

用法

<Toggle initial={false}>
  {({ on, toggle }) => <Checkbox onClick={toggle} checked={on} />}
</Toggle>
複製代碼

源碼

核心就是利用 Value 組件,value 重命名爲 on,增長了 toggle 方法,繼承 set reset 方法:

export default {
  toggle: () => set(on => !on);
}
複製代碼

理所因當,將 value 值限定在 boolean 範圍內。

2.3. Counter

與 Toggle 相似,這也是繼承了 Value 就能夠實現的功能,計數器。

用法

<Counter initial={0}>
  {({ count, inc, dec }) => (
    <CartItem
      productName="Lorem ipsum"
      unitPrice={19.9}
      count={count}
      onAdd={inc}
      onRemove={dec}
    />
  )}
</Counter>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 count,增長了 inc dec incBy decBy 方法,繼承 set reset 方法。

與 Toggle 相似,Counter 將 value 限定在了數字,那麼好比 inc 就會這麼實現:

export default {
  inc: () => set(value => value + 1);
}
複製代碼

這裏用到了 Value 組件 set 函數的多態用法。通常 set 的參數是一個值,但也能夠是一個函數,回調是當前的值,這裏返回一個 +1 的新值。

2.4. List

操做數組。

用法

<List initial={['#react', '#babel']}>
  {({ list, pull, push }) => (
    <div>
      <FormInput onSubmit={push} />
      {list.map({ tag }) => (
        <Tag onRemove={() => pull(value => value === tag)}>
          {tag}
        </Tag>
      )}
    </div>
  )}
</List>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 list,增長了 first last push pull sort 方法,繼承 set reset 方法。

export default {
  list: value,
  first: () => value[0],
  last: () => value[Math.max(0, value.length - 1)],
  set: list => set(list),
  push: (...values) => set(list => [...list, ...values]),
  pull: predicate => set(list => list.filter(complement(predicate))),
  sort: compareFn => set(list => [...list].sort(compareFn)),
  reset
};
複製代碼

爲了利用 React Immutable 更新的特性,所以將 sort 函數由 Mutable 修正爲 Immutable,push pull 同理。

2.5. Set

存儲數組對象,能夠添加和刪除元素。相似 ES6 Set。和 List 相比少了許多功能函數,所以只承擔添加、刪除元素的簡單功能。

用法

須要注意的是,initial 是數組,而不是 Set 對象。

<Set initial={["react", "babel"]}>
  {({ values, remove, add }) => (
    <TagManager>
      <FormInput onSubmit={add} />
      {values.map(tag => (
        <Tag onRemove={() => remove(tag)}>{tag}</Tag>
      ))}
    </TagManager>
  )}
</Set>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 values 且初始值爲 [],增長了 add remove clear has 方法,保留 reset 方法。

實現依然很簡單,add remove clear 都利用 Value 提供的 set 進行賦值,只要實現幾個操做數組方法便可:

const unique = arr => arr.filter((d, i) => arr.indexOf(d) === i);
const hasItem = (arr, item) => arr.indexOf(item) !== -1;
const removeItem = (arr, item) =>
  hasItem(arr, item) ? arr.filter(d => d !== item) : arr;
const addUnique = (arr, item) => (hasItem(arr, item) ? arr : [...arr, item]);
複製代碼

has 方法則直接複用 hasItem。核心仍是利用 Value 的 set 函數一招通吃,將操做目標鎖定爲數組類型罷了。

2.6. map

Map 的實現與 Set 很像,相似 ES6 的 Map。

用法

與 Set 不一樣,Map 容許設置 Key 名。須要注意的是,initial 是對象,而不是 Map 對象。

<Map initial={{ sounds: true, music: true, graphics: "medium" }}>
  {({ set, get }) => (
    <Tings>
      <ToggleCheck checked={get("sounds")} onChange={c => set("sounds", c)}>
        Game Sounds
      </ToggleCheck>
      <ToggleCheck checked={get("music")} onChange={c => set("music", c)}>
        Bg Music
      </ToggleCheck>
      <Select
        label="Graphics"
        options={["low", "medium", "high"]}
        selected={get("graphics")}
        onSelect={value => set("graphics", value)}
      />
    </Tings>
  )}
</Map>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 values 且初始值爲 {},增長了 set get clear has delete 方法,保留 reset 方法。

因爲使用對象存儲數據結構,操做起來比數組方便太多,已經不須要再解釋了。

值得吐槽的是,做者使用了 != 判斷 has:

export default {
  has: key => values[key] != null;
}
複製代碼

這種代碼並不值得提倡,首先是不該該使用二元運算符,其次比較推薦寫成 values[key] !== undefined,畢竟 set('null', null) 也應該算有值。

2.7. state

State 純粹爲了替代 React setState 概念,其本質就是換了名字的 Value 組件。

用法

值得注意的是,setState 支持函數和值做爲參數,是 Value 組件自己支持的,State 組件額外適配了 setState 的另外一個特性:合併對象。

<State initial={{ loading: false, data: null }}>
  {({ state, setState }) => {
    const onStart = data => setState({ loading: true });
    const onFinish = data => setState({ data, loading: false });

    return (
      <DataReceiver data={state.data} onStart={onStart} onFinish={onFinish} />
    );
  }}
</State>
複製代碼

依然利用 Value 組件,value 重命名爲 state 且初始值爲 {},增長了 setState 方法,保留 reset 方法。

setState 實現了合併對象的功能,也就是傳入一個對象,並不會覆蓋原始值,而是與原始值作 Merge:

export default {
  setState: (updater, cb) =>
    set(
      prev => ({
        ...prev,
        ...(typeof updater === "function" ? updater(prev) : updater)
      }),
      cb
    );
}
複製代碼

2.8. Active

這是一個內置鼠標交互監聽的容器,監聽了 onMouseUponMouseDown,並依此判斷 active 狀態。

用法

<Active>
  {({ active, bind }) => (
    <div {...bind}>
      You are {active ? "clicking" : "not clicking"} this div.
    </div>
  )}
</Active>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 active 且初始值爲 false,增長了 bind 方法。

bind 方法也巧妙利用了 Value 提供的 set 更新狀態:

export default {
  bind: {
    onMouseDown: () => set(true),
    onMouseUp: () => set(false)
  }
};
複製代碼

2.9. Focus

與 Active 相似,Focus 是當 focus 時才觸發狀態變化。

用法

<Focus>
  {({ focused, bind }) => (
    <div>
      <input {...bind} placeholder="Focus me" />
      <div>You are {focused ? "focusing" : "not focusing"} the input.</div>
    </div>
  )}
</Focus>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 focused 且初始值爲 false,增長了 bind 方法。

bind 方法與 Active 一模一樣,僅是監聽時機變成了 onFocusonBlur

2.10. FocusManager

不知道出於什麼考慮,FocusManager 的官方文檔是空的,並且 Help wanted。。

正如名字描述的,這是一個 Focus 控制器,你能夠直接調用 blur 來取消焦點。

用法

筆者給了一個例子,在 5 秒後自動失去焦點:

<FocusFocusManager>
  {({ focused, blur, bind }) => (
    <div>
      <input
        {...bind}
        placeholder="Focus me"
        onClick={() => {
          setTimeout(() => {
            blur();
          }, 5000);
        }}
      />
      <div>You are {focused ? "focusing" : "not focusing"} the input.</div>
    </div>
  )}
</FocusFocusManager>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 focused 且初始值爲 false,增長了 bind blur 方法。

blur 方法直接調用 document.activeElement.blur() 來觸發其 bind 監聽的 onBlur 達到更新狀態的效果。

By the way, 還監聽了 onMouseDownonMouseUp:

export default {
  bind: {
    tabIndex: -1,
    onBlur: () => {
      if (canBlur) {
        set(false);
      }
    },
    onFocus: () => set(true),
    onMouseDown: () => (canBlur = false),
    onMouseUp: () => (canBlur = true)
  }
};
複製代碼

可能意圖是防止在 mouseDown 時觸發 blur,由於 focus 的時機通常是 mouseDown

2.11. Hover

與 Focus 相似,只是觸發時機爲 Hover。

用法

<Hover>
  {({ hovered, bind }) => (
    <div {...bind}>
      You are {hovered ? "hovering" : "not hovering"} this div.
    </div>
  )}
</Hover>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 hovered 且初始值爲 false,增長了 bind 方法。

bind 方法與 Active、Focus 一模一樣,僅是監聽時機變成了 onMouseEnteronMouseLeave

2.12. Touch

與 Hover 相似,只是觸發時機爲 Hover。

用法

<Touch>
  {({ touched, bind }) => (
    <div {...bind}>
      You are {touched ? "touching" : "not touching"} this div.
    </div>
  )}
</Touch>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 touched 且初始值爲 false,增長了 bind 方法。

bind 方法與 Active、Focus、Hover 一模一樣,僅是監聽時機變成了 onTouchStartonTouchEnd

2.13. Field

與 Value 組件惟一的區別,就是

用法

這個用法和 Value 沒區別:

<Field>
  {({ value, set }) => (
    <ControlledField value={value} onChange={e => set(e.target.value)} />
  )}
</Field>
複製代碼

可是用 bind 更簡單:

<Field initial="hello world">
  {({ bind }) => <ControlledField {...bind} />}
</Field>
複製代碼

源碼

依然利用 Value 組件,value 保留不變,初始值爲 '',增長了 bind 方法,保留 set reset 方法。

與 Value 的惟一區別是,支持了 bind 並封裝 onChange 監聽,與賦值受控屬性 value

export default {
  bind: {
    value,
    onChange: event => {
      if (isObject(event) && isObject(event.target)) {
        set(event.target.value);
      } else {
        set(event);
      }
    }
  }
};
複製代碼

2.14. Form

這是一個表單工具,有點相似 Antd 的 Form 組件。

用法

<Form initial={{ firstName: "", lastName: "" }}>
  {({ field, values }) => (
    <form
      onSubmit={e => {
        e.preventDefault();
        console.log("Form Submission Data:", values);
      }}
    >
      <input
        type="text"
        placeholder="Your First Name"
        {...field("firstName").bind}
      />
      <input
        type="text"
        placeholder="Your Last Name"
        {...field("lastName").bind}
      />
      <input type="submit" value="All Done!" />
    </form>
  )}
</Form>
複製代碼

源碼

依然利用 Value 組件,value 重命名爲 values 且初始值爲 {},增長了 setValues field 方法,保留 reset 方法。

表單最重要的就是 field 函數,爲表單的每個控件作綁定,同時設置一個表單惟一 key:

export default {
  field: id => {
    const value = values[id];
    const setValue = updater =>
      typeof updater === "function"
        ? set(prev => ({ ...prev, [id]: updater(prev[id]) }))
        : set({ ...values, [id]: updater });

    return {
      value,
      set: setValue,
      bind: {
        value,
        onChange: event => {
          if (isObject(event) && isObject(event.target)) {
            setValue(event.target.value);
          } else {
            setValue(event);
          }
        }
      }
    };
  }
};
複製代碼

能夠看到,爲表單的每一項綁定的內容與 Field 組件同樣,只是 Form 組件的行爲是批量的。

2.15. Interval

Interval 比較有意思,將定時器以 JSX 方式提供出來,而且提供了 stop resume 方法。

用法

<Interval delay={1000}>
  {({ start, stop }) => (
    <>
      <div>The time is now {new Date().toLocaleTimeString()}</div>
      <button onClick={() => stop()}>Stop interval</button>
      <button onClick={() => start()}>Start interval</button>
    </>
  )}
</Interval>
複製代碼

源碼

提供了 start stop toggle 方法。

實現方式是,在組件內部維護一個 Interval 定時器,實現了組件更新、銷燬時的計時器更新、銷燬操做,能夠認爲這種定時器的生命週期綁定了 React 組件的生命週期,不用擔憂銷燬和更新的問題。

具體邏輯就不列舉了,利用 setInterval clearInterval 函數基本上就能夠了。

2.16. Compose

Compose 也是個有趣的組件,能夠將上面提到的任意多個組件組合使用。

用法

<Compose components={[Counter, Toggle]}>
  {(counter, toggle) => (
    <ProductCard
      {...productInfo}
      favorite={toggle.on}
      onFavorite={toggle.toggle}
      count={counter.count}
      onAdd={counter.inc}
      onRemove={counter.dec}
    />
  )}
</Compose>
複製代碼

源碼

經過遞歸渲染出嵌套結構,並將每一層結構輸出的值存儲到 propsList 中,最後一塊兒傳遞給組件。這也是爲何每一個函數 value 通常都要重命名的緣由。

精讀《Epitath 源碼 - renderProps 新用法》 文章中,筆者就介紹了利用 generator 解決高階組件嵌套的問題。

精讀《React Hooks》 文章中,介紹了 React Hooks 已經實現了這個特性。

因此當你瞭解了這三種 "compose" 方法後,就能夠在合適的場景使用合適的 compose 方式簡化代碼。

3. 總結

看完了源碼分析,不知道你是更感興趣使用這個庫呢,仍是已經躍躍欲試開始造輪子了呢?不論如何,這個庫的思想在平常的業務開發中都應該大量實踐。

另外 Hooks 版的 PowerPlug 已經 4 個月沒有更新了(非官方):react-powerhooks,也許下一個維護者/貢獻者 就是你。

討論地址是:精讀《React PowerPlug》 · Issue #129 · dt-fe/weekly

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

相關文章
相關標籤/搜索