4 個問題帶你進階 React Hooks

木子星兮,現阿里巴巴高級前端工程師,致力於深挖細節點背後的原理,從點到面將細碎的點串聯起來,在理解的基礎上記憶,夯實基礎知識。

4 個問題帶你進階 React Hooks


前言

相信大部分人都已經在使用 React hooks 了,但是在開發過程中,我們要 知其然知其所以然。整理了一下最近使用 React hooks 遇到的一些問題,如果有問題或更好的答案,歡迎一起交流。

目錄

  • 為什麼要在 React 中引入Hook ? Hooks 解決了什麼問題
  • mixin、HOC 、render props、hooks
  • React Hooks 原理
  • Hooks 中閉包的坑

一、為什麼要在React中引入Hook? Hooks解決了什麼問題

1. 組件複用邏輯難

沒有hooks之前使用 render props和 高階組件。render props是接受一個組件作為props, HOC是一個函數,接受一個組件作為參數,返回另一個組件。使用這些開發的組件會形成“嵌套地獄”,調試困難。

2. 複雜組件狀態邏輯多

很多組件在最開始寫的時候都是很簡單的,基本上就是隻做一件事,當你的業務邏輯變得複雜之後,組件也會變得複雜起來。大多數情況下,我們不大可能把組件拆分的更小,因為可能有很多共用的狀態邏輯,拆分後,組件之間的通信也會很複雜,甚至需要引用 Redux 來管理組件之間的狀態。

3. class學習成本高

要想用好 class 組件,你必須瞭解 ES6 中的class,理解 JavaSript 中 this的工作方式,要注意綁定事件處理器,清楚當前this的指向。

詳細查看react 官方文檔 Hook 簡介[1]

二、mixin、HOC 、render props、hooks

我們在平常開發中經常會遇到很多的頁面有一些公共的邏輯,我們不能每次遇到的時候都直接把原來的代碼 copy 過來改扒改扒,改的時候又要全局搜索改掉(很難保證沒有漏的,費時費力)所以要想辦法去複用,mixin、HOC , render props等都是實現邏輯複用的方式。

mixin

vue和react中都曾用過mixin(react目前已經拋棄) mixin(混入)本質上就是將對象複製到另一個對象上。

<code>const mixin = function (obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);

for(let prop in mixins) {
if(mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop];
}
}
return newObj;
}
const obj = {
sayHello() {
console.log('hello');
}

};
const otherObj = function() {
console.log('otherObj');
}
const Obj = mixin(otherObj, obj);
const a = new Obj(); // otherObj
a.sayHello(); // hello
/<code>

mixin存在的幾個問題:

  • 相關依賴:mixin有可能去依賴其他的mixin,當我們修改其中一個的時候,可能會影響到其他的mixin
  • 命名衝突:不同的人在寫的時候很有可能會有命名衝突,比如像 handleChange等類似常見的名字
  • 增加複雜性:當我們一個組件引入過多的mixin時,代碼邏輯將會非常複雜,因為在不停的引入狀態,和我們最初想的每個組件只做單一的功能背道而馳。

HOC

HOC是React社區提出的新的方式用來取代mixin的。高階函數是函數式編程中一個基本的概念,它描述了一種這樣的函數:接受函數作為輸入,或是返回一個函數,比如 map, reduce等都是高階函數。高階組件( higher-order component),類似於高階組件接受一個組件作為參數,返回另一個組件。

<code>function getComponent(WrappedComponent) {
return class extends React.Component {

render() {
return <wrappedcomponent>;
}
};
}
/<code>

HOC的優點為:

  • 不會影響組件內部的狀態

HOC的問題是:

  • 需要在原組件上進行包裹和嵌套,如果大量使用 HOC,將會產生非常多的嵌套,這讓調試變得非常困難
  • HOC可以劫持props,在不遵守約定的情況下也可能造成衝突

Render Props

render props[2]: 通過props接受一個返回react element 的函數,來動態決定自己要渲染的結果

<code><dataprovider> (

Hello {data.target}


)}/>
/<dataprovider>/<code>

React Router[3]中就用到了 Render Props

<code><router>
<route>
Home
} />
/<route>/<router>,

/<code>

它有哪些問題呢

  • 很容易造成“嵌套地獄”

使用 hooks

具體實現就是通過一個函數來封裝跟狀態有關的邏輯,將這些邏輯從組件中抽取出來。而這個函數中我們可以使用其他的Hooks,也可以單獨進行測試,甚至將它貢獻給社區。

<code>import { useState, useEffect } from 'react';
function useCount() {
const [count, setCount] = useState(0);
useEffect(() = {
document.title = `You clicked ${count} times`;
});

return count
}
/<code>

hooks的引入就是為了解決上面提到的這麼問題,因為 使用函數式組件,我們在開發組件的時候,可以當做平常寫函數一樣自由。

函數複用是比較容易的,直接傳不同的參數就可以渲染不同的組件,複雜組件實現,我們完全可以多封裝幾個函數,每個函數只單純的負責一件事。而且很多公用的代碼邏輯和一些場景我們可以抽出來,封裝成自定義hooks使用,比如 Umi Hooks[4]庫封裝了很多共用的邏輯,比如 useSearch,封裝了異步搜索場景的邏輯;比如 useVirtualList,就封裝了虛擬列表的邏輯。

