hooks自定义实现
# 设计理念
# 基本原理
# useState
/**第一版*/
function useState(initVal) {
let val = initVal;
function setVal(newVal) {
val = newVal;
render(); // 修改val后 重新渲染页面
}
return [val, setVal];
}
/**第二版*/
let val; // 放到全局作用域
function useState(initVal) {
val = val|| initVal; // 判断val是否存在 存在就使用
function setVal(newVal) {
val = newVal;
render(); // 修改val后 重新渲染页面
}
return [val, setVal];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# useEffect
useState
/**第一版:不考虑第二个参数*/
function useEffect(fn){
fn();
}
/**第二版:不考虑第二个参数*/
let watchArr; // 为了记录状态变化 放到全局作用域
function useEffect(fn,watch){
// 判断是否变化
const hasWatchChange = watchArr?
!watch.every((val,i)=>{ val===watchArr[i] }):true;
if( hasWatchChange ){
fn();
watchArr = watch;
}
}
/**第三版:解决同时调用多个 useState useEffect的问题*/
let memoizedState = [];// 通过数组维护变量
let currentCursor = 0;
function useState(initVal) {
memoizedState[currentCursor] = memoizedState[currentCursor] || initVal;
function setVal(newVal) {
memoizedState[currentCursor] = newVal;
render();
}
// 返回state 然后 currentCursor+1
return [memoizedState[currentCursor++], setVal];
}
function useEffect(fn, watch) {
const hasWatchChange = memoizedState[currentCursor]
? !watch.every((val, i) => val === memoizedState[currentCursor][i])
: true;
if (hasWatchChange) {
fn();
memoizedState[currentCursor] = watch;
currentCursor++; // 累加 currentCursor
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 图解Hook原理
如上图我们根据调用hook顺序,将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState。
上面状态更新图,我们可以看到执行setCount(count + 1)或setData(data + 2)时,先将旧数组memoizedState中对应的值取出来重新复值,从而生成新数组memoizedState。对于是否执行useEffect通过判断其第二个参数是否发生变化而决定的。
function FunctionalComponent () {
const [state1, setState1] = useState(1);
const [state2, setState2] = useState(2);
const [state3, setState3] = useState(3);
}
1
2
3
4
5
2
3
4
5
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'bar',
next: null
}
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 自定义实现
# useState
- useState 就是一个 Hook
- 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
- useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
- useState 唯一的参数就是初始 state
- 返回一个 state,以及更新 state 的函数
- 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
- setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列
const [state, setState] = useState(initialState);
1
# 1: 计数器
Class方式与函数方式的比较
import React,{useState} from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.setState({ number: this.state.number + 1 })}>
+ </button>
</div>
);
}
}
function Counter2(){
const [number,setNumber] = useState(0);
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
export default Counter2;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2: 每次渲染都是独立的闭包
- 每一次渲染都有它自己的 Props and State
- 每一次渲染都有它自己的事件处理函数
- alert会“捕获”我点击按钮时候的状态。
- 组件函数每次渲染都会被调用,但是每一次调用中number值都是常量,并且它被赋予了当前渲染中的状态值
- 在单次渲染的范围内,props和state始终保持不变
- making-setinterval-declarative-with-react-hooks (opens new window)
function Counter2(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
alert(number);
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
function Counter() {
const [number, setNumber] = useState(0);
const savedCallback = useRef();
function alertNumber() {
setTimeout(() => {
alert(savedCallback.current);
}, 3000);
}
return (
<>
<p>{number}</p>
<button onClick={() => {
setNumber(number + 1);
savedCallback.current = number + 1;
}}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 3: 函数式更新
- 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值
function Counter2(){
const [number,setNumber] = useState(0);
let numberRef = useRef(number);
numberRef.current = number;
function alertNumber(){
setTimeout(()=>{
alert(numberRef.current);
},3000);
}
+ function lazy(){
+ setTimeout(()=>{
+ setNumber(number+1);
+ },3000);
+ }
+ function lazyFunc(){
+ setTimeout(()=>{
+ setNumber(number=>number+1);
+ },3000);
+ }
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={lazy}>lazy+</button>
<button onClick={lazyFunc}>lazyFunc+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 4: 惰性初始 state
- initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
- 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
function Counter3(){
const [{name,number},setValue] = useState(()=>{
return {name:'计数器',number:0};
});
return (
<>
<p>{name}:{number}</p>
<button onClick={()=>setValue({number:number+1})}>+</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 5: 性能优化
# 1) Object.is
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
function Counter4(){ const [counter,setCounter] = useState({name:'计数器',number:0}); console.log('render Counter') return ( <> <p>{counter.name}:{counter.number}</p> <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>-</button> </> ) }
1
2
3
4
5
6
7
8
9
10
11
# 2) 减少渲染次数
- 把内联回调函数及依赖项数组作为参数传入
useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新 - 把创建函数和依赖项数组作为参数传入
useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
function Child({onButtonClick,data}){
console.log('Child render');
return (
<button onClick={onButtonClick} >{data.number}</button>
)
}
Child = memo(Child);
function App(){
const [number,setNumber] = useState(0);
const [name,setName] = useState('samy');
const addClick = useCallback(()=>setNumber(number+1),[number]);
const data = useMemo(()=>({number}),[number]);
return (
<div>
<input type="text" value={name} onChange={e=>setName(e.target.value)}/>
<Child onButtonClick={addClick} data={data}/>
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3) useState+useCallback+useMemo实现原理
自己实现 useState+useCallback+useMemo
import React from 'react';
import ReactDOM from 'react-dom';
let hookStates = [];//放着此组件的所有的hooks数据
let hookIndex = 0;//代表当前的hooks的索引
function useState(initialState){
//如果有老状态,则使用老状态,如果没有则使用默认值
//hookStats[0]=0;
hookStates[hookIndex]=hookStates[hookIndex]||initialState;
let currentIndex = hookIndex;
function setState(newState){
hookStates[currentIndex]=newState;
render();
}
return [hookStates[hookIndex++],setState];
}
//let lastCallback,lastCallbackDeps;
function useCallback(callback,deps){
if(hookStates[hookIndex]){
let [lastCallback,lastCallbackDeps] = hookStates[hookIndex];
let same = deps.every((item,index)=>item === lastCallbackDeps[index]);
if(same){//如果老依赖和新的依赖都相同,则直接返回老的,如果不一相同,则返回新的
hookIndex++;
return lastCallback;
}else{
hookStates[hookIndex++]=[callback,deps];
return callback;
}
}else{
hookStates[hookIndex++]=[callback,deps];
return callback;
}
}
//let lastMemo,lastMemoDeps;
function useMemo(factory,deps){
if(hookStates[hookIndex]){
let [memo,lastDeps] = hookStates[hookIndex];
let same = deps.every((item,index)=>item === lastDeps[index]);
if(same){//如果老依赖和新的依赖都相同,则直接返回老的,如果不一相同,则返回新的
hookIndex++;
return memo;
}else{
let newMemo = factory();
hookStates[hookIndex++]=[newMemo,deps];
return newMemo;
}
}else{
let newMemo = factory();
hookStates[hookIndex++]=[newMemo,deps];
return newMemo;
}
}
function Child(props){
console.log('Child render');//渲染子组件
return (
<div>
<button onClick={props.add1} >{props.data1.number}</button>
<button onClick={props.add2} >{props.data2.number}</button>
</div>
)
}
//memo备记录 如果你想让一个函数组件有一个功能,如果属性不变,就不要刷新
function memo(OldComponent){
return class extends React.Component{
shouldComponentUpdate(nextProps, nextState){
if(nextProps === this.props){
return false;
}
if(Object.keys(this.props).length !== Object.keys(nextProps).length){
return true;
}
if(this.props == null || nextProps == null){
return true;
}
for(let key in this.props){
if(this.props[key] !== nextProps[key]){
return true;
}
}
return false;
//return nextProps!==this.props;
// return !(Object.is(nextProps,this.props) && Object.is(nextState,this.state))
}
render(){
return <OldComponent {...this.props}/>
}
}
}
let MemoChild = memo(Child);
//在源码里也是在进入一个新的组件的时候设置的过
function App(){
hookIndex =0;
//如果一旦进入函数组件,索引先归为0
//每调用一个hook,索引加会+1
let [number,setNumber]=useState(0);//0
let [name,setName] = useState('zhufeng');//hookIndex=1
const add1 = useCallback(()=>setNumber(number+1),[number]);//hookIndex=2
const add2 = useCallback(()=>setNumber(number+2),[number]);//hookIndex=3
//useMemo 缓存函数的返回值 {number:0}
//第2个参数是依赖数组,依赖变量化才会重新计算number值 空数组则意味不依赖任何变量
const data1 = useMemo(()=>({number:number}),[number]);//hookIndex=4
const data2 = useMemo(()=>({number:-number}),[number]);//hookIndex=5
return (
<div>
<input type="text" value={name} onChange={event=>setName(event.target.value)}/>
<MemoChild add1={add1} add2={add2} data1={data1} data2={data2}/>
</div>
)
}
function render(){
ReactDOM.render(
<App/>,
document.getElementById('root')
);
}
render();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# 6: 注意事项
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
import React, { useEffect, useState, useReducer } from 'react'; import ReactDOM from 'react-dom'; function App() { const [number, setNumber] = useState(0); const [visible, setVisible] = useState(false); if (number % 2 == 0) { useEffect(() => { setVisible(true); }, [number]); } else { useEffect(() => { setVisible(false); }, [number]); } return ( <div> <p>{number}</p> <p>{visible && <div>visible</div>}</p> <button onClick={() => setNumber(number + 1)}>+</button> </div> ) } ReactDOM.render(<App />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# useReducer
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
# 基本用法
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = 0;
function reducer(state, action) {
switch (action.type) {
case 'add':
return {number: state.number + 1};
case 'minus':
return {number: state.number - 1};
default:
throw new Error();
}
}
function init(initialState){
return {number:initialState};
}
function Counter(){
const [state, dispatch] = useReducer(reducer, initialState,init);
return (
<>
Count: {state.number}
<button onClick={() => dispatch({type: 'add'})}>+</button>
<button onClick={() => dispatch({type: 'minus'})}>-</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# useContext
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
- 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
- useContext(MyContext) 相当于 class 组件中的
static contextType = MyContext
或者<MyContext.Consumer>
- useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
const CounterContext = React.createContext();
function reducer(state, action) {
switch (action.type) {
case 'add':
return {number: state.number + 1};
case 'minus':
return {number: state.number - 1};
default:
throw new Error();
}
}
function Counter(){
let {state,dispatch} = useContext(CounterContext);
return (
<>
<p>{state.number}</p>
<button onClick={() => dispatch({type: 'add'})}>+</button>
<button onClick={() => dispatch({type: 'minus'})}>-</button>
</>
)
}
function App(){
const [state, dispatch] = useReducer(reducer, {number:0});
return (
<CounterContext.Provider value={{state,dispatch}}>
<Counter/>
</CounterContext.Provider>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# useEffect
- 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
- 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
- useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的
componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个 API - 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
useEffect(didUpdate);
1
# 1 通过class实现修标题
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
componentDidMount() {
document.title = `你点击了${this.state.number}次`;
}
componentDidUpdate() {
document.title = `你点击了${this.state.number}次`;
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.setState({ number: this.state.number + 1 })}>
+
</button>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行
# 2 通过effect实现
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number,setNumber] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。
# 3 跳过 Effect 进行性能优化
- 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
function Counter(){
const [number,setNumber] = useState(0);
// 相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[]);
return (
<>
<p>{number}</p>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4 清除副作用
- 副作用函数还可以通过返回一个函数来指定如何清除副作用
- 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
- React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:
import React, { useEffect, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [number, setNumber] = useState(0);
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(() => {
setNumber(number => number + 1);
}, 1000);
return () => {
console.log('销毁老的定时器');
clearInterval($timer);
}
});
return (
<>
<p>{number}</p>
</>
)
}
function App() {
let [visible, setVisible] = useState(true);
return (
<div>
{visible && <Counter />}
<button onClick={() => setVisible(false)}>stop</button>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- useEffect的执行时机
- 同步才是理解effects的心智模型
import React,{useEffect,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log('setInterval',count);
setCount(count + 1);
}, 1000);
return () => {
console.log('clearInterval');
clearInterval(id);
};
}, [count]);
return <h1>{count}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
干掉对
count
的依赖function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count=>count + 1); }, 1000); return () => { clearInterval(id); }; }, []); return <h1>{count}</h1>; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5 useEffect+useReducer
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 假如我们不想在step改变后重启定时器,我们该如何从effect中移除对step的依赖呢?
import React, { useEffect, useReducer } from 'react';
import ReactDOM from 'react-dom';
const initialState = {
count: 0,
step: 1,
};
function reducer(state = initialState, action) {
const { count, step } = state;
switch (action.type) {
case 'tick':
return { count: count + step, step };
case 'step':
return { count, step: action.step };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => dispatch({ type: 'step' ,step:Number(e.target.value)})} />
</>
);
}
function render() {
ReactDOM.render(
<Counter />,
document.getElementById('root')
);
}
render();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 6 竞态
- 请求更早但返回更晚的情况会错误地覆盖状态值
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
const API = {
async fetchArticle(id){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({id,title:`title_${id}`});
},1000*(5-id));
});
}
}
function Article({ id }) {
const [article, setArticle] = useState({});
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
return (
<div>
<p>{article.title}</p>
</div>
)
}
function App(){
let [id,setId] = useState(1);
return (
<div>
<p>id:{id}</p>
<Article id={id}/>
<button onClick={()=>setId(id+1)}>改变id</button>
</div>
)
}
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 5 useRef
- useRef 返回一个可变的 ref 对象,其
.current
属性被初始化为传入的参数(initialValue) - 返回的 ref 对象在组件的整个生命周期内保持不变
const refContainer = useRef(initialValue);
1
# 1) useRef
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
<Child />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
</>
)
}
let input;
function Child() {
const inputRef = useRef();
console.log('input===inputRef', input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>获得焦点</button>
</>
)
}
ReactDOM.render(<Parent />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2) forwardRef
- 将ref从父组件中转发到子组件中的dom元素上
- 子组件接受props和ref作为参数
function Child(props,ref){
return (
<input type="text" ref={ref}/>
)
}
Child = forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
const inputRef = useRef();
function getFocus(){
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3) useImperativeHandle
useImperativeHandle
可以让你在使用 ref 时自定义暴露给父组件的实例值- 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
function Child(props,ref){
const inputRef = useRef();
useImperativeHandle(ref,()=>(
{
focus(){
inputRef.current.focus();
}
}
));
return (
<input type="text" ref={inputRef}/>
)
}
Child = forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
const inputRef = useRef();
function getFocus(){
console.log(inputRef.current);
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 6 effect回调里读取最新的值
- 有时候你可能想在effect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法是使用refs
index.js
import React,{useEffect,useRef,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
latestCount.current = count;
setTimeout(() => {
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
return (
<div>
<p>{count}</p>
<button onClick={()=>setCount(count+1)}>+</button>
</div>
)
}
function render(){
ReactDOM.render(
<Counter/>,
document.getElementById('root')
);
}
render();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# useLayoutEffect
- 其函数签名与
useEffect
相同,但它会在所有的DOM
变更之后同步调用 effect useEffect
不会阻塞浏览器渲染,而useLayoutEffect
会浏览器渲染useEffect
会在浏览器渲染结束后执行,useLayoutEffect
则是在DOM
更新完成后,浏览器绘制之前执行
# 1 事件循环
- 从宏任务队列中取出一个宏任务执行
- 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,则会继续执行新的微任务
- 进入更新渲染阶段,判断是否需要渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的,一般为60帧/秒
- 如果确定要更新会进入下面的步骤,否则本循环结束
- 如果窗口大小发生了变化,执行监听的
resize
事件
- 如果窗口大小发生了变化,执行监听的
- 如果页面发生了滚动,执行
scroll
方法
- 如果页面发生了滚动,执行
- 执行帧动画回调,也就是
requestAnimationFrame
的回调
- 执行帧动画回调,也就是
- 重新渲染用户界面
- 判断是否宏任务和微任务队列为空则判断是否执行
requestIdleCallback
的回调函数
- 判断是否宏任务和微任务队列为空则判断是否执行
# 2 使用
import React, {useRef } from 'react';
import ReactDOM from 'react-dom';
let lastDependencies;
function useEffect(callback,dependencies){
if(lastDependencies){
let changed = !dependencies.every((item,index)=>item==lastDependencies[index]);
if(changed){
setTimeout(callback)
lastDependencies=dependencies;
}
}else{
setTimeout(callback)
lastDependencies=dependencies;
}
}
let lastLayoutDependencies;
function useLayoutEffect(callback,dependencies){
if(lastLayoutDependencies){
let changed = !dependencies.every((item,index)=>item==lastLayoutDependencies[index]);
if(changed){
queueMicrotask(callback);
lastLayoutDependencies=dependencies;
}
}else{
Promise.resolve().then(callback);
lastLayoutDependencies=dependencies;
}
}
const Animate = ()=>{
const ref = useRef();
useLayoutEffect(()=>{
ref.current.style.WebkitTransform = `translate(500px)`;
ref.current.style.transition = `all 500ms`;
});
let style = {
width:'100px',
height:'100px',
backgroundColor:'red'
}
return (
<div>
<div style={style} ref={ref}></div>
</div>
)
}
function render(){
ReactDOM.render(<Animate/>,document.getElementById('root'));
}
render();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#
上次更新: 2022/04/15, 05:41:29