精讀《React Conf 2019 - Day2》
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.createClass
中this.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
取數,此時取數時機已晚。
隊列加載
假設 Composer
與 NewsFeed
組件內部都通過 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-spring 和 react-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; });
將 useDrag
與 y
綁定後,就可以用在 UI 組件上了:
<StyledNotification as={animated.div} onTouchStart={bind().onTouchStart} style={{ opacity: y.interpolate([0, 100], [1, 0]), transform: y.interpolate(y => `translateY(${y}px)`) }} />
將 opacity
與 transform
與位置 y
綁定就可以做出下拉消失的效果。
滑動的洞見
接着講到了滑動的三個洞見:
- 要立刻響應,任何延遲都會造成用戶額外精神負擔。
- 滾動速度衰減可以提升用戶體驗:
<img width=500 src=" https://img.alicdn.com/tfs/TB... ;>
接着我們需要預測用戶的意圖,比如在一個類似微信消息列表頁左右滑動時:
- 是否想取消手勢交互?
- 是否想展示出更多交互按鈕?
- 是否想刪除所有內容?
這需要更多設計思考。
- 橡皮筋滾動,即列表頁可以一直向下拉,上面部分像橡皮筋一樣可以被拉出空白頁的效果。
在設計手勢動畫時要考慮三個要點:
- 使用移動增量作爲手勢動畫的基準點。
- 動畫和手勢應該隨時可以被中斷,通過 springs 即可實現。
- 完成手勢後的動畫速度應該與手勢速度相當,這樣視覺體驗更自然。
最後提到了動畫兼容性與性能,比如儘量只使用 transform
與 opacity
可以保證移動端的流暢度,不同移動設備的默認手勢效果不同,最好通過 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 對象。這個拓展函數可以拿到 state
、 render
並返回新的 state
作爲 extract
時 render
的輸入。使用例子是這樣的:
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 是 value
與 onChange
,如果要拓展成一個既支持 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 許可證 )