網站首頁 編程語言 正文
React Hooks已經推出一段時間,大家應該比較熟悉,或者多多少少在項目中用過。寫這篇文章簡單分析一下Hooks的原理,并帶大家實現一個簡易版的Hooks。
這篇寫的比較細,相關的知識點都會解釋,給大家刷新一下記憶。
Hooks
Hooks是React 16.8推出的新功能。以這種更簡單的方式進行邏輯復用。之前函數組件被認為是無狀態的。但是通過Hooks,函數組件也可以有狀態,以及類組件的生命周期方法。
useState用法示例:
import React, { useState } from 'react';
function Example() {
// count是組件的狀態
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}
閉包
開始之前,我們來簡單回顧一下閉包的概念,因為Hooks的實現是高度依賴閉包的。
閉包(Closure),Kyle Simpson在《你不知道的Javascript》中總結閉包是:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
閉包就是,函數可以訪問到它所在的詞法作用域,即使是在定義以外的位置調用。
閉包的一個重要應用就是,實現內部變量/私有數據。
var counter = 0;
// 給計數器加1
function add() {
counter += 1;
}
// 調用 add() 3次
add(); // 1
add(); // 2
counter = 1000;
add(); // 1003
這里因為counter不是內部變量,所以誰都能修改它的值。我們不想讓人隨意修改counter怎么辦?這時候就可以用閉包:
function getAdd() {
var counter = 0;
return function add() {counter += 1;}
}
var add = getAdd();
add(); // 1
add(); // 2
add(); // 3
counter = 1000 // error! 當前位置無法訪問counter
我們還可以把函數的定義挪到調用的位置,用一個立即執行函數表達式IIFE(Immediately Invoked Function Expression):
var add = (function getAdd() {
var counter = 0;
return function add() {counter += 1;}
})();
add(); // 1
add(); // 2
add(); // 3
這種通過IIFE創建閉包的方式也叫做模塊模式(Module Pattern),它創建了一個封閉的作用域,只有通過返回的對象/方法來操縱作用域中的值。這個模式由來已久了,之前很多Javascript的庫,比如jQuery,就是用它來導出自己的實例的。
開始動手實現
理清閉包的概念后可以著手寫了。從簡單的入手,先來實現setState。
function useState(initialValue) {
var _val = initialValue; // _val是useState的變量
function state() {
// state是一個內部函數,是閉包
return _val;
}
function setState(newVal) {
_val = newVal;
}
return [state, setState];
}
var [foo, setFoo] = useState(0);
console.log(foo()); // 0
setFoo(1);
console.log(foo()) // 1
根據useState的定義來實現。比較簡單不需要多解釋。
參考 前端進階面試題詳細解答
將useState應用到組件中
現在我們將這個簡易版的useState應用到一個Counter組件中:
function Counter() {
const [count, setCount] = useState(0);
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 }
這里簡單起見,就不render真實DOM了,因為我們只關心組件的狀態,所以每次render的時候打印count的值。
這里點擊click之后,counter的值加一,useState的基本功能實現了。但現在state是一個函數而不是一個變量,這和React的API不一致,接下來我們就來改正這一點。
過期閉包
function useState(initialValue) {
var _val = initialValue
// 去掉了state()函數
function setState(newVal) {
_val = newVal
}
return [_val, setState] //直接返回_val
}
var [foo, setFoo] = useState(0)
console.log(foo) // 0
setFoo(1) // 更新_val
console.log(foo) // 0 - BUG!
如果我們直接把state從函數改成變量,問題就出現了,state不更新了。無論點擊幾次,Counter的值始終不變。這個是過期閉包問題(Stale Closure Problem)。因為在useState返回的時候,state就指向了初始值,所以后面即使counter的值改變了,打印出來的仍然就舊值。我們想要的是,返回一個變量的同時,還能讓這個變量和真實狀態同步。那如何來實現呢?
模塊模式
解決辦法就是將閉包放在另一個閉包中。
const MyReact = (function() {
let _val //將_val提升到外層閉包
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useState(initialValue) {
_val = _val || initialValue //每次刷新
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()
我們運用之前提到的模塊模式,創建一個MyReact模塊(第一層閉包),返回的對象中包含useState方法(第二層閉包)。useState返回值中的state,指向的是useState閉包中的_val,而每次調用useState,_val都會重新綁定到上層的_val上,保證返回的state的值是最新的。解決了過期閉包的問題。
MyReact還提供了另外一個方法render,方法中調用組件的render方法來“渲染”組件,也是為了不渲染DOM的情況下進行測試。
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 }
這里每次調用MyReact.render(Counter),都會生成新的Counter實例,調用實例的render方法。render方法中調用了MyReact.useState()。MyReact.useState()在多次執行之間,外層閉包中的_val值保持不變,所以count會綁定到當前的_val上,這樣就可以打印出正確的count值了。
實現useEffect
實現了useState之后,接下來實現useEffect。
const MyReact = (function() {
let _val, _deps // 將狀態和依賴數組保存到外層的閉包中
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]
}
}
})()
// usage
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)
// // 沒有執行effect
// render {count: 1}
App.click()
App = MyReact.render(Counter)
// effect 2
// render {count: 2}
在MyReact.useEffect中,我們將依賴數組保存到_deps,每次調用,都和前一次的依賴數組進行比對。發生變化才觸發回調。
注意這里在比較依賴時用的是Object.is, React在比較state變化時也是用它。注意Object.is在比較時不會做類型轉換(和==不同)。另外NaN === NaN返回false,但是Object.is(NaN, NaN)會返回true。
(簡單起見,我們實現的useEffect,回調函數是同步執行的,所以打印出來的log是effect先執行,然后才是render。實際React中useEffect的回調函數應該是異步執行的)
支持多個Hooks
到此為止我們已經簡單實現了useState和useEffect。但還有一個問題,就是useState和useEffect每個組件中只能用一次。
那么怎么才能支持使用多次hooks呢,我們可以將hooks保存到一個數組中。
const MyReact = (function() {
let hooks = [],
currentHook = 0 // 存儲hooks的數組,和數組指針
return {
render(Component) {
const Comp = Component() // 執行effect
Comp.render()
currentHook = 0 // 每次render后,hooks的指針清零
return Comp
},
useEffect(callback, depArray) {
const hasNoDeps = !depArray
const deps = hooks[currentHook]
const hasChangedDeps = deps ? !depArray.some((el, i) => !Object.is(el, deps[i])) : true
if (hasNoDeps || hasChangedDeps) {
callback()
hooks[currentHook] = depArray
}
currentHook++ // 每調用一次指針加一
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook // 注意??這句不是沒用。是避免過期閉包問題。
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
}
})()
注意這里用了一個新的變量setStateHookIndex來保存currentHook的值。這是為了避免useState閉包包住舊的currentHook的值。
將改動應用到組件中:
function Counter() {
const [count, setCount] = MyReact.useState(0)
const [text, setText] = MyReact.useState('foo') // 第二次用了useState
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)
// // 不運行effect
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}
實現多個hooks支持的基本思路,就是用一個數組存放hooks。每次使用hooks時,將hooks指針加1。每次render以后,將指針清零。
Custom Hooks
接下來,可以借助已經實現的hooks繼續實現custom hooks:
function Component() {
const [text, setText] = useSplitURL('www.google.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', 'google', 'com' ] }
App.type('www.reactjs.org')
App = MyReact.render(Component)
// { text: [ 'www', 'reactjs', 'org' ] }}
重新理解Hooks規則
了解Hooks的實現可以幫助我們理解Hooks的使用規則。還記得使用Hooks的原則嗎?hooks只能用到組件最外層的代碼中,不能包裹在if或者循環里,原因是在React內部,通過數組來存儲hooks。所以必須保證每次render,hooks的順序不變,數量不變,才能做deps的比對。
原文鏈接:https://blog.csdn.net/helloworld1024fd/article/details/128283312
相關推薦
- 2022-07-04 python如何處理matlab的mat數據_python
- 2022-07-20 C語言雙向鏈表的原理與使用操作_C 語言
- 2022-04-17 Spring Security前后端分離實現
- 2022-08-18 python打印日志方法的使用教程(logging模塊)_python
- 2022-06-08 FreeRTOS實時操作系統Cortex-M內核使用注意事項_操作系統
- 2022-08-23 Python中應用Winsorize縮尾處理的操作經驗_python
- 2022-07-13 Android?Studio實現簡單繪圖板_Android
- 2023-03-25 Android?Jetpack組件ViewModel基本用法詳解_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支