「每日一瞥📰」0218~0308

NO IMAGE
  • useState vs useReducer
  • 關於 finally 的一些特殊場景
  • TSLint in 2019
  • Screenshot To Code
  • 優化 React App 性能的 5 個建議
  • 禁用大體積依賴的 import
  • 理解 TS 類型註解
  • Back/forward cache for Chrome
  • ES 新提案:Promise.any(promises)
  • 為什麼這個函數不能 new
  • 函數組件和類組件的根本差異
  • Preact X Alpha 0 released
  • Chromium Edge 截圖透出
  • React 函數組件的 TypeScript 寫法
  • 用 Jest 和 Enzyme 寫測試

目錄

useState vs useReducer

這兩個內置的 React Hooks 都可以處理狀態,那麼我們應該如何對二者進行選擇呢?

根據我個人的實踐來看,在 H5 項目中,useReducer 可以很好的按照 Redux 的模式完成許多工作,同時又可以不引入 Redux 的依賴。那麼大佬們是怎麼說的呢?

Matt Hamlin 的文章 useReducer, don’t useState 對這個問題進行了討論。在本文中,我們先來看下作者在他自己的項目中被問到的一個問題

「每日一瞥📰」0218~0308

我們來看看所謂的 Subscribe 組件的實現:

const [submitted, setSubmitted] = React.useState(false)
const [loading, setLoading] = React.useState(false)
const [response, setResponse] = React.useState(null)
const [errorMessage, setErrorMessage] = React.useState(null)

是的,因為有了這麼些狀態,就不免相應的有很多調用狀態更新方法的地方:

async function handleSubmit(values) {
setSubmitted(false)
setLoading(true)
try {
const responseJson = await fetch(/* stuff */).then(r => r.json())
setSubmitted(true)
setResponse(responseJson)
setErrorMessage(null)
} catch (error) {
setSubmitted(false)
setErrorMessage('Something went wrong!')
}
setLoading(false)
}

可如果我們用 useReducer 重寫上面的邏輯,就會變成下面這樣:

const [state, dispatch] = React.useReducer(reducer, {
submitted: false,
loading: false,
response: null,
errorMessage: null,
})

而 reducer 的實現就像下面這樣:

const types = {
SUBMIT_STARTED: 0,
SUBMIT_COMPLETE: 1,
SUBMIT_ERROR: 2,
};
function reducer(state, action) {
switch (action.type) {
case types.SUBMIT_STARTED: {
return { ...state, submitted: false, loading: true };
}
case types.SUBMIT_COMPLETE: {
return {
...state,
submitted: true,
response: action.response,
errorMessage: null,
loading: false,
};
}
case types.SUBMIT_ERROR: {
return {
...state,
submitted: false,
errorMessage: action.errorMessage,
loading: false,
};
}
default: {
return state;
}
}
}

相應的 handleSubmit 方法就可以調整為:

async function handleSubmit(values) {
dispatch({ type: types.SUBMIT_STARTED });
try {
const responseJson = await fetch(/* stuff */).then(r => r.json());
dispatch({ type: types.SUBMIT_COMPLETE, response: responseJson });
} catch (error) {
dispatch({ type: types.SUBMIT_ERROR, errorMessage: 'Something went wrong!' });
}
}

Matt Hamlin 在他的文章中列舉了如下幾點來表明 useReducer 相比 useState 的優越性:

  • 更容易管理較大較複雜的狀態
  • 更容易被其他開發者所理解
  • 更容易測試

對於上面這種特定的例子,作者並不覺得第一條和第二條真的成立。只是 4 個狀態元素很難說就是個大的複雜的狀態,而且前後似乎也看不出有什麼更容易理解。作者認為這兩種寫法是一樣的。

至於測試,作者表示絕對同意 reducer 更容易讀力測試,如果有很多業務邏輯,這確實是個很好的優勢。

在作者看來,有一個原因他會更願意使用 useState:

當我們並不確定一個組件的具體實現同時我們需要構建這一組件的時候

當我們構建一個新的組件,我們經常需要在組件的實現代碼中添加/刪除狀態。作者認為,面對這種情況,reducer 的寫法調整起來會更加麻煩。一旦你確定了你希望你的組件是什麼樣的,你就可以決定是否從若干個 useState 的寫法轉換成一個 useReducer 來寫。此外,你也可以考慮使用 useReducer 來實現其中的一部分,並使用 useState 來實現其他部分的邏輯。作者認為,等到確定代碼究竟要調整成什麼樣子時再開始抽象會更好一些

總的來說,二者自然是兼具優點與缺點的,具體還是得看要怎麼用。結合我自己的實踐,我發覺文中的意思乍看有些廢話,但實際上是有道理的。在我使用 useState 寫完之後確定了邏輯,就會開始覺得 useState 的寫法有些凌亂,此時在已經確定了各個狀態的情況下,再調整成 useReducer 就水到渠成了。

源地址:https://kentcdodds.com/blog/should-i-usestate-or-usereducer


關於 finally 的一些特殊場景

今天我們來看下 finally 在一些特殊場景下的行為。

場景列舉

在 catch 中 throw

如果我們在 catch 塊中拋出一個異常,且異常沒有相應的 catch 來捕獲,會發生什麼情況呢?

function example() {
try {
fail()
}
catch (e) {
console.log("Will finally run?")
throw e
}
finally {
console.log("FINALLY RUNS!")
}
console.log("This shouldn't be called eh?")
}
example()

「每日一瞥📰」0218~0308

finally 會執行,即使最後一句 console 並沒有。finally 是比較特殊的,它使得我們可以執行在拋出異常和離開函數之間的東西,即使異常本身是在 catch 塊中拋出的。

沒有 catch 的情況

如果沒有 catch,finally 又會有怎樣的行為呢?當然這一點一般都知道。

function example() {
try {
console.log("Hakuna matata")
}
finally {
console.log("What a wonderful phrase!")
}
}
example()

「每日一瞥📰」0218~0308

即使 try 中沒有錯誤,finally 仍然會執行,這一點一般都是知道的。當然,如果有錯誤,那麼 finally 也是會執行的。也就是說,finally 會覆蓋這兩種情況,如下:

「每日一瞥📰」0218~0308

try 中 return 的 finally

如果我們沒有出現錯誤,並且只是 return 了,會怎麼執行呢?

function example() {
try {
console.log("I'm picking up my ball and going home.")
return
}
finally {
console.log('Finally?')
}
}
example()

「每日一瞥📰」0218~0308

總結

finally 總會執行,即使因為拋出異常或執行 return 而提前結束。

這樣就讓其非常有用,當我們有無論如何都要執行的東西時,放在 finally 就總是可以執行,例如 cleanup 等等。

源地址:https://frontarm.com/james-k-nelson/will-finally-run-quiz/


TSLint in 2019

Palantir 是 TSLint 的創建及主要維護團隊,而 TSLint 是 TypeScript 的標準 linter。由於 TypeScript 社區致力於統一 TypeScript 和 JavaScript 的開發體驗,因而作者他們支持 TSLint 和 ESLint 的融合工作。在這篇博文中,他們解釋了這樣做的原因並闡述瞭如何去做。

TSLint 與 ESLint 的現狀

如今,TSLint 已經是事實上的用 TypeScript 實現的項目和 TypeScript 自身實現的標準 linter。 TSLint 生態系統由核心規則集、社區維護的自定義規則和配置包組成。

與之相對的是,ESLint 是標準的 JavaScript linter。和 TSLint 一樣,它由核心規則集和社區維護的自定義規則組成。 ESLint 支持 TSLint 缺少的許多功能,例如條件 lint 配置自動縮進。與之相對的是,ESLint 規則不能(至少現在不能)從 TypeScript 提供的靜態分析和類型推理中受益,因此無法捕獲 TSLint 語義規則所涵蓋的一類錯誤和代碼嗅探。

TypeScript + ESLint

TypeScript 團隊的戰略方向(Roadmap)是為了實現「每家每戶每個 JavaScript 程序員都可以用上類型」,哈哈哈搞笑。換句話說,他們的方向是使用類型和靜態分析等 TypeScript 功能,漸進式的豐富 JavaScript 開發人員的體驗,直到 TypeScript 和 JavaScript 開發人員體驗融合統一為止。

很明顯,linting 是 TypeScript 和 JavaScript 開發人員使用體驗的一個核心部分,因此 Palantir 的 TSLint 團隊與 Redmond 的 TypeScript 核心團隊會面,討論 TypeScript / JavaScript 的融合之於 linting 的意義。TypeScript 社區旨在滿足 JavaScript 開發人員的需求,ESLint 是 JavaScript linting 的首選工具。我們計劃棄用 TSLint,集中精力為 ESLint 改進 TypeScript 支持。我們認為這是正確的前進道路,兼具戰略性和務實性:

  • 降低使用門檻

JavaScript 開發人員遷移到 TypeScript 的障礙之一是從 ESLint 到 TSLint 的並不輕鬆的遷移。允許開發人員從他們現有的 ESLint 設置開始,逐步添加 TypeScript 特定的靜態分析可以減少這一障礙。

  • 統一社區

ESLint 和 TSLint 共同的核心目標是:通過強大的核心規則集和大量插件提供出色的代碼 linting 體驗。現在,在 ESLint 中可以使用 TypeScript 解析,我們認為社區最好能夠做到標準化,而不是在競爭中維護不同的代碼。

  • 性能更好的分析架構

ESLint API 允許更有效地實現某些類檢查。雖然可以重構 TSLint 的 API,但是利用 ESLint 的架構並將我們的開發資源集中在其他地方似乎是明智的。

下一步

Palantir 將通過一系列功能和插件為 ESLint 的平滑過渡提供支持,並以此回饋 TSLint 社區。例如:

  • 在 TypeScript 中使用 ESLint 規則的支持及文檔issue
  • typescript-eslint 的測試架構:ESLint 內置的檢查並不好用,且語法很難閱讀。我們想要帶來類似TSLint’s testing infrastructure 的東西以確保 TSLint 規則的開發體驗。
  • 語義化的基於類型的檢查規則:移植並添加使用 TypeScript 語言服務的新規則。

一旦我們認為,ESLint 已經參照 TSLint 完成了各個特性,我們就會廢棄 TSLint 並幫助用戶遷移到 ESLint。我們總結下目前的主要任務是:

  • 繼續 TSLint 的支持:最重要的維護任務是確保新的變異版本和特性的兼容性。
  • TSLint -> ESLint 兼容包:一旦 ESLint 的靜態分析檢查可以與 TSLint 相提並論,我們就會推出 eslint-config-palantir 包,一個插入式的替代 TSLint 規則的 ESLint 包。

源地址:https://medium.com/palantir/tslint-in-2019-1a144c2317a9


Screenshot To Code

從設計稿變前端代碼的故事已經說了很久,而這兩天再次更新的 Screenshot To Code 是除了 pix2code 等之外的更讓人興奮的模型。

看看效果

我們來看下整個三步走,首先是將設計稿傳入訓練好的網絡模型(圖中其實是在 Jupyter Notebook 裡執行 python 腳本):

「每日一瞥📰」0218~0308

然後模型就會將圖片轉換成 HTML 標籤:

「每日一瞥📰」0218~0308

還是很騷的哈。最後渲染出來的靜態頁面如下,看上去效果很酷啊,不過我們也都知道,展示效果嘛。

「每日一瞥📰」0218~0308

簡單分析

框架使用的還是 Keras,然後倉庫中提供了 HTML 和 Bootstrap 版本。具體核心邏輯可以參考這篇文章,筆者尚未深入研讀,這裡就不秀了。

大致的意思是,HTML 標籤的輸出,是以一個標籤和 screenshoot 為輸入得到下一個標籤,然後再以已有的標籤再去推測後續的標籤。

「每日一瞥📰」0218~0308

這種思路是借鑑了一個 word 跟著一個 word 的預測,因此網絡模型中也大量用到了 LSTM。

「每日一瞥📰」0218~0308

還是可以再瞭解瞭解這塊東西的,晚點再深入讀來看看。

源地址:https://yuque.antfin-inc.com/es2049/wl24q5/pivs9p/edit


優化 React App 性能的 5 個建議

「每日一瞥📰」0218~0308

本篇將從 render 角度來探討 5 個 React App 的優化技巧。需要聲明的是,文中將涉及部分 React 16.8.2 的內容,也就是說會有些 Hooks 相關內容。當然,這不是全部,不過理解了 React Hooks 後食用效果更佳。


當我們討論 React App 的性能問題時,不可避免的就是要探討我們的組件渲染的有多快。在進入到具體優化建議之前,我們先要理解以下 3 點:

  1. 當我們在說「渲染」時,我們在說什麼?
  2. 什麼時候會有「渲染」?
  3. 在「渲染」過程中會發生什麼?

關於 render 函數

這部分我們將從一種更簡單的方式來理解 reconciliation 和 diffing 的概念,當然文檔在這裡

哪個是 render 函數?

這個問題其實寫過 React 的人都會知道,這裡簡單說下:

在 class 組件中,指的是 render 方法:

class Foo extends React.Component {
render() {
return <h1> Foo </h1>;
}
}

在函數式組件中,我們指的是函數組件本身:

function Foo() {
return <h1> Foo </h1>;
}

render 什麼時候會執行?

render 函數會在兩種場景下被調用:

1. 狀態更新時

