内容简介:在 React Conf 2018 上,React团队提出了Hooks提案。如果你想知道什么是 Hooks,及它能解决什么问题,查看我们的讲座(介绍),理解React Hooks(常见的误解)。最初你可能会不喜欢 Hooks:
在 React Conf 2018 上,React团队提出了Hooks提案。
如果你想知道什么是 Hooks,及它能解决什么问题,查看我们的讲座(介绍),理解React Hooks(常见的误解)。
最初你可能会不喜欢 Hooks:
它们就像一段音乐,只有经过几次用心聆听才会慢慢爱上:
当你阅读文档时,不要错过关于最重要的部分——创造属于你自己的Hooks!太多的人纠结于反对我们的观点(class学习成本高等)以至于错过了Hooks更重要的一面,Hooks像 functional mixins
,可以让你创造和搭建属于自己的Hook。
Hooks受启发于一些现有技术,但在 Sebastian 和团队分享他的想法之后,我才知道这些。不幸的是,这些API和现在在用的之间的关联很容易被忽略,通过这篇文章,我希望可以帮助更多的人理解 Hooks提案中争议较大的点。
接下来的部分需要你知道 Hook API 的 useState
和如何写自定义Hook。如果你还不懂,可以看看早先的链接。
(免责说明:文章的观点仅代表个人想法,与React团队无关。话题大且复杂,其中可能有错误的观点。)
一开始当你学习时你可能会震惊,Hooks 重渲染时是依赖于固定顺序调用的,这里有说明。
这个决定显然是有争议的,这也是为什么会有人反对我们的提案。我们会在恰当的时机发布这个提案,当我们觉得文档和讲座可以足够好的描绘它时。
如果你在关注 Hooks API 的某些点,我建议你阅读下 Sebastian对 1000+ 评论RFC的 全部回复 , 足够透澈但内容非常多,我可能会将评论中的每一段都变成自己的博客文章。(事实上,我已经做过一次!)
我今天要关注一个具体部分。你可能还记得,每个 Hook 可以在组件里被多次使用,例如,我们可以用 useState
声明多个state:
function Form() { const [name, setName] = useState('Mary'); // State variable 1 const [surname, setSurname] = useState('Poppins'); // State variable 2 const [width, setWidth] = useState(window.innerWidth); // State variable 3 useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }); function handleNameChange(e) { setName(e.target.value); } function handleSurnameChange(e) { setSurname(e.target.value); } return ( <> <input value={name} onChange={handleNameChange} /> <input value={surname} onChange={handleSurnameChange} /> <p>Hello, {name} {surname}</p> <p>Window width: {width}</p> </> ); } 复制代码
注意我们用数组解构语法来命名 useState()
返回的 state 变量,但这些变量不会连接到React组件上。相反,这个例子中, React将 name
视为“第一个state变量”, surname
视为“第二个state变量”,以此类推 。它们在重新渲染时用 顺序调用 来保证被正确识别。这篇文章详细的解释了原因。
表面上看,依赖于顺序调用只是 感觉有问题 ,直觉是一个有用的信号,但它有时会误导我们 —— 特别是当我们还没有完全消化困惑的问题。 这篇文章,我会提到几个经常有人提出修改Hooks的方案,及它们存在的问题 。
这篇文章不会详尽无遗,如你所见,我们已经看过十几种至数百种不同的替代方案,我们一直在 考虑 替换组件API。
诸如此类的博客很棘手,因为即使你涉及了一百种替代方案,也有人强行提出一个来:“哈哈,你没有想到 这个 ”!
在实践中,不同替代方案提到的问题会有很多重复,我不会列举 所有 建议的API(这需要花费数月时间),而是通过几个具体示例展示最常见的问题,更多的问题就考验读者举一反三的能力了。
这不是说 Hooks 就是完美的 ,但是一旦你了解其他解决方案的缺陷,你可能会发现 Hooks 的设计是有道理的。
缺陷 #1:无法提取 custom hook
出乎意料的是,大多数替代方案完全没有提到custom hooks。可能是因为我们在“motivation”文档中没有足够强调 custom hooks,不过在弄懂 Hooks 基本原理之前,这是很难做到的。就像鸡和蛋问题,但很大程度上 custom hooks 是提案的重点。
例如:有个替代方案是限制一个组件调用多次 useState()
,你可以把 state 放在一个对象里,这样还可以兼容class不是更好吗?
function Form() { const [state, setState] = useState({ name: 'Mary', surname: 'Poppins', width: window.innerWidth, }); // ... } 复制代码
要清楚,Hooks 是允许这种风格写的,你不必将state拆分成一堆state变量(请参阅参见问题解答中的建议)。
但支持多次调用 useState()
的关键在于,你可以从组件中提取出部分有状态逻辑(state + effect)到 custom hooks 中,同时可以单独使用本地 state 和 effects:
function Form() { // 在组件内直接定义一些state变量 const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); // 我们将部分state和effects移至custom hook const width = useWindowWidth(); // ... } function useWindowWidth() { // 在custom hook内定义一些state变量 const [width, setWidth] = useState(window.innerWidth); useEffect(() => { // ... }); return width; } 复制代码
如果你只允许每个组件调用一次 useState()
,你将失去用 custom hook 引入 state 能力,这就是 custom hooks 的关键。
缺陷 #2: 命名冲突
一个常见的建议是让组件内 useState()
接收一个唯一标识key参数(string等)区分 state 变量。
和这主意有些出入,但看起来大致像这样:
// :warning: This is NOT the React Hooks API function Form() { // 我们传几种state key给useState() const [name, setName] = useState('name'); const [surname, setSurname] = useState('surname'); const [width, setWidth] = useState('width'); // ... 复制代码
这试图摆脱依赖顺序调用(显示key),但引入了另外一个问题 —— 命名冲突。
当然除了错误之外,你可能无法在同一个组件调用两次 useState('name')
,这种偶然发生的可以归结于其他任意bug,但是,当你使用一个 custom hook 时,你总会遇到想添加或移除state变量和effects的情况。
这个提议中,每当你在 custom hook 里添加一个新的 state 变量时,就有可能破坏使用它的任何组件(直接或者间接),因为 可能已经有同名的变量 位于组件内。
这是一个没有针对变化而优化的API,当前代码可能看起来总是“优雅的”,但应对需求变化时十分脆弱,我们应该从错误中吸取教训。
实际中 Hooks 提案通过依赖顺序调用来解决这个问题:即使两个 Hooks 都用 name
state变量,它们也会彼此隔离,每次调用 useState()
都会获得独立的 “内存单元”。
我们还有其他一些方法可以解决这个缺陷,但这些方法也有自身的缺陷。让我们加深探索这个问题。
缺陷 #3:同一个 Hook 无法调用两次
给 useState
“加key”的另一种衍生提案是使用像Symbol这样的东西,这样就不冲突了对吧?
// :warning: This is NOT the React Hooks API const nameKey = Symbol(); const surnameKey = Symbol(); const widthKey = Symbol(); function Form() { // 我们传几种state key给useState() const [name, setName] = useState(nameKey); const [surname, setSurname] = useState(surnameKey); const [width, setWidth] = useState(widthKey); // ... 复制代码
这个提案看上去对提取出来的 useWindowWidth
Hook 有效:
// :warning: This is NOT the React Hooks API function Form() { // ... const width = useWindowWidth(); // ... } /********************* * useWindowWidth.js * ********************/ const widthKey = Symbol(); function useWindowWidth() { const [width, setWidth] = useState(widthKey); // ... return width; } 复制代码
但如果尝试提取出来的 input handling,它会失败:
// :warning: This is NOT the React Hooks API function Form() { // ... const name = useFormInput(); const surname = useFormInput(); // ... return ( <> <input {...name} /> <input {...surname} /> {/* ... */} </> ) } /******************* * useFormInput.js * ******************/ const valueKey = Symbol(); function useFormInput() { const [value, setValue] = useState(valueKey); return { value, onChange(e) { setValue(e.target.value); }, }; } 复制代码
(我承认 useFormInput()
Hook 不是特别好用,但你可以想象下它处理诸如验证和 dirty state 标志之类,如 Formik 。)
你能发现这个bug吗?
我们调用 useFormInput()
两次,但 useFormInput()
总是用同一个 key 调用 useState()
,就像这样:
const [name, setName] = useState(valueKey); const [surname, setSurname] = useState(valueKey); 复制代码
我们再次发生了冲突。
实际中 Hooks 提案没有这种问题,因为 每次 调用 useState()
会获得单独的state 。依赖于固定顺序调用使我们免于担心命名冲突。
缺陷 #4:钻石问题(多层继承问题)
从技术上来说这个和上一个缺陷相同,但它的臭名值得说说,甚至维基百科都有介绍。(有些时候还被称为“致命的死亡钻石” —— cool!)
我们自己的 mixin 系统就受到了伤害。
比如 useWindowWidth()
和 useNetworkStatus()
这两个 custom hooks 可能要用像 useSubscription()
这样的 custom hook,如下:
function StatusMessage() { const width = useWindowWidth(); const isOnline = useNetworkStatus(); return ( <> <p>Window width is {width}</p> <p>You are {isOnline ? 'online' : 'offline'}</p> </> ); } function useSubscription(subscribe, unsubscribe, getValue) { const [state, setState] = useState(getValue()); useEffect(() => { const handleChange = () => setState(getValue()); subscribe(handleChange); return () => unsubscribe(handleChange); }); return state; } function useWindowWidth() { const width = useSubscription( handler => window.addEventListener('resize', handler), handler => window.removeEventListener('resize', handler), () => window.innerWidth ); return width; } function useNetworkStatus() { const isOnline = useSubscription( handler => { window.addEventListener('online', handler); window.addEventListener('offline', handler); }, handler => { window.removeEventListener('online', handler); window.removeEventListener('offline', handler); }, () => navigator.onLine ); return isOnline; } 复制代码
这是一个真实可运行的示例。 custom hook 作者准备或停止使用另一个 custom hook 应该是要安全的,而不必担心它是否已在链中某处“被用过了” 。
(作为反例,遗留的 React createClass()
的 mixins 不允许你这样做,有时你会有两个 mixins,它们都是你想要的,但由于扩展了同一个 “base” mixin,因此互不兼容。)
这是我们的 “钻石”::gem:
/ useWindowWidth() \ / useState() :red_circle: Clash Status useSubscription() \ useNetworkStatus() / \ useEffect() :red_circle: Clash 复制代码
依赖于固定的顺序调用很自然的解决了它:
/ useState() :white_check_mark: #1. State / useWindowWidth() -> useSubscription() / \ useEffect() :white_check_mark: #2. Effect Status \ / useState() :white_check_mark: #3. State \ useNetworkStatus() -> useSubscription() \ useEffect() :white_check_mark: #4. Effect 复制代码
函数调用不会有“钻石”问题,因为它们会形成树状结构。:christmas_tree:
缺陷 #5:复制粘贴的主意被打乱
或许我们可以通过引入某种命名空间来挽救给 state 加“key”提议,有几种不同的方法可以做到这一点。
一种方法是使用闭包隔离state的key,这需要你在 “实例化” custom hooks时给每个 hook 裹上一层 function:
/******************* * useFormInput.js * ******************/ function createUseFormInput() { // 每次实例化都唯一 const valueKey = Symbol(); return function useFormInput() { const [value, setValue] = useState(valueKey); return { value, onChange(e) { setValue(e.target.value); }, }; } } 复制代码
这种作法非常繁琐,Hooks 的设计目标之一就是避免使用高阶组件和render props的深层嵌套函数。在这里,我们不得不在使用 任何 custom hook 时进行“实例化” —— 而且在组件主体中只能单次使用生产的函数,这比直接调用 Hooks 麻烦好多。
另外,你不得不操作两次才能使组件用上 custom hook。一次在最顶层(或在编写 custom hook 时的函数里头),还有一次是最终的调用。这意味着即使一个很小的改动,你也得在顶层声明和render函数间来回跳转:
// :warning: This is NOT the React Hooks API const useNameFormInput = createUseFormInput(); const useSurnameFormInput = createUseFormInput(); function Form() { // ... const name = useNameFormInput(); const surname = useNameFormInput(); // ... } 复制代码
你还需要非常精确的命名,总是需要考虑“两层”命名 —— 像 createUseFormInput
这样的工厂函数和 useNameFormInput
、 useSurnameFormInput
这样的实例 Hooks。
如果你同时调用两次相同的 custom hook “实例”,你会发生state冲突。事实上,上面的代码就是这种错误 —— 发现了吗? 它应该为:
const name = useNameFormInput(); const surname = useSurnameFormInput(); // Not useNameFormInput! 复制代码
这些问题并非不可克服,但我认为它们会比遵守Hooks规则 的阻力大些。
重要的是,它们打破了复制粘贴的小算盘。在没有封装外层的情况下这种 custom hook 仍然可以使用,但它们只可以被调用一次(这在使用时会产生问题)。不幸的是,当一个API看起来可以正常运行,一旦你意识到在链的某个地方出现了冲突时,就不得不把所有定义好的东西包起来了。
缺陷 #6:我们仍然需要一个代码检查工具
还有另外一种使用密钥state来避免冲突的方法,如果你知道,可能会真的很生气,因为我不看好它,抱歉。
这个主意就是每次写 custom hook 时 组合 一个密钥,就像这样:
// :warning: This is NOT the React Hooks API function Form() { // ... const name = useFormInput('name'); const surname = useFormInput('surname'); // ... return ( <> <input {...name} /> <input {...surname} /> {/* ... */} </> ) } function useFormInput(formInputKey) { const [value, setValue] = useState('useFormInput(' + formInputKey + ').value'); return { value, onChange(e) { setValue(e.target.value); }, }; } 复制代码
和其他替代提议比,我最不喜欢这个,我觉得它没有什么价值。
一个 Hook 经过多次调用或者与其他 Hook 冲突之后,代码可能 意外产出 非唯一或合成无效密钥进行传递。更糟糕的是,如果它是在某些条件下发生的(我们会试图 “修复” 它对吧?),可能在一段时间后才发生冲突。
我们想提醒大家,记住所有通过密钥来标记的 custom hooks 都很脆弱,它们不仅增加了运行时的工作量(别忘了它们要转成 密钥 ),而且会渐渐增大 bundle 大小。 但如果说我们非要提醒一个问题,是哪个问题呢 ?
如果非要在条件判断里声明 state 和 effects,这种方法可能是有作用的,但按过去经验来说,我发现它令人困惑。事实上,我不记得有人会在条件判断里定义 this.state
或者 componentMount
的。
这段代码到底意味着什么?
// :warning: This is NOT the React Hooks API function Counter(props) { if (props.isActive) { const [count, setCount] = useState('count'); return ( <p onClick={() => setCount(count + 1)}> {count} </p>; ); } return null; } 复制代码
当 props.isActive
为 false
时 count
是否被保留?或者由于 useState('count')
没有被调用而重置 count
?
如果条件为保留 state,effect 又会发生什么?
// :warning: This is NOT the React Hooks API function Counter(props) { if (props.isActive) { const [count, setCount] = useState('count'); useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); return ( <p onClick={() => setCount(count + 1)}> {count} </p>; ); } return null; } 复制代码
无疑它不会在 props.isActive
第一次是 true
之前 运行,但一旦变成 true
,它会停止运行吗?当 props.isActive
转变为 false
时 interval 会重置吗?如果是这样,effect 与 state(我们说不重置时) 的行为不同令人困惑。如果 effect 继续运行,那么 effect 外层的 if
不再控制 effect,这也令人感到困惑,我们不是说我们想要基于条件控制的 effects 吗?
如果在渲染期间我们没有“使用” state 但 它却被重置,如果有多个 if
分支包含 useState('count')
但只有其中一个会在给定时间里运行,会发生什么?这是有效的代码吗?如果我们的核心思想是 “以密钥分布”,那为什么要 “丢弃” 它?开发人员是否希望在这之后从组件中提前 return
以重置所有state呢? 其实如果我们真的需要重置state,我们可以通过提取组件使其明确:
function Counter(props) { if (props.isActive) { // Clearly has its own state return <TickingCounter />; } return null; } 复制代码
无论如何这可能成为是解决这些困惑问题的“最佳实践”,所以不管你选择哪种方式去解释,我觉得条件里 声明 state 和 effect的语义怎样都很怪异,你可能会不知不觉的感受到。
如果还要提醒的是 —— 正确地组合密钥的需求会变成“负担”,它并没有给我们带来任何想要的。但是,放弃这个需求(并回到最初的提案)确实给我们带来了一些东西,它使组件代码能够安全地复制粘贴到一个 custom hook 中,且不需要命名空间,减小bundle大小及轻微的效率提升(不需要Map查找)。
慢慢理解。
缺陷 #7:Hooks 之间无法传值
Hooks 有个最好的功能就是可以在它们之间传值。
以下是一个选择信息收件人的模拟示例,它显示了当前选择的好友是否在线:
const friendList = [ { id: 1, name: 'Phoebe' }, { id: 2, name: 'Rachel' }, { id: 3, name: 'Ross' }, ]; function ChatRecipientPicker() { const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID); return ( <> <Circle color={isRecipientOnline ? 'green' : 'red'} /> <select value={recipientID} onChange={e => setRecipientID(Number(e.target.value))} > {friendList.map(friend => ( <option key={friend.id} value={friend.id}> {friend.name} </option> ))} </select> </> ); } function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); const handleStatusChange = (status) => setIsOnline(status.isOnline); useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } 复制代码
当改变收件人时, useFriendStatus
Hook 就会退订上一个好友的状态,订阅接下来的这个。
这是可行的,因为我们可以将 useState()
Hook 返回的值传给 useFriendStatus()
Hook:
const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID); 复制代码
Hooks之间传值非常有用。例如:React Spring可以创建一个尾随动画,其中多个值彼此“跟随”:
const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast }); const [{ pos2 }] = useSpring({ pos2: pos1, config: slow }); const [{ pos3 }] = useSpring({ pos3: pos2, config: slow }); 复制代码
(这是demo。)
在Hook初始化时添加默认参数或者将Hook写在装饰器表单中的提议,很难实现这种情况的逻辑。
如果不在函数体内调用 Hooks,就不可以轻松地在它们之间传值了。你可以改变这些值结构,让它们不需要在多层组件之间传递,也可以用 useMemo
来存储计算结果。但你也无法在 effects 中引用这些值,因为它们无法在闭包中被获取到。有些方法可以通过某些约定来解决这些问题,但它们需要你在心里“核算”输入和输出,这违背了 React 直接了当的风格。
在 Hooks 之间传值是我们提案的核心,Render props 模式在没有 Hooks 时是你最先能想到的,但像Component Component 这样的库,是无法适用于你遇到的所有场景的,它由于“错误的层次结构”存在大量的语法干扰。Hooks 用扁平化层次结构来实现传值 —— 且函数调用是最简单的传值方式。
缺陷 #8:步骤繁琐
有许多提议处于这种范畴里。他们尽可能的想让React摆脱对 Hooks 的依赖感,大多数方法是这么做的:让 this
拥有内置 Hooks,使它们变成额外的参数在React中无处不在,等等等。
我觉得 Sebastian的回答 比我的描述,更能说服这种方式,我建议你去了解下“注入模型”。
我只想说这和 程序员 倾向于用 try
/ catch
捕获方法中的错误代码是一样的道理,同样对比 AMD由我们自己传入 require
的“显示”声明,我们更喜欢 import
(或者 CommonJS require
) 的 ES模块。
// 有谁想念 AMD? define(['require', 'dependency1', 'dependency2'], function (require) { var dependency1 = require('dependency1'), var dependency2 = require('dependency2'); return function () {}; }); 复制代码
是的,AMD 可能更“诚实” 的陈述了在浏览器环境中模块不是同步加载的,但当你知道了这个后,写 define
三明治 就变成做无用功了。
try
/ catch
、 require
和 React Context API都是我们更喜欢“环境”式体验,多于直接声明使用的真实例子(即使通常我们更喜欢直爽风格),我觉得 Hooks 也属于这种。
这类似于当我们声明组件时,就像从 React
抓个 Component
过来。如果我们用工厂的方式导出每个组件,可能我们的代码会更解耦:
function createModal(React) { return class Modal extends React.Component { // ... }; } 复制代码
但在实际中,这最后会变得多此一举而令人厌烦。当我们真的想以某种方式抓React时,我们应该在模块系统层面上实现。
这同样适用于 Hooks。尽管如此,正如 Sebastian的回答 中提到的,在 技术上 可以做到从 react
中“直接”导入不同实现的 Hooks。(我以前的文章有提到过。)
另一种强行复杂化想法是把Hooksmonadic(单子化) 或者添加像 React.createHook()
这样的class理念。除了runtime之外,其他任何添加嵌套的方案都会失去普通函数的优点: 便于调试 。
在调试过程中,普通函数中不会夹杂任何类库代码,且可以清晰的知道组件内部值的流向,间接性很难做到这点。像启发于高阶组件(“装饰器” Hooks)或者 render props( adopt
提案 或 generators的 yield
等)类似的方案,都存在这样的问题。间接性也使静态类型变得复杂。
如我之前提到的,这篇文章不会详尽无遗,在其他提案中有许多有趣的问题,其中有一些更加晦涩(例如于并发和高级编译相关),这可能是在未来另一篇文章的主题。
Hooks并非完美无瑕,但这是我们可以找到解决这些问题的最佳权衡。还有一些我们 仍然需要修复 的东西,这些问题在 Hooks 中比在 class 中更加别扭,这也会写在别的文章里头。
无论我是否覆盖掉你喜欢的替换方案,我希望这篇文章有助于阐述我们的思考过程及我们在选择API时考虑的标准。如你所见,很多(例如确保复制粘贴、移动代码、按希望的方式进行增删依赖包)不得不针对变化而优化。我希望React开发者们会看好我们所做的这些决定。
翻译原文 Why Do React Hooks Rely on Call Order? (2018-12-13)
以上所述就是小编给大家介绍的《为什么顺序调用对 React Hooks 很重要?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Promise.then链式调用顺序
- 对vue里函数的调用顺序介绍
- Node.JS如何按顺序调用async函数,如何判断是否为async函数,在mocha中自动化测试async/await代码
- ViewGroup 默认顺序绘制子 View,如何修改?什么场景需要修改绘制顺序?
- JavaScript万物产生顺序
- SpringBoot配置加载顺序
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Designing for Emotion
Aarron Walter / Happy Cog / 2011-10-18 / USD 18.00
Make your users fall in love with your site via the precepts packed into this brief, charming book by MailChimp user experience design lead Aarron Walter. From classic psychology to case studies, high......一起来看看 《Designing for Emotion》 这本书的介绍吧!