大綱
- ?? 函數(shù)式編程
- ?? 什么是純函數(shù)
- ?? 什么是副作用(Effect)
- ?? 為什么要使用純函數(shù)
- ?? React函數(shù)組件和類組件的區(qū)別
- ?? 為什么會(huì)出現(xiàn)React Hooks
- ?? 前言
-?? React 組件設(shè)計(jì)理論
-?? 什么是Hooks? - ?? React Hooks解決了什么問題
- ?? 常用的hooks
- ?? 前言
- ?? USESTATE + USEEFFECT:初來乍到
- ?? 理解函數(shù)式組件的運(yùn)行過程
- ?? useState: 使用淺析
- ?? useEffect: 使用淺析
- ?? useState + useEffect:漸入佳境
- ?? 深入 useState 的本質(zhì)
- ?? 深入 useEffect 的本質(zhì)
- ?? React Hooks源碼解析-剖析useState的執(zhí)行過程
- ?? React Fiber
- ?? React Hooks 如何保存狀態(tài)(重點(diǎn))
- ?? React hooks的調(diào)度邏輯
- ?? mount 階段:mountState
- ?? update 階段:updateState
- ?? React Hooks源碼解析-剖析useEffect的執(zhí)行過程
- ?? mount 階段:mountEffect
- ?? update 階段:updateEffect
- ?? 回顧與總結(jié)
- ?? 為什么是鏈表
- ?? 站在巨人肩上
函數(shù)式編程
關(guān)于純函數(shù)的知識點(diǎn)可以看我之前寫的文章前端基礎(chǔ)—帶你理解什么是函數(shù)式編程
React函數(shù)組件和類組件的區(qū)別
使用上的區(qū)別
區(qū)別點(diǎn) | 函數(shù)組件 | 類組件 |
---|---|---|
生命周期 | 無 | 有 |
this | 無 | 有 |
state | 無 | 有 |
改變state | React.Hooks : useState | this.setState() |
性能 | 高(不用實(shí)例化) | 低(需要實(shí)例化) |
其他區(qū)別
1.編程方法和設(shè)計(jì)理念不同
嚴(yán)格地說串塑,類組件和函數(shù)組件是有差異的送矩。不同的寫法,代表了不同的編程方法論:
以類組件的寫法為例:
import React from 'react'
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
alert(this.state.count);
}
componentDidUpdate() {
alert(this.state.count);
}
addcount = () => {
let newCount = this.state.count;
this.setState({
count: newCount +=1
});
}
render() {
return <h1>{this.props.name}</h1>
}
}
export default Welcome
類(class)是數(shù)據(jù)和邏輯的封裝烛芬。 也就是說健蕊,組件的狀態(tài)和操作方法是封裝在一起的累澡。如果選擇了類的寫法,就應(yīng)該把相關(guān)的數(shù)據(jù)和操作额嘿,都寫在同一個(gè) class 里面。
函數(shù)一般來說劣挫,只應(yīng)該做一件事册养,就是返回一個(gè)值。 如果你有多個(gè)操作压固,每個(gè)操作應(yīng)該寫成一個(gè)單獨(dú)的函數(shù)球拦。而且,數(shù)據(jù)的狀態(tài)應(yīng)該與操作方法分離帐我。根據(jù)這種理念坎炼,React 的函數(shù)組件只應(yīng)該做一件事情:返回組件的 HTML 代碼,而沒有其他的功能拦键。
以函數(shù)組件為例谣光。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
export default Welcome
這個(gè)函數(shù)只做一件事,就是根據(jù)輸入的參數(shù)芬为,返回組件的 HTML 代碼萄金。這種只進(jìn)行單純的數(shù)據(jù)計(jì)算(換算)的函數(shù),在函數(shù)式編程里面稱為 "純函數(shù)"(pure function)媚朦。
2.組件內(nèi)部state存在差異
雖然React hooks讓函數(shù)組件也有了 state氧敢,但是 函數(shù)組件 state 和 類組件 state 還是有一些差異:
- 函數(shù)組件 state 的粒度更細(xì),類組件 state 過于無腦询张。
- 函數(shù)組件 state 保存的是快照孙乖,類組件 state 保存的是最新值。
- 要修改的state為引用類型的情況下份氧,類組件 state 不需要傳入新的引用唯袄,而 function state 必須保證是個(gè)新的引用。
2.1快照(閉包) vs 最新值(引用)
先拋出這么一個(gè)問題:在 3s 內(nèi)頻繁點(diǎn)擊3次按鈕蜗帜,下面代碼的執(zhí)行表現(xiàn)是什么越妈?
class CounterClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
handleAddCount = () => {
setTimeout(() => {
setState({count: this.state.count + 1});
}, 3000);
};
render() {
return (
<div>
<p>He clicked {this.state.count} times</p>
<button onClick={this.handleAddCount.bind(this)}>
Show count
</button>
</div>
);
}
如果是這段代碼呢?它又會(huì)是什么表現(xiàn)钮糖?
function CounterFunction() {
const [count, setCount] = useState(0);
const handleAddCount = () => {
setTimeout(() => {
setCount(count + 1);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleAddCount}>Show count</button>
</div>
);
}
如果你能成功答對,那么恭喜你,你已經(jīng)掌握了 接下來要講的useState 的用法店归。在第一個(gè)例子中阎抒,連續(xù)點(diǎn)擊3次,頁面上的數(shù)字會(huì)從0增長到3消痛。而第二個(gè)例子中且叁,連續(xù)點(diǎn)擊3次,頁面上的數(shù)字只會(huì)從0增長到1秩伞。
這個(gè)是為什么呢逞带?其實(shí)這主要是引用和閉包的區(qū)別。
類組件里面可以通過 this.state 引用到 count纱新,所以每次 setTimeout 的時(shí)候都能通過引用拿到上一次的最新 count展氓,所以點(diǎn)擊多少次最后就加了多少。
在 函數(shù)組件 里面每次更新都是重新執(zhí)行當(dāng)前函數(shù)脸爱,也就是說 setTimeout 里面讀取到的 count 是通過閉包獲取的遇汞,而這個(gè) count 實(shí)際上只是初始值,并不是上次執(zhí)行完成后的最新值,所以最后只加了1次。
2.2快照和引用的轉(zhuǎn)換
如果我想讓函數(shù)組件也是從0加到3术辐,那么該怎么來解決呢悲雳?聰明的你一定會(huì)想到,如果模仿類組件里面的 this.state
廉涕,我們用一個(gè)引用來保存 count 不就好了嗎?沒錯(cuò),這樣是可以解決埋凯,只是這個(gè)引用該怎么寫呢?我在 state 里面設(shè)置一個(gè)對象好不好看尼?就像下面這樣:
const [state, setState] = useState({ count: 0 })
答案是不行递鹉,因?yàn)榧词?state 是個(gè)對象,但每次更新的時(shí)候藏斩,要傳一個(gè)新的引用進(jìn)去躏结,這樣的引用依然是沒有意義。
setState({ count: count + 1})
想要解決這個(gè)問題狰域,那就涉及到另一個(gè)新的 Hook 方法 —— useRef媳拴。useRef 是一個(gè)對象,它擁有一個(gè) current 屬性兆览,并且不管函數(shù)組件執(zhí)行多少次屈溉,而 useRef 返回的對象永遠(yuǎn)都是原來那一個(gè)。
function CounterFunction() {
const [count, setCount] = useState(0);
const ref = useRef(0);
const handleAddCount = () => {
setTimeout(() => {
setCount(ref.current + 1);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleAddCount}>Show count</button>
</div>
);
}
useRef 有下面這幾個(gè)特點(diǎn):
-
useRef
是一個(gè)只能用于函數(shù)組件的方法抬探。 -
useRef
是除字符串ref
子巾、函數(shù)ref
帆赢、createRef
之外的第四種獲取ref
的方法。 -
useRef
在渲染周期內(nèi)永遠(yuǎn)不會(huì)變线梗,因此可以用來引用某些數(shù)據(jù)椰于。 - 修改
ref.current
不會(huì)引發(fā)組件重新渲染。
為什么會(huì)出現(xiàn)React Hooks
1.前言
React Hooks 是 React 16.8 引入的新特性仪搔,允許我們在不使用 Class 的前提下使用 state 和其他特性瘾婿。React Hooks 要解決的問題是狀態(tài)共享,是繼 render-props 和 higher-order components 之后的第三種狀態(tài)邏輯復(fù)用方案烤咧,不會(huì)產(chǎn)生 JSX 嵌套地獄問題偏陪。
2.React 組件設(shè)計(jì)理論
React以一種全新的編程范式定義了前端開發(fā)約束,它為視圖開發(fā)帶來了一種全新的心智模型:
- React認(rèn)為煮嫌,UI視圖是數(shù)據(jù)的一種視覺映射笛谦,即
UI = F(DATA)
,這里的F
需要負(fù)責(zé)對輸入數(shù)據(jù)進(jìn)行加工立膛、并對數(shù)據(jù)的變更做出響應(yīng) - 公式里的
F
在React里抽象成組件揪罕,React是以組件(Component-Based)為粒度編排應(yīng)用的,組件是代碼復(fù)用的最小單元 - 在設(shè)計(jì)上宝泵,React采用
props
屬性來接收外部的數(shù)據(jù)好啰,使用state
屬性來管理組件自身產(chǎn)生的數(shù)據(jù)(狀態(tài)),而為了實(shí)現(xiàn)(運(yùn)行時(shí))對數(shù)據(jù)變更做出響應(yīng)需要儿奶,React采用基于類(Class)的組件設(shè)計(jì)框往! - 除此之外,React認(rèn)為組件是有生命周期的闯捎,因此開創(chuàng)性地將生命周期的概念引入到了組件設(shè)計(jì)椰弊,從組件的create到destory提供了一系列的API供開發(fā)者使用
這就是React組件設(shè)計(jì)的理論基礎(chǔ)
3.什么是Hooks?
Hooks的單詞意思為“鉤子”。
React Hooks 的意思是瓤鼻,組件盡量寫成純函數(shù)秉版,如果需要外部功能和副作用,就用鉤子把外部代碼"鉤"進(jìn)來茬祷。而React Hooks 就是我們所說的“鉤子”清焕。
那么Hooks要怎么用呢?“你需要寫什么功能祭犯,就用什么鉤子”秸妥。對于常見的功能,React為我們提供了一些常用的鉤子沃粗,當(dāng)然有特殊需要粥惧,我們也可以寫自己的鉤子。
4.React Hooks能解決什么問題
React 一直在解決一個(gè)問題最盅,如何實(shí)現(xiàn)分離業(yè)務(wù)邏輯代碼突雪,實(shí)現(xiàn)組件內(nèi)部相關(guān)業(yè)務(wù)邏輯的復(fù)用起惕。
一般情況下,我們都是通過組件和自上而下傳遞的數(shù)據(jù)流將我們頁面上的大型UI組織成為獨(dú)立的小型UI挂签,實(shí)現(xiàn)組件的重用疤祭。但是我們經(jīng)常遇到很難侵入一個(gè)復(fù)雜的組件中實(shí)現(xiàn)重用,因?yàn)榻M件的邏輯是有狀態(tài)的饵婆,無法提取到函數(shù)組件當(dāng)中。當(dāng)我們在組件中連接外部的數(shù)據(jù)源戏售,然后希望在組件中執(zhí)行更多其他的操作的時(shí)候侨核,我們就會(huì)把組件搞得特別糟糕:
- 問題1:難以共享組件中的與
狀態(tài)
相關(guān)的邏輯,容易產(chǎn)生很多巨大的組件灌灾。
對于共享組件中的與狀態(tài)相關(guān)的邏輯搓译,React團(tuán)隊(duì)給出過許多的方案,早期使用CreateClass + Mixins锋喜,在使用Class Component取代CreateClass之后又設(shè)計(jì)了Render Props和Higher Order Component些己。但是都沒有很好的解決這個(gè)問題。反而讓組件體積變得更加臃腫嘿般。組件的可讀性進(jìn)一步降低段标。
HOC使用(老生常談)的問題:
(1)嵌套地獄,每一次HOC調(diào)用都會(huì)產(chǎn)生一個(gè)組件實(shí)例
(2)可以使用類裝飾器緩解組件嵌套帶來的可維護(hù)性問題炉奴,但裝飾器本質(zhì)上還是HOC
(3)包裹太多層級之后逼庞,可能會(huì)帶來props屬性的覆蓋問題
Render Props:
(1)數(shù)據(jù)流向更直觀了,子孫組件可以很明確地看到數(shù)據(jù)來源
(2)但本質(zhì)上Render Props是基于閉包實(shí)現(xiàn)的瞻赶,大量地用于組件的復(fù)用將不可避免地引入了callback hell問題
(2)丟失了組件的上下文赛糟,因此沒有this.props
屬性,不能像HOC那樣訪問this.props.children
- 問題2: 我們構(gòu)建React類組件的方式與組件的生命周期是耦合的砸逊。這一鴻溝順理成章的迫使整個(gè)組件中散布著業(yè)務(wù)相關(guān)的邏輯璧南。產(chǎn)生了不可避免的代碼冗余。比如在不同的生命周期中處理組件的狀態(tài)师逸,或者在不同生命周期中執(zhí)行setTimeOut和clearTimeOut等等司倚。在下面的示例中,我們可以清楚地了解到這一點(diǎn)字旭。我們需要2個(gè)生命周期中(componentDidMount对湃、componentDidUpdate)來完成相同的任務(wù)——使repos與任何props.id同步。遗淳。
class ReposGrid extends React.Component {
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos (id) {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
if (this.state.loading === true) {
return <Loading />
}
return (
<ul>
{this.state.repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
- 問題3:令人頭疼的 this 管理拍柒,容易引入難以追蹤的 Bug。
React Hooks的出現(xiàn)很好的解決了這些問題:
針對問題1:
前面我們提到過屈暗,React難以共享組件中的與狀態(tài)相關(guān)的邏輯拆讯,這導(dǎo)致了像高階組件或渲染道具這樣過于復(fù)雜的模式脂男。Hooks對此有自己的解決方案---創(chuàng)建我們自己的自定義Hook,自定義Hook以use開頭种呐。如下這個(gè)useRepos hook將接受我們想要獲取的Repos的id宰翅,并返回一個(gè)數(shù)組,其中第一項(xiàng)為loading狀態(tài)爽室,第二項(xiàng)為repos狀態(tài)汁讼。
function useRepos (id) {
const [ repos, setRepos ] = useState([])
const [ loading, setLoading ] = useState(true)
useEffect(() => {
setLoading(true);
fetchRepos(id)
.then((repos) => {
setRepos(repos)阔墩;
setLoading(false)嘿架;
});
}, [id])啸箫;
return [loading, repos]耸彪;
}
這樣任何與獲取repos相關(guān)的邏輯都可以在這個(gè)自定義Hook中抽象。現(xiàn)在忘苛,不管我們在哪個(gè)組件中蝉娜,每當(dāng)我們需要有關(guān)repos的數(shù)據(jù)時(shí),我們都可以使用useRepos自定義Hook扎唾。
function ReposGrid ({ id }) {
const [ loading, repos ] = useRepos(id)召川;
...
}
function Profile ({ user }) {
const [ loading, repos ] = useRepos(user.id);
...
}
針對問題2和問題3:
當(dāng)使用ReactHooks時(shí)稽屏,我們需要忘記所知道的關(guān)于通俗的React生命周期方法以及這種思維方式的所有東西扮宠。我們已經(jīng)看到了考慮組件的生命周期時(shí)產(chǎn)生的問題-“這(指生命周期)順理成章的迫使整個(gè)組件中散布著相關(guān)的邏輯『疲”相反坛增,考慮一下同步。想想我們曾經(jīng)用到生命周期事件的時(shí)候薄腻。不管是設(shè)置組件的初始狀態(tài)收捣、獲取數(shù)據(jù)、更新DOM等等庵楷,最終目標(biāo)總是同步罢艾。通常,把React land之外的東西(API請求尽纽、DOM等)與Reactland之內(nèi)的(組件狀態(tài))同步咐蚯,反之亦然。當(dāng)我們考慮同步而不是生命周期事件時(shí)弄贿,它允許我們將相關(guān)的邏輯塊組合在一起春锋。為此,Reaction給了我們另一個(gè)叫做useEffect的Hook差凹。
很肯定地說useEffect使我們能在function組件中執(zhí)行副作用操作期奔。它有兩個(gè)參數(shù)侧馅,一個(gè)函數(shù)和一個(gè)可選數(shù)組。函數(shù)定義要運(yùn)行的副作用呐萌,(可選的)數(shù)組定義何時(shí)“重新同步”(或重新運(yùn)行)effect馁痴。
React.useEffect(() => {
document.title = `Hello, ${username}`
}, [username]);
在上面的代碼中肺孤,傳遞給useEffect的函數(shù)將在用戶名發(fā)生更改時(shí)運(yùn)行罗晕。因此,將文檔的標(biāo)題與Hello, ${username}解析出的內(nèi)容同步赠堵。
現(xiàn)在攀例,我們?nèi)绾问褂么a中的useEffect Hook來同步repos和fetchRepos API請求?
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
相當(dāng)巧妙,對吧顾腊?我們已經(jīng)成功地?cái)[脫了React.Component, constructor, super, this,更重要的是挖胃,我們的業(yè)務(wù)邏輯不再在整個(gè)組件生命周期中散布杂靶。
5.常用的hooks
React Hooks常用鉤子有如下5種:
- useState() 狀態(tài)鉤子
- useContext() 共享狀態(tài)鉤子
- useReducer(). Action 鉤子
- useCallback. function 鉤子
- useEffect() 副作用鉤子
使用hooks 我們會(huì)發(fā)現(xiàn)沒有了繼承,渲染邏輯酱鸭,生命周期等吗垮, 代碼看起來更加的輕便簡潔了。
React 約定凹髓,鉤子一律使用 use 前綴命名 (自定義鉤子都命名為:useXXXX)
關(guān)于常用hooks結(jié)束可以看
React Hooks 常用鉤子及基本原理
聊聊useCallback
詳解 React useCallback & useMemo
USESTATE + USEEFFECT:初來乍到
首先烁登,讓我們從最最最常用的兩個(gè) Hooks 說起:useState
和 useEffect
。很有可能蔚舀,你在平時(shí)的學(xué)習(xí)和開發(fā)中已經(jīng)接觸并使用過了(當(dāng)然如果你剛開始學(xué)也沒關(guān)系啦)饵沧。不過在此之前,我們先熟悉一下 React 函數(shù)式組件的運(yùn)行過程赌躺。
1.理解函數(shù)式組件的運(yùn)行過程
我們知道狼牺,Hooks 只能用于 React 函數(shù)式組件。因此理解函數(shù)式組件的運(yùn)行過程對掌握 Hooks 中許多重要的特性很關(guān)鍵礼患,請看下圖:
可以看到是钥,函數(shù)式組件嚴(yán)格遵循 UI = render(data)
的模式。當(dāng)我們第一次調(diào)用組件函數(shù)時(shí)缅叠,觸發(fā)初次渲染悄泥;然后隨著 props
的改變,便會(huì)重新調(diào)用該組件函數(shù)肤粱,觸發(fā)重渲染弹囚。
你也許會(huì)納悶,動(dòng)畫里面為啥要并排畫三個(gè)一樣的組件呢狼犯?因?yàn)槲蚁胪ㄟ^這種方式直觀地闡述函數(shù)式組件的一個(gè)重要思想:
每一次渲染都是完全獨(dú)立的余寥。
后面我們將沿用這樣的風(fēng)格领铐,并一步步地介紹 Hook 在函數(shù)式組件中扮演怎樣的角色。
2.useState 使用淺析
首先我們來簡單地了解一下 useState
鉤子的使用宋舷,官方文檔介紹的使用方法如下:
const [state, setState] = useState(initialValue);
其中 state
就是一個(gè)狀態(tài)變量绪撵,setState
是一個(gè)用于修改狀態(tài)的 Setter 函數(shù),而 initialValue
則是狀態(tài)的初始值祝蝠。
光看代碼可能有點(diǎn)抽象音诈,請看下面的動(dòng)畫:
與之前的純函數(shù)式組件相比,我們引入了 useState
這個(gè)鉤子绎狭,瞬間就打破了之前 UI = render(data)
的安靜畫面——函數(shù)組件居然可以從組件之外把狀態(tài)和修改狀態(tài)的函數(shù)“鉤”過來细溅!并且仔細(xì)看上面的動(dòng)畫,通過調(diào)用 Setter 函數(shù)儡嘶,居然還可以直接觸發(fā)組件的重渲染喇聊!
提示
你也許注意到了所有的“鉤子”都指向了一個(gè)綠色的問號,我們會(huì)在下面詳細(xì)地分析那是什么蹦狂,現(xiàn)在就暫時(shí)把它看作是組件之外可以訪問的一個(gè)“神秘領(lǐng)域”誓篱。
結(jié)合上面的動(dòng)畫,我們可以得出一個(gè)重要的推論:每次渲染具有獨(dú)立的狀態(tài)值(畢竟每次渲染都是完全獨(dú)立的嘛)凯楔。也就是說窜骄,每個(gè)函數(shù)中的 state
變量只是一個(gè)簡單的常量,每次渲染時(shí)從鉤子中獲取到的常量摆屯,并沒有附著數(shù)據(jù)綁定之類的神奇魔法邻遏。
這也就是老生常談的 Capture Value 特性∨捌铮可以看下面這段經(jīng)典的計(jì)數(shù)器代碼(來自 Dan 的這篇精彩的文章):
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
實(shí)現(xiàn)了上面這個(gè)計(jì)數(shù)器后(也可以直接通過這個(gè) Sandbox 進(jìn)行體驗(yàn))准验,按如下步驟操作:1)點(diǎn)擊 Click me 按鈕,把數(shù)字增加到 3富弦;2)點(diǎn)擊 Show alert 按鈕沟娱;3)在 setTimeout
觸發(fā)之前點(diǎn)擊 Click me,把數(shù)字增加到 5腕柜。
結(jié)果是 Alert 顯示 3济似!
如果你覺得這個(gè)結(jié)果很正常,恭喜你已經(jīng)理解了 Capture Value 的思想盏缤!如果你覺得匪夷所思嘛……來簡單解釋一下:
- 每次渲染相互獨(dú)立砰蠢,因此每次渲染時(shí)組件中的狀態(tài)、事件處理函數(shù)等等都是獨(dú)立的唉铜,或者說只屬于所在的那一次渲染
- 我們在
count
為 3 的時(shí)候觸發(fā)了handleAlertClick
函數(shù)台舱,這個(gè)函數(shù)所記住的count
也為 3 - 三秒種后,剛才函數(shù)的
setTimeout
結(jié)束,輸出當(dāng)時(shí)記住的結(jié)果:3
這道理就像竞惋,你翻開十年前的日記本柜去,雖然是現(xiàn)在翻開的,但記錄的仍然是十年前的時(shí)光拆宛∩ど荩或者說,日記本 Capture 了那一段美好的回憶浑厚。
3.useEffect 使用淺析
你可能已經(jīng)聽說 useEffect
類似類組件中的生命周期方法股耽。但是在開始學(xué)習(xí) useEffect
之前,建議你暫時(shí)忘記生命周期模型钳幅,畢竟函數(shù)組件和類組件是不同的世界物蝙。官方文檔介紹 useEffect
的使用方法如下:
useEffect(effectFn, deps)
effectFn
是一個(gè)執(zhí)行某些可能具有副作用的 Effect 函數(shù)(例如數(shù)據(jù)獲取、設(shè)置/銷毀定時(shí)器等)敢艰,它可以返回一個(gè)清理函數(shù)(Cleanup)诬乞,例如大家所熟悉的 setInterval
和 clearInterval
:
useEffect(() => {
const intervalId = setInterval(doSomething(), 1000);
return () => clearInterval(intervalId);
});
可以看到,我們在 Effect 函數(shù)體內(nèi)通過 setInterval
啟動(dòng)了一個(gè)定時(shí)器钠导,隨后又返回了一個(gè) Cleanup 函數(shù)丽惭,用于銷毀剛剛創(chuàng)建的定時(shí)器。
OK辈双,聽上去還是很抽象,再來看看下面的動(dòng)畫吧:
動(dòng)畫中有以下需要注意的點(diǎn):
- 每個(gè) Effect 必然在渲染之后執(zhí)行柜砾,因此不會(huì)阻塞渲染湃望,提高了性能
- 在運(yùn)行每個(gè) Effect 之前,運(yùn)行前一次渲染的 Effect Cleanup 函數(shù)(如果有的話)
- 當(dāng)組件銷毀時(shí)痰驱,運(yùn)行最后一次 Effect 的 Cleanup 函數(shù)
提示
將 Effect 推遲到渲染完成之后執(zhí)行是出于性能的考慮证芭,如果你想在渲染之前執(zhí)行某些邏輯(不惜犧牲渲染性能),那么可使用
useLayoutEffect
鉤子担映,使用方法與useEffect
完全一致废士,只是執(zhí)行的時(shí)機(jī)不同。
再來看看 useEffect
的第二個(gè)參數(shù):deps
(依賴數(shù)組)蝇完。從上面的演示動(dòng)畫中可以看出官硝,React 會(huì)在每次渲染后都運(yùn)行 Effect。而依賴數(shù)組就是用來控制是否應(yīng)該觸發(fā) Effect短蜕,從而能夠減少不必要的計(jì)算氢架,從而優(yōu)化了性能。具體而言朋魔,只要依賴數(shù)組中的每一項(xiàng)與上一次渲染相比都沒有改變岖研,那么就跳過本次 Effect 的執(zhí)行。
仔細(xì)一想警检,我們發(fā)現(xiàn) useEffect
鉤子與之前類組件的生命周期相比孙援,有兩個(gè)顯著的特點(diǎn):
- 將初次渲染(
componentDidMount
)害淤、重渲染(componentDidUpdate
)和銷毀(componentDidUnmount
)三個(gè)階段的邏輯用一個(gè)統(tǒng)一的 API 去解決 - 把相關(guān)的邏輯都放到一個(gè) Effect 里面(例如
setInterval
和clearInterval
),更突出邏輯的內(nèi)聚性
在最極端的情況下拓售,我們可以指定 deps
為空數(shù)組 []
窥摄,這樣可以確保 Effect 只會(huì)在組件初次渲染后執(zhí)行。實(shí)際效果動(dòng)畫如下:
可以看到邻辉,后面的所有重渲染都不會(huì)觸發(fā) Effect 的執(zhí)行溪王;在組件銷毀時(shí),運(yùn)行 Effect Cleanup 函數(shù)值骇。
注意
如果你熟悉 React 的重渲染機(jī)制莹菱,那么應(yīng)該可以猜到
deps
數(shù)組在判斷元素是否發(fā)生改變時(shí)同樣也使用了Object.is
進(jìn)行比較。因此一個(gè)隱患便是吱瘩,當(dāng)deps
中某一元素為非原始類型時(shí)(例如函數(shù)道伟、對象等),每次渲染都會(huì)發(fā)生改變使碾,從而失去了deps
本身的意義(條件式地觸發(fā) Effect)蜜徽。我們會(huì)在接下來講解如何規(guī)避這個(gè)困境。
useState + useEffect:漸入佳境
在上一步驟中票摇,我們在 App
組件中定義了一個(gè) State 和 Effect拘鞋,但是實(shí)際應(yīng)用不可能這么簡單,一般都需要多個(gè) State 和 Effect矢门,這時(shí)候又該怎么去理解和使用呢盆色?
1.深入 useState 的本質(zhì)
在上一節(jié)的動(dòng)畫中,我們看到每一次渲染組件時(shí)祟剔,我們都能通過一個(gè)神奇的鉤子把狀態(tài)”鉤“過來隔躲,不過這些鉤子從何而來我們打了一個(gè)問號。現(xiàn)在物延,是時(shí)候解開謎團(tuán)了宣旱。
注意
以下動(dòng)畫演示并不完全對應(yīng) React Hooks 的源碼實(shí)現(xiàn),但是它能很好地幫助你理解其工作原理叛薯。當(dāng)然浑吟,也能幫助你去啃真正的源碼。
我們先來看看當(dāng)組件初次渲染(掛載)時(shí)耗溜,情況到底是什么樣的:
注意以下要點(diǎn):
- 在初次渲染時(shí)买置,我們通過
useState
定義了多個(gè)狀態(tài); - 每調(diào)用一次
useState
强霎,都會(huì)在組件之外生成一條 Hook 記錄忿项,同時(shí)包括狀態(tài)值(用useState
給定的初始值初始化)和修改狀態(tài)的 Setter 函數(shù); - 多次調(diào)用
useState
生成的 Hook 記錄形成了一條鏈表; - 觸發(fā)
onClick
回調(diào)函數(shù)轩触,調(diào)用setS2
函數(shù)修改s2
的狀態(tài)寞酿,不僅修改了 Hook 記錄中的狀態(tài)值,還即將觸發(fā)重渲染脱柱。
OK伐弹,重渲染的時(shí)候到了,動(dòng)畫如下:
可以看到榨为,在初次渲染結(jié)束之后惨好、重渲染之前,Hook 記錄鏈表依然存在随闺。當(dāng)我們逐個(gè)調(diào)用 useState
的時(shí)候日川,useState
便返回了 Hook 鏈表中存儲的狀態(tài),以及修改狀態(tài)的 Setter矩乐。
提示
當(dāng)你充分理解上面兩個(gè)動(dòng)畫之后龄句,其實(shí)就能理解為什么這個(gè) Hook 叫
useState
而不是createState
了——之所以叫use
,是因?yàn)闆]有的時(shí)候才創(chuàng)建(初次渲染的時(shí)候)散罕,有的時(shí)候就直接讀染拾薄(重渲染的時(shí)候)碧浊。
通過以上的分析捐韩,我們不難發(fā)現(xiàn) useState
在設(shè)計(jì)方面的精巧(摘自張立理:對 React Hooks 的一些思考):
- 狀態(tài)和修改狀態(tài)的 Setter 函數(shù)兩兩配對膏孟,并且后者一定影響前者,前者只被后者影響误甚,作為一個(gè)整體它們完全不受外界的影響
- 鼓勵(lì)細(xì)粒度和扁平化的狀態(tài)定義和控制繁调,對于代碼行為的可預(yù)測性和可測試性大有幫助
- 除了
useState
(和其他鉤子),函數(shù)組件依然是實(shí)現(xiàn)渲染邏輯的“純”組件靶草,對狀態(tài)的管理被 Hooks 所封裝了起來
深入 useEffect 的本質(zhì)
在對 useState
進(jìn)行一波深挖之后,我們再來揭開 useEffect
神秘的面紗岳遥。實(shí)際上奕翔,你可能已經(jīng)猜到了——同樣是通過一個(gè)鏈表記錄所有的 Hook,請看下面的演示:
注意其中一些細(xì)節(jié):
-
useState
和useEffect
在每次調(diào)用時(shí)都被添加到 Hook 鏈表中浩蓉; -
useEffect
還會(huì)額外地在一個(gè)隊(duì)列中添加一個(gè)等待執(zhí)行的 Effect 函數(shù)派继; - 在渲染完成后,依次調(diào)用 Effect 隊(duì)列中的每一個(gè) Effect 函數(shù)捻艳。
至此驾窟,上一節(jié)的動(dòng)畫中那兩個(gè)“問號”的身世也就揭曉了——只不過是鏈表罷了!回過頭來认轨,我們想起來 React 官方文檔 Rules of Hooks 中強(qiáng)調(diào)過一點(diǎn):
Only call hooks at the top level. 只在最頂層使用 Hook绅络。
具體地說,不要在循環(huán)、嵌套恩急、條件語句中使用 Hook——因?yàn)檫@些動(dòng)態(tài)的語句很有可能會(huì)導(dǎo)致每次執(zhí)行組件函數(shù)時(shí)調(diào)用 Hook 的順序不能完全一致杉畜,導(dǎo)致 Hook 鏈表記錄的數(shù)據(jù)失效。具體的場景就不畫動(dòng)畫啦衷恭,自行腦補(bǔ)吧~
React Hooks源碼解析-剖析useState的執(zhí)行過程
1.React Fiber
關(guān)于React Fiber詳細(xì)解釋,可以看我之前寫的一篇文章
由淺入深快速掌握React Fiber
我們本節(jié)只需了解這2個(gè)點(diǎn)即可:
- React現(xiàn)在的渲染都是由Fiber來調(diào)度
- Fiber調(diào)度過程中的兩個(gè)階段(以Render為界)
Fiber是比線程還細(xì)的控制粒度此叠,是React 16中的新特性,旨在對渲染過程做更精細(xì)的調(diào)整随珠。
產(chǎn)生原因:
(1)Fiber之前的reconciler(被稱為Stack reconciler)自頂向下的遞歸mount/update
灭袁,無法中斷(持續(xù)占用主線程),這樣主線程上的布局窗看、動(dòng)畫等周期性任務(wù)以及交互響應(yīng)就無法立即得到處理茸歧,影響體驗(yàn)
(2)渲染過程中沒有優(yōu)先級可言
React Fiber的調(diào)度方式:
把一個(gè)耗時(shí)長的任務(wù)分成很多小片,每一個(gè)小片的運(yùn)行時(shí)間很短烤芦,雖然總時(shí)間依然很長举娩,但是在每個(gè)小片執(zhí)行完之后,都給其他任務(wù)一個(gè)執(zhí)行的機(jī)會(huì)构罗,這樣唯一的線程就不會(huì)被獨(dú)占铜涉,其他任務(wù)依然有運(yùn)行的機(jī)會(huì)。
React Fiber把更新過程碎片化遂唧,執(zhí)行過程如下面的圖所示芙代,每執(zhí)行完一段更新過程,就把控制權(quán)交還給React負(fù)責(zé)任務(wù)協(xié)調(diào)的模塊盖彭,看看有沒有其他緊急任務(wù)要做纹烹,如果沒有就繼續(xù)去更新,如果有緊急任務(wù)召边,那就去做緊急任務(wù)铺呵。
維護(hù)每一個(gè)分片的數(shù)據(jù)結(jié)構(gòu),就是Fiber隧熙。
-
有了分片之后片挂,更新過程的調(diào)用棧如下圖所示,中間每一個(gè)波谷代表深入某個(gè)分片的執(zhí)行過程贞盯,每個(gè)波峰就是一個(gè)分片執(zhí)行結(jié)束交還控制權(quán)的時(shí)機(jī)音念。讓線程處理別的事情
image.png
Fiber的調(diào)度過程分為以下兩個(gè)階段:
render/reconciliation階段 — 里面的所有生命周期函數(shù)都可能被中斷、執(zhí)行多次
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
Commit階段 — 不能被打斷躏敢,只會(huì)執(zhí)行一次
componentDidMount
componentDidUpdate
compoenntWillunmount
Fiber的增量更新需要更多的上下文信息闷愤,之前的vDOM tree顯然難以滿足,所以擴(kuò)展出了fiber tree(Current 樹)件余,更新過程就是根據(jù)輸入數(shù)據(jù)以及現(xiàn)有的fiber tree構(gòu)造出新的fiber tree(workInProgress 樹)讥脐。
Current 樹和 workInProgress 樹
在React中最多會(huì)同時(shí)存在兩棵Fiber樹遭居。當(dāng)前屏幕上顯示內(nèi)容對應(yīng)的Fiber樹稱為current Fiber樹,當(dāng) React 開始處理更新時(shí)攘烛,會(huì)在內(nèi)存中再次構(gòu)建一棵Fiber樹魏滚,稱為workInProgress Fiber樹,它反映了要刷新到屏幕的未來狀態(tài)坟漱。current Fiber樹中的Fiber節(jié)點(diǎn)被稱為current fiber鼠次。workInProgress Fiber樹中的Fiber節(jié)點(diǎn)被稱為workInProgress fiber,它們通過alternate屬性連接芋齿。
React應(yīng)用的根節(jié)點(diǎn)通過current指針在不同F(xiàn)iber樹的rootFiber間切換來實(shí)現(xiàn)Fiber樹的切換腥寇。當(dāng)workInProgress Fiber樹構(gòu)建完成交給Renderer渲染在頁面上后,應(yīng)用根節(jié)點(diǎn)的current指針指向workInProgress Fiber樹觅捆,此時(shí)workInProgress Fiber樹就變?yōu)閏urrent Fiber樹赦役。每次狀態(tài)更新都會(huì)產(chǎn)生新的workInProgress Fiber樹,通過current與workInProgress的替換栅炒,完成DOM更新掂摔。由于有兩顆fiber樹,實(shí)現(xiàn)了異步中斷時(shí)赢赊,更新狀態(tài)的保存乙漓,中斷回來以后可以拿到之前的狀態(tài)。并且兩者狀態(tài)可以復(fù)用释移,節(jié)約了從頭構(gòu)建的時(shí)間叭披。
React所有的 work 都是在 workInProgress 樹的 Fibler 節(jié)點(diǎn)上進(jìn)行的。當(dāng) React 遍歷 current 樹時(shí)玩讳,它為每個(gè)Fiber節(jié)點(diǎn)創(chuàng)建一個(gè)替代節(jié)點(diǎn)涩蜘。這些節(jié)點(diǎn)構(gòu)成了 workInProgress 樹。一旦處理完所有 update 并完成所有相關(guān) work熏纯,React 將把 workInProgress 樹刷新到屏幕同诫。一旦在屏幕上渲染 workInProgress 樹之后,workInProgress 樹將替換 原有current 樹成為新的current 樹樟澜。
hooks掛載在workInProgress樹的 Fibler 節(jié)點(diǎn)上的memoizedState屬性下误窖。當(dāng)我們函數(shù)組件執(zhí)行之后,hooks
和workInProgress
將是如圖的關(guān)系:
Fiber節(jié)點(diǎn)結(jié)構(gòu)如下:
FiberNode { // fiber結(jié)構(gòu)
memoizedState: any, // 類組件存儲最新的state往扔,函數(shù)組件存儲hooks鏈表
stateNode: new ClickCounter,
type: ClickCounter, // 判斷標(biāo)簽類型是原生或react
alternate: null, // 指向current fiber節(jié)點(diǎn)
key: null, 節(jié)點(diǎn)key
updateQueue: null, // 更新的隊(duì)列。
tag: 1, // 判斷組件的類型是函數(shù)組件還是類組件
child,
return,
sibling,
...
}
stateNode
保存對類組件實(shí)例熊户,DOM 節(jié)點(diǎn)或與 fiber 節(jié)點(diǎn)關(guān)聯(lián)的其他 React 元素類型的引用萍膛。一般來說,此屬性用于保存與 fiber 關(guān)聯(lián)的 local state嚷堡。type
定義與此 fiber 關(guān)聯(lián)的函數(shù)或類蝗罗。對于類組件艇棕,它指向構(gòu)造函數(shù),對于 DOM 元素串塑,它指定 HTML 標(biāo)記沼琉。
我把這個(gè)字段理解為 fiber 節(jié)點(diǎn)與哪些元素相關(guān)。tag
定義 fiber節(jié)點(diǎn)類型桩匪,在 reconciliation 算法中使用它來確定按函數(shù)組件還是類組件完成接下來的工作打瘪。updateQueue
state 更新,回調(diào)以及 DOM 更新的隊(duì)列傻昙。memoizedState
用于創(chuàng)建輸出的 fiber 的state闺骚。處理更新時(shí),它反映了當(dāng)前渲染在屏幕上內(nèi)容的 state妆档。
memoziedState這個(gè)字段很重要僻爽,是組件更新的唯一依據(jù)。在class組件里贾惦,它就是this.state的結(jié)構(gòu)胸梆,調(diào)用this.setState的時(shí)候,其實(shí)就是修改了它的數(shù)據(jù)须板,數(shù)據(jù)改變了組件就會(huì)重新執(zhí)行碰镜。
也就是說,即使是class組件逼纸,也不會(huì)主動(dòng)調(diào)用任何生命周期函數(shù)洋措,而是在memoziedState改變后,組件重新執(zhí)行杰刽,在執(zhí)行的過程中才會(huì)經(jīng)過這些周期菠发。
所以,這就解釋了函數(shù)式組件為什么可以通過讓hooks(useState)返回的方法改變狀態(tài)來觸發(fā)組件的更新贺嫂,實(shí)際上就是修改了對應(yīng)fiber節(jié)點(diǎn)的memoziedState滓鸠。
memoizedProps
在上一次渲染期間用于創(chuàng)建輸出的 fiber 的 props 。pendingProps
在 React element 的新數(shù)據(jù)中更新并且需要應(yīng)用于子組件或 DOM 元素的 props第喳。(子組件或者 DOM 中將要改變的 props)key
唯一標(biāo)識符糜俗,當(dāng)具有一組 children 的時(shí)候,用來幫助 React 找出哪些項(xiàng)已更改曲饱,已添加或已從列表中刪除悠抹。與這里所說的React的 “列表和key” 功能有關(guān)
2.React Hooks 如何保存狀態(tài)(重點(diǎn))
React 官方文檔中有提到,React Hooks 保存狀態(tài)的位置其實(shí)與類組件的一致,上面也提到了狀態(tài)存儲在FiberNode的memoziedState
中扩淀;翻看源碼后楔敌,我發(fā)現(xiàn)這樣的說法沒錯(cuò),但又不全面:
- 兩者的狀態(tài)值都被掛載在組件實(shí)例對象FiberNode的
memoizedState
屬性中驻谆。 - 兩者保存狀態(tài)值的數(shù)據(jù)結(jié)構(gòu)完全不同卵凑;類組件是直接把 state 屬性中掛載的這個(gè)開發(fā)者自定義的對象給保存到
memoizedState
屬性中庆聘;而 React Hooks 是用Hooks 鏈表來保存狀態(tài)的,memoizedState
屬性保存的實(shí)際上是這個(gè)鏈表的頭指針勺卢。
下面我們來看看這個(gè)Hooks鏈表的節(jié)點(diǎn)是什么樣的 :
// Hook類型定義
type Hook = {
memoizedState: any, // useState中 保存 state信息 | useEffect 中 保存著 effect 對象 | useMemo 中 保存的是緩存的值和deps | useRef中保存的是ref 對象
baseState: any, // usestate和useReducer中,一次更新中 伙判,產(chǎn)生的最新state值。
baseUpdate: Update<any, any> | null, // usestate和useReducer中 保存最新的更新隊(duì)列黑忱。
queue: UpdateQueue<any, any> | null, // 保存待更新隊(duì)列 pendingQueue 宴抚,更新函數(shù) dispatch 等信息。
next: Hook | null, // 是一個(gè)指針杨何、指向下一個(gè) hooks對象
}
官方文檔一直強(qiáng)調(diào) React Hooks 的調(diào)用只能放在函數(shù)組件/自定義 Hooks 函數(shù)體的頂層酱塔,這是因?yàn)槲覀冎荒芡ㄟ^ Hooks 調(diào)用的順序來與實(shí)際保存的數(shù)據(jù)結(jié)構(gòu)來關(guān)聯(lián)。舉個(gè)例子:
function App() {
const [ n1, setN1 ] = useState(1);
if (n1 > 0) {
const [ n2, setN2 ] = useState(2);
}
const [ n3, setN3 ] = useState(3);
}
初始化Hook存儲(鏈表)結(jié)構(gòu):
組件初始化時(shí)危虱,會(huì)按順序從上到下將組件中使用到的hook增加到hooks鏈表羊娃。
一旦在條件語句中聲明hooks,在下一次函數(shù)組件更新埃跷,hooks鏈表結(jié)構(gòu)將會(huì)被破壞蕊玷,current樹的memoizedState緩存hooks信息,和當(dāng)前workInProgress不一致弥雹,如果涉及到讀取state等操作垃帅,就會(huì)發(fā)生異常。
- 假如執(zhí)行了setN1(-1)觸發(fā)了組件的re-render剪勿。re-render時(shí)會(huì)從第一行代碼開始重新執(zhí)行整個(gè)組件贸诚,即會(huì)按順序執(zhí)行整個(gè)Hooks鏈,這時(shí)n1小于0厕吉,則會(huì)執(zhí)行
useState(3)
分支酱固,相反useState(2)則不會(huì)執(zhí)行到,導(dǎo)致useState(3)
返回的值其實(shí)是2头朱,因?yàn)?strong>首次render之后运悲,只能通過useState返回的dispatch修改對應(yīng)Hook的memoizedState,通過‘索引’獲取緩存的state项钮,因此必須要保證Hooks的順序不變班眯,所以不能在分支調(diào)用Hooks,只有在頂層調(diào)用才能保證各個(gè)Hooks的執(zhí)行順序烁巫!
3.React hooks的調(diào)度邏輯
調(diào)度邏輯如下:
Fiber調(diào)度的開始:從beginWork談起
之前已經(jīng)說過署隘,React有能力區(qū)分不同的組件,所以它會(huì)給不同的組件類型打上不同的tag(tag為0-24的數(shù)字)亚隙, 所以在beginWork函數(shù)的主要功能就是通過 switch (workInProgress.tag)對不同的組件做不同的更新處理磁餐。源碼如下:
// ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
/** 省略與本文無關(guān)的部分 **/
// 根據(jù)不同的組件類型走不同的方法
switch (workInProgress.tag) {
// 不確定組件
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
// 加載初始組件
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
// 函數(shù)組件
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 更新函數(shù)組件
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 類組件
case ClassComponent {
/** 細(xì)節(jié)略 **/
}
}
renderWithHooks,調(diào)用函數(shù)組件渲的主要函數(shù)
我們從上邊可以看出來恃鞋,beginWork會(huì)根據(jù)組件類型判斷執(zhí)行mountIndeterminateComponent或執(zhí)行updateFunctionComponent崖媚,這兩個(gè)方法都會(huì)用到renderWithHooks
這個(gè)函數(shù).renderWithHooks函數(shù)作用是調(diào)用function組件函數(shù)的主要函數(shù)。我們重點(diǎn)看看renderWithHooks做了些什么恤浪?
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime,
) {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (workInProgress.expirationTime === renderExpirationTime) {
// ....這里的邏輯我們先放一放
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
// 掛載時(shí)的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useMemo: mountMemo,
useState: mountState,
// ...
};
// 更新時(shí)的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
// ...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useRef: updateRef,
useState: updateState,
// ....
};
所有的函數(shù)組件執(zhí)行畅哑,都是在這里方法中,首先我們應(yīng)該明白幾個(gè)感念,這對于后續(xù)我們理解useState是很有幫助的水由。
workInProgress.expirationTime: react用不同的expirationTime,來確定更新的優(yōu)先級荠呐。
currentHook : 可以理解 current樹上的指向的當(dāng)前調(diào)度的 hooks節(jié)點(diǎn)。
workInProgressHook : 可以理解 workInProgress樹上指向的當(dāng)前調(diào)度的 hooks節(jié)點(diǎn)砂客。
總結(jié)了一下Fiber調(diào)度邏輯為
- 在renderWithHooks中泥张,會(huì)先根據(jù)fiber的memoizedState是否為null,來判斷組件是否掛載鞠值。因?yàn)閙emoizedState在函數(shù)式組件中是存放hooks鏈表的媚创。memoizedState為null則掛載,否則更新
- 在mount(掛載)時(shí)彤恶,函數(shù)式組件執(zhí)行钞钙,Dispatcher為HooksDispatcherOnMount,hooks被調(diào)用會(huì)初始化hooks鏈表声离、initialState芒炼、dispatch函數(shù),并返回术徊。(首次渲染過程)
- 在update(更新)時(shí)本刽,函數(shù)式組件執(zhí)行,Dispatcher為HooksDispatcherOnUpdate赠涮,接著updateWorkInProgressHook獲取當(dāng)前work的Hook子寓。然后根據(jù)numberOfReRenders 是否大于0來判斷是否處理re-render狀態(tài):是的話,執(zhí)行renderPhaseUpdates世囊,獲取第一個(gè)update别瞭,執(zhí)行update然后獲取下一個(gè)update循環(huán)執(zhí)行,直到下一個(gè)update為null株憾;(更新過程)
以useState為例蝙寨,具體執(zhí)行過程如下
接下來將以useState和useEffect在mount 階段和update階段源碼為例進(jìn)一步分析Hooks內(nèi)部執(zhí)行過程:
3. mount 階段:mountState
首先我們需要知道,在組件里嗤瞎,多次調(diào)用useState墙歪,或者其他hook,那react怎么知道我們當(dāng)前是哪一個(gè)hook呢贝奇。其實(shí)在react內(nèi)部虹菲,所有的hook api第一次被調(diào)用的時(shí)候都會(huì)先創(chuàng)建一個(gè)hook對象,來保存相應(yīng)的hook信息掉瞳。然后毕源,這個(gè)hook對象浪漠,會(huì)被加到一個(gè)鏈表上,這樣我們每次渲染的時(shí)候霎褐,只要從這個(gè)鏈表上面依次的去取hook對象址愿,就知道了當(dāng)前是哪一個(gè)hook了。
下面我們就看一下這個(gè)hook對象的具體格式冻璃。
const hook: Hook = {
memoizedState: null, // 緩存當(dāng)前state的值
baseState: null, // 初始化initState响谓,以及每次dispatch之后的newState
queue: null, // update quene
baseUpdate: null, //基于哪一個(gè)hook進(jìn)行更新,循環(huán)update quene的起點(diǎn)
next: null, // 指向下一個(gè)hook
};
我們從引入 hooks開始省艳,以useState為例子娘纷,當(dāng)我們在項(xiàng)目中這么寫:
import { useState } from 'react'
...
const [count, setCount] = useState(0);
于是乎我們?nèi)フ襲seState,看看它到底是哪路神仙?
打開react源碼跋炕。我們進(jìn)入ReactHooks.js來看看赖晶,發(fā)現(xiàn)useState的實(shí)現(xiàn)竟然異常簡單,只有短短兩行
// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
看來重點(diǎn)都在這個(gè)dispatcher上辐烂,dispatcher通過resolveDispatcher()來獲取嬉探,這個(gè)函數(shù)同樣也很簡單,只是將ReactCurrentDispatcher.current的值賦給了dispatcher,并返回dispatcher棉圈;
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
掛載階段Dispatcher會(huì)指向HooksDispatcherOnMount 對象涩堤。也就是說這個(gè)階段的執(zhí)行useState實(shí)際執(zhí)行的是mountState方法
// 在掛載和更新狀態(tài)下ReactCurrentDispatcher.current指向的值不同
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 掛載時(shí)的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useMemo: mountMemo,
useState: mountState,
// ...
};
以掛載為例,mountState具體如下:
function mountState < S > (initialState: (() = >S) | S, ) : [S, Dispatch < BasicStateAction < S >> ] {
// 創(chuàng)建一個(gè)新的hook對象分瘾,并返回當(dāng)前workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 第二步:獲取初始值并初始化hook對象
const queue = hook.queue = { // 新建一個(gè)隊(duì)列
// 保存 update 對象
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 一次新的dispatch觸發(fā)前最新的reducer
// useState 保存固定函數(shù): 可以理解為一個(gè)react 內(nèi)置的reducer
// (state, action) => { return typeof action === 'function' ? action(state) : action }
lastRenderedReducer: reducer
// 一次新的dispatch觸發(fā)前最新的state
lastRenderedState: (initialState: any),
last: null, // 最后一次的update對象
}
// 綁定當(dāng)前 fiber 和 queue.
const dispatch: Dispatch < BasicStateAction < S > ,
>=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
// 返回當(dāng)前狀態(tài)和修改狀態(tài)的方法
return [hook.memoizedState, dispatch];
}
第一步胎围,創(chuàng)建hook對象,并將該hook對象加到hook鏈的末尾,這一步通過mountWorkInProgressHook函數(shù)執(zhí)行德召,代碼如下:
第二步:初始化hook對象的狀態(tài)值白魂,也就是我們傳進(jìn)來的initState的值。
第三步:創(chuàng)建更新隊(duì)列上岗,這個(gè)隊(duì)列是更新狀態(tài)值的時(shí)候用的福荸。
第四步:綁定dispatchAction函數(shù)。我們可以看到最后一行返回的就是這個(gè)函數(shù)肴掷。也就是說這個(gè)函數(shù)敬锐,其實(shí)就是我們改變狀態(tài)用的函數(shù),就相當(dāng)于是setState函數(shù)呆瞻。這里它先做了一個(gè)綁定當(dāng)前quene和fiber對象的動(dòng)作台夺,就是為了在調(diào)用setState的時(shí)候,知道該更改的是那一個(gè)狀態(tài)的值痴脾。
function mountWorkInProgressHook() {
// 初始化的hook對象
var hook = {
memoizedState: null,
// 存儲更新后的state值
baseState: null,
// 存儲更新前的state
baseQueue颤介, // 更新函數(shù)
queue: null,
// 存儲多次的更新行為
next: null // 指向下一次useState的hook對象
};
// workInProgressHook是一個(gè)全局變量,表示當(dāng)前正在處理的hook
// 如果workInProgressHook鏈表為null就將新建的hook對象賦值給它,如果不為null滚朵,那么就加在鏈表尾部冤灾。
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
mountWorkInProgressHook這個(gè)函數(shù)做的事情很簡單,首先每次執(zhí)行一個(gè)hooks函數(shù)辕近,都產(chǎn)生一個(gè)hook對象瞳购,里面保存了當(dāng)前hook信息,然后將每個(gè)hooks以鏈表形式串聯(lián)起來,并賦值給workInProgress的memoizedState亏推。也就證實(shí)了上述所說的,函數(shù)組件用memoizedState存放hooks鏈表年堆。
至于hook對象中都保留了那些信息吞杭?我這里先分別介紹一下 :
memoizedState
: useState中 保存 state 信息 | useEffect 中 保存著 effect 對象 | useMemo 中 保存的是緩存的值和 deps | useRef 中保存的是 ref 對象。
baseQueue
: usestate和useReducer中 保存最新的更新隊(duì)列变丧。
baseState
: usestate和useReducer中,一次更新中 芽狗,產(chǎn)生的最新state值。
queue
: 保存待更新隊(duì)列 pendingQueue 痒蓬,更新函數(shù) dispatch 等信息童擎。
next
: 指向下一個(gè) hooks對象。
從上面的代碼可以看到攻晒,hook其實(shí)是以鏈表的形式存儲起來的顾复。每一個(gè)hook都有一個(gè)指向下一個(gè)hook的指針。如果我們在組件代碼中聲明了多個(gè)hook鲁捏,那這些hook對象之間是這樣排列的:
React會(huì)把hook對象掛到Fiber節(jié)點(diǎn)的memoizedState
屬性上:
初始化完成后芯砸,怎樣對state值進(jìn)行更新的呢?實(shí)際上就是通過dispatchAction方法進(jìn)行更新的给梅,如下:
// currentlyRenderingFiber$1是一個(gè)全局變量假丧,表示當(dāng)前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
dispatchAction邏輯如下:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/** 省略Fiber調(diào)度相關(guān)代碼 **/
// 創(chuàng)建新的新的update, action就是我們setCount里面的值(count+1, count+2, count+3…)
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 重點(diǎn):構(gòu)建query
// queue.last是最近的一次更新,然后last.next開始是每一次的action
const last = queue.last;
if (last === null) {
// 只有一個(gè)update, 自己指自己-形成環(huán)
update.next = update;
} else {
const first = last.next;
if (first !== null) {
update.next = first;
}
last.next = update;
}
queue.last = update;
/** 省略特殊情況相關(guān)代碼 **/
// 創(chuàng)建一個(gè)更新任務(wù)
scheduleWork(fiber, expirationTime);
}
簡單理解:
省略無關(guān)代碼动羽,我們可以看到實(shí)際上包帚,dispatchAction這個(gè)函數(shù)主要做了兩件事情。
第一件就是創(chuàng)建了一個(gè)update對象运吓,這個(gè)對象上面保存了本次更新的相關(guān)信息渴邦,包括新的狀態(tài)值action。
-
第二件拘哨,就是將所有的update對象串成了一個(gè)環(huán)形鏈表几莽,保存在我們hook對象的queue屬性上面。所以我們就知道了queue這個(gè)屬性的意義宅静,它是保存所有更新行為的地方章蚣。
dispatchAction
函數(shù)是更新state的關(guān)鍵,在dispatchAction中維護(hù)了一份queue的數(shù)據(jù)結(jié)構(gòu)。queue是一個(gè)環(huán)形鏈表纤垂,規(guī)則:- queue.last指向最近一次更新
- last.next指向第一次更新
- 后面就依次類推矾策,最終倒數(shù)第二次更新指向last,形成一個(gè)環(huán)形鏈表峭沦,如下圖贾虽。
所以每次插入新update時(shí),就需要將原來的first指向queue.last.next吼鱼。再將update指向queue.next蓬豁,最后將queue.last指向update.
- 在這里我們可以看到,我們要更改的狀態(tài)值并沒有真的改變菇肃,只是被緩存起來了地粪。那么真正改變狀態(tài)值的地方在哪呢?答案就是在下一次render時(shí)琐谤,函數(shù)組件里的useState又一次被調(diào)用了蟆技,這個(gè)時(shí)候才是真的更新state的時(shí)機(jī)。
- 理論上可以同時(shí)調(diào)用多次dispatch斗忌,但只有最后一次會(huì)生效(queue的last指針指向最后一次update的state)
- 注意
useState
更新數(shù)據(jù)和setState
不同的是质礼,后者會(huì)與old state做merge,我們只需把更改的部分傳進(jìn)去织阳,但是useState
則是直接覆蓋眶蕉!
下面這張圖,是我自己畫的簡易版useState源碼的流程圖唧躲。
至此妻坝,mount階段的useState講解完畢,接下來講解狀態(tài)更新后的useState
update 階段:updateState
上述介紹了第一次渲染函數(shù)組件惊窖,react-hooks useState初始化都做些什么刽宪,接下來,我們分析一下界酒,react-hooks useState update和re-render:
更新階段resolveDispatcher函數(shù)中Dispatcher會(huì)指向HooksDispatcherOnUpdate 對象圣拄。也就是說這個(gè)階段的執(zhí)行useState實(shí)際執(zhí)行的是updateState方法
// 更新時(shí)的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
// ...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useRef: updateRef,
useState: updateState,
// ....
};
這里就是我們組件更新時(shí),調(diào)用useState時(shí)真正走的邏輯了毁欣。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
reducer: (S, A) => S, // 對于useState來說就是basicStateReducer
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook(); // 獲取當(dāng)前正在工作的hook庇谆,Q1
const queue = hook.queue; // 更新隊(duì)列
// The last update in the entire queue
const last = queue.last; // 最后一次的update對象
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 上一輪更新的最后一次更新對象
const baseState = hook.baseState; // 上一次的action,現(xiàn)在是初始值
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null; // 因?yàn)閝uene是一個(gè)環(huán)形鏈表凭疮,所以這里要置空
}
first = baseUpdate.next; // 第一次是用的last.next作為第一個(gè)需要更新的update,第二次之后就是基于上一次的baseUpdate來開始了(baseUpdate就是上一次的最后一個(gè)更新)
} else {
first = last !== null ? last.next : null; // last.next是第一個(gè)update
}
if (first !== null) { // 沒有更新饭耳,則不需要執(zhí)行,直接返回
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do { // 循環(huán)鏈表执解,執(zhí)行每一次更新
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
...
} else { // 正常邏輯
// This update does have sufficient priority.
// Process this update.
if (update.eagerReducer === reducer) { // 如果是useState寞肖,他的reducer就是basicStateReducer
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) { // 不跳過,就更新baseUpdate和baseState
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
...
hook.memoizedState = newState; // 更新hook對象
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateState做的事情,實(shí)際上就是拿到更新隊(duì)列新蟆,循環(huán)隊(duì)列觅赊,并根據(jù)每一個(gè)update對象對當(dāng)前hook進(jìn)行狀態(tài)更新。最后返回最終的結(jié)果琼稻。
對于更新階段吮螺,說明上一次 workInProgress 樹已經(jīng)賦值給了 current 樹。存放hooks信息的memoizedState帕翻,此時(shí)已經(jīng)存在current樹上鸠补,react對于hooks的處理邏輯和fiber樹邏輯類似。
對于一次函數(shù)組件更新嘀掸,當(dāng)再次執(zhí)行hooks函數(shù)的時(shí)候紫岩,比如 useState(0) ,首先要從current的hooks中找到與當(dāng)前workInProgressHook對應(yīng)的currentHook横殴,然后復(fù)制一份currentHook給workInProgressHook,接下來hooks函數(shù)執(zhí)行的時(shí)候,把最新的狀態(tài)更新到workInProgressHook,保證hooks狀態(tài)不丟失卿拴。
所以函數(shù)組件每次更新衫仑,每一次react-hooks函數(shù)執(zhí)行,都需要有一個(gè)函數(shù)去做上面的操作堕花,這個(gè)函數(shù)就是updateWorkInProgressHook,我們接下來一起看這個(gè)updateWorkInProgressHook文狱。
updateWorkInProgressHook
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) { /* 如果 currentHook = null 證明它是第一個(gè)hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一個(gè)hooks,那么指向下一個(gè) hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook
if (workInProgressHook === null) { //第一次執(zhí)行hooks
// 這里應(yīng)該注意一下缘挽,當(dāng)函數(shù)組件更新也是調(diào)用 renderWithHooks ,memoizedState屬性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 這個(gè)情況說明 renderWithHooks 執(zhí)行 過程發(fā)生多次函數(shù)組件的執(zhí)行 瞄崇,我們暫時(shí)先不考慮 */
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook = { //創(chuàng)建一個(gè)新的hook
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) { // 如果是第一個(gè)hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else { // 重新更新 hook
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
這一段的邏輯大致是這樣的:
首先如果是第一次執(zhí)行hooks函數(shù),那么從current樹上取出memoizedState 壕曼,也就是舊的hooks苏研。
然后聲明變量nextWorkInProgressHook,這里應(yīng)該值得注意腮郊,正常情況下嗓节,一次renderWithHooks執(zhí)行两疚,workInProgress上的memoizedState會(huì)被置空,hooks函數(shù)順序執(zhí)行,nextWorkInProgressHook應(yīng)該一直為null珊蟀,那么什么情況下nextWorkInProgressHook不為null,也就是當(dāng)一次renderWithHooks執(zhí)行過程中,執(zhí)行了多次函數(shù)組件浪讳,也就是在renderWithHooks中這段邏輯吸耿。
if (workInProgress.expirationTime === renderExpirationTime) {
// ....這里的邏輯我們先放一放
}
這里面的邏輯,實(shí)際就是判定掸绞,如果當(dāng)前函數(shù)組件執(zhí)行后泵三,當(dāng)前函數(shù)組件的還是處于渲染優(yōu)先級,說明函數(shù)組件又有了新的更新任務(wù),那么循壞執(zhí)行函數(shù)組件切黔。這就造成了上述的砸脊,nextWorkInProgressHook不為 null 的情況。
最后復(fù)制current的hooks纬霞,把它賦值給workInProgressHook,用于更新新的一輪hooks狀態(tài)凌埂。
執(zhí)行updateWorkInProgressHook獲取到hook之后,我們繼續(xù)看接下來的操作:
看起來很復(fù)雜诗芜,讓我們慢慢吃透瞳抓,首先將上一次更新的pending queue
合并到 basequeue
,為什么要這么做伏恐,比如我們再一次點(diǎn)擊事件中這么寫孩哑,
function Index(){
const [ number ,setNumber ] = useState(0)
const handerClick = ()=>{
// setNumber(1)
// setNumber(2)
// setNumber(3)
setNumber(state=>state+1)
// 獲取上次 state = 1
setNumber(state=>state+1)
// 獲取上次 state = 2
setNumber(state=>state+1)
}
console.log(number) // 3
return <div>
<div>{ number }</div>
<button onClick={ ()=> handerClick() } >點(diǎn)擊</button>
</div>
}
點(diǎn)擊按鈕, 打印 3
三次setNumber
產(chǎn)生的update
會(huì)暫且放入pending queue
翠桦,在下一次函數(shù)組件執(zhí)行時(shí)候横蜒,三次 update
被合并到 baseQueue
。結(jié)構(gòu)如下圖:
setState.jpg
接下來會(huì)把當(dāng)前useState
或是useReduer
對應(yīng)的hooks
上的baseState
和baseQueue
更新到最新的狀態(tài)销凑。會(huì)循環(huán)baseQueue
的update
丛晌,復(fù)制一份update
,更新 expirationTime
,對于有足夠優(yōu)先級的update
(上述三個(gè)setNumber
產(chǎn)生的update
都具有足夠的優(yōu)先級)斗幼,我們要獲取最新的state
狀態(tài)澎蛛。,會(huì)一次執(zhí)行useState
上的每一個(gè)action
蜕窿。得到最新的state
谋逻。
更新state
sset1.jpg
這里有會(huì)有兩個(gè)疑問???:
- 問題一:這里不是執(zhí)行最后一個(gè)
action
不就可以了嘛?
答案: 原因很簡單,上面說了 useState
邏輯和useReducer
差不多桐经。如果第一個(gè)參數(shù)是一個(gè)函數(shù)毁兆,會(huì)引用上一次 update
產(chǎn)生的 state
, 所以需要循環(huán)調(diào)用,每一個(gè)update
的reducer
阴挣,如果setNumber(2)
是這種情況荧恍,那么只用更新值,如果是setNumber(state=>state+1)
,那么傳入上一次的 state
得到最新state
屯吊。
- 問題二:什么情況下會(huì)有優(yōu)先級不足的情況(
updateExpirationTime < renderExpirationTime
)送巡?
答案: 這種情況,一般會(huì)發(fā)生在盒卸,當(dāng)我們調(diào)用setNumber
時(shí)候骗爆,調(diào)用scheduleUpdateOnFiber
渲染當(dāng)前組件時(shí),又產(chǎn)生了一次新的更新蔽介,所以把最終執(zhí)行reducer
更新state
任務(wù)交給下一次更新摘投。