a. 繼承自 React.Component 的 class 組件更新狀態時
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
render() {
return <Foo />;
}
}
class Foo extends React.Component {
state = { count: 0 };
increment = () => {
const { count } = this.state;
const newCount = count < 10 ? count + 1 : count;
this.setState({ count: newCount });
};
render() {
const { count } = this.state;
console.log("Foo render");
return (
<div>
<h1> {count} </h1>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

可以看到,代碼中的邏輯是我們點擊就會更新 count,到 10 以後,就會維持在 10。增加一個 console.log,這樣我們就可以知道 render 是否被調用了。

總結:繼承了 React.Component 的 class 組件,即使狀態沒變化,只要調用了setState 就會觸發 render。

b. 函數式組件更新狀態時

我們用函數實現相同的組件,當然因為要有狀態,我們用上了 useState hook:

import React, { useState } from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
render() {
return <Foo />;
}
}
function Foo() {
const [count, setCount] = useState(0);
function increment() {
const newCount = count < 10 ? count + 1 : count;
setCount(newCount);
}
console.log("Foo render");
return (
<div>
<h1> {count} </h1>
<button onClick={increment}>Increment</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

我們可以注意到,當狀態值不再改變之後,render 的調用就停止了。

總結:對函數式組件來說,狀態值改變時會觸發 render 函數的調用。

2. 父容器重新渲染時

import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
state = { name: "App" };
render() {
return (
<div className="App">
<Foo />
<button onClick={() => this.setState({ name: "App" })}>
Change name
</button>
</div>
);
}
}
function Foo() {
console.log("Foo render");
return (
<div>
<h1> Foo </h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

只要點擊了 App 組件內的 Change name 按鈕,就會重新 render。而且可以注意到,不管 Foo 具體實現是什麼,Foo 都會被重新渲染。

總結:無論組件是繼承自 React.Component 的 class 組件還是函數式組件,只要父容器重新 render 了,組件的 render 都會被再次調用。

render 函數執行時發生了什麼?

只要 render 函數被調用,就會有兩個步驟按順序執行。這兩個步驟非常重要,理解了它們才好知道如何去優化 React App。

Diffing

在此步驟中,React 將新調用的 render 函數返回的樹與舊版本的樹進行比較,這一步是 React 決定如何更新 DOM 的必要步驟。雖然 React 使用高度優化的算法執行此步驟,但仍然需要付出一定性能開銷。

Reconciliation

基於 diffing 的結果,React 更新 DOM 樹。這一步同樣要因為卸載和掛載 DOM nodes 帶來了許多的性能開銷。

Tip

Tip #1:慎重分配 state 以避免不必要的 render 調用

我們以下面的例子為例,其中 App 會渲染兩個組件:

  • CounterLabel,接收 count 值和一個增加父組件 App 中的狀態 count 的值的方法。
  • List,接收 item 的列表。
import React, { useState } from "react";
import ReactDOM from "react-dom";
const ITEMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
function App() {
const [count, setCount] = useState(0);
const [items, setItems] = useState(ITEMS);
return (
<div className="App">
<CounterLabel count={count} increment={() => setCount(count + 1)} />
<List items={items} />
</div>
);
}
function CounterLabel({ count, increment }) {
return (
<>
<h1>{count} </h1>
<button onClick={increment}> Increment </button>
</>
);
}
function List({ items }) {
console.log("List render");
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item} </li>
))}
</ul>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

只要父組件 App 中的狀態被更新,CounterLabelList 就都會更新。

當然,CounterLabel 重新渲染是正常的,因為 count 發生了變化,自然要重新渲染。但是對於 List 而言,就完全是不必要的更新了,因為它的渲染是獨立於 count 值的。儘管 React 並不會在 reconciliation 階段真的更新 DOM,畢竟完全沒變化,但是仍然會執行 diffing 階段來對前後的樹進行對比,這仍然存在性能開銷。

還記得 render 執行的 diffing 和 reconciliation 階段嗎?前面講過的東西在這裡碰到了。

因此,為了避免不必要的 diffing 開銷,我們應當考慮將特定的狀態值放到更低的層級或組件中(與 React 中所說的「提升」概念正好相反)。在這個例子中,我們就是要將 count 值放到 CounterLabel 組件中管理來解決這個問題。

Tip #2:合併狀態更新

因為每次狀態更新都會觸發新的 render 調用,那麼更少的狀態更新也就可以更少的調用 render 了。

我們知道,React class 組件有 componentDidUpdate(prevProps, prevState) 的鉤子,可以用來檢測 props 或 state 有沒有發生變化。儘管有時有必要在 props 發生變化時再觸發 state 更新,但我們總可以避免在一次 state 變化後再進行一次 state 更新這種操作:

import React from "react";
import ReactDOM from "react-dom";
function getRange(limit) {
let range = [];
for (let i = 0; i < limit; i++) {
range.push(i);
}
return range;
}
class App extends React.Component {
state = {
numbers: getRange(7),
limit: 7
};
handleLimitChange = e => {
const limit = e.target.value;
const limitChanged = limit !== this.state.limit;
if (limitChanged) {
this.setState({ limit });
}
};
componentDidUpdate(prevProps, prevState) {
const limitChanged = prevState.limit !== this.state.limit;
if (limitChanged) {
this.setState({ numbers: getRange(this.state.limit) });
}
}
render() {
return (
<div>
<input
onChange={this.handleLimitChange}
placeholder="limit"
value={this.state.limit}
/>
{this.state.numbers.map((number, idx) => (
<p key={idx}>{number} </p>
))}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

這裡渲染了一個範圍的數字序列,範圍為 0 到 limit。只要用戶改變了 limit 值,我們就會在 componentDidUpdate 中進行檢測,並設定新的數字列表。

毫無疑問,上面的代碼是可以滿足需求的,但是,我們仍然可以進行優化。

上面的代碼中,每次 limit 發生改變,我們都會觸發兩次狀態更新:第一次是為了修改 limit,第二次是為了修改展示的數字列表。這樣一來,每次 limit 的變化會帶來兩次 render 開銷:

// 初始狀態
{ limit: 7, numbers: [0, 1, 2, 3, 4, 5, 6]
// 更新 limit -> 4
render 1: { limit: 4, numbers: [0, 1, 2, 3, 4, 5, 6] } // 
render 2: { limit: 4, numbers: [0, 2, 3]

我們的代碼邏輯帶來了下面的問題:

  • 我們觸發了比實際需要更多的狀態更新;
  • 我們出現了「不連續」的渲染結果,數字列表與 limit 不匹配。

為了改進,我們應避免在不同的狀態更新中改變數字列表。事實上,我們可以在一次狀態更新中搞定:

import React from "react";
import ReactDOM from "react-dom";
function getRange(limit) {
let range = [];
for (let i = 0; i < limit; i++) {
range.push(i);
}
return range;
}
class App extends React.Component {
state = {
numbers: [1, 2, 3, 4, 5, 6],
limit: 7
};
handleLimitChange = e => {
const limit = e.target.value;
const limitChanged = limit !== this.state.limit;
if (limitChanged) {
this.setState({ limit, numbers: getRange(limit) });
}
};
render() {
return (
<div>
<input
onChange={this.handleLimitChange}
placeholder="limit"
value={this.state.limit}
/>
{this.state.numbers.map((number, idx) => (
<p key={idx}>{number} </p>
))}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Tip #3:使用 PureComponent 和 React.memo 以避免不必要的 render 調用

我們在之前的例子中看到將特定狀態值放到更低的層級來避免不必要渲染的方法,不過這並不總是有用。

我們來看下下面的例子:

import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [isFooVisible, setFooVisibility] = useState(false);
return (
<div className="App">
{isFooVisible ? (
<Foo hideFoo={() => setFooVisibility(false)} />
) : (
<button onClick={() => setFooVisibility(true)}>Show Foo </button>
)}
<Bar name="Bar" />
</div>
);
}
function Foo({ hideFoo }) {
return (
<>
<h1>Foo</h1>
<button onClick={hideFoo}>Hide Foo</button>
</>
);
}
function Bar({ name }) {
return <h1>{name}</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

可以看到,只要父組件 App 的狀態值 isFooVisible 發生變化,Foo 和 Bar 就都會被重新渲染。

這裡因為需要決定 Foo 是否要被渲染出來,我們需要將 isFooVisible 放在 App中維護,因此也就不能將狀態拆除放到更低的層級。不過,在 isFooVisible 發生變化時重新渲染 Bar 仍然是不必要的,因為 Bar 並不依賴 isFooVisible。我們只希望 Bar 在傳入屬性 name 變化時重新渲染。

那我們該怎麼搞呢?兩種方法。

其一,對 Bar 做記憶化(memoize):

const Bar = React.memo(function Bar({name}) {
return <h1>{name}</h1>;
});

這就能保證 Bar 只在 name 發生變化時才重新渲染。

此外,另一個方法就是讓 Bar 繼承 React.PureComponent 而非 React.Component:

class Bar extends React.PureComponent {
render() {
return <h1>{name}</h1>;
}
}

是不是很熟悉?我們經常提到使用 React.PureComponent 能帶來一定的性能提升,避免不必要的 render。

總結:避免組件不必要的渲染的方法有:React.memo 包起來的函數式組件,繼承自 React.PureComponent 的 class 組件

為什麼不讓每個組件都繼承 PureComponent 或者用 memo 包呢?

如果這條建議可以讓我們避免不必要的重新渲染,那我們為什麼不把每個 class 組件變成 PureComponent、把每個函數式組件用 React.memo 包起來?為什麼有了更好的方法還要有 React.Component 呢?為什麼函數式組件不默認記憶化呢?

毫無疑問,這些方法並不總是萬靈藥呀。

嵌套對象的問題

我們先來考慮下 PureComponent 和 React.memo 的組件到底做了什麼?

每次更新的時候(包括狀態更新或上層組件重新渲染),它們就會在新 props、state 和舊 props、state 之間對 key 和 value 進行淺比較。淺比較是個嚴格相等的檢查,如果檢測到差異,render 就會執行:

// 基本類型的比較
shallowCompare({ name: 'bar'}, { name: 'bar'}); // output: true
shallowCompare({ name: 'bar'}, { name: 'bar1'}); // output: false

儘管對於基本類型(如字符串、數字、布爾)的比較工作的很好,如對象這類複雜的值可能就會帶來意想不到的行為:

shallowCompare({ name: {first: 'John', last: 'Schilling'}},
{ name: {first: 'John', last: 'Schilling'}}); // output: false

上述兩個 name 對象的引用是不同的。

我們重新看下之前的例子,然後修改我們傳入 Bar 的 props:

import React, { useState } from "react";
import ReactDOM from "react-dom";
const Bar = React.memo(function Bar({ name: { first, last } }) {
console.log("Bar render");
return (
<h1>
{first} {last}
</h1>
);
});
function Foo({ hideFoo }) {
return (
<>
<h1>Foo</h1>
<button onClick={hideFoo}>Hide Foo</button>
</>
);
}
function App() {
const [isFooVisible, setFooVisibility] = useState(false);
return (
<div className="App">
{isFooVisible ? (
<Foo hideFoo={() => setFooVisibility(false)} />
) : (
<button onClick={() => setFooVisibility(true)}>Show Foo</button>
)}
<Bar name={{ first: "John", last: "Schilling" }} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

儘管 Bar 做了記憶化且 props 值並沒有發生變動,每次父組件重新渲染時它仍然會重新渲染。這是因為每次比較的兩個對象儘管擁有相同的值,卻因為淺比較的引用不同觸發重新渲染。

函數 props 的問題

我們也可以把函數作為屬性向組件傳遞,當然,在 JavaScript 中函數也是傳遞的引用,因此淺比較也是基於其傳遞的引用。

因此,如果我們傳遞的是箭頭函數(匿名函數),組件仍然會在父組件重新渲染時重新渲染

Tip #4:更好的 props 寫法

前面的問題的一種解決方法是改寫我們的 props。

我們不傳遞對象作為 props,而是將對象拆分成基本類型

<Bar firstName="John" lastName="Schilling" />

而對於傳遞箭頭函數的場景,我們可以代以只唯一聲明過一次的函數,從而總可以拿到相同的引用,如下所示:

class App extends React.Component{
constructor(props) {
this.doSomethingMethod = this.doSomethingMethod.bind(this);    
}
doSomethingMethod () { // do something}
render() {
return <Bar onSomething={this.doSomethingMethod} />
}
}

Tip #5:控制更新

還是那句話,任何方法總有其適用範圍。

第三條建議雖然處理了不必要的更新問題,但我們也不總能使用它。

而第四條,在某些情況下我們並不能拆分對象,如果我們傳遞了某種嵌套確實複雜的數據結構,那我們也很難將其拆分開來。

不僅如此,我們並不總能傳遞只聲明瞭一次的函數。比如在我們的例子中,如果 App 是個函數式組件,恐怕就不能做到這一點了(在 class 組件中,我們可以用 bind 或者類內箭頭函數來保證 this 的指向及唯一聲明,而在函數式組件中則可能會導致一些問題)。

幸運的是,無論是 class 組件還是函數式組件,我們都有辦法控制淺比較的邏輯

在 class 組件中,我們可以使用生命週期鉤子 shouldComponentUpdate(prevProps, prevState) 來返回一個布爾值,當返回值為 true 時才會觸發 render。

而如果我們使用 React.memo,我們可以傳遞一個比較函數作為第二個參數。

**注意!**React.memo 的第二參數(比較函數)和 shouldComponentUpdate 的邏輯是相反的,只有當返回值為 false 的時候才會觸發 render。參考文檔

const Bar = React.memo(
function Bar({ name: { first, last } }) {
console.log("update");
return (
<h1>
{first} {last}
</h1>
);
},
(prevProps, newProps) =>
prevProps.name.first === newProps.name.first &&
prevProps.name.last === newProps.name.last
);

儘管這條建議是可行的,但我們仍要注意比較函數的性能開銷。如果 props 對象過深,反而會消耗不少的性能。

總結

上述場景仍不夠全面,但多少能帶來一些啟發性思考。當然我們還有許多其他的問題需要考慮,但遵守上述的準則仍能帶來相當不錯的性能提升。

源地址:https://medium.com/@siffogh3/yeah-hooks-are-good-but-have-you-tried-faster-react-components-e698a8db468c


禁用大體積依賴的 import

Addy Osmani 大佬推薦了一個 ESLint 的方法來達成這一效果。

有時候我們可能在團隊的項目中禁用大體積依賴包的引入,而這一點我們可以藉助 ESLint 來指定項目中不引用特定的依賴來做到,也即 no-restricted-modules 規則。

如下的例子展示的是禁用 moment.js 的引入(這類體積大的直接引用的話肯定就爆了,不過我們也常常會考慮按需加載)。這條規則也支持自定義提示信息,所以錯誤提示可以建議大家使用小一些的包來代替,如 date-fns、Luxon 等。

{
"rules": {
"no-restricted-imports": ["error", {
"paths":  [{
"name": "moment",
"message": "Use date-fns or Luxon instead!"
}]
}]
}
}

這樣一來,當團隊中有人嘗試著如下寫法,就會報錯:

import moment from 'moment';

另一個例子是禁用 lodash

{
"rules": {
"no-restricted-imports": ["error", {
"name": "lodash",
"message": "Use lodash-es instead!",
}],
}
}

當然,我們也可以不適用錯誤提示,一個數組搞定:

{
"rules": {
"no-restricted-imports": ["error", "underscore", "bluebird"]
}
}

當然,還有一些高級用法,no-restricted-modules 還支持類似 .gitignore 風格的模式。例如,如果有人嘗試著引入匹配了 legacy/* 模式的包,我們就會報錯,如 import helpers from 'legacy/helpers'; 這種:

{
"rules": {
"no-restricted-imports": ["error", {
"patterns": ["legacy/*"]
}],
}
}

源地址:https://addyosmani.com/blog/disallow-imports/


理解 TS 類型註解

這篇文章來自 Dr. Axel Rauschmayer 的博客,對 TypeScript 的靜態類型註解進行了梳理。今天來學習下。


1. 我們會學到什麼

在讀完本篇文章後,我們應該能夠理解下面這段代碼的含義:

interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U): U;
···
}

上面這一坨看上去讓人暈眩。一旦理解,就可以通過上面的表達很快明白整個代碼行為。

2. 嘗試運行文中的例子

TypeScript 有個 在線測試網站。為了得到最全面的檢查,我們應該把 Options 所有項都打開。這就等同於 TypeScript 編譯器打開 –strict 模式。

3. 指定類型檢測的嚴格度(comprehensiveness)

一般來說,使用 TypeScript 還是要打開最嚴格的設置的,即 –strict,不然的話,程序本身可能會更好寫一些,但是肯定會失去靜態類型檢查的諸多好處。目前,這一設置會打開如下子設置項:

  • –noImplicitAny: 如果 TypeScript 不能推斷出類型,你就必須手動指定。這通常用於函數方法的參數:有了這一設定,我們就必須註解參數類型。
  • –noImplicitThis: 如果 this 的類型不明確,就會報問題。
  • –alwaysStrict: 儘可能使用 JavaScript 的嚴格模式。
  • –strictNullChecks: null 什麼類型也不是 (除了它本身的類型:null) ,而且必須顯示指明。
  • –strictFunctionTypes: 對於函數類型進行更嚴格的檢查。
  • –strictPropertyInitialization: 如果一個屬性不能夠為 undefined,那麼它必須在構造函數中被初始化。

4. 類型(Types)

本文定義類型就是「一些值的集合」。JavaScript (不是 TypeScript!) 有 7 種類型:

  • Undefined: 只有 undefined 的集合。
  • Null: 只有 null 的集合。
  • Boolean: 有 false 和 true 的集合。
  • Number: 所有數字的集合。
  • String: 所有字符串的集合。
  • Symbol: 所有符號的集合。
  • Object: 所有對象的集合(包含函數和數組)。

所有這些類型都是動態的,我們可以在運行時使用它們。

TypeScript 為 JavaScript 帶來了額外的一層:靜態類型。靜態類型只存在於編譯或對源代碼進行類型檢查的時候。每個存儲位置(變量或屬性)都有一個靜態類型,類型檢查會確保其對值類型的預測正確。當然,還有很多可以靜態檢查的方式。如果函數調用的參數類型為數字,那麼傳入字符串就會報錯。

5. 類型註解(Type annotations)

變量名後的冒號後跟著的就是類型註解:這個類型簽名標識變量可以擁有什麼樣的值。
A colon after a variable name starts a type annotation: the type signature after the colon describes what values the variable can have. 例如下面的代碼標識 x 只能為數字類型:

let x: number;

如果我們用 undefined 對 x 進行初始化(畢竟一個變量在未賦值的狀態下默認就是 undefined),那麼 TypeScript 會讓我們無法對其進行賦值。

6. 類型推理(Type inference)

儘管在 TypeScript 中每一個存儲位置裡都帶有靜態類型,我們並不總是需要顯示指定。TypeScript 是可以推測類型的。例如:

let x = 123;

上面的代碼就可以直接推測出數字類型。

7. 描述類型(Describing types)

What comes after the colon of a type annotation is a so-called type expression. These range from simple to complex and are created as follows.

在冒號後跟著的類型註解就是所謂的類型表達式,基本的類型如下:

  • JavaScript 動態類型相應的靜態類型:
    • undefined, null
    • boolean, number, string
    • symbol
    • object.
    • 注意: 值 undefined vs 類型 undefined (取決於使用的位置)
  • TypeScript 特定類型:
    • Array (準確的說並不是 JS 的類型)
    • any (任意類型)
    • Etc.

注意,作為值的 undefined 和作為類型的 undefined 都寫作 undefined,而具體是什麼取決於我們使用的位置。同樣,對於 null 也是相同的情況。

我們可以通過類型運算符來組合基本類型,從而得到負責的類型表達式,本質上來說就是取並集和交集。

接下來幾塊就要講解下 TypeScript 提供的一些類型運算符。

8. 數組類型

  • 列表:擁有相同類型元素,數組長度可變;
  • 元組:數組長度固定,元素並不必須擁有相同類型。

8.1 list

有兩種方式來表示一個擁有數組類型元素的列表:

let arr: number[] = [];
let arr: Array<number> = [];

一般來說,如果有賦值的話,TypeScript 是可以推斷出變量的類型的。不過在上述場景中,我們必須得指定,因為從空數組中是無法進行推斷的。

晚些我們會斷尖括號進行闡述。

8.2 tuple

如果我們要在數組中存儲二維點,那麼我們就要以元組的方式使用數組。

let point: [number, number] = [8, 3];

在這種情況下,我們其實可以不寫類型註解。

另一個例子是 Object.entries(obj) 的返回結果:obj 的每個屬性都會返回一個 [key, value]:

> Object.entries({ a: 1, b: 2 })
[ ['a', 1], ['b', 2] ]

那麼 Object.entries() 返回值的類型其實就是:

Array<[string, any]>

9. 函數類型

函數類型的例子如下:

(num: nubmer) => string

含義不過多描述,如果我們想實際聲明一個類型註解,可以如下(這裡 String 表示一個函數):

const func: (num: number) => string = String;

當然這裡 TypeScript 是知道 String 類型的,因此可以推測出 func 的類型。

下面的代碼可能更實際一些:

function stringify123(callback: (num: nubmer) => string) {
return callback(123);
}

我們用一個函數類型來描述 stringify123 的參數 callback 的類型。因為這樣進行了註解,下面的寫法就會報錯:

f(Number)

但是這個就可以:

f(String);

9.1 函數聲明的返回類型

一個好的實踐是函數的所有參數都加上註解,當然也可以指定返回類型(TypeScript 還是相當擅長推斷的):

function stringify123(callback: (num: number) => string): string {
const num = 123;
return callback(num);
}

特定返回類型 void

void 是一種特殊的返回類型:它可以告訴 TypeScript 函數總是返回 undefined(無論顯示或隱式):

function f1(): void { return undefined } // OK
function f2(): void { } // OK
function f3(): void { return 'abc' } // error

9.2 可選參數

標識符後的問號意味著參數是可選的:

function stringify123(callback?: (num: number) => string) {
const num = 123;
if (callback) {
return callback(num); // (A)
}
return String(num);
}

如果在 –strict 模式下運行 TypeScript,如果提前做了檢查,它將只允許你在 A 行執行。

參數默認值

TypeScript 支持 ES6 默認值寫法:

function createPoint(x=0, y=0) {
return [x, y];
}

默認值會使得參數變得可選。我們通常會忽略類型註解,因為 TypeScript 可以推測出類型。例如,它可以推測出 x 和 y 都擁有數字類型。

如果我們想要添加類型註解,可以這麼寫:

function createPoint(x:number = 0, y:number = 0) {
return [x, y];
}

9.3 rest 展開類型

還可以在 TypeScript 參數定義的時候使用 ES6 展開運算符。對應參數的類型必須是數組:

function joinNumbers(...nums: number[]): string {
return nums.join('-');
}
joinNumbers(1, 2, 3); // '1-2-3'

10. 聯合類型

在 JavaScript 中,變量有時可能有幾種類型。為了描述這類變量,我們使用聯合類型。例如,在下面代碼中,x 可以為 null 或者數字類型:

let x = null;
x = 123;

這種情況下,x 的類型就可以描述如下:

let x:null|number = null;
x = 123;

類型表達式 s|t 就是用集合論中的聯合符號來表達這一運算的含義的。

現在我們來重寫下上面的 stringify123():這次我們不希望參數 callback 是可選的,而是明確定義的。如果調用者不想傳入函數,也要顯式傳入一個 null。實現如下:

function stringify123(
callback: null | ((num: number) => string)) {
const num = 123;
if (callback) { // (A)
return callback(123); // (B)
}
return String(num);
}

仍然要注意的是,我們需要檢查 callback 是否實際上是函數(如 A 行所示),然後我們才可以調用 callback(如 B 行)。如果不進行檢查,TypeScript 會報錯。

10.1 Optional vs. undefined|T

類型 T 的可選參數和類型 undefined|T 相當相似。

主要的區別在於,我們可以忽略可選參數

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK

但不可以忽略類型為 undefined|T 的參數

function f2(x: undefined | number) { }
f2(); // error
f2(undefined); // OK
f2(123); // OK

10.2 值 null 和 undefined 通常並不是類型

在許多編程語言中,null 是一種類型。例如,如果 Java 中的參數類型是 String,你仍可以傳入 null 而 Java 並不會報錯。

與之相對的,TypeScript 中,undefined 和 null 是分開處理的。如果想做到上述效果,就需要指定 undefined|string 或 null|string,這兩者是不同的。

11. 類型對象(接口)

和數組相似,對象扮演著兩種角色(有時候會有所混合或者更加動態):

  • 記錄:在開發階段可以存儲定量的已知屬性,每種屬性都可以有不同類型。
  • 字典:在開發階段存放的未知鍵值的屬性,每個屬性鍵(字符串或符號)及值都有相同的類型。

我們會忽略字典的用法,事實上,Maps 類型是個更合適的選擇。

11.1 通過接口指定作為記錄的對象的類型

接口可以描述作為記錄使用的對象:

interface Point {
x: number;
y: number;
}

TypeScript 類型系統的一個優勢是,它可以按結構工作,而不是按名義:

function pointToString(p: Point) {
return `(${p.x},${p.y})`;
}
pointToString({x: 5, y: 7}); // '(5, 7)'

與之相對的是,Java 的名義類型系統需要類來 implement 接口。

11.2 可選屬性

如果屬性可以被忽略,同樣用問號即可:

interface Person {
name: string;
company?: string;
}

11.3 方法

接口可以包含函數方法:

interface Point {
x: number;
y: number;
distance(other: Point): number;
}

12. 類型變量 & 通用類型

其實下面這段我都看不懂什麼意思,如果我理解的沒錯,尖括號其實就是模板的概念。

有了靜態類型,我們就有了兩種層級:

  • 值處於對象層級
  • 類型處於元層級

一般的變量可以通過 const、let 等定義,類型變量則通過上文提到的尖括號(<>)。例如,下面的代碼包含了類型變量 T:

interface Stack<T> {
push(x: T): void;
pop(): T;
}

我們可以看到,類型參數 T 在 Stack 定義體中出現了兩次。因此,這個接口就可以這麼理解:

  • Stack 是一種棧類型,其元素都是類型 T,我們需要在使用 Stack 的時候指定 T;
  • push 方法接受一個類型 T 的值;
  • pop 方法會返回類型 T 的值。

如果我們使用 Stack,我們必須賦「類型」給 T,接下來的代碼展示了一個傻乎乎的棧:

const dummyStack: Stack<number> = {
push(x: number) {},
pop() { return 123 },
};

12.1 例子:Maps

如下是 Map 類型的使用方法:

const myMap: Map<boolean, string> = new Map([
[false, 'no'],
[true, 'yes'],
]);

12.2 函數的類型變量

函數也可以使用類型變量:

function id<T>(x: T): T {
return x;
}

這樣的話,函數就可以這麼用了:

id<number>(123);

因為可以做類型推斷,所以代碼也可以忽略類型參數:

id(123);

12.3 傳遞類型參數

函數可以傳遞參數給接口、類等等:

function fillArray<T>(len: number, elem: T) {
return new Array<T>(len).fill(elem);
}

類型 T 出現了 3 次:

  • fillArray<T>:引入類型變量
  • elem: T:使用從參數得到的類型變量
  • Array<T>:傳遞 T 到數組構造函數

13. 總結

現在我們回過頭去看最初的那段代碼:

interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U): U;
···
}

代碼定義了一個數組的接口,元素類型為 T:

  • concat 方法有一個或多個參數(通過展開運算符定義),每個參數都擁有類型 T[]|T,也就是說每個元素都可能是 T 的數組或一個 T 類型的值。
  • reduce 方法引入了類型變量 U。U 表示後續的 U 類型都有一樣的類型:

Back/forward cache for Chrome

Chrome 團隊正在研發一種新的回退、前進緩存技術,當用戶導到其他頁面時,將頁面緩存在內存中(維護 JavaScript 和 DOM 的狀態)。這項技術還是相當牛的,可以明顯加快瀏覽器回退、前進導航的速度

當導航離開一個頁面的時候,回退/前進緩存(後文縮寫為 bfcahce)會緩存整個(包括 JavaScript 的堆),這樣當用戶導航回來的時候,就可以恢復整個頁面的狀態,這就有點像暫停了一個頁面然後離開,過了一會又回來了我們繼續「播放」這個頁面一樣舒爽。

下面的視頻是在筆記本上跑的 bfcache 早期原型效果,可以看到右側運用了 bfcache 的效果非常炫(建議直接拖到中間看):

embed: Chrome bfcache early developer preview.mp4

如果是上傳到外網然後不可見的話,也可以考慮點這個油管鏈接

下面的則是在安卓機 Chrome 上的效果:

embed: bfcache on Chrome for Android.mp4

如果是上傳到外網然後不可見的話,也可以考慮點這個油管鏈接

這項技術估計可以為移動端 Chrome 的導航提升 19% 的性能。

一些細節可參考這裡

  • 調整 Chrome 的導航棧來創建新的幀,而不是複用原有的部分;
  • 修改 Blink 的代碼來保證在頁面進入 bfcache 的時候,所有頁面相關的任務都會被凍結;
  • 根據資源和隱私限制,這緩存可緩存的頁面。

源地址:https://developers.google.com/web/updates/2019/02/back-forward-cache


ES 新提案:Promise.any(promises)

Promise.any 的提案目前處於 stage-0,例子如下:

「每日一瞥📰」0218~0308

Promise.any 會在任一 promise 完成之後返回相應的值,這是在 Promise.allSettled、Promise.all 和 Promise.race 之後的又一 Promise 相關的能力。

那麼這幾個東西有什麼區別呢?

「每日一瞥📰」0218~0308

問題來了,settled 和 fulfilled 之間有什麼區別呢?在推特的評論中有這麼一句解釋:

Settled means fulfilled or rejected, not pending. Fulfilled means it resolved, not rejected.

也就是說,settled 意味著確定的結果,與 pending 相對,可能是 fullfilled 或者是 rejected;而 fulfilled 與 rejected 相對,意味著 resolved。

所以在上面表中,就可以明白 Promise.race 和 Promise.any 的區別了:

Promise.race will reject if any of the promises reject.

即,如果任一一個 promise 被 reject 了,那麼 Promise.race 就會 reject,而 Promise.all 並不會,這就是 settled 和 fulfilled 的區別。

最後複習下 MDN 上關於 Promise 的圖:

「每日一瞥📰」0218~0308

源地址:https://github.com/tc39/proposal-promise-any


為什麼這個函數不能 new

「每日一瞥📰」0218~0308

本篇討論的就是上面圖中的問題:為什麼 foo 可以用 new 初始化,而 bar 不行呢?

method syntax

這事可以從 ES2015 的一個縮寫語法說起。ES2015 增加了一種在對象初始化時在其中定義方法的縮寫語法:

var obj = {
foo() {
return 'bar';
}
}
console.log(obj.foo());
// expected output: "bar"

當然,可定義的方法類型很多,除了普通的函數方法,也可以掛上 generator、async 方法、計算屬性等:

var obj = {
property( parameters… ) {},
*generator( parameters… ) {},
async property( parameters… ) {},
async* generator( parameters… ) {},
// with computed keys:
[property]( parameters… ) {},
*[generator]( parameters… ) {},
async [property]( parameters… ) {},
// compare getter/setter syntax:
get property() {},
set property(value) {}
};

那麼回想下最開始的例子,如果我們有如下定義:

var obj = {
foo: function() {
/* code */
},
bar: function() {
/* code */
}
};

我們就可以縮寫成這種方式:

var obj = {
foo() {
/* code */
},
bar() {
/* code */
}
};

也就是說,bar 其實就是 foo 的縮寫,怎麼還整出區別了呢?

研究一下

MDN 文檔上有這麼一段

var obj = { 
method() {}
};
new obj.method; // TypeError: obj.method is not a constructor
var obj = { 
* g() {} 
};
new obj.g; // TypeError: obj.g is not a constructor (changed in ES2016)

具體為什麼,不妨在瀏覽器中試驗一下,可以得到如下結果:

「每日一瞥📰」0218~0308

從這張圖中其實可以看到,不同定義方式對 foo() 和 bar() 的結果產生了影響:foo() 本身是 callable 的,在它的原型 prototype 上有 constructor;bar() 並不是 callable 的,它並沒有原型 prototype 而只有 __proto__。

感覺還是有些理不清呀,這裡祭出一張珍藏多年的圖,我們對照著看:

「每日一瞥📰」0218~0308

先說 foo(),作為一個函數,它的 prototype 就是 foo.prototype,而 foo.prototype 上也確實有 constructor,而 foo.prototype.constructor 正是 foo 本身,使用 new 來調用沒有問題。此外,foo.prototype 上有 __proto__,它正是 Object.prototype,而它的 constructor 就是 Object 函數。

再說 bar(),它沒有 prototype,而這正是行為差異的所在。它只有 __proto__,指向 Function.prototype,而 Function.prototype 的 constructor 正是 ƒ Function()

哈哈是不是捋清楚了一點。

整理

ES2015 對函數的兩種類型進行了區分:

  • callable functions:不使用 new 的函數,如 foo();
  • constructable functions:使用 new 的函數,如 bar();

函數定義的方式決定了一個函數是 callable 的還是 constructable 的。標準中已經說明了通過方法語法定義的函數不是 constructable 的。

具體劃分如下:

  • Constructable functions(要 new 的函數):

    • Classes
  • Callable functions(不 new 的函數):

    • Arrow functions
    • Object/class methods (via method syntax)
    • Generator functions
    • Async functions
  • 用不用 new 都行:

    • Function declarations/expressions

函數組件和類組件的根本差異

我們肯定看過很多形式上的差異,比如同樣的效果按函數方式和類的方式去實現會有什麼樣的區別:

「每日一瞥📰」0218~0308

看上去,二者也只是在實現時存在些寫法的差異,再不然我們可能會有一些「很權威」的結論,比如類組件能有更多的特性(如狀態)。但是因為有了 hooks,這些差異都不再是真正的差異了。

Dan 總結了差異的真正所在:

Function components capture the rendered values.

不過我們不著急,先往下看。

以一個例子開始

那麼真正的差異究竟是什麼呢?我們看一個下面的例子:

「每日一瞥📰」0218~0308

具體源碼是這樣的:

// index.js
import React from "react";
import ReactDOM from "react-dom";
import ProfilePageFunction from './ProfilePageFunction';
import ProfilePageClass from './ProfilePageClass';
class App extends React.Component {
state = {
user: 'Dan',
};
render() {
return (
<>
<label>
<b>Choose profile to view: </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="Dan">Dan</option>
<option value="Sophie">Sophie</option>
<option value="Sunil">Sunil</option>
</select>
</label>
<h1>Welcome to {this.state.user}’s profile!</h1>
<p>
<ProfilePageFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
<ProfilePageClass user={this.state.user} />
<b> (class)</b>
</p>
<p>
Can you spot the difference in the behavior?
</p>
</>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// ProfilePageClass.js
import React from 'react';
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
export default ProfilePage;
// ProfilePageFunction.js
import React from 'react';
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
export default ProfilePage;

handleClick 就是模擬了一個異步請求的操作,代碼中其實就是用函數和類分別實現了這樣的功能。

然後操作如下:

  • 點擊其中一個 Follow;
  • 在 3 秒鐘過去前修改選中的 profile;
  • 讀取 alert 文案;

然後我們就會發現二者的區別:

  • 使用 ProfilePage 函數實現的方法,在 Dan 那裡點擊完 Follow 然後切換到 Sophie 仍會彈出 ‘Followed Dan’.
  • 使用 ProfilePage 類實現的方法,則會彈出 ‘Followed Sophie’:

很顯然,前者才是正確的,或者說,在函數式實現中,this.props.user 才是我們想要的。

分析

我們再來看看 class 實現的的代碼到底是怎樣的:

class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};

This class method reads from this.props.user. Props are immutable in React so they can never change. However, this is, and has always been, mutable.

類方法讀取了 this.props.user。在 React 中,Props 是 immutable 的,因此它們並不會發生改變。但是,this 永遠是 mutable 的。

事實上,這就是 class 中的 this 的全部目的。React 會不斷地改變 this,從而在 render 和生命週期方法中總能得到新版本的組件。

所以,如果我們的組件在請求過程中發生了重新渲染,this.props 也會改變。也就是說,showMessage 讀取到了「太新」 的 props。

這也是對交互界面的本質的一個有趣的觀察視角。如果說 UI 本質上是當前應用狀態的函數,那 event handler 就是渲染結果的一部分。我們的 handler是「從屬於」特定的 render,對應著特定的 props 和 state。

但是呢,讀取 this.props 的 setTimeout 中的回調方法破壞了這一聯繫。showMessage 並沒有和特定的 render 聯繫在一起,並且沒有拿到爭取的 props

那麼究竟應該怎麼做呢? 無論什麼方法什麼前端庫,其實都會導致上面所說的問題。如果沒有 hooks,而我們又想拿到正確的值,解決問題的關鍵在於閉包

儘管很多時候我們會避免閉包行為,但是在 React 中,props 和 state 是 immutable 的,而這就避免了閉包可能帶來的不好的影響。如果我們針對特定 render 閉包 props 和 state,我們就總能將它們的關係一一對應起來。

class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;
// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}

這樣,我們就總能將 props 與 render 對應起來,不再出現上面展示的那種 bug。

但是還不夠,上面的寫法未免太扯了。我們不妨包上一層:

function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}

這一次,我們就能得到正確的結果了。

因此,Dan 針對這一差異做了總結:

Function components capture the rendered values.

而毫無疑問的,Hooks 解決了這一問題。事實上在文檔中,反覆提及了所謂的「副作用是 render 結果的一部分」這樣的理念。

源地址:https://overreacted.io/how-are-function-components-different-from-classes/


Preact X Alpha 0 released

Preact X 是下一個 major release,旨在提供一些迫切需要的特性如 FragmentscomponentDidCatchcreateContexthooks,以及其他一些和三方庫之間兼容性問題的改進。

Fragments✅

與 React 中的 React.Fragments 類似,Preact 終於也支持了這種寫法。當然,在 React 中,我們還可以使用 <><\/> 來進一步簡化代碼。

此外,Preact 還可以將組件以數組的方式返回:

function Columns() {
return [
<td>Hello</td>
<td>World</td>
];
}

componentDidCatch✅

componentDidCatch 是 React 16 中的新的生命週期方法,在之前的一期中更新過。當一個 class 內定義了這個生命週期方法,這個組件就變成了一個 error boundary。

現在在 Preact X 中終於也提供了,我們可以用它來捕獲 render 或其他生命中其方法中的錯誤。有了它,就可以展示更友好的錯誤信息,或者向外部服務發送 log 信息。

class Foo extends Component {
state = { error: false };
componentDidCatch(err) {
logErrorToBackend(err);
this.setState({ error: true });
}
render() {
// If an error happens somewhere down the tree
// we display a nice error message.
if (this.state.error) {
return <div class="error">Something went wrong...</div>;
}
return <Bar />;
}
}

Hooks

緊跟著 React 16.8 的腳步,Preact 也開始支持 Hooks 了。React 的 Hooks 文檔真的很不錯,無論如何都推薦認真讀一讀,對 UI 的理解可能會有些不同。

Preact 引入 hooks 是按需引入的,當我們使用打包工具打包時,沒用到的 hooks 就不會打包到 App 中:

import { h, render } from 'preact';
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
// ^ default state value
return (
<div class="counter">
Current count: {count}
<button onClick={() => setCount(count + 1}}> +1 </button>
<button onClick={() => setCount(count - 1}}> -1 </button>
</div>
);
}
render(<Counter />, document.body);

createContext✅

現在 React 16.4 中的特性也支持了,通過它可以做一些 pub/sub 模式的工作。不過老實說,我不是很喜歡這種方式。但總的來說,Preact X 確實支持了很多 React 已有的實用特性。

import { createContext } from "preact";
const Theme = createContext("red");
function Button() {
return <Theme.Consumer>
{value => <button style={{ color: value }}>click</button>}
</Theme.Consumer>;
}
function App() {
return <Theme.Provider value="blue">
<Button />
</Theme.Provider>;
}

可以看出,基本是完全一樣的寫法。

setState 異步

在 Preact 之前的版本中,setState 是同步執行的。這讓我想到了小程序中的 setData 也是同步的,終於 Preact 也走向了異步更新的道路,只是不知道內部是不是 batchUpdate 的思路。

總的來說,Preact 似乎仍然是以更輕量的 React 來定位自己,在某些對包體積非常敏感的場景,隨著 Preact 的發展,其地位應該會越來越高吧。

源地址:https://github.com/developit/preact/releases/tag/10.0.0-alpha.0


Chromium Edge 截圖透出

去年年底,微軟表示其計劃在 Chromium 上重建 Edge 瀏覽器。該軟件巨頭一直在測試新版瀏覽器的日常開發版本,現在透出的一些截圖展示了微軟的進展。雖然微軟當前版本的 Edge 擁有自己獨特的 UI,但基於 Chromium 的 Edge 版本看起來很像 Chrome。

Neowin 發佈了許多 Chromium Edge 主要界面、功能的截圖和一些可用的設置。這個新瀏覽器顯然處於早期階段,微軟似乎正在測試擴展支持、同步和一種包括當天和大多數訪問過的網站的圖像的新標籤視圖。現在的版本支持現有的 Edge 擴展,Microsoft 也應該會支持 Chrome 自己的 web 擴展。

「每日一瞥📰」0218~0308

瀏覽器界面本身包含與 Chrome 相同的刷新、主頁和導航按鈕。地址欄上甚至還有收藏夾和個人資料圖片。這是因為微軟的工作建立在 Chromium 的核心之上,但由於這些是非常早期的版本,我們可能還沒有看到任何界面變化。

目前還不清楚微軟計劃何時公開測試其 Chromium 版本的 Edge。 Microsoft 員工可以使用日常構建版本,並且很快就會對瀏覽器進行更廣泛的測試。如果有興趣在可用之後對其進行測試,可以註冊來參與到 Microsoft 的 Edge Insider 計劃,在微軟準備公測後收到通知。

源地址:https://www.theverge.com/2019/3/5/18251263/microsoft-edge-chromium-screenshots-leak-browser


React 函數組件的 TypeScript 寫法

今天來看下引入 TS 之後,Hooks 方面的代碼怎麼寫。嗯,Hooks 的東西寫太多了。

Functional Component with TypeScript

因為過去函數式組件是沒有狀態的,被稱為 Stateless Function Components,因此過去我們在定義函數式組件的時候使用的是 React.SFC。現在因為有了 Hooks 的引入,函數式組件同樣有了狀態,可以使用到生命週期的能力,因此調整成 React.FC。

import * as React from 'react'
interface IProps {
// ... props interface 
}
// NEW syntax for typing function components
const MyNewComponent: React.FC<IProps> = (props) => {...};
// OLD syntax for typing function components
const MyOldComponent: React.SFC<IProps> = (props) => {...};

傳入屬性的定義在 FunctionComponent:

interface FunctionComponent<P = {}> {
(props: P & { children?: ReactNode }, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}

注意:React 團隊正在考慮移除函數組件的 defaultProps。事實上這一點在自己開發的過程中也有體會,因為函數的默認參數已經能夠滿足需求,引入 defaultProps 純屬增加複雜度。

useState with TypeScript

畢竟函數組件有了 Hooks,所以本文可能要講述下這塊的寫法和變化。使用 useState 並沒有什麼特別需要修改的地方,我們看下下面的例子:


import * as React from 'react';
const MyComponent: React.FC = () => {
const [count, setCount] = React.useState(0);
return (
<div onClick={() => setCount(count + 1)}>
{count}
</div>
);
};

對於簡單的函數來說,useState 可以從初始值推斷出類型並且基於函數使用的值來返回結果。對於複雜的狀態,我們可以通過泛型 useState<T> 來指定類型。下面的例子就是指 user 狀態既可為對象,也可是 null:

import * as React from 'react';
interface IUser {
username: string;
email:  string;
password: string;
}
const ComplexState = ({ initialUserData }) => {
const [user, setUser] = React.userState<IUser | null>(initialUserDate);
if (!user) {
// do something else when our user is null
}
return (
<form>
<input value={user.username} onChange={e => setUser({...user, username: e.target.value})} />
<input value={user.email} onChange={e => setUser({...user, email: e.target.value})} />
<input value={user.password} onChange={e => setUser({...user, password: e.target.value})} />
</form>  
);
}
view raw

可以看出,主要是利用接口來定義了複雜狀態的類型,同時使用的聯合類型保證了它可以為 null。

官方定義的類型如下:

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);

useEffect with TypeScript

在使用 useEffect 的時候,並不會有什麼需要注意的類型定義,畢竟 useEffect 本身就是傳入一個回調函數和觸發副作用的 trigger。我們可以直接看一下官方定義:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
// The first argument, `effect`
type EffectCallback = () => (void | (() => void | undefined));
// The second argument, `deps?`
type DependencyList = ReadonlyArray<any>;

可以看出,第一個參數是一個函數類型,函數可以返回空或者指定一個回調,也就是一個 cleanup 函數;第二個參數是可選的依賴列表,其類型是隻讀的、元素為任意類型的列表。

useReducer with TypeScript

useReducer 也是常用的一個 hooks,它的使用則相對複雜一些(其實也不復雜):

import * as React from 'react';
enum ActionType {
Increment = 'increment',
Decrement = 'decrement',
}
interface IState {
count: number;
}
interface IAction {
type: ActionType;
payload: {
count: number; 
};
}
const initialState: IState = {count: 0};
const reducer: React.Reducer<IState, IAction> = (state, action) => {
switch (action.type) {
case ActionType.Increment:
return {count: state.count + action.payload.count};
case ActionType.Decrement:
return {count: state.count - action.payload.count};
default:
throw new Error();
}
}
const ComplexState = () => {
const [state, dispatch] = React.useReducer<React.Reducer<IState, IAction>, IState>(reducer, initialState);
return (
<div>
<div>Count: {state.count}</div>
<button onClick={
() => dispatch({type: ActionType.Increment, payload: { count: 1 } })
}>+</button>
<button onClick={
() => dispatch({type: ActionType.Decrement, payload: { count: 1 }})
}>-</button>
</div>  
);

可以看到,action 本身被定義成枚舉類型,這其實與我們將類型分別定義成常量並 export 沒什麼區別;同時,初始狀態被定義成 IState 類型,而 action 則被定義成 IAction 類型,而 reducer 內部的實現,與普通 redux 寫法並無不同。

其他 hooks 因為不常使用,就不再贅敘。

源地址:https://levelup.gitconnected.com/usetypescript-a-complete-guide-to-react-hooks-and-typescript-db1858d1fb9c


用 Jest 和 Enzyme 寫測試

Jest 是一個單元測試框架,由臉書團隊製作;Enzyme 則是用於測試組件的框架,可以寫斷言來證明 UI 能夠正確的工作(尋找組件並交互,如有問題則給出錯誤標識)。這兩者是獨立的工具,但二者相互補充。

因為筆者尚未對測試方面有什麼瞭解,因此本文很簡單,就是用 cra 跑個項目來對這兩個工具有個體感。

cra 已經包括了 Jest,我們只需要再額外安裝 enzymeenzyme-adapter-react-16

yarn add enzyme enzyme-adapter-react-16 --dev

創建一個設置文件,並將其放置到 src 目錄下:

// setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

這樣就引入了 Enzyme 並配置了運行測試用的適配器。

測試快照

「快照測試」是為了跟蹤應用 UI 的變化。快照非常有用,因為可以捕獲一個組件在某一時刻的代碼,從而讓我們在組件的不同狀態下對組件進行比較。

第一次運行時,組件代碼的快照就會生成並保存在 src 目錄下的 __snapshots__。後面只要運行測試,當前的 UI 就會與已經存在的版本進行比較。下面是一個例子:

it("renders correctly", () => {
const wrapper = shallow(
<App />
);
exprect(wrapper).toMatchSnapshot();
});

然後執行 yarn run test 即可。

每次生成的新的快照都會保存在 __tests__ 目錄下。

如果我們修改了 UI 層的內容,例如將 h2 標籤的內容進行了修改,就會得到如下內容:

「每日一瞥📰」0218~0308

但或許我們是故意這麼修改的,那麼為了通過我們的測試,我們可以講標籤內容改為之前的內容,也可以更新我們的快找文件。Jest 提供了關於更新快照的知道信息,這樣我們就不比人更去更新快照了:

Inspect your code changes or press `u` to update them.

有個細節是 shallow 方法,這是 Enzyme 包內高速測試只在一個單一組件上進行,即使子組件也不進行測試。將代碼進行拆分有助於調試,尤其對於簡單的沒有交互的組件。

與之相反的是 render 方法,它將包含子組件的信息。

總結:快照類的測試可以較好地比較 UI,對於變化不頻繁的共用組件、頁面還是很好用的。

測試組件的生命週期

雖然按目前的習慣已經是基本不用 class 了,所以也不存在使用生命週期的場景了,但還是來看看。

生命週期本身也是 React 過去提供的鉤子,我們常常在組件的掛在階段進行一些 data fetching 的工作。我們可以利用 Jest 來測試生命週期方法是否被調用,從而讓我們可以模擬 React 應用的生命週期方法。

it('calls componentDidMount', () => {
jest.spyOn(App.prototype, 'componentDidMount')
const wrapper = shallow(<App />)
expect(App.prototype.componentDidMount.mock.calls.length).toBe(1)
})

上面的方法就是監控 componentDidMount() 並斷言該方法被調用了一次。

暫時沒看明白這個有什麼特別的用處。

測試組件的屬性

這個還是很重要的,尤其是項目中有大量按照貧血模型實現而出現的受控組件。使用 Enzyme 的 API 可以讓我們來模擬傳入的屬性。

假設我們講過App 組件中的用戶信息傳入 Profile 組件,那麼我們 mock 一下傳入的屬性:

const user = {
name: 'hydraz320',
email: '[email protected]',
username: 'hydraz',
image: null
}

我們使用 describe 來包裹需要測試的組件,如下例子:

describe ('<Profile />', () => {
it ('contains h4', () => {
const wrapper = mount(<Profile user={user} />)
const value = wrapper.find('h4').text()
expect(value).toEqual('John Doe')
})
it ('accepts user props', () => {
const wrapper = mount(<Profile user={user} />);
expect(wrapper.props().user).toEqual(user)
})
})

這裡包含了兩部分測試。首先我們向掛載的 Profile 組件傳入屬性,然後我們檢查是否能在其中找到 Profile 組件中本該包含的 h4 元素。然後,我們檢查了傳入的屬性是不是與我們創建的 mock 屬性相等。注意,即使我們在 Profile 組件內部通過結構的方式處理屬性,也不會影響測試結果。

模擬 API 調用

測試 API 的一個注意點在於我們並不是真的想去請求 API。有些 API 的調用次數有限,甚至可能要花錢,因此我們需要避免真的去做請求來測試。我們可以使用 Jest 來 mock axios 請求。

首先我們會創建一個新的 __mock__ 目錄,這裡放置我們想要執行的 mock request:

module.exports = {
get: jest.fn(() => {
return Promise.resolve({
data: [
{
id: 1,
name: 'Jane Doe',
email: '[email protected]',
username: 'jdoe'
}
]
})
})
}

我們想要測試是否發出了 GET 請求,因此我們通過如下語句:

import axios from 'axios';
// ...
jest.mock('axios')

Jest 有個方法是 spyOn(),它有個 accessType 參數可用於檢測我們是否「get」到了 API 調用的返回結果。

it('fetches a list of users', () => {
const getSpy = jest.spyOn(axios, 'get')
const wrapper = shallow(
<App />
)
expect(getSpy).toBeCalled()
})

整體來看,僅就目前而言,前端方面的測試並不是一個已經解決的問題,還有很多的探索值得去做。

源地址:https://css-tricks.com/writing-tests-for-react-applications-using-jest-and-enzyme/

相關文章

神器推薦|辦公室久坐的你不可錯過的神器

源碼給你,零基礎搭建一個免費的網址導航站

最好用的web端代碼文本編輯器ACE

簡單,真誠「阿里新零售CRO技術部大前端團隊」春招實習!