[譯]深入淺出:React Hooks是如何工作的蹬挺?

翻譯自netlify博客里的一篇文章豹休。

Hooks 是在用戶界面中封裝有狀態(tài)的行為和副作用(side effects)的一種基礎性的更加簡單的方法。他們被React首次引入 ,已經(jīng)被其他前端框架如Vue克蚂,Svelte俱诸,甚至是通用JS函數(shù)式編程框架等廣泛采納。但是维雇,它們函數(shù)式的設計需要開發(fā)者對JS中的閉包有一個好的理解淤刃。

這篇文章,我們通過寫一個小型的克隆版React Hooks來再次介紹閉包谆沃。主要有兩個目的——演示閉包的有效用例和向你們展示如何只用29行具備可讀性的JS代碼來寫一套Hooks钝凶。最后,我們會介紹自定義Hooks是如何自然地出現(xiàn)的。

?? 注意:你并不需要跟著寫這些代碼耕陷。練習寫這些代碼可能對你的JS基礎有一定幫助掂名。別擔心,沒有那么難哟沫!

什么是閉包饺蔑?

使用Hooks的很多賣點之一就是可以避免類組件和高階組件的復雜性。然而嗜诀,有些人用上Hooks猾警,可能感覺從一個坑掉進了另一個坑。雖然不用再擔心綁定上下文的問題隆敢,但是我們現(xiàn)在又要擔心閉包发皿。正如Mark Dalgleish那句令人印象深刻的總結(jié)

一張關(guān)于React Hooks和閉包的星球大戰(zhàn)的惡搞圖片

閉包是JS中的基礎概念。盡管如此拂蝎,對新手來說它的難于理解可是臭名昭著了穴墅。You Don’t Know JS 的作者Kyle Simpson對閉包有一個著名的定義:

閉包是指當一個函數(shù)在它的詞法作用域以外執(zhí)行的時候,依然可以記憶和使用它的詞法作用域温自。

它們明顯跟詞法作用域的概念是緊密相關(guān)的玄货。MDN是這樣定義的:“當函數(shù)嵌套在一起時,語法分析器如何找到變量名定義的地方”悼泌。讓我們通過一個實際的例子來更好地說明:

// 樣例 0
function useState(initialValue) {
  var _val = initialValue // _val是useState函數(shù)里定義的局部變量
  function state() {
    // state是一個內(nèi)部函數(shù)松捉,是閉包
    return _val // state() 使用了它的父函數(shù)里聲明的變量_val
  }
  function setState(newVal) {
    // 同樣
    _val = newVal // 設置_val的值,但是沒有暴露_val
  }
  return [state, setState] // 暴露出函數(shù)以便外部使用
}
var [foo, setFoo] = useState(0) // 數(shù)組解構(gòu)
console.log(foo()) // 打印0 - 我們給的初始值
setFoo(1) // 設置useState作用域里的_val
console.log(foo()) // 打印1 - 新值馆里,即使調(diào)用的是相同的方法

這里我們寫了一個簡單的模仿React的useState hook隘世。在我們的函數(shù)里,有兩個內(nèi)部函數(shù)鸠踪,statesetState以舒。state返回在上面定義的一個局部變量_valsetState將傳入的參數(shù)值設置給這個局部變量(i.e.newVal)慢哈。

我們這里實現(xiàn)的state是一個getter函數(shù)蔓钟,這個并不理想,我們過會兒來修改它卵贱。重點在于通過foosetFoo滥沫,我們可以使用和修改 (a.k.a. “close over”)內(nèi)部的變量_val。它們保留了對useState作用域的引用键俱,這就叫閉包兰绣。在React和其他前端框架中,這看上去像state编振,實際上就是state缀辩。

如果你想深入探索閉包,我推薦你讀讀MDN, YDKJSDailyJS中有關(guān)這個話題的內(nèi)容,但是如果你理解了上面的代碼樣例臀玄,其實就足夠了瓢阴。

在函數(shù)組件中的用法

讓我們用看上去更熟悉一些的方式應用一下我們新打造的useState。我們來寫一個Counter組件健无!

// 樣例 1
function Counter() {
  const [count, setCount] = useState(0) // 跟上面一樣的useState
  return {
    click: () => setCount(count() + 1),
    render: () => console.log('render:', { count: count() })
  }
}
const C = Counter()
C.render() // render: { count: 0 }
C.click()
C.render() // render: { count: 1 }

這里我們選擇只是console.log出來我們的state而不是渲染到DOM荣恐。我們還為我們的Counter組件暴露了一組API,這樣就可以在腳本里調(diào)用累贤,而不需要綁定一個事件處理函數(shù)叠穆。采用這樣的設計,我們可以模擬組件的渲染和對用戶行為的反應臼膏。

雖然程序可以工作硼被,但是真正的React.useState不是調(diào)用getter函數(shù)去拿到state的。讓我們修改一下渗磅。

過時的閉包

如果我們想貼近真實的React API祷嘶,我們不得不把state從函數(shù)改成變量。如果我們只是簡單地暴露_val而不是包住變量_val的函數(shù)的話夺溢,我們會遇到一個bug:

// 樣例 0, 再審視 - 這是有bug的!
function useState(initialValue) {
  var _val = initialValue
  // 沒有 state() 函數(shù)了
  function setState(newVal) {
    _val = newVal
  }
  return [_val, setState] // 直接暴露_val
}
var [foo, setFoo] = useState(0)
console.log(foo) // 打印 0 不需要調(diào)用函數(shù)
setFoo(1) // 設置useState作用域里的_val
console.log(foo) // 打印 0 - 喔!!

這是一種過時閉包的表現(xiàn)形式。當我們從useState的返回值解構(gòu)出foo時烛谊,它的值等于最初調(diào)用useState時的_val风响,并且再也不會變了!這不是我們想要的丹禀;我們通常需要我們的組件state作為變量而不是作為函數(shù)就可以反映當前的狀態(tài)状勤!這兩個目標似乎完全相反。

模塊中的閉包

我們可以通過把我們的閉包移動到另一個閉包里面來解決我們的useState難題双泪。(Yo dawg 我聽說你喜歡閉包…)

// 樣例 2
const MyReact = (function() {
  let _val // 在模塊作用域中聲明狀態(tài)
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue // 每次運行都重新賦值
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

這里我們選擇使用模塊模式來重構(gòu)我們的克隆版React持搜。同React一樣,它要追蹤組件狀態(tài)(在我們的例子里焙矛,它用保存狀態(tài)的_val只追蹤一個組件)葫盼。這種設計模式使MyReact可以“render”你的函數(shù)組件,通過正確的閉包它每次運行都可以給內(nèi)部的_val賦值:

// 樣例 2 繼續(xù)
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count })
  }
}
let App
App = MyReact.render(Counter) // render: { count: 0 }
App.click()
App = MyReact.render(Counter) // render: { count: 1 }

現(xiàn)在這看上去很像有Hooks的React了村斟!

你可以在YDKJS里讀到更多關(guān)于模塊模式和閉包的內(nèi)容贫导。

復制useEffect

目前為止,我們已經(jīng)介紹了最基礎的React HookuseState蟆盹。另一個非常重要的hook是useEffect孩灯。與setState不同,useEffect是異步執(zhí)行的逾滥,這意味著更可能會遇到閉包問題峰档。

我們可以這樣擴展已經(jīng)寫好的MyReact:

// 樣例 3
const MyReact = (function() {
  let _val, _deps // 在作用域里聲明狀態(tài)和依賴變量
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        _deps = depArray
      }
    },
    useState(initialValue) {
      _val = _val || initialValue
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

// 用法
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  MyReact.useEffect(() => {
    console.log('effect', count)
  }, [count])
  return {
    click: () => setCount(count + 1),
    noop: () => setCount(count),
    render: () => console.log('render', { count })
  }
}
let App
App = MyReact.render(Counter)
// effect 0
// render {count: 0}
App.click()
App = MyReact.render(Counter)
// effect 1
// render {count: 1}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1}
App.click()
App = MyReact.render(Counter)
// effect 2
// render {count: 2}

為了追蹤依賴項的變化(因為當依賴項變化,useEffect會再次執(zhí)行),我們引入了另一個變量_deps讥巡。

沒有魔法掀亩,只是數(shù)組

我們有了一個非常不錯的克隆版的useStateuseEffect,但都是實現(xiàn)得不太好的單例 (分別只能有一個存在尚卫,否則會有bug)归榕。為了做點有意思的東西(也為了演示最后一個過時閉包的例子),我們需要使它們可以有任意數(shù)量的狀態(tài)和副作用吱涉。幸運的是刹泄,正如Rudi Yardley寫到的,React Hooks不是魔法怎爵,僅僅是數(shù)組特石。所以我們定義了一個hooks數(shù)組。我們也利用這個機會把_val_deps放進了hooks數(shù)組里:

// 樣例 4
const MyReact = (function() {
  let hooks = [],
    currentHook = 0 // hooks數(shù)組鳖链,和一個數(shù)組下標姆蘸!
  return {
    render(Component) {
      const Comp = Component() // 執(zhí)行效果
      Comp.render()
      currentHook = 0 // 為下一次render重置hooks數(shù)組下標
      return Comp
    },
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const deps = hooks[currentHook] // 類型: 數(shù)組 | undefined
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        hooks[currentHook] = depArray
      }
      currentHook++ // 這個hook運行結(jié)束
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue // 類型: 任意
      const setStateHookIndex = currentHook // 用于setState的閉包!
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()

請注意這里setStateHookIndex的用法芙委,看上去好像什么都沒做逞敷,但其實它是用來避免setState成為currentHook的閉包!如果你把它拿掉灌侣,setState將因為被它閉包的currentHook的值已經(jīng)過時而不能正常工作推捐。(試一下!)

// 樣例 4 繼續(xù) - 用法
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  const [text, setText] = MyReact.useState('foo') // 第二個hook!
  MyReact.useEffect(() => {
    console.log('effect', count, text)
  }, [count, text])
  return {
    click: () => setCount(count + 1),
    type: txt => setText(txt),
    noop: () => setCount(count),
    render: () => console.log('render', { count, text })
  }
}
let App
App = MyReact.render(Counter)
// effect 0 foo
// render {count: 0, text: 'foo'}
App.click()
App = MyReact.render(Counter)
// effect 1 foo
// render {count: 1, text: 'foo'}
App.type('bar')
App = MyReact.render(Counter)
// effect 1 bar
// render {count: 1, text: 'bar'}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}

所以從基本的直覺出發(fā)侧啼,我們應該聲明一個hooks數(shù)組和一個元素索引牛柒。每當一個hook被調(diào)用的時候,元素索引會遞增痊乾,每當組件被渲染的時候皮壁,元素索引被重置。

你還免費獲得了自定義hooks

// 樣例 4, 再次審視
function Component() {
  const [text, setText] = useSplitURL('www.netlify.com')
  return {
    type: txt => setText(txt),
    render: () => console.log({ text })
  }
}
function useSplitURL(str) {
  const [text, setText] = MyReact.useState(str)
  const masked = text.split('.')
  return [masked, setText]
}
let App
App = MyReact.render(Component)
// { text: [ 'www', 'netlify', 'com' ] }
App.type('www.reactjs.org')
App = MyReact.render(Component)
// { text: [ 'www', 'reactjs', 'org' ] }}

這真的就是“不是魔法”的hooks的基本原理——自定義Hooks僅僅是從框架提供的基本特性中發(fā)展而來的——不論是React還是我們剛剛寫的克隆版哪审。

推導Hooks的規(guī)則

注意從這里你可以粗淺地理解Hooks的第一條規(guī)則只能在頂層調(diào)用Hooks蛾魄。我們已經(jīng)用currentHook變量清楚地模擬了React對Hooks調(diào)用順序的依賴。你可以帶著我們的代碼實現(xiàn)讀一遍Hooks規(guī)則的完整解釋 湿滓,完整地理解正在發(fā)生的一切畏腕。

還要注意第二條規(guī)則,“只能從React函數(shù)中調(diào)用Hooks”茉稠,雖然在我們的代碼實現(xiàn)中不是必要的描馅,但遵守這條規(guī)則可以讓你從代碼里清楚地區(qū)分出有狀態(tài)的那部分邏輯,這確實是好的實踐而线。(作為一個不錯副作用铭污,它也使編寫工具來確保你遵守了第一條原則更加容易恋日。你就不會一不小心在循環(huán)和條件判斷中使用有狀態(tài)的而且像普通的JavaScript函數(shù)那樣命名的函數(shù),搬起石頭砸自己的腳嘹狞。遵守規(guī)則2能幫助你遵守規(guī)則1岂膳。)

結(jié)論

到這里我們可能已經(jīng)最大程度地擴展了這個練習。你可以試著用一行代碼實現(xiàn)useRef磅网,或者使render函數(shù)用JSX語法把元素實際渲染到DOM上谈截,或者完善我們在這28行React Hooks克隆版代碼里忽略的其他重要的細節(jié)。希望你已經(jīng)收獲了在上下文中使用閉包的一些經(jīng)驗涧偷,和解密React Hooks是如何工作的一個有效的思維方式簸喂。

我想感謝Dan AbramovDivya Sasidharan審閱了這篇文章的草稿,用他們的寶貴意見完善了它燎潮。剩下的所有錯誤都算我的..

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喻鳄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子确封,更是在濱河造成了極大的恐慌除呵,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爪喘,死亡現(xiàn)場離奇詭異颜曾,居然都是意外死亡,警方通過查閱死者的電腦和手機秉剑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門泛豪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秃症,你說我怎么就攤上這事÷来猓” “怎么了种柑?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匹耕。 經(jīng)常有香客問我聚请,道長,這世上最難降的妖魔是什么稳其? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任驶赏,我火速辦了婚禮,結(jié)果婚禮上既鞠,老公的妹妹穿的比我還像新娘煤傍。我一直安慰自己,他們只是感情好嘱蛋,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布蚯姆。 她就那樣靜靜地躺著五续,像睡著了一般。 火紅的嫁衣襯著肌膚如雪龄恋。 梳的紋絲不亂的頭發(fā)上疙驾,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音郭毕,去河邊找鬼它碎。 笑死,一個胖子當著我的面吹牛显押,可吹牛的內(nèi)容都是我干的扳肛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼煮落,長吁一口氣:“原來是場噩夢啊……” “哼敞峭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝉仇,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤旋讹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后轿衔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉迹,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年害驹,在試婚紗的時候發(fā)現(xiàn)自己被綠了鞭呕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡宛官,死狀恐怖葫松,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情底洗,我是刑警寧澤腋么,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站亥揖,受9級特大地震影響珊擂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜费变,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一摧扇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挚歧,春花似錦扛稽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锡搜。三九已至,卻和暖如春瞧掺,著一層夾襖步出監(jiān)牢的瞬間耕餐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工辟狈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肠缔,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓哼转,卻偏偏與公主長得像明未,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子壹蔓,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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

  • React是現(xiàn)在最流行的前端框架之一趟妥,它的輕量化,組件化佣蓉,單向數(shù)據(jù)流等特性把前端引入了一個新的高度披摄,現(xiàn)在它又引入的...
    老鼠AI大米_Java全棧閱讀 5,777評論 0 26
  • 在學會使用React Hooks之前,可以先看一下相關(guān)原理學習React Hooks 前言 在 React 的世界...
    DC_er閱讀 9,095評論 1 16
  • 作為一個合格的開發(fā)者勇凭,不要只滿足于編寫了可以運行的代碼疚膊。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,449評論 1 33
  • 你還在為該使用無狀態(tài)組件(Function)還是有狀態(tài)組件(Class)而煩惱嗎虾标?——擁有了hooks寓盗,你再也不需...
    水落斜陽閱讀 82,329評論 11 100
  • 你還在為該使用無狀態(tài)組件(Function)還是有狀態(tài)組件(Class)而煩惱嗎?——擁有了hooks璧函,你再也不需...
    米亞流年閱讀 943評論 0 5