1 引言

這是繼 精讀《React Conf 2019 - Day1》 之後的第二篇,補充了 React Conf 2019 第二天的內容。

2 概述 & 精讀

第二天的內容更爲精彩,筆者會重點介紹比較乾貨的部分。

Fast refresh

Fast refresh 是更好的 react-hot-loader 替代方案,目前僅支持 react-native 平臺,很快就會支持 react-dom 平臺。

相比不支持 Function component、無法錯誤恢復、更新經常失靈的 hot reloading 來說,fast refresh 還擁有以下幾個優點:

  • 狀態保持。
  • 支持 Function Component Hooks。
  • 更快的更新速度。

Fast refresh 更新速度更快,是基於 Function Component 生成了 “簽名”,從而最大成都避免銷燬重渲染,儘可能保持對組件的 rerender 刷新。下面介紹簽名機制的工作原理。

Fast refresh 對每個 Function component 都生成了一份專屬簽名,用以描述這個組件核心狀態,當這個核心狀態改變時,就只能銷燬重渲染了,但對於不觸及核心的修改就能進行代價非常小的 rerender。

這個簽名包含了 hooks 和參數名:

// signature: "useState{isLoggedIn}"

function ExampleComponent() {
  const [isLoggedIn, setIsLoggedIn] = useState(true);
}

比如當參數名變更時,這個組件的邏輯已發生改動,此時只能銷燬並重渲染了。因此實際上通過對簽名的對比來判斷是否要銷燬並重刷新組件:

// signature: "useState{isLoggedOut}"

function ExampleComponent() {
  const [isLoggedOut, setIsLoggedOut] = useState(true);
}

同理,當 hooks 從 useState 改成了 useReducer ,簽名也會發生變化從而導致徹底的重渲染。

但除此之外, 比如對樣式的修改、Dom 結構的修改都不會觸發簽名的變化 ,從而保證了 “對不觸及邏輯的改動進行高效的輕量 renreder”。

然而 Fast refresh 也有如下侷限性:

  • 還不能友好支持 Class component。
  • 混合導出 React 和非 React 組件時無法精確的 hot reload。
  • 更高的內存要求。

可以看到,Fast Refresh 隨着功能推廣與內置,現在已經覆蓋了 Facebook 95% 以上 hot reload 場景了:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

這部分內容不僅揭開了 hot reload 技術內幕,還對其功能進行了進一步優化,2019 年的 React 開發體系已經進入精細化階段。

重寫 React devtools

React devtools 的更新終於被正式介紹了,本來筆者以爲新的 devtools 只是支持了 hooks,但聽完分享後發現還有更多有用的改進,包括:

  • 更高的性能。
  • 更多特性支持。
  • 更好用戶體驗。

找到節點渲染鏈路

並不是每個 React 節點都參與渲染,新版 React devtools 可以展示出 rendered by:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

調試 Suspense

在 Day1 中講到的 Suspense 特性可以在 React devtools 調試了:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

通過點擊時鐘 icon,可以模擬 Suspense 處於 pendding 或 ready 狀態。

增強調試能力

可以通過點擊直接跳轉到組件源碼:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

最新版已增強至點擊按鈕後直接通過 Source 打開源碼位置, 這樣可以快速通過 UI 尋找到代碼 。同時還可以看到,通過點擊 debugger 按鈕將當前組件信息打到控制檯調試。

除此之外還可以動態修改組件的 props 與 hook state,大大增強了調試能力。

profiler

分析工具也得到了增強,現在可以看到每個組件被渲染了幾次以及重新渲染的原因:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

比如上圖組件被渲染了 4 次,主要有兩個原因:Hooks 改變與 Props 改變。

除此之外,還優化了更多細節體驗,比如高亮搜索、HOC 的展示優化、嵌套層級過多時不會佔用過多的橫向寬度等等。

react codemod