三、React Hooks原理

在使用hooks的時候,你可能會對它的規則有很多疑問,比如:

  1. 只在最頂層使用 Hook,不能在循環、條件判斷或嵌套函數中調用hook。
  2. 只在 React 函數中調用 Hook,不能在普通函數中調用hook
  3. React怎麼知道哪個state對應哪個useState
  4. ...

我們先來看一下官方文檔[5]給出的解釋

每個組件內部都有一個「記憶單元格」列表。它們只不過是我們用來存儲一些數據的 JavaScript 對象。當你用 useState() 調用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),然後把指針移動到下一個。這就是多個 useState() 調用會得到各自獨立的本地 state 的原因。

React中是通過類似單鏈表的形式來實現的,通過 next 按順序串聯所有的 hook。可以看下 源碼部分[6]

<code>export type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update | null,

queue: UpdateQueue | null,
next: Hook | null,
|};
export type Effect = {|
tag: HookEffectTag,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
|};
/<mixed>
/<code>

更詳細的推薦查看 React Hooks 原理[7] 和 Under the hood of React’s hooks system[8]。

四、Hooks中閉包的坑

我們先來看一下使用 setState 的更新機制:

在React的setState函數實現中,會根據一個變量isBatchingUpdates 判斷是直接更新this.state還是放到 隊列中回頭再說。而isBatchingUpdates 默認是false,也就表示setState會同步更新this.state。但是,有一個函數 batchedUpdates, 這個函數會把isBatchingUpdates修改為true,而當React在調用事件處理函數之前就會調用這個batchedUpdates,造成的後果,就是由React控制的事件處理程序過程setState不會同步更新this.state。

知道這些,我們下面來看兩個例子。

下面的代碼輸出什麼?

<code>class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};

}

componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log

this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log

setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log 1

this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log 2
}, 0);
}

render() {
return null;
}
};
/<code>

打印結果是:0, 0, 2, 3。

  1. 第一次和第二次都是在react自身生命週期內,觸發 isBatchingUpdates 為true, 所以並不會直接執行更新state, 而是加入了 dirtyComponents,所以打印時獲取的都是更新前的狀態 0
  2. 兩次setState時,獲取到 this.state.val 都是 0,所以執行時都是將0設置為1,在react內部會被合併掉,只執行一次。設置完成後 state.val值為1。
  3. setTimeout中的代碼,觸發時 isBatchingUpdates為false,所以能夠直接進行更新,所以連著輸出 2, 3

上面代碼改用react hooks的話

<code>import React, { useEffect, useState } from 'react';
const MyComponent = () => {
const [val, setVal] = useState(0);
useEffect(() => {
setVal(val+1);
console.log(val);
setVal(val+1);
console.log(val);
setTimeout(() => {
setVal(val+1);
console.log(val);
setVal(val+1);
console.log(val);
}, 0)
}, []);
return null
};
export default MyComponent;
/<code>

打印輸出: 0, 0, 0, 0。

更新的方式沒有改變。首先是因為 useEffect 函數只運行一次,其次setTimeout是個閉包,內部獲取到值val一直都是 初始化聲明的那個值,所以訪問到的值一直是0。以例子來看的話,並沒有執行更新的操作。

在這種情況下,需要使用一個容器,你可以將更新後的狀態值寫入其中,並在以後的 setTimeout中訪問它,這是useRef的一種用例。可以將狀態值與ref的current屬性同步,並在setTimeout中讀取當前值。

關於這部分詳細內容可以查看 React useEffect的陷阱[9]

參考

  • 2019年17道高頻React面試題及詳解[10]
  • Umi Hooks - 助力擁抱 React Hooks[11]
  • React Hooks 原理[12]
  • 9102,作為前端必須知道 hook 怎麼玩了[13]
  • React useEffect的陷阱[14]
  • 【React深入】從Mixin到HOC再到Hook[15]

[1]

Hook 簡介: https://zh-hans.reactjs.org/docs/hooks-intro.html

[2]

render props: https://react.docschina.org/docs/render-props.html

[3]

React Router: https://reacttraining.com/react-router/web/api/Route/render-func

[4]

Umi Hooks: https://github.com/umijs/hooks

[5]

官方文檔: https://zh-hans.reactjs.org/docs/hooks-faq.html#under-the-hood

[6]

源碼部分: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js#L175

[7]

React Hooks 原理: https://github.com/brickspert/blog/issues/26

[8]

Under the hood of React’s hooks system: https://medium.com/the-guild/under-the-hood-of-reacts-hooks-system-eb59638c9dba

[9]

React useEffect的陷阱: https://zhuanlan.zhihu.com/p/84697185

[10]

2019年17道高頻React面試題及詳解: https://juejin.im/post/5d5f44dae51d4561df7805b4

[11]

Umi Hooks - 助力擁抱 React Hooks: https://zhuanlan.zhihu.com/p/103150605

[12]

React Hooks 原理: https://github.com/brickspert/blog/issues/26

[13]

9102,作為前端必須知道 hook 怎麼玩了: https://juejin.im/post/5d00a67cf265da1b8a4f156f

[14]

React useEffect的陷阱: https://zhuanlan.zhihu.com/p/84697185

[15]

【React深入】從Mixin到HOC再到Hook: https://juejin.im/post/5cad39b3f265da03502b1c0a


分享到:


相關文章: