1. 弁言
东西型文章要跳读,而文学典范就要重复研读。假如说 React 0.14
版本带来的种种生命周期能够类比到东西型文章,那末 16.7
带来的 Hooks 就要像文学典范一样重复研读。
Hooks API 不论从简约水平,照样运用深度角度来看,都大大优于之前生命周期的 API,所以必需重复明白,重复实践,不然只能停留在外表原地踏步。
比拟 useState
或许自定义 Hooks 而言,最有明白难度的是 useEffect
这个东西,愿望藉著 a-complete-guide-to-useeffect 一文,深切明白 useEffect
。
原文异常长,所以概述是笔者精简后的。作者是
Dan Abramov,React 中心开辟者。
2. 概述
unLearning,也就是学会遗忘。你之前的进修履历会障碍你进一步进修。
想要明白好 useEffect
就必需先深切明白 Function Component 的衬着机制,Function Component 与 Class Component 功能上的差别在上一期精读 精读《Function VS Class 组件》 已引见,而他们还存在头脑上的差别:
Function Component 是更完整的状况驱动笼统,以至没有 Class Component 生命周期的观点,只需一个状况,而 React 担任同步到 DOM。 这是明白 Function Component 以及 useEffect
的症结,背面还会细致引见。
由于原文异常异常的长,所以笔者精简下内容再从新整理一遍。原文异常长的另一个缘由是采用了启发式思索与逐层递进的体式格局写作,笔者最大水平保存这个头脑框架。
从几个疑问最先
假定读者有比较丰富的前端 & React 开辟履历,而且写过一些 Hooks。那末你或许以为 Function Component 很好用,但美中不足的是,总有一些迷惑萦绕在心中,比方:
- 🤔 如何用
useEffect
替代componentDidMount
? - 🤔 如何用
useEffect
取数?参数[]
代表什么? - 🤔
useEffect
的依靠能够是函数吗?是哪些函数? - 🤔 为什么有时刻取数会触发死循环?
- 🤔 为什么有时刻在
useEffect
中拿到的 state 或 props 是旧的?
第一个题目能够已自问自答过无数次了,但下次写代码的时刻照样会忘。笔者也一样,而且在三期差别的精读中都离别引见过这个题目:
但第二天就遗忘了,由于 用 Hooks 完成生命周期确切别扭。 讲真,假如想完整处理这个题目,就请你忘记 React、忘记生命周期,从新明白一下 Function Component 的头脑体式格局吧!
上面 5 个题目的解答就不赘述了,读者假如有迷惑能够去
原文 TLDR 检察。
要说清晰 useEffect
,最好先从 Render 观点最先明白。
每次 Render 都有本身的 Props 与 State
能够以为每次 Render 的内容都邑构成一个快照并保存下来,因而当状况变动而 Rerender 时,就构成了 N 个 Render 状况,而每一个 Render 状况都具有本身牢固稳定的 Props 与 State。
看下面的 count
:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
在每次点击时,count
只是一个不会变的常量,而且也不存在应用 Proxy
的双向绑定,只是一个常量存在于每次 Render 中。
初始状况下 count
值为 0
,而跟着按钮被点击,在每次 Render 历程当中,count
的值都邑被固化为 1
、2
、3
:
// During first render
function Counter() {
const count = 0; // Returned by useState()
// ...
<p>You clicked {count} times</p>;
// ...
}
// After a click, our function is called again
function Counter() {
const count = 1; // Returned by useState()
// ...
<p>You clicked {count} times</p>;
// ...
}
// After another click, our function is called again
function Counter() {
const count = 2; // Returned by useState()
// ...
<p>You clicked {count} times</p>;
// ...
}
实在不仅是对象,函数在每次衬着时也是自力的。这就是 Capture Value 特征,背面碰到这类状况就不会逐一睁开,只形貌为 “此处具有 Capture Value 特征”。
每次 Render 都有本身的事宜处置惩罚
诠释了为什么下面的代码会输出 5
而不是 3
:
const App = () => {
const [temp, setTemp] = React.useState(5);
const log = () => {
setTimeout(() => {
console.log("3 秒前 temp = 5,如今 temp =", temp);
}, 3000);
};
return (
<div
onClick={() => {
log();
setTemp(3);
// 3 秒前 temp = 5,如今 temp = 5
}}
>
xyz
</div>
);
};
在 log
函数实行的谁人 Render 历程里,temp
的值能够看做常量 5
,实行 setTemp(3)
时会交由一个全新的 Render 衬着,所以不会实行 log
函数。而 3 秒后实行的内容是由 temp
为 5
的谁人 Render 发出的,所以效果天然为 5
。
缘由就是 temp
、log
都具有 Capture Value 特征。
每次 Render 都有本身的 Effects
useEffect
也一样具有 Capture Value 的特征。
useEffect
在现实 DOM 衬着终了后实行,那 useEffect
拿到的值也遵照 Capture Value 的特征:
function Counter() {
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
在每次 Render 历程当中,拿到的 count
都是固化下来的常量。
如何绕过 Capture Value
应用 useRef
就可以够绕过 Capture Value 的特征。能够以为 ref
在统统 Render 历程当中坚持着唯一援用,因而统统对 ref
的赋值或取值,拿到的都只需一个终究状况,而不会在每一个 Render 间存在断绝。
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
是 Mutable 的,而 state
是 Immutable 的。
接纳机制
在组件被烧毁时,经由过程 useEffect
注册的监听须要被烧毁,这一点能够经由过程 useEffect
的返回值做到:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
在组件被烧毁时,会实行返回值函数内回调函数。一样,由于 Capture Value 特征,每次 “注册” “接纳” 拿到的都是成对的牢固值。
用同步庖代 “生命周期”
Function Component 不存在生命周期,所以不要把 Class Component 的生命周期观点搬过来试图对号入座。Function Component 仅形貌 UI 状况,React 会将其同步到 DOM,仅此而已。
既然是状况同步,那末每次衬着的状况都邑固化下来,这包含 state
props
useEffect
以及写在 Function Component 中的统统函数。
然则舍弃了生命周期的同步会带来一些机能题目,所以我们须要通知 React 如何比对 Effect。
通知 React 如何对照 Effects
虽然 React 在 DOM 衬着时会 diff 内容,只对转变部份举行修正,而不是团体替代,但却做不到对 Effect 的增量修正辨认。因而须要开辟者经由过程 useEffect
的第二个参数通知 React 用到了哪些外部变量:
useEffect(() => {
document.title = "Hello, " + name;
}, [name]); // Our deps
直到 name
转变时的 Rerender,useEffect
才会再次实行。
然则手动保护比较贫苦而且能够脱漏,因而能够应用 eslint 插件自动提醒 + FIX:
<img width=500 src=”https://user-images.githubuse…;>
不要对 Dependencies 说谎
假如你明显运用了某个变量,却没有说明在依靠中,你即是向 React 撒了谎,效果就是,当依靠的变量转变时,useEffect
也不会再次实行:
useEffect(() => {
document.title = "Hello, " + name;
}, []); // Wrong: name is missing in dep
这看上去很蠢,但看看另一个例子呢?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
setInterval
我们只想实行一次,所以我们自以为智慧的向 React 撒了谎,将依靠写成 []
。
“组件初始化实行一次 setInterval
,烧毁时实行一次 clearInterval
,如许的代码相符预期。” 你内心能够这么想。
然则你错了,由于 useEffect 相符 Capture Value 的特征,拿到的 count
值永久是初始化的 0
。相当于 setInterval
永久在 count
为 0
的 Scope 中实行,你后续的 setCount
操纵并不会发生任何作用。
忠实的价值
笔者稍稍修正了一下题目,由于忠实是要付出价值的:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
你忠实通知 React “嘿,等 count
变化后再实行吧”,那末你会获得一个好消息和两个坏消息。
好消息是,代码能够平常运行了,拿到了最新的 count
。
坏消息有:
- 计时器不准了,由于每次
count
变化时都邑烧毁并从新计时。 - 频仍 天生/烧毁 定时器带来了肯定机能累赘。
如何既忠实又高效呢?
上述例子运用了 count
,然则如许的代码很别扭,由于你在一个只想实行一次的 Effect 里依靠了外部变量。
既然要忠实,那只好 想办法不依靠外部变量:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
setCount
另有一种函数回调形式,你不须要体贴当前值是什么,只需对 “旧的值” 举行修正即可。如许虽然代码永久运行在第一次 Render 中,但老是能够接见到最新的 state
。
将更新与行动解耦
你能够发明了,上面投机倒把的体式格局并没有完整处理统统场景的题目,比方同时依靠了两个 state
的状况:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
你会发明不得不依靠 step
这个变量,我们又回到了 “忠实的价值” 那一章。固然 Dan 肯定会给我们解法的。
应用 useEffect
的兄弟 useReducer
函数,将更新与行动解耦就可以够了:
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
这就是一个部分 “Redux”,由于更新变成了 dispatch({ type: "tick" })
所以不论更新时须要依靠若干变量,在挪用更新的行动里都不须要依靠任何变量。 详细更新操纵在 reducer
函数里写就可以够了。在线 Demo。
Dan 也将
useReducer
比作 Hooks 的的金手指形式,由于这充足绕过了 Diff 机制,不过确切能处理痛点!
将 Function 挪到 Effect 里
在 “通知 React 如何对照 Diff” 一章引见了依靠的主要性,以及对 React 要忠实。那末假如函数定义不在 useEffect
函数体内,不仅能够会脱漏依靠,而且 eslint 插件也没法协助你自动网络依靠。
你的直觉会通知你如许做会带来更多贫苦,比方如何复用函数?是的,只需不依靠 Function Component 内变量的函数都能够平安的抽出去:
// ✅ Not affected by the data flow
function getFetchUrl(query) {
return "https://hn.algolia.com/api/v1/search?query=" + query;
}
然则依靠了变量的函数如何办?
假如非要把 Function 写在 Effect 表面呢?
假如非要这么做,就用 useCallback
吧!
function Parent() {
const [query, setQuery] = useState("react");
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
由于函数也具有 Capture Value 特征,经由 useCallback
包装过的函数能够看成平常变量作为 useEffect
的依靠。useCallback
做的事变,就是在其依靠变化时,返回一个新的函数援用,触发 useEffect
的依靠变化,并激活其从新实行。
useCallback 带来的优点
在 Class Component 的代码里,假如愿望参数变化就从新取数,你不能直接比对取数函数的 Diff:
componentDidUpdate(prevProps) {
// 🔴 This condition will never be true
if (this.props.fetchData !== prevProps.fetchData) {
this.props.fetchData();
}
}
反之,要比对的是取数参数是不是变化:
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.props.fetchData();
}
}
但这类代码不内聚,一旦取数参数发生变化,就会激发多处代码的保护危急。
反观 Function Component 中应用 useCallback
封装的取数函数,能够直接作为依靠传入 useEffect
,useEffect
只需体贴取数函数是不是变化,而取数参数的变化在 useCallback
时体贴,再合营 eslint 插件的扫描,能做到 依靠不丢、逻辑内聚,从而轻易保护。
更更更内聚
除了函数依靠逻辑内聚以外,我们再看看取数的全历程:
一个 Class Component 的平常取数要斟酌这些点:
- 在
didMount
初始化发要求。 - 在
didUpdate
推断取数参数是不是变化,变化就挪用取数函数从新取数。 - 在
unmount
生命周期增加 flag,在didMount
didUpdate
两处做兼容,当组件烧毁时作废取数。
你会以为代码跳来跳去的,不仅同时体贴取数函数与取数参数,还要在差别生命周期里保护多套逻辑。那末换成 Function Component 的头脑是如何的呢?
笔者应用 useCallback 对原 Demo 举行了革新。
function Article({ id }) {
const [article, setArticle] = useState(null);
// 取数函数:只体贴依靠的 id
const fetchArticle = useCallback(async () => {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}, [id]);
// 副作用,只体贴依靠了取数函数
useEffect(() => {
// didCancel 赋值与变化的位置更内聚
let didCancel = false;
fetchArticle(didCancel);
return () => {
didCancel = true;
};
}, [fetchArticle]);
// ...
}
当你真的明白了 Function Component 理念后,就可以够明白 Dan 的这句话:虽然 useEffect
前期进修本钱更高,但一旦你准确运用了它,就可以比 Class Component 更好的处置惩罚边沿状况。
useEffect
只是底层 API,将来营业接触到的是更多封装后的上层 API,比方 useFetch
或许 useTheme
,它们会更好用。
3. 精读
原文有 9000+ 单词,异常长。但同时也合营一些 GIF 动图活泼诠释了 Render 实行道理,假如你想用好 Function Component 或许 Hooks,这篇文章几乎是必读的,由于没有人能猜到什么是 Capture Value,然则不能明白这个观点,Function Component 也不能用的随手。
从新捋一下这篇文章的思绪:
- 从引见 Render 引出 Capture Value 的特征。
- 拓展到 Function Component 统统都可 Capture,除了 Ref。
- 从 Capture Value 角度引见 useEffect 的 API。
- 引见了 Function Component 只关注衬着状况的现实。
- 激发了如何进步 useEffect 机能的思索。
- 引见了不要对 Dependencies 说谎的基本原则。
- 从不得不说谎的惯例中引见了如何用 Function Component 头脑处理这些题目。
- 当你学会用 Function Component 理念思索时,你逐步发明它的一些上风。
- 末了点出了逻辑内聚,高阶封装这两大特性,让你同时领悟到 Hooks 的壮大与文雅。
能够看到,比写框架更高的境地是发明代码的美感,比方 Hooks 本是为加强 Function Component 才能而制造,但在抛出题目-处理题目的历程当中,能够不停看到划定规矩限定,换一个角度突破它,末了体会到团体的逻辑之美。
从这篇文章中也能够读到如何加强进修才能。作者通知我们,学会遗忘能够更好的明白。我们不要拿生命周期的固化头脑往 Hooks 上套,由于那会障碍我们明白 Hooks 的理念。
另补充一些细碎的内容。
useEffect 另有什么上风
useEffect
在衬着结束时实行,所以不会壅塞浏览器衬着历程,所以运用 Function Component 写的项目平常都有效更好的机能。
天然相符 React Fiber 的理念,由于 Fiber 会依据状况停息或插队实行差别组件的 Render,假如代码遵照了 Capture Value 的特征,在 Fiber 环境下会保证值的平安接见,同时弱化生命周期也能处理中缀实行时带来的题目。
useEffect
不会在服务端衬着时实行。
由于在 DOM 实行终了后才实行,所以能保证拿到状况见效后的 DOM 属性。
4. 总结
末了,提两个最主要的点,来磨练你有无读懂这篇文章:
- Capture Value 特征。
- 一致性。将注重放在依靠上(
useEffect
的第二个参数[]
),而不是关注什么时候触发。
你对 “一致性” 有哪些更深的解读呢?迎接留言复兴。
假如你想介入议论,请 点击这里,每周都有新的主题,周末或周一宣布。前端精读 – 帮你挑选靠谱的内容。
关注
前端精读微信民众号
<img width=200 src=”https://img.alicdn.com/tfs/TB…;>
special Sponsors
版权声明:自在转载-非商用-非衍生-坚持签名(
创意同享 3.0 许可证)