react-hooks

对react-hooks系统的做一次总结吧,不然总是写着查写着查

对于react-hooks不多赘述,就理解为这些hooks让函数组件拥有了类组件的大部分功能并代替类组件使用就行

然后react中有七大hooks

  1. useState 函数组件中使用State
  2. useEffect/ useLayoutEffect 函数组件中的副作用使用
  3. useContext 全局数据共享
  4. useReducer 简化版redux
  5. userMemo / useCallback 缓存, 优化性能
  6. useRef 引用实例,保存变量
  7. 自定义hook

useState

useState就是衍生自setState(),它让函数组件从无状态的简单组件变成了复杂组件,使用方式和setState()差不多,每个组件通过维护自己的state去处理一些业务逻辑,其实使用上是比较简单的,但深究起来…龟龟,就设计到react的底层逻辑了,就是每次setState后会发生什么。

所以,为了了解useState的用法,我们还是先探究一下setState()的工作机制。

对于setState的使用其实很简单,react官方文档的例子看看就能懂

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
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

具体我就不解释了,就是每次setState()会重新render然后又依次调用生命周期函数,然后DidMount后又tick了一下,this.state()指向一个全新的引用,又重新render,就出现了一个每秒钟递增的计时器效果。其他对于state的基本使用官方文档讲的已经很明白了,这里不赘述,我们在这里就说一下setState()会经常被提起的两个问题

  1. setState()是同步更新还是异步更新
  2. setState()是合并更新还是替换更新

先说第二点吧,直接结论,setState是合并更新,直接看代码

1
2
3
4
5
6
7
8
9
this.state = {
a: 1,
b: 2,
c: 3
}

setState({d: 4})

// 实际上是 setState({...this.state, d: 4})

为什么是合并更新很好理解嘛,因为控制某个组件的状态都得放在this.state这一个对象里面,然后我们想想看,假设这个页面需要的状态很多,也就是要有一堆key value,但我们每次更改state应该只会更改其中的一项,那我们每次写都要

setState({...this.state, a: xx})

万一哪次我们忘了,会咋样,就看上面那块,整个state就变成了{d: 4},那用到a,b的地方渲染的就是undefined,Oh,然后GG,所以为了避免这种情况发生react就在底层做了这么一层浅合并的处理,当然,这也不完全是好的。我们在看下面这一串代码

1
2
3
4
5
6
this.state = {a:1, b:'muxiMfe'}

onClick() {
setState({...this.state, a: 2})
setState({...this.state, b:"muxiMbe"}
}

想想看,最后我们console.log(this.state),输出是什么?是{a: 2, b:”muxiMbe”}吗? 这个问题放在后面讲了异步同步的问题再来解答,大家伙好好想想。

而useState()是替换更新,这个待会儿再讲。所以,在明确setState()是合并更新后,我们再来看第一个问题

setState()是同步更新还是异步更新?

我们看看react官网怎么说

1
2
State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

啥意思呢,假设有如下代码 (为了方便就简写一些伪代码)

1
2
3
4
5
6
7
this.state = {a: 1}

onClick() {
setState({a: a + 1})
}

console.log(a) //a 输出?

大家应该都知道a会输出1,这就说明了setState是异步执行的,所以a拿到的还是之前的值,那为什么setState()会被异步执行?

1
2
3
4
if (xxx) {
setState() // 1
}
setState() // 2

如果是同步更新,会出现什么样的情况? 就是当代码执行到1的时候, 页面被重新render一次,然后到2,又重新render一次,所以我们每一次调用setState()都会重新渲染页面,这对性能的消耗是巨大的。

接下来说的要涉及到Js的事件循环,这部分也比较重要,说起来比较麻烦,这里就不讲了。可以自行了解。

为了解决这个问题,react就在底层实现了一个缓存队列,每次的setState()都不会立刻执行,都会放到这个缓存队列里面去(类似于原生Js的任务队列),然后去执行其他语句(执行栈),等到执行栈空后,再来合并更新缓存队列里面的state,这样哪怕一个事件处理函数里有多个setState(),也只会重新调用一次render。

那,官网上说 可能是异步的,所以什么时候是同步的呢?

我们先想想我们什么时候会用到setState()

  1. 生命周期里面发送请求后重绘页面
  2. 与用户发生交互
  3. 定时器触发

这里注意到,生命周期和与用户交互的合成事件,都是react自己封装的API,所以他们都会对这里面调用的setState()进行包装处理放入缓存队列中,也就是会异步执行,而定时器(又或者addEventsListener),是浏览器自带的API,react并没有对其封装重写,所以定时器里的setState()就会跳出react的控制,不会被放到缓存队列中,也就是说:

1
2
3
this.state = {a: 1}
setTimeout(() => setState({a: a + 1}))
console.log(a) // 这里的a 就会是2

而setState()是如何执行异步/同步的呢?

查看react源码发现,代码中有一个变量锁isBatchingUpdates,isBatchingUpdates表示是否进行批量更新,初始化时默认为false,batchedUpdates方法会将isBatchingUpdates设为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,

batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
},
};