codemod 是一個代碼重構的方式,通過 AST 方式精準觸達代碼,我們可以認爲 codemod 是一個更聰明的“查找/替換”。

codemod 主要有以下三種使用方式:

  • 重命名。
  • 代碼排序。
  • 一定程度的代碼替換。

接下來就講到 react codemod 了,它是 react 場景的 codemod 解決方案,facebook 是這麼使用 react codemod 的:

  • 遷移 facebook 代碼。
  • 涉及幾萬個組件。
  • 修復了 3500 個文件的 React.PropTypes。
  • 修復了 8500 個文件的生命週期 unsafe。
  • 修復了 20000 個文件的 createClass 轉 JSX。

使用方式:

npx react-codemod React-PropTypes-to-prop-types

可以看到,通過 cli 對文件進行一次性重構處理。除此之外,再列舉幾種使用場景:

  • create-element-to-jsx 將 React.createElement 轉換爲 JSX。
  • error-boundaries 將 unstable_handleError 改爲 componentDidCatch
  • findDOMNode 將 React.createClassthis.getDOMNode() 改爲 React.findDOMNode
  • sort-comp 將 Class Component 生命週期按照規範排序, eslint-plugin-react 插件也有相同能力。

理論上來講,所有 codemode 做的事情都可以替換爲 eslint 的 autofix 來完成,比如 sort-comp 就同時被 codemode 和 eslint 支持。

Suspense

要理解 Suspense,就要理解 Suspense 與普通 loading 有什麼區別。

從代碼角度來說,Suspense 可以類比爲 try/catch 的體驗。爲了簡化代碼複雜度,我們可以用 try/catch 包裹代碼,從而簡化 try 區塊代碼複雜度,並將兜底代碼放在 catch 區塊:

try {
  // 只要考慮正確情況
} catch {
  // 錯誤時 fallback
}

Suspense 也一樣,它在渲染 React 組件時如果遇到了 Promise 拋出的 Error,就會進入 fallback ,所以 fallback 含義是 Loading 中狀態:

<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

與此同時,實際業務組件中的取數也不需要擔心取數是否正在進行中,只要直接處理拿到數據的情況就好了:

function ProfileDetails() {
  // 直接使用 user,不用擔心失敗。
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

進一步的,如果要處理組件渲染的異常,再使用 ErrorBoundary 包裹即可,此時的 fallback 含義是組件加載異常的錯誤狀態:

function Home(props) {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<Placeholder />}>
        <Composer />
      </Suspense>
    </ErrorBoundary>
  );
}

Suspense 模式的取數好處是 “fetch on render”,即渲染與取數同時進行,而普通模式的取數是 “fetch after render”,即渲染完成後再通過 useEffect 取數,此時取數時機已晚。

隊列加載

假設 ComposerNewsFeed 組件內部都通過 useQuery 取數,那麼並行取數時加載機制如下:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

這可能有兩個問題:組件內部加載順序不統一與組件間加載順序不統一。

如果組件內部有圖片,可能圖片與組件渲染實際不一致,此時可以利用 Suspense 統一 hold 所有子組件的特性,將圖片加載改爲 Suspense 模式:

<div>
  <YourImage src={uri} alt={...} />
  <MoreComposer />
</div>

同一個 Suspense 可以等待所有子元素都 Ready 後纔會一把渲染出 UI,因此可以看到網頁被一次性刷新而不是分部刷新。

第二個問題是組件間加載順序不統一,可能導致先渲染了文章內容,再渲染出文章頭部,此時如果區塊高度不固定,文章頭部可能會撐開,導致文章內容下移,用戶的閱讀體驗會遭到打斷。可以通過 suspense ordering 解決這個問題:

function Home(props) {
  return (
    <SuspenseList revealOrder="forwards">
      <Suspense fallback={<ComposerFallback />}>
        <Composer />
      </Suspense>
      <Suspense fallback={<FeedFallback />}>
        <NewsFeed />
      </Suspense>
    </SuspenseList>
  );
}

