Effect是一个响应式的,当依赖prop和state改变的时候,执行Effect。一个Effect做两件事情:
- 依赖改变则同步执行一些任务
- 停止同步
linter检查传入的依赖是否和Effect内部使用到的依赖一致,如果不一致就会报错。这样保证了依赖和同步任务是同步的。
一个Effect的生命周期
React组件的生命周期:
- mount,组件挂载到屏幕上后调用的函数
- update,当props或者stae改变后触发重新渲染,重新渲染后调用的函数
- unmount,组件从屏幕上移除后调用的函数
函数式编程的useEfect是上面3个生命周期的集合,当mount、update和unmount时触发执行Effect。
我们看看Effect连接服务器的例子:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Effect的body定义了如何同步:
const connection = createConnection(serverUrl, roomId);
connection.connect();
Effect返回的清除函数定义了如何停止同步:
return () => {
connection.disconnect();
};
直觉上你可能觉得React在组件挂载时开始同步在挂载时停止同步,事实上不只是这样。在组件挂载后卸载前期间,组件更新的时候可能或多次执行和停止同步。
为什么同步不只需要一次
假设有一个聊天室组件ChatRoom, 接受prop参数roomId。给roomId赋初始值为"general"。界面显示的是"general"聊天室:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
聊天室信息展示在界面后,React将执行Effect开始执行同步任务,建立连接连接"general"房间:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...
然后,用户从下拉框中选择另外一个聊天室(例如"travel")。首先React将会更新UI:
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
界面从"general"聊天室更新到"travel"聊天室。但是上次执行的Effect一直连接的是"general"聊天室。prop roomId改变后,界面显示的和实际连接的不一致。所以你希望React做两件事:
- 停止旧的"genarel"聊天室连接
- 开始新的"travel"连接
Effect的body定义如何同步,清除函数定义如何停止同步。React接下来要做的是:
- 如何以正确的顺序调用它们
- 如何根据正确的props和state触发去调用
如何重新同步Effect
聊天室ChatRoom组件的prop roomId改变为"travel"后,React需要重新同步Effec去重新连接不同的聊天室。
React调用Effect上次返回的清除函数停止同步,清除函数断开"general"连接:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...
然后,React同步新的连接,建立"travel"聊天室连接:
function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...
现在建立的连接和界面显示的聊天室是一样的。
每一次改变roomId重新渲染以后,Effect将会重新同步。例如用户又把roomId的"travel"改为"music"。React将会再次调用清理函数断开"travel"聊天室的连接,然后执行Effect的body建立新的"music"连接。
最后,当用户转向其他的界面时,卸载ChatRoom组件。现在不需要建立任何连接。React调用上次Effect返回的清除函数断开"music"聊天室连接。
我们从聊天室组件ChatRoom角度看:
- roomId初始值为"general",挂载组件
- roomId设置为"travel",更新组件
- roomId设置为"music",更新组件
- 组件卸载
在上面的组件的生命周期的每个阶段,Effect做了不同的事情:
- Effect连接"general"聊天室
- Effect断开"general"连接,连接"travel"聊天室
- Effect断开"travel"连接,连接"music"聊天室
- effect断开"music"聊天室
如何触发Effect重新同步
React根据依赖列表的值是否改变,来重新同步。当依赖列表中的元素改变时,Effect就会重新同步。roomId在依赖列表中:
function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...
下面看看roomId如何使用的:
- roomId是一个prop,可能以后值会改变。
- Effect使用到了roomId变量,逻辑依赖roomId的值
- 所以把roomId写到Effect依赖列表中,当roomId改变时同步
每一次在组件重新渲染后,React将会查看依赖列表。如果依赖列表中的任何一个元素值和上次渲染的值不一样,React将同步Effect。
例如,在初始化渲染时传递的是["general"],后来在下次渲染的值为["travel"],React将会对比"general"和"travel",这两个值是不同的,所以将会重新同步Effect。如果你的组件重新渲染了,但是roomId没有改变,不会同步effect。
每个Effect代表一个单独的同步过程
不要在Effect添加不相关的逻辑,因为这个逻辑也会在Effect一起执行。比如,你想分析用户访问房间的情况,你已经有一个依赖roomId的Effect,所以你可能想把写日志逻辑写到里面:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
假如以后你向这个Effect添加了另外一个依赖来重建连接,如这个Effect重新同步,将会调用logVisisit(roomId)
,即使roomId没有改变。记录访问日志是一个单独的过程。下面写两个单独的Effects:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}
每一个Effect代表一个单独和独立的同步过程。删除一个Effect不会影响另一个Effect的逻辑。这样代码看起来很干净。但是不能过于去拆分Effect。如果是一个高内聚的逻辑,拆分开让代码难维护。
Effect对React的响应类型数据做出响应
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Effect使用了两个变量serverUrl和roomId,但是只把roomId定义为一个依赖,serverUrl不需要是一个依赖吗?
因为serverUrl不会因为重新渲染而改变值。无论组件重新渲染多少次serverUrl总是相同的值,把它设置为依赖是没有意义的。
Props,state和与它们相关的值都是reactive类型的数据,因为在渲染期间它们被计算并且参与到React数据流中。
如果serverUrl设置为一个state变量,那么它是reactive数据,reactive数据必须包含在依赖列表中:
function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}
把serverUrl放在依赖列表中,确保serverUl改变时Effect同步。
依赖列表为空
如果依赖列表为空,是怎么执行的?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // All dependencies declared
// ...
}
现在Effect没有使用任何reactive值,因此依赖列表是可以为空的。 如果是空的依赖列表,只组件挂载和组件卸载时执行Effect,组件更新时不执行Effect.
Props和State以及从它们计算的得到值都是响应式的
除了Props和state是响应式的,从它们计算得到的值也是响应式的。所以从它们计算得到的变量也应该添加到依赖列表中。
有一个这样的场景,有一个默认的serverUrl存储在context中,用户通过下拉列表选择连接的serverUrl,如果用户没有通过下拉框选择serverUrl就使用默认的serverUrl:
function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}
在上面的例子中,serverUrl不是一个prop或state, 它是在渲染期间计算的常规变量。它在渲染期间被计算,所以在重新渲染期间它可以改变。所以它是响应式的。
props,state和从它们计算的变量都是响应式的,任何响应值在重新渲染期间可以改变,你需要把响应值放入依赖列表中。
React Linter检查依赖项是否在依赖列表中
如果你配置了React linter,linter将检查在Effect中使用的响应值是否在依赖列表中,如果不在将会报错。例如,roomId和serverUrl是响应值,并在Effect中使用,lint会报错:
function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // <-- Something's wrong here!
//...
}
React指出代码的错误。roomId和serverUrl可能会改变,但是你忘记当它们改变的时候同步Effect。即使用户在界面选择了不同的值后,仍然连接的是初始的roomId和serverUrl。
修复bug,把roomId和serverUrl放入Effect的依赖列表中:
function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // <-- Something's wrong here!
//...
}
如何不同步
在前面的例子中,你修复lint检查错误,把roomId和serverUrl放入依赖类表中。
但是,你可以告诉linter这些值不是响应值。也就是说,它们不因为重新渲染而改变。例如,如果serverUrl和roomId不依赖渲染并且总是相同的值,你可以把它移到组件的外面,现在它们不需要是依赖:
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}
你也可以把它们移到Effect里面。它们在渲染期间不被计算,所以它们不是响应式的:
function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}
Effects是响应式代码块。当里面的值改变时则会重新同步逻辑。不像事件处理函数,在交互时仅仅执行一次,当无论什么时候需要同步的时候Effects都会执行。
Effect内部使用到的响应值都要放到依赖列表中,linter检查强制检查依赖列表。
参考文献: