在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染。使用 React Hooks 相比于从前的类组件有以下几点好处:
Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state
以及其他的 React 特性
。
Hooks只能在函数式组件中使用,既无状态组件(所有钩子在用时都要先引入)
Hook 就是JavaScript 函数
,但是使用它们会有两个额外的规则:
1、只能在函数最外层调用 Hook
。不要在循环、条件判断
或者嵌套函数(子函数)
中调用。
2、只能在 React 的函数组件
中调用 Hook
。不要在其他 JavaScript 函数中调用。
3、在多个useState()
调用中,渲染之间的调用顺序
必须相同
。
该 Hook 接收一个包含命令式、且可能有副作用
代码的函数。
在函数组件主体内(这里指在 React 渲染阶段
)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许
的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
。所以我们要使用 useEffect 完成副作用操作
。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情
况下,effect 将在每轮渲染结束后执行
;但你可以选择让它在只有某些值改变的时候
才执行。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
问:useEffect 做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
问:为什么在组件内部调用 useEffect?
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)
。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
问:useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行
。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
useEffect可以传两个参数。
参数一:
参数二:
数组
,如果数组中的值(依赖)
变化才会触发。useEffect的第二个参数为一个```空数组,初始化调用一次之后不再执行,相当于componentDidMount。组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。
useEffect(
() => {
const subscription = props.source.subscribe();
//return里面就是清除函数
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
useEffect的第二个参数为一个空数组
,初始化调用一次之后不再执行
,相当于componentDidMount
。
function Demo () {
useEffect(() => {
console.log('hello world')
}, [])
return (
<div>
hello world
</div>
)
}
// 等价于
class Demo extends Component {
componentDidMount() {
console.log('hello world')
}
render() {
return (
<div>
hello world
</div>
);
}
}
import React, { useState ,useEffect} from 'react';
const App3 = () => {
const [data,setData]=useState(0)
useEffect(() => {
console.log('useEffect')
}, [])
const handleClick=()=>{
console.log('点击我')
setData(data=>data+1)
}
return (
console.log('render'),
<div>
{data}
<button onClick={handleClick}>点击我</button>
</div>
)
};
export default App3;
当useEffect没有第二个参数
时,组件的初始化和更新
都会执行
import React, { Component } from 'react';
class App3 extends Component {
state = {
data : 0
}
componentDidMount(){
console.log('useEffect')
}
componentDidUpdate(){
console.log('useEffect')
}
handleClick = () => {
console.log('点击我')
this.setState({
data:this.state.data+1
})
}
render() {
return (
console.log('render'),
<div>
{this.state.data}
<button onClick={this.handleClick}>点击我</button>
</div>
);
}
}
export default App3;
//等价于
import React, { useState ,useEffect} from 'react';
const App3 = () => {
const [data,setData]=useState(0)
useEffect(() => {
console.log('useEffect')
})
const handleClick=()=>{
console.log('点击我')
setData(data=>data+1)
}
return (
console.log('render'),
<div>
{data}
<button onClick={handleClick}>点击我</button>
</div>
)
};
export default App3;
import React, { useState ,useEffect} from 'react';
const App3 = () => {
const [data,setData]=useState(0)
useEffect(() => {
console.log('useEffect')
})
const handleClick=()=>{
console.log('点击我')
setData(data=>data+1)
}
return (
console.log('render'),
<div>
{data}
<button onClick={handleClick}>点击我</button>
</div>
)
};
export default App3;
在useEffect返回一个函数,这个函数会在组件卸载时执行。
class Example extends Component {
constructor (props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
this.id = setInterval(() => {
this.setState({count: this.state.count + 1})
}, 1000);
}
componentWillUnmount() {
clearInterval(this.id)
}
render() {
return <h1>{this.state.count}</h1>;
}
}
// 等价于
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>hello world</h1>
}
与 componentDidMount、componentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后
,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新
。
然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook
来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同
。
虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。
全部渲染完毕
后才会执行浏览器 layout 之后,painting 之前
执行读取 DOM 布局并同步
触发重渲染执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
使用标准的 useEffect 以避免阻塞
视图更新实例一:useLayoutEffect里面进行alert拦截
import React,{useState,useEffect,useLayoutEffect} from 'react';
const App3 = () => {
const [color, setColor] = useState('red');
useLayoutEffect(() => {
alert(color);
console.log('useLayoutEffect_color',color)
});
useEffect(() => {
// alert(color);
console.log('useEffect_color', color);
});
return (
<>
<div id="myDiv" style={{ background: color }}>颜色</div>
<button onClick={() => setColor('red')}>红</button>
<button onClick={() => setColor('yellow')}>黄</button>
<button onClick={() => setColor('blue')}>蓝</button>
</>
);
};
export default App3;
效果图:
可以看出,在页面绘制之前
,useLayoutEffect里面的alert进行了拦截,此时页面DOM元素还没有在页面上绘制
点击“确定”之后,发现DOM元素在页面上呈现,在控制台中,先打印“useLayoutEffect_color”,随后打印“useEffect_color”
由此说明useLayoutEffect HOOK
比 useEffect HOOK
触发的时机早
实例二:useEffect里面进行alert拦截
import React,{useState,useEffect,useLayoutEffect} from 'react';
const App3 = () => {
const [color, setColor] = useState('red');
useLayoutEffect(() => {
// alert(color);
console.log('useLayoutEffect_color',color)
});
useEffect(() => {
alert(color);
console.log('useEffect_color', color);
});
return (
<>
<div id="myDiv" style={{ background: color }}>颜色</div>
<button onClick={() => setColor('red')}>红</button>
<button onClick={() => setColor('yellow')}>黄</button>
<button onClick={() => setColor('blue')}>蓝</button>
</>
);
};
export default App3;
效果图:
我们发现,在alert拦截时,页面上面已经进行了绘制
,并且在控制台中打印“useLayoutEffect_color”,但是没有打印“useEffect_color”
点击“确定”之后,控制台中才打印“useEffect_color”
由此说明useLayoutEffect HOOK
比 useEffect HOOK
触发的时机早
不会阻塞浏览器渲染进程
,所以使用 Function Component 写的项目一般都有用更好的性能。自然符合 React Fiber 的理念
,因为 Fiber 会根据情况暂停或插队执行不同组件的 Render,如果代码遵循了 Capture Value 的特性,在 Fiber 环境下会保证值的安全访问,同时弱化生命周期也能解决中断执行时带来的问题。保证拿到状态生效后的 DOM 属性
。一般来说,在依赖列表中省略函数是否安全?答案是:不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数
。
这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}
如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 [ ]:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值
只有当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
下面这个案例有一个 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 这样是无效的,因为 `fetchProduct` 使用了 `productId`
// ...
}
推荐的修复方案是把那个函数移动到你的 effect 内部。
这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
// ...
}
这同时也允许你通过 effect 内部的局部变量来处理无序的响应:
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]);
如果出于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:
你可以尝试把那个函数移动到你的组件之外
。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。转而在 effect 之外调用它, 并让 effect 依赖于它的返回值
。把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook
。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变:function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ useCallback 的所有依赖都被指定了
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
// ...
}
注意在上面的案例中,我们 需要让函数出现在依赖列表中
。这确保了 ProductPage 的 productId prop 的变化会自动触发
ProductDetails 的重新获取。
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
只在组件挂载时运行一次,并非重新渲染时
。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当effect 执行时,我们会创建一个闭包
,并将 count 的值被保存在该闭包当中
,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count 永远不会超过 1
。效果图:
如图所示,每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count 永远不会超过 1
。
指定 [count] 作为依赖列表就能修复这个 Bug
,但会导致每次改变发生时定时器都被重置。
事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
return <h1>{count}</h1>;
}
(setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)
此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值
,(在回调中变量命名为 c)。