比如 forwards 表示從上到下,那麼一定會先渲染頭部再渲染文章內容,這樣文章內容就不會都抖動了。

Render as you fetch

相比 “fetch on render”,更高級別的優化是 “Render as you fetch”,即取數在渲染時機之前。

比如頁面路由的跳轉、Hover 到一個區塊,此時如果取數由這個動作觸發,就可以再次將取數時機提前,Facebook 爲此創造了一個新的 Hook: usePreloadedQuery

用法是,在某個事件中取數,比如點擊頁面跳轉按鈕時,通過 preloadQuery 預取數,得到的結果並不是取數結果,而是一個標識,在渲染組件中,把這個標識傳給 usePreloadedQuery 可以拿到真實取數結果:

// 組件 A 的 onClick
const reference = preloadQuery(query, variables);
// 組件 B 的 render
const data = usePreloadedQuery(query, reference);

可以看到,取數真正觸發的時機在渲染函數執行之前,所以在 usePreloadedQuery 調用時取數肯定已經在路上,甚至已經完成。相比之下,普通的 useQuery 函數存在下面幾個問題:

  • 由於取數過程存在狀態變化,可能導致組件在 “取數無意義” 狀態下重新渲染多次。
  • 可能取數還未完成就觸發重渲染。

    • 沒有取消的機制,沒有清除結果的機制。
  • 沒有辦法唯一標識組件。

preloadQuery 的好處就是將取數時機與 UI 分離,這樣可以更細粒度的控制邏輯:

  • 調用 preloadQuery 時:

    • 在組件銷燬時取消取數。
    • 有新取數觸發時取消取數。
    • 銷燬一些輪詢機制。
  • 渲染組件調用 usePreloadedQuery 時:

    • 不會再觸發取數,不會觸發意外的 re-render。
    • 不需要清空,因爲取數不在這裏發起。
    • 不需要清理輪詢。

可見 preloadQuery 相比 useQuery 的確有了一些體驗提升,然而這個優化比較追求極致,對大部分國內項目來說可能還走不到 facebook 這麼極致的性能優化,所以投入產出比顯得不是那麼高,而且這個開發方式對開發者不是太友好,因爲它讓請求的時機割裂到兩個模塊中。

但畢竟用戶體驗是大於開發者體驗的,React 儘量通過提高開發者體驗來間接提高用戶體驗,使雙方都滿意,但像 preloadQuery 就無法兩者兼顧了,爲了用戶體驗可以適當的降低一些開發者體驗。

如何維護代碼

這個分享講述瞭如何提升代碼維護效率,畢竟一個月後可能連自己寫的代碼都看不懂了。 hydrosquall 通過類比地圖的方式解釋了程序員是如何維護代碼的。

首先看我們是如何認路的。認路分爲三個層次:

  • 隨意走走。

    • 通過一些地標判斷方向。
  • 有方向的尋路。

    • 通過跟隨同伴或者瞭解更多本地信息找到目的地。
  • 地圖。

    • 通過 GPS 定位。
    • 通過模擬地圖方式指出路線。

可以看到這三種方式是逐層遞進的,那麼類比到代碼就有意思了:

  • 隨意走走(滾動查看源代碼 + ctrl/f 查找代碼 + grep 搜索)。

    • 入口(找到入口節點,查看數據結構)。
    • 標記(查看代碼註釋、查看 README)。
    • 發信號彈(斷點、console.log 等調試行爲)
  • 找到方向。

    • git blame 查看 owner,或直接根據文檔找到 codeowners。
  • 地圖。

    • 幸運的話你可以找到一份架構流程圖。

可以看到,地圖有幾種抽象層次,比如忽略了細節的紐約地鐵線路圖:

<img width=400 src=" https://img.alicdn.com/tfs/TB... ;>

或者是包含豐富地面信息的地鐵線路圖:

<img width=400 src=" https://img.alicdn.com/tfs/TB... ;>

抽象到什麼層次取決於用戶使用的場景,那麼代碼抽象也是如此。 hydrosquall 做了一個工具自動分析出代碼調用關係: js-callgraph

這就像路牌一樣,可以更高效的看出代碼結構,也包括了數據流結構,由於篇幅限制,感興趣的同學可以看 原視頻 瞭解更多。

寫作與寫代碼

本章講了寫作(小說)與寫代碼的關聯,總結出如下幾個重點:

  • 寫小說和寫代碼都是創造行爲。
  • 寫代碼需要抽象思維,寫小說也要有抽象思維構造人物和情節。
  • Show, don't tell,寫作天然就是申明式的,和數據驅動很相似。

更多可以去看 原視頻

移動端動畫最佳實踐

首先要使用一個真實的手機設備調試,否則可能出現 PC Chrome 一切正常,而手機上實際效果性能很差的情況!

手勢下拉退出

利用 react-springreact-use-gesture 做一個下滑消失的 Demo:

import { animated, useSpring } from "react-spring";
import { useDrag } from "react-use-gesture";

const [{ y }, set] = useSpring(() => {
  y: 0;
});

首先定義一個 y 縱向位置,通過 useDrag 將拖拽操作與 UI 綁定,通過回調將其與 y 數據綁定:

const bind = useDrag(({ last, movement: [, movementY], memo = y.value }) => {
  if (last) {
    // 拖拽結束時,如果偏移量超過 50 則效果和結束一樣,直接將 y 設置爲 100
    const notificationClosed = movementY > 50;

    return set({
      y: notificationClosed ? 100 : 0,
      onReset: notificationClosed && removeNotification
    });
  }

  // y 的位置區間在 0~100
  set([{ y: clamp(0, 100, memo + movementY) }]);

  return memo;
});

useDragy 綁定後,就可以用在 UI 組件上了:

<StyledNotification
  as={animated.div}
  onTouchStart={bind().onTouchStart}
  style={{
    opacity: y.interpolate([0, 100], [1, 0]),
    transform: y.interpolate(y => `translateY(${y}px)`)
  }}
/>

opacitytransform 與位置 y 綁定就可以做出下拉消失的效果。

滑動的洞見

接着講到了滑動的三個洞見:

  1. 要立刻響應,任何延遲都會造成用戶額外精神負擔。
  2. 滾動速度衰減可以提升用戶體驗:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

接着我們需要預測用戶的意圖,比如在一個類似微信消息列表頁左右滑動時:

  • 是否想取消手勢交互?
  • 是否想展示出更多交互按鈕?
  • 是否想刪除所有內容?

這需要更多設計思考。

  1. 橡皮筋滾動,即列表頁可以一直向下拉,上面部分像橡皮筋一樣可以被拉出空白頁的效果。

在設計手勢動畫時要考慮三個要點:

  • 使用移動增量作爲手勢動畫的基準點。
  • 動畫和手勢應該隨時可以被中斷,通過 springs 即可實現。
  • 完成手勢後的動畫速度應該與手勢速度相當,這樣視覺體驗更自然。

最後提到了動畫兼容性與性能,比如儘量只使用 transformopacity 可以保證移動端的流暢度,不同移動設備的默認手勢效果不同,最好通過 touch-action 禁用默認行爲以達到更好的兼容性與效果。

唱片與 React

J.Dash 擁有十年軟件開發經驗,同時也賣過很多唱片,他介紹了唱片行業與軟件開發的共同點。

唱片行業需要音樂編排能力,這與編碼能力類似,都存在良好的設計模式,並且需要團隊合作,開發過程中會遇到一些痛苦的經歷,但最終完成音樂和項目時都會獲得滿足的喜悅。

函數式編程

Declaratives UIs are the future, and the future is Comonadic. - Phil Freeman

申明式 UI 是未來,未來則是 Comonadic。

所謂申明式 UI 可以用下面的公式表達:

type render = (state: State) => View;

然後用一段公式介紹了 Comonadic:

class Functor w => Comonad w where
  extract   :: w a -> a
  duplicate :: w a -> w (w a)
  extend    :: (w a -> a) -> w a -> w b

用 JS 版本做一個解釋:

const Store = ({ state, render }) => ({
  extend: f => Store({ state, render: state => f(Store({ state, render })) }),
  extract: () => render(state)
});

extract 調用後會進行申明式渲染 UI,即 render(state)

extend 表示拓展,接收一個拓展函數作爲參數,返回一個新的 Store 對象。這個拓展函數可以拿到 staterender 並返回新的 state 作爲 extractrender 的輸入。使用例子是這樣的:

const App = Store({
  state: { msg: "World" },
  render: ({ msg }) => <p>Hello {msg}</p>
});

App.extend(({ state }) =>
  state.msg === "World" ? { msg: "ReactConf" } : state
).extract(); // <p> Hello ReactConf </p>

然而尷尬的是,筆者看了很久也沒看懂 Store 函數,最後運行了一下發現這個 Demo 拋出了異常 :joy:。

下面是筆者稍微修改後的例子,至少能跑起來:

const Store = ({ state, render }) => ({
  extend: f => Store({ state, render: state => render(f({ state, render })) }),
  extract: () => render(state)
});

const app = Store({
  state: { msg: "Hello World" },
  render: ({ msg }) => console.log("render " + msg)
});

app
  .extend(({ state }) => {
    return { msg: state.msg + " extend1" };
  })
  .extend(({ state }) => {
    return { msg: state.msg + " extend2" };
  })
  .extract(); // render Hello World extend2 extend1

然而作者的意思仍是未解之謎,希望對函數式瞭解的同學可以在評論區指點一下。

wick editor

wick editor 是一個開源的動畫、遊戲製作軟件。

wick editor 是一個動畫製作工具,但拓展了一些 js 編程能力,因此可以很好的將動畫與遊戲結合在一起:

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

演講介紹了 wick editor 的演化過程:

從很簡陋的 MVP 版本開始(1 周)

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

到 Pre-Alpha(4 月)

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

Alpha(5 月)

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

Beta(1.5 年)

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

重點是 1.0 版本採用 React 重寫了!繼 Beta 之後又經歷了 1 年:

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

這個團隊最棒的地方是,將遊戲與教育結合,針對不同場景做了很多用戶調研並根據反饋持續改進。

React Select

react-select 的作者 Jed Watson 被請來啦。作爲一個看上去很簡單組件(select)的開發者,卻擁有如此大的關注量(1.8w star),那作者有着怎樣的心路歷程呢?

react-select 看似簡單的名字背後其實有挺多的功能,比如作者列舉了一些功能層面的內容:

  • autocomplete - 輸入時搜索。
  • 單、多選。
  • focus 管理。
  • 下拉框層級與位置,比如可以放在根 DOM 節點,也可以作爲當前節點的子元素。
  • 異步下拉框內容。
  • 鍵盤、觸控。
  • Createble,即在搜索時如果沒有內容可以動態創建。
  • 等等。

<img width=300 src=" https://img.alicdn.com/tfs/TB... ;>

在設計層面:

  • 申明式。
  • 可以被定製。
  • 性能要求。
  • 等等。

隨着 Star 逐漸上漲,越來越多的需求被提出,核心庫代碼量越來越大,甚至許多需求之間都是相互衝突的,而且作者每天都會被上百個 Issue 與 PR 吵醒。做一個業務 Select 可能只要 5 分鐘,但做一個開源 Select 卻要 5 年,原因是一個簡單的 Select 如何滿足所有不同業務場景?這絕對是個巨大的挑戰。

比如用戶即需要受控也要非受控的組件,如何滿足好這個需求同時又讓代碼更可維護呢?

假設我們擁有一個受控的組件 SelectComponent ,那麼它的主要 props 是 valueonChange ,如果要拓展成一個既支持 defaultValue (非受控)又支持 value (受控)的組件,我們可以創建一個 manageState 組件對 SelectComponent 進行封裝:

const manageState = SelectComponent => ({
  value: valueProps,
  onChange: onChangeProp,
  defaultValue,
  ...props
}) => {
  const [valueState, setValue] = useState(defaultValue);

  const value = valueProps !== undefined ? valueProps : valueState;

  const onChange = (newValue, actionMeta) => {
    if (typeof onChangeProp === "function") {
      onChangeProp(newValue, actionMeta);
    }
    setValue(newValue);
  };

  return <SelectComponent {...props} value={value} onChange={onChange}>
};

這樣就可以組合爲一個受控/非受控的綜合 Select 組件:

import BaseSelect from "./Select";
import manageState from "./manageState";

export default manageState(Select);

同理對異步的封裝也可以放在 makeAsync 函數中:

const makeAsync = SelectComponent => ({
  getOptions,
  defaultOptions,
  ...props
}) => {
  const [options, setOptions] = useState(defaultOptions);
  const [isLoading, setIsLoading] = useState(false);

  const onInputChange = async newValue => {
    setIsLoading(true);
    const newOptions = await getOptions(newValue);
    setIsLoading(false);
    setOptions(newOptions);
  };

  return (
    <SelectComponent
      {...props}
      options={options}
      isLoading={isLoading}
      onInputChange={onInputChange}
    />
  );
};

可以看到, SelectComponent 是一個完全受控的數據驅動的 UI,無論是 manageState 還是 makeAsync 都是對數據處理的拓展,所以這三者之間纔可以融洽的組合:

import BaseSelect from "./Select";
import manageState from "./manageState";
import makeAsync from "./async";

export default manageState(Select);

export const AsyncSelect = manageState(makeAsync(Select));

後面還有一些風格化、開源協作的思考,這裏就不展開了,對這部分感興趣的同學可以查看原視頻瞭解更多。

React + 政府財政透明項目

usaspending.gov 這個網站使用 React 建設,可以查看美國政府支持財政的明細,通過流暢的體驗讓更多用戶可以瞭解國家財政支出,進一步推動財政支出的透明化。由於並不涉及前端技術的介紹,主要是產品介紹,因此精讀就不詳細展開了。

順便說一句,智能分析數據就用 QuickBI ,QuickBI 是我們團隊研發的一款智能 BI 服務平臺,如果你將美國政府的財政支持作爲數據集輸入,你會分析得更透徹。

React + 星艦模擬器

最後介紹的是使用 React 製作的星艦模擬器,看上去像一個遊戲:

<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>

有星系圖、船體、駕駛員信息、武器裝備、燃料、通信等等內容。甚至可以模擬太空駕駛,進行任務,可以實時多人協同。對太空迷們的吸引力很大,感興趣的同學建議直接觀看 視頻

3 總結

第二天的內容非常全面,涉及了 React API、開發者周邊、codemod 工具、代碼維護、寫作/音樂與代碼、動畫、函數式編程、看似簡單的 React 組件、使用 React 製作的各種腦洞大開的項目,等等。

React Conf 要展示的是一個完整的 React 世界,第一天提到了 React 是一個橋樑,正因爲這個橋樑,連接了各行各業不同的人羣以及不同的項目,大家都有一個共同的語言:React。

"We not only react code, but react the world"。

討論地址是: 精讀《React Conf 2019 - Day2》 · Issue #217 · dt-fe/weekly

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

關注 前端精讀微信公衆號

<img width=200 src=" https://img.alicdn.com/tfs/TB... ;>

版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證

相關文章