精讀《React PowerPlug 原始碼》

hzy666666發表於2019-02-25

1. 引言

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

React 專案中,一般一個檔案就是一個類,狀態最細粒度就是檔案的粒度。然而檔案粒度並非狀態管理最合適的粒度,所以有了 Redux 之類的全域性狀態庫。

同樣,檔案粒度也並非狀態管理的最細粒度,更細的粒度或許更合適,因此有了 React PowerPlug。

比如你會在專案中看到這種眼花繚亂的 state:

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

  render () { /**/ }
}

其實真正 App 級別的狀態並沒有那麼多,很多 諸如受控元件 onChange 臨時儲存的無意義 Value 找不到合適的地方儲存。

這時候可以用 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 還是必不可少的。

今天我們就來解讀一下 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

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

相關文章