React v16.7.0-alpha中提供了hooks功能,目前处于提案阶段。
- 概述
- useState
- useEffect
- 基本规则
- useContext,useReducer,useMemo,useCallback,useRef,useImperativeMethods,useLayoutEffect
概述
React定义一个组件最简单的方式是使用JavaScript函数,也被称为函数式组件:
// 函数式组件 function Welcome(props) { return <h1>Hello, {props.name}</h1>; } // 类组件 class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
函数式组件不能定义state,不能使用生命周期方法,所以适合无状态纯UI展现的组件。
一种说法是函数式组件的代码量少(类本质是语法糖,会额外有一些转义代码)能提升性能。我表示存疑,减少这么一丢丢代码,能提升多少?没有双盲实验的数据分析都是YY,这点大小上的差别是造成性能不佳的瓶颈吗?
函数式组件存在的意义更多体现在编程理念上。因为没有生命周期,所以父组件刷新时,它一定会刷新,没有state,导致它和数据处理彻底解耦,所以它是个彻底的UI展现组件。
耐不住寂寞FB推出了hooks,允许你在函数式组件里hook住state和生命周期。
useState
const [state, setState] = useState(initialState);:参数initialState只用于第一次渲染,之后就没什么卵用了,即第一次渲染后,返回值state和initialState相同。
如果初始状态比较复杂,可用函数来实现懒初始化(Lazy initialization):
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; });
返回值是成对的:state和更新它的函数setState。类组件中用this.state.count访问,函数式组件中用hook创建的state不用this,可以直接读取。通常,当函数退出时变量就销毁了,但hook创建的state,React会替我们保留。setCount支持两种方式:
// 方式一 setCount(newState); // 方式二 setState(prevState => { return {...prevState, ...updatedValues}; });
useEffect
useEffect(effect[, [values]]);:参数effect是个函数,用于执行副作用。可以理解为将componentDidMount,componentDidUpdate,componentWillUnmount生命周期统一成了一个API,React在每次渲染后会自动运行effect。effect因为在组件内声明,所以可以访问props,state。
有些副作用的动作,在不同生命周期中要区分对待,例如在componentDidMount中订阅,在componentWillUnmount中取消订阅。可以让effect返回一个函数,React将在清理时运行它:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // 订阅 return function cleanup() { // React将在清理时运行取消订阅 ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
这里的清理,不仅指componentWillUnmount。effect默认每次渲染后都会触发,而且始终会创建新的effect以替换旧的effect,所以这里的清理,包括每次清理旧的effect,参照下面的时间序列:
// Mount with { friend: { id: 100 } } props ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect // Update with { friend: { id: 200 } } props ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect // Update with { friend: { id: 300 } } props ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect // Unmount ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
但每次创建新的effect以替换旧的effect会带来不必要的性能问题。例如订阅时,仅当属性改变时才订阅,不用每次渲染后都重新创建新订阅。你需要第二个参数,它是effect依赖的值数组:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]);
最后如果第二个参数传递空数组[],相当于告诉React你的effect仅在mount时运行,并在unmount时执行清理,永远不会在update后运行。
完整点的例子:
import React, { useState, useEffect } from 'react'; export default function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `点了 ${count} 次`; }); return ( <> <p>点了 {count} 次</p> <button onClick={() => setCount(count + 1)}>点我</button> <button onClick={() => setCount(0)}>重置</button> </> ); }
基本规则
hook有两个规则,你可以用eslint-plugin-react-hooks插件来帮助你检查代码中是否违反了这两个规则:
- 只能在顶层调用Hook,不要在循环,条件或嵌套函数中调用Hook。
- 仅在React函数式组件中调用Hook,不要在普通JS函数调用Hook。
因为React调用hook时,依赖它们间的顺序,例如:
function Form() { const [name, setName] = useState('Mary'); useEffect(function persistForm() { localStorage.setItem('formData', name); }); const [surname, setSurname] = useState('Poppins'); useEffect(function updateTitle() { document.title = name + ' ' + surname; }); ... }
上述代码的调用顺序是固定的:
// 第一次渲染 useState('Mary') // 1. 用'Mary'初始化 name useEffect(persistForm) // 2. 将 name 保存进 localStorage useState('Poppins') // 3. 用 'Poppins' 初始化 surname useEffect(updateTitle) // 4. 将 surname 更新到 title 上 // 第二次渲染 useState('Mary') // 1. 读取当前 name(忽略参数) useEffect(persistForm) // 2. 将 name 保存进 localStorage useState('Poppins') // 3. 读取当前 surname(忽略参数) useEffect(updateTitle) // 4. 将 surname 更新到 title 上
如果打破第一条规则,将hook放置在条件语句中,会导致顺序错乱,导致bug。应该将hook放在顶层:
// 错误的方式: if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); } ---- useState('Mary') // 1. 读取 name 状态变量(忽略参数) // useEffect(persistForm) // 这个Hook被跳过了 useState('Poppins') // 2 (之前是 3). 读取 surname 失败 useEffect(updateTitle) // 3 (之前是 4). 将 surname 更新到 title 上失败 ---- // 正确的方式: useEffect(function persistForm() { if (name !== '') { localStorage.setItem('formData', name); } });
其他API
useContext
const context = useContext(Context);:可以获取context对象,即React.createContext返回的值
useReducer
const [state, dispatch] = useReducer(reducer, initialState);:它是useState的替代方案。参数reducer就是Redux里的reducer,即(state, action) => newState。返回值是state和dispatch。例如:
import React, { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'reset': return initialState; case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } } function Counter({ initialCount }) { const [state, dispatch] = useReducer(reducer, { count: initialCount }); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </> ); }
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);:只会在其中一个输入发生变化时才重新计算memoized值并返回,有助于避免在每个渲染上进行高开销的计算。
useCallback
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);:等价于useMemo,区别是返回一个memoized回调函数。
useRef
const refContainer = useRef(initialValue);:可以在函数式组件内使用ref
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>聚焦</button> </> ); }
useImperativeMethods
useImperativeMethods(ref, createInstance, [inputs]):和forwardRef一起使用
function FancyInput(props, ref) { const inputRef = useRef(); useImperativeMethods(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput); // 渲染 <FancyInput ref={fancyInputRef} /> 的父组件将能够调用 fancyInputRef.current.focus()
useLayoutEffect
签名同useEffect,但在所有DOM变化后同步触发。使用它来从DOM读取layout并重新渲染。
最后,hooks还很新,并没有在项目中大量尝试,形成很深的体会。我更在乎的是开发效率和日常维护,最好所有代码都是最直白的if-else,让阅读代码的人留个神,脑中过个弯的都不是好代码。但不妨碍我们持续关注并学习这些新特效。
v16.8 已经正式发布了