實(shí)戰(zhàn)+源碼 帶你快速掌握React Hooks

React Hooks

大綱

  • ?? 函數(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ù)式編程

函數(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 里面。

image

函數(shù)一般來說劣挫,只應(yīng)該做一件事册养,就是返回一個(gè)值。 如果你有多個(gè)操作压固,每個(gè)操作應(yīng)該寫成一個(gè)單獨(dú)的函數(shù)球拦。而且,數(shù)據(jù)的狀態(tài)應(yīng)該與操作方法分離帐我。根據(jù)這種理念坎炼,React 的函數(shù)組件只應(yīng)該做一件事情:返回組件的 HTML 代碼,而沒有其他的功能拦键。

image

以函數(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>
    );
}
image

useRef 有下面這幾個(gè)特點(diǎn):

  1. useRef 是一個(gè)只能用于函數(shù)組件的方法抬探。
  2. useRef 是除字符串 ref子巾、函數(shù) ref帆赢、createRef 之外的第四種獲取 ref 的方法。
  3. useRef 在渲染周期內(nèi)永遠(yuǎn)不會(huì)變线梗,因此可以用來引用某些數(shù)據(jù)椰于。
  4. 修改 ref.current 不會(huì)引發(fā)組件重新渲染。

為什么會(huì)出現(xiàn)React Hooks

Why React Hooks.png
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 說起:useStateuseEffect 。很有可能蔚舀,你在平時(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)鍵礼患,請看下圖:

image

可以看到是钥,函數(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)畫:

image

與之前的純函數(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腕柜。

image

結(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)诬乞,例如大家所熟悉的 setIntervalclearInterval

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)畫吧:

image

動(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 里面(例如 setIntervalclearInterval),更突出邏輯的內(nèi)聚性

在最極端的情況下拓售,我們可以指定 deps 為空數(shù)組 [] 窥摄,這樣可以確保 Effect 只會(huì)在組件初次渲染后執(zhí)行。實(shí)際效果動(dòng)畫如下:

image

可以看到邻辉,后面的所有重渲染都不會(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í)耗溜,情況到底是什么樣的:

image

注意以下要點(diǎn):

  1. 在初次渲染時(shí)买置,我們通過 useState 定義了多個(gè)狀態(tài);
  2. 每調(diào)用一次 useState 强霎,都會(huì)在組件之外生成一條 Hook 記錄忿项,同時(shí)包括狀態(tài)值(用 useState 給定的初始值初始化)和修改狀態(tài)的 Setter 函數(shù);
  3. 多次調(diào)用 useState 生成的 Hook 記錄形成了一條鏈表
  4. 觸發(fā) onClick 回調(diào)函數(shù)轩触,調(diào)用 setS2 函數(shù)修改 s2 的狀態(tài)寞酿,不僅修改了 Hook 記錄中的狀態(tài)值,還即將觸發(fā)重渲染脱柱。

OK伐弹,重渲染的時(shí)候到了,動(dòng)畫如下:

image

可以看到榨为,在初次渲染結(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,請看下面的演示:

image

注意其中一些細(xì)節(jié):

  1. useStateuseEffect 在每次調(diào)用時(shí)都被添加到 Hook 鏈表中浩蓉;
  2. useEffect 還會(huì)額外地在一個(gè)隊(duì)列中添加一個(gè)等待執(zhí)行的 Effect 函數(shù)派继;
  3. 在渲染完成后,依次調(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í)行之后,hooksworkInProgress將是如圖的關(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):

image
  • 組件初始化時(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)度邏輯如下:

image.png

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í)行過程如下

image.png

接下來將以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了。


image.png

下面我們就看一下這個(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屬性上:

image.png

初始化完成后芯砸,怎樣對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ī)則:

    1. queue.last指向最近一次更新
    2. last.next指向第一次更新
    3. 后面就依次類推矾策,最終倒數(shù)第二次更新指向last,形成一個(gè)環(huán)形鏈表峭沦,如下圖贾虽。
image.png

所以每次插入新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源碼的流程圖唧躲。


image.png

至此妻坝,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上的baseStatebaseQueue更新到最新的狀態(tài)销凑。會(huì)循環(huán)baseQueueupdate丛晌,復(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è)updatereducer阴挣,如果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ù)交給下一次更新摘投。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煮寡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子犀呼,更是在濱河造成了極大的恐慌幸撕,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件外臂,死亡現(xiàn)場離奇詭異坐儿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宋光,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門貌矿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罪佳,你說我怎么就攤上這事逛漫。” “怎么了赘艳?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵酌毡,是天一觀的道長。 經(jīng)常有香客問我蕾管,道長枷踏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任娇掏,我火速辦了婚禮呕寝,結(jié)果婚禮上勋眯,老公的妹妹穿的比我還像新娘婴梧。我一直安慰自己,他們只是感情好客蹋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布塞蹭。 她就那樣靜靜地躺著,像睡著了一般讶坯。 火紅的嫁衣襯著肌膚如雪番电。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天辆琅,我揣著相機(jī)與錄音漱办,去河邊找鬼。 笑死婉烟,一個(gè)胖子當(dāng)著我的面吹牛娩井,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播似袁,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洞辣,長吁一口氣:“原來是場噩夢啊……” “哼咐刨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扬霜,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤定鸟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后著瓶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體联予,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年蟹但,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躯泰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡华糖,死狀恐怖麦向,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情客叉,我是刑警寧澤诵竭,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站兼搏,受9級特大地震影響卵慰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜佛呻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一裳朋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吓著,春花似錦鲤嫡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至纺裁,卻和暖如春诫肠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欺缘。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工栋豫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谚殊。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓丧鸯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親络凿。 傳聞我的和親對象是個(gè)殘疾皇子骡送,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容