module.exports = ReactDefaultBatchingStrategy;


为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件,这个队列就是dirtyComponents。当isBatchingUpdates为true时,将会执行 dirtyComponents.push(component); 将组件push到dirtyComponents队列。
调用setState()时,其实已经调用了ReactUpdates.batchedUpdates,此时isBatchingUpdates便是true。

1
2
3
4
5
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);

至此,setState实现了合并和批量处理。

好,这时候我们再来看看刚刚那个遗留问题,console.log(a)到底会输出啥?答案是{a:1, b:”muxiMbe”}

为啥? 想想看,

1
2
3
4
5
6
7
this.state = {a:1, b:'muxiMfe'}

onClick() {
setState({...this.state, a: 2}) //执行这一句的时候,this.state是对{a:1, b:'muxiMfe'}的引用,推入缓存队列的新状态应该是{a:2, b:'muxiMfe'}
setState({...this.state, b:"muxiMbe"} // 这一句,this.state仍然是对{a:1, b:'muxiMfe'}的引用,推入缓存队列的状态是{a:1, b:"muxiMbe"}
}
// 执行栈执行完毕,合并更新处理缓存队列的state,后面的会覆盖前面的,故输出{a:1, b:"muxiMbe"}

那再想想这个输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
this.state = {a:1, b:'muxiMfe'}

onClick1() {
setState({a: 2})
setState({...this.state, b:'muxiMbe'})
}

onClick2() {
setState({a: 2})
setState({b: 'muxiMbe'})
}

// 1输出{a:1, b:'muxiMbe'} 2输出{a:2, b:'muxiMbe'}

可能现在还对setState()有点迷糊,但没事,哎,我们的重点是啥,useState()!

首先我们明确,useState()是让函数组件中有了state,返回一个xx(某个state的名字)和setXX() (设置这个state的方法),然后这个setXX,大部分特性setState()的特性是一样的,但又有些不一样,这里官方文档也提到了

1
2
3
4
5
注意

你可能想知道:为什么叫 useState 而不叫 createState?

“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是以 use 开头的一个原因。我们将在后面的 Hook 规则中了解原因。

想想看为啥,类组件如何渲染虚拟DOM?render方法,他的state则是类的一个属性,所以无论render几次,this.state也只是进行浅拷贝合并更改而已,也就是说整个类不会被销毁重新创建,只是会依次调用相应的生命周期方法,而函数组件呢? 他只有一个return,所以每次状态改变,都会重新调用一次这个函数,函数作用域销毁重建,内部变量也是这样一个步骤。所以为了保存上一次的变量,要么用闭包(很多一些简易版的useState()的源码就是通过闭包实现),要么用ref,所以这里也可以解答为什么useState()是替换更新而不是合并更新了。

先看看一般大佬们自己封装的简易版useState()

1
2
3
4
5
6
7
8
9
10
11
12
13
let memoizedState: any[] = [] // hooks 的值存放在这个数组里
let cursor = 0 // 当前 memoizedState 的索引

function useState(initialValue: any) {
memoizedState[cursor] = memoizedState[cursor] || initialValue
const currentCursor = cursor
function setState(newState: any) {
memoizedState[currentCursor] = newState
cursor = 0
render(<App />, document.getElementById('root'))
}
return [memoizedState[cursor++], setState] // 返回当前 state,并把 cursor 加 1
}

可以很明显的看出来返回的setState就运用到了闭包来访问memmoizedState,源码更为复杂,比如这里就没有涉及到刚刚提及的异步问题,这里的useState()都是同步执行。

所以,useState()也和setState()一样在合成事件中是异步的吗?答案是肯定的。和setState一样,useState在合成事件以及useEffect这些react封装的API呈现异步更新,而在定时器等webAPI中的话就是同步执行。

而如何异步执行大体流程也和setState一样,先放入缓存队列后在批量更新,重新执行函数组件的每一行代码。那就带来另一个问题,重新执行每一行代码,那为什么useState中的state不会被传进去的initState覆盖? 这里看上面的代码memoizedState[cursor] = memoizedState[cursor] || initialValue就能解释,react底层同样做了类似的处理(应该是更流弊的处理不过我还没看懂)来让initialValue只传入一次。

所以,其实弄懂了setState(),useState()也没什么特别难懂的地方。但还是要注意一些问题

  1. useState如何同步获取代码

既然都说了useState在大多数情况下其实是异步更新的?那现在有个需求,我就是想更新state后立刻拿到最新的state,我该如何操作?先看setState()

其实setState有两个参数,第一个就是新的state的值,第二个是一个callback函数,这个callback函数会在缓存队列被清空后,然后其他的生命周期都走完了,你的this.state的指向已经指向了一个新对象,然后再执行,这样就可以拿到最新的state了。 那函数组件呢? 哎,用useEffect, useEffect也会在每次函数组件更新时都执行一遍,在这里拿到state最新的代码就行(注意依赖)

  1. 用useState返回的setState更新状态后会发生什么

还是一样,先想想看setState后发生什么? 首先this.state指向一个新对象,然后重新调render(),其他的生命周期再走一遍

而useState没那么多复杂,比如你的函数组件叫App, 那在useState这个hooks里,更新你的state后,直接App(),对,就是直接再调用一下这个函数而已。

所以我们看一下代码,再来检测一下对setState/useState工作机制的掌握程度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Component() {
const [a, setA] = useState(1)
console.log('a', a)

function handleClickWithPromise() {
Promise.resolve().then(() => {
setA((a) => a + 1)
setA((a) => a + 1)
})
}

function handleClickWithoutPromise() {
setA((a) => a + 1)
setA((a) => a + 1)
}

return (
<Fragment>
<button onClick={handleClickWithPromise}>{a} 异步执行</button>
<button onClick={handleClickWithoutPromise}>{a} 同步执行</button>
</Fragment>
)
}

所以点击两个按钮,分别会发生什么?

第一个两行输出,a 2 和 a 3 第二个只会输出一行 a 3 setState同理,具体原因不解释了,再仔细看看之前说的那些。

至此,对于state的机制应该算是说的差不多了,至于源码究竟是怎么实现的,目前还看不懂呜呜呜。

但你们以为这就结束了?不不不,我们再来想一个问题。

首先我们明确,react代码最后会被编译解释成原生的Js,那问题来了,对于setState(),他到底是宏任务还是微任务?

简单解释一下什么是宏任务和微任务,具体了解还得和前面提到的事件循环机制一起去好好看看。

就举个例子,我们把Js的任务队列当成一个银行柜台,然后里面的每个任务当成顾客,顾客一个个去银行柜台办理业务,任务就一个个执行。那银行柜台是怎么给顾客办理业务的?顾客按照先来后到的顺序编号,然后一个个叫号,叫到谁谁去,所以在这里,每一个顾客,就是一个宏任务,然后办理业务的时候,总会遇见一些大爷大妈,你给他办完了银行业务,准备叫下一个了,哎,他不走,他非要把你拽住,说“小姑娘我看你怪俊俏的嘞,我隔壁村头有个小伙子蛮不错的你们要不要认识一下?”然后柜台又得皱着眉头把这些杂七杂八的事给处理完,哎,这些事,就是微任务。

所以我们可以得出

  • 宏任务永远在微任务前执行

  • 微任务没有执行完成前不得进行下一个宏任务

宏任务有

1
2
3
4
5
6
script(整体代码)
setTimeout()
setInterval()
postMessage
I/O
UI交互事件

微任务有

1
2
3
4
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

哎,那我们想想看,setState作为一个“异步操作”,那他到底是会被放入宏任务队列,还是微任务队列呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
handleClick = () => 
setTimeout(() => {
console.log('宏任务触发')
})
Promise.resolve().then(() => {
console.log('微任务触发')
})
this.setState({
...this.state
}, () => {
console.log('setState')
})
}

输出啥?

1
2
3
setState
微任务触发
宏任务触发

在一个事件循环中,入执行栈的事件比微任务晚,但执行时间却早于微任务,怎么,setState是同步任务?可不对啊,前面不是很明白的说了setState在合成事件中和生命周期里面是异步任务吗?哎,都说了,是在react合成事件中和生命周期里面被react底层处理后,才显示异步的状态,那我们把他拿出来看看呢

1
2
3
4
5
6
7
8
9
10
11
12
13
handleClick = () => {
setTimeout(() => {
console.log('开始运行')
this.setState({
...this.State
}, () => {
console.log('setState')
})
console.log('结束运行')
})
}


结果毫无意外的是

1
2
3
开始运行
setState
结束运行

所以!!!setState在本质上就是在一个事件循环中,在运行上是基于同步代码的实现,他并不会被推到任务队列里面去!他只是有异步的行为而已,说得通俗一点,setState是一个伪异步,或者可以称为defer,即延迟执行但本身还在一个事件循环,所以它的执行顺序在同步代码后、异步代码前。

至此,对于state的机制大概就到这里了,因为useState在hooks里面的特性还是与setState高度类似的,所以可能主要还是再讲setState。

接下来就看看另一个贼重要的hooks——useEffect()

useEffect

这个hooks,说难也难,说简单也简单,但我就想明确一点

千万 不要 把 useEffect() 和生命周期 划等号!!!

懂我意思?别把useEffect()看作函数组件的生命周期,函数组件没有生命周期!! 不然这个hook应该叫use生命周期()而不是useEffect()。好,接下来不扯淡了,我们开始聊一下useEffect(),先简单的看一下他的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
发ajax请求数据获取
设置订阅 / 启动定时器
手动更改真实DOM
(3). 语法和说明:
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => { // 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
第二个参数限制了调用这个hooks的范围,则就会只有在statevalue发生改变时调用这个hook

(4). 可以把 useEffect Hook 看做如下三个函数的组合
componentDidMount()
componentDidUpdate()
componentWillUnmount()

上面这些引用自我之前写的todolist,里面记了一些对hooks的基本使用。

这时候有人就说了,哎哎哎你自己都写了能把useEffect()看做成三个生命周期方法的组合,现在又说不能把他看作生命周期,你不是自己打自己脸嘛。

哎呀,凡事我们先看看官方文档嘛

1
2
3
4
5
6
7
8
effect 的执行时机
与 componentDidMount、componentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。


看到没有,官方文档明确的说明,useEffect的执行时机都和生命周期不同了,那怎么可以把useEffect看作生命周期呢? 我们换个角度理解: effect 是什么意思,作用,影响,那Js函数中提到作用和影响第能想到什么?函数的副作用。 而函数组件从某种意义上来说是什么? 纯函数——接受同样的参数(props)就会返回同样的值(UI),所以useEffect就可以理解为,我们让函数组件这个纯函数有了副作用,让他变成了非纯函数,也就是—— **函数通过参数和返回值以外的渠道,和外界进行数据交换。比如读取/修改全局变量,都叫作以隐式的方式和外界进行数据交换。 **仔细想想,我们在useEffect()中订阅数据然后渲染UI,不就是一个非纯函数的实现?

所以,useState是让函数组件有了状态,而useEffect则是让函数组件有了副作用,而并不是有了生命周期。

那刚刚说到执行时机,我们在想想,执行时机不同?怎么个不同法?

这里,我们先简单的了解一下浏览器的渲染机制 参考链接

MDN

在了解浏览器渲染过程之前,先来了解一下页面的加载流程。有助于更好理解后续渲染过程。从浏览器地址中从输入url地址到渲染出一个页面,会经过以下过程。 1.浏览器输入的url地址经过DNS解析获得对应的IP 2.向服务器发起TCP的3次握手 3.建立链接后,浏览器向该IP地址发送http请求 4.服务器接收到请求,返回一堆 HMTL 格式的字符串代码 5.浏览器获得html代码,解析成DOM树 6.获取CSS并构建CSSOM 7.将DOM与CSSOM结合,创建渲染树 8.找到所有内容都处于网页的哪个位置,布局渲染树 9.最终绘制出页面

浏览器渲染机制

我们将要介绍的浏览器渲染过程主要步骤是5-9步,可以用下面的图来形象的展示

看图,就简单的说一下,详细的渲染过程大家还是自己去看。

  1. 第一步就是先根据html生成DOM树,在构建过程中,如果有图片 css等资源,会发起请求,但不会阻塞页面继续渲染

(react的虚拟DOM优化就在这,如果没有react,就是从一大串的html文件里面慢慢生成真实DOM,而有了react,则会在下面那一步的加载执行Js代码里面根据VDOM树渲染出真实的DOM树——执行Js脚本的速度比解析html中的真实DOM快的多)

  1. 根据css生成CSSOM树

(然后会加载执行Js代码)

  1. DOM树和CSSOM树结合生成Render树

​ 这两种情况会重新构建render树

​ 1)增删元素(包括插入后续请求到的图片资源)

​ 2)改变盒模型

  1. 对Render树布局,就是你网页里每个区域的宽高啊,一个图片该放在哪儿啊,做这些计算布局(Layout)渲染
  2. 把最后形成的页面绘制到屏幕上,至此渲染过程完成
  • 如果页面的DOM发生变化就会引起浏览器的重绘和回流,重绘就是重新执行第五步,回流则要重新执行第四步,DOM的外观发生变化,比如颜色啥的会引起重绘,而如果DOM的结构发生变化则会引起浏览器的回流,比如尺寸布局节点none等等等等,这也是直接操作DOM性能低的原因,因为react的VDOM会在每次更改后合并然后只更改diff,所以每次页面变更都只会进行一次重绘/回流

useEffect()在哪儿执行? 在第五步之后,浏览器绘制屏幕后才会执行。而生命周期函数呢? 在浏览器绘制之前,也就是第四步和第五步之间,所以真的想要生命周期的功能,还得用react提供的另一个API——useLayoutEffect(),注意这个Layout,不就刚好对应第四步的布局render树那一步?

所以,事实上,useLayout()才是react提供用来代替生命周期的hook,看官方文档怎么说

1
2
3
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新

为什么useLayoutEffect()会阻塞视觉更新呢? 很明显啊,你这个hook在浏览器绘制前执行,然后你在这个hook里面写了一些计算量比较大的代码,那浏览器不得等你个贼娃子执行完了再开始进入渲染,所以这也算react团队做出来的一个性能优化。

可以拿以下代码去试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
const [count, setCount] = useState(0);

useEffect(() => {
if (count === 0) {
const randomNum = 10 + Math.random()*200
setCount(randomNum);
}
}, [count]);

return (
<div onClick={() => setCount(0)}>{count}</div>
);
}

当我们快速点击的时候,会出现闪烁效果,为啥,你先setCount把界面渲染成0了,然后又执行useEffect()中的setCount,页面其实被快速的重绘了两次——从0跳到randomNum,而生命周期函数和useLayoutEffect()就不会闪烁,他们会同步更新执行setState,直接渲染出randomNum。

讲明白这个后,对于useEffect就好理解了,接下来再明白这几个概念就行

  • useEffect会捕获当前渲染的props 和 state
  • useEffect的清除副作用的回调函数是在下一次浏览器绘制后,下一个useEffect执行前 执行的

先看 useEffect会捕获当前渲染的props 和 state 这句话怎么理解? 我们之前在useState()那里讲过,每一次我们改变函数组件的状态,都会重新调用该函数组件,所以,我们如果有如下代码

1
2
3
4
5
6
7
8
9
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(count)
})
return (
<div onClick = {() => setCount((c) => c + 1)}>{count}</div>
)
}

每次调用setCount的时候 可以理解为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 首次渲染
const App = () => {
const count = 0
useEffect(() => {
console.log(count)
})
}

// 第一次点击
const App = () => {
const count = 1
useEffect(() => {
console.log(count)
})
}

// 第二次点击
const App = () => {
const count = 2
useEffect(() => {
console.log(count)
})
}
// .....

所以,看下面这串代码,点击show alert,然后点击把count增加到3,最后alert的值是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>
);
}

是0,对吧,因为首次渲染把定时器推入到任务队列中,他所捕获到count值就是0,不会随着后续count的值改变

所以这串代码呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

1 - 5 依次打印,是吧,牢记住“捕获”这个点就行,哎,那一直说了和生命周期不同,我们把这串代码放到类组件里面用生命周期表示呢?

1
2
3
4
5
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}

这就会打印五个五了,为啥,哎这生命周期方法不也是个函数嘛,他为什么不捕获呢? 他确实捕获了,但他捕获的不是this.state.count,他捕获的很简单,就是这个this的指向,我们可以把这个this理解为一个指针嘛,指向这个class组件实例,好,我不断点击,我就不断捕获这个指针变量,最后我打印,我就拿着这个指针去找啊,this..嗯..state..嗯,哎count!找到了,5!那我直接输出5。 所以这就是五个5出现的原因,那我再修改一下

1
2
3
4
5
6
componentDidUpdate() {
const {count} = this.state
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
}

这就输出啥? 1,2,3,4,5依次输出,很好理解,每次都会捕获this,this不变,this指向的count在改变,所以直接打印count的话就是1,2,3,4,5依次输出了。

那我要是再想用useEffect()打印5个5呢?函数组件又没有this,哎,那我们可以用ref

1
2
3
4
5
6
7
8
9
10
11
12
13
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);

useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...

为啥? 因为ref在整个渲染过程中,是一个不变的对象,所以对于他指针地址的捕获是不变的,只是改变他身上原本的属性而已,和class不是一致?class在渲染过程中this始终不变,仅仅是他的state.count改变了而已。

useEffect的清除副作用的回调函数是在下一次浏览器绘制后,下一个useEffect执行前 执行的 这个怎么理解?之前不说说过,useEffect是在浏览器绘制后执行嘛,那我这里面有个定时器控制轮播图,我渲染成功后轮播图开始播,然后我直接return一个cleanInterval(),得嘞,轮播图瞬间变海报。所以这个清除副作用的函数只能在下一个useEffect执行前执行。(这里简单的提一下,react对hooks的实现是把所有的hooks放在了一个链表里面——hooks链表然后依次执行hook的,这个有时间在自己去深挖吧。)

最后不得不提的一点就是useEffect的依赖了,注意点比较多,然后有一些细究和优化的写法,比如用useCallback/useMemo解决依赖问题,把函数提到最外层解决依赖,使用useReducer解决依赖等等。参考这篇文章就行