设计Hooks主要是解决ClassComponent的几个问题:html
同时,也为了让 FunctionalComponent 也拥有 ClassComponent 的一些特性。node
使用注意:react
React 中提供的 hooks:web
const [state, setState] = useState(initialState)
复制代码
setter
函数,可传任意类型的变量,或者一个接收 state 旧值的函数,其返回值做为 state 新值。
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount) // Lazy initialization const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props) return initialState }) return ( <> Count: {count} <button onClick={() => setCount(0)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ) } 复制代码
注意: set 方法不会像类组件的 setState 同样作 merge,因此建议:redux
{...state, value}
形势。
const [state, dispatch] = useReducer(reducer, initialArg, init)
复制代码
init
函数来计算初始状态/值,而不是显式的提供值。若是初始值可能会不同,这会很方便,最后会用计算的值来代替初始值。
注意: React 不使用 state = initialState
这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,所以须要在调用 Hook 时指定。若是你特别喜欢上述的参数约定,能够经过调用 useReducer(reducer, undefined, reducer)
来模拟 Redux 的行为,但不鼓励你这么作。api
function init(initialCount) {
return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } function render () { ReactDOM.render(<Counter initialCount={0} />, document.getElementById('root')); } 复制代码
同时,useReucer 也是 useState 的内部实现,useState 和 useReucer 的实现原理:数组
let memoizedState
function useReducer(reducer, initialArg, init) { let initState = void 0 if (typeof init !== 'undefined') { initState = init(initialArg) } else { initState = initialArg } function dispatch(action) { memoizedState = reducer(memoizedState, action) // React的渲染 // render() } memoizedState = memoizedState || initState return [memoizedState, dispatch] } function useState(initState) { return useReducer((oldState, newState) => { if (typeof newState === 'function') { return newState(oldState) } return newState }, initState) } 复制代码
在某些场景下,useReducer 比 useState 更加适用。Kent C. Dodds 提供了一个 useReducer
的最佳实践:当你一个元素中的状态,依赖另外一个元素中的状态,最好使用 useReducer。浏览器
useEffect(effect, array);
复制代码
useEffect 接收两个参数,没有返回值。缓存
[]
,componentDidUpdate 时不会触发 returnFunction 和 effect。
useLayoutEffect(effect, array);
复制代码
与 useEffect 使用方法同样,只是执行回调函数的时机有着略微区别,运行时机更像是 componentDidMount 和 componentDidUpdate。可是要注意的是,该方法是同步方法,在浏览器 paint 以前执行,会阻碍浏览器 paint,只有当咱们须要进行DOM的操做时才使用该函数(好比设定 DOM 布局尺寸,这样能够防抖动)。性能优化
useLayoutEffect 与 useEffect
正常状况用默认的 useEffect 钩子就够了,这能够保证状态变动不阻塞渲染过程,但若是 effect 更新(清理)中涉及 DOM 更新操做,用 useEffect 就会有意想不到的效果,这时咱们最好使用 useLayoutEffect 。
好比逐帧动画 requestAnimationFrame ,要作一个 useRaf hook 就得用上后者,须要保证同步变动。这也符合做者说到的 useEffect的时期是很是晚,能够保证页面是稳定下来再作事情。
钩子的执行顺序:useLayoutEffect > requestAnimationFrame > useEffect
要理解 Context Hooks 中的 api,首先须要了解 context 和其使用场景。
设计目的: context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据。
使用场景: context 经过组件树提供了一个传递数据的方法,从而避免了在每个层级手动的传递 props 属性。
注意点: 不要仅仅为了不在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件须要访问相同数据的情景。
const {Provider, Consumer} = React.createContext(defaultValue, calculateChangedBits)
复制代码
该方法建立一对{ Provider, Consumer }
。当 React 渲染 context 组件 Consumer 时,它将从组件树的上层中最接近的匹配的 Provider 读取当前的 context 值。Consumer 是 Provider 提供数据的使用者。
若是上层的组件树没有一个匹配的 Provider,而此时你须要渲染一个 Consumer 组件,那么你能够用到 defaultValue 。这有助于在不封装它们的状况下对组件进行测试。例如:
import React, { useContext} from 'react';
import ReactDOM from 'react-dom'; /* 结果读取为123,由于没有找到Provider */ const { Provider, Consumer } = React.createContext(123); function Bar() { return <Consumer>{color => <div>{color}</div>}</Consumer>; } function Foo() { return <Bar />; } function App() { return ( <Foo /> ); } ReactDOM.render( <App />, document.getElementById('root') ) 复制代码
React 组件容许 Consumers 订阅 context 的改变
。而 Provider 就是发布这种状态的组件,该组件接收一个 value 属性
传递给 Provider 的后代 Consumers。一个 Provider 能够联系到多个 Consumers
。Providers 能够被嵌套以覆盖组件树内更深层次的值。
export const ProviderComponent = props => {
return ( <Provider value={}> {props.children} </Provider> ) } 复制代码
在createContext()
函数中的第二个参数为calculateChangedBits
,它是一个接受 newValue 与 oldValue 的函数,返回值做为 changedBits,在 Provider 中,当 changedBits = 0,将再也不触发更新。而在 Consumer 中有一个不稳定的 props,unstable_observedBits,若 Provider 的changedBits & observedBits = 0
,也将不触发更新。
const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
let result = 0 if (a.foo !== b.foo) { result |= 0b01 } if (a.bar !== b.bar) { result |= 0b10 } return result }) 复制代码
<Consumer>
{value => /* render something based on the context value */} </Consumer> 复制代码
createContext()
的 defaultValue 。
每当 Provider 的值发生改变时, 做为 Provider 后代的全部 Consumers 都会从新渲染。 从 Provider 到其后代的Consumers 传播不受 shouldComponentUpdate 方法的约束,所以即便祖先组件退出更新时,后代Consumer也会被更新。
// 建立一个 theme Context, 默认 theme 的值为 light
const ThemeContext = React.createContext('light'); function ThemedButton(props) { // ThemedButton 组件从 context 接收 theme return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ) } // 中间组件 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ) } class App extends React.Component { render() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ) } } 复制代码
const context = useContext(Context)
复制代码
使用效果和 Consumer 相似,可是是函数式的使用方式,仍然须要与 Provider 配合使用。
该函数接收一个 Context 类型的参数(就是包裹了 Provider 和 Consumer 的那个对象),返回 Provider 中的 value 属性对象的值。
const Context = React.createContext('light');
// Provider class Provider extends Component { render() { return ( <Context.Provider value={'dark'}> <DeepTree /> </Context.Provider> ) } } 复制代码
// Consumer
function Consumer(props) { const context = useContext(Context) return ( <div> {context} // dark </div> ) } 复制代码
// Color.jsx
import React, { createContext, useReducer } from 'react' export const ColorContext = createContext() export const UPDATE_COLOR = 'UPDATE_COLOR' function reducer(state, action) { switch (action.type) { case UPDATE_COLOR: return action.color default: return state } } export const Color = props => { const [color, dispatch] = useReducer(reducer, 'blue') return ( <ColorContext.Provider value={{ color, dispatch }}> {props.children} </ColorContext.Provider> ) } 复制代码
// Button.jsx
import React, { useContext } from 'react' import { ColorContext, UPDATE_COLOR } from './Color' function Buttons() { const { dispatch } = useContext(ColorContext) return ( <div> <button onClick={() => { dispatch({ type: UPDATE_COLOR, color: 'red' }) }} > red </button> <button onClick={() => { dispatch({ type: UPDATE_COLOR, color: 'yellow' }) }} > yellow </button> </div> ) } export default Buttons 复制代码
// ShowArea.jsx
import React, { useContext } from 'react' import { ColorContext } from './Color' function ShowArea() { const { color } = useContext(ColorContext) return <div style={{ color }}>color:{color}</div> } export default ShowArea 复制代码
// index.jsx
import React from 'react' import ShowArea from './ShowArea' import Buttons from './Buttons' import { Color } from './Color' function Demo() { return ( <div> <Color> <ShowArea /> <Buttons /> </Color> </div> ) } export default Demo 复制代码
const RefElement = createRef(initialValue)
复制代码
useRef 能够须要传递一个参数,该参数通常是用于 useRef 的另外一种用法,若是是引用元素对象通常不传参数,返回一个可变的 ref 对象,该对象下面有一个 current 属性指向被引用对象的实例。
要说到 useRef,咱们须要说到 createRef ,以及为何要有这个 api 出现。(createRef 使用方法和 useRef 一致,返回的是一个 ref 对象)
二者当作 ref 正常使用时效果基本彻底同样:
createRef
import { React, createRef } from 'react'
const FocusInput = () => { const inputElement = createRef() const handleFocusInput = () => { inputElement.current.focus() } return ( <> <input type='text' ref={inputElement} /> <button onClick={handleFocusInput}>Focus Input</button> </> ) } export default FocusInput 复制代码
useRef
import { React, useRef } from 'react'
const FocusInput = () => { const inputElement = useRef() const handleFocusInput = () => { inputElement.current.focus() } return ( <> <input type='text' ref={inputElement} /> <button onClick={handleFocusInput}>Focus Input</button> </> ) } export default FocusInput 复制代码
可是,这二者对应 ref 的引用实际上是有着本质区别的:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
像这样:
const App = () => {
const [renderIndex, setRenderIndex] = React.useState(1) const refFromUseRef = React.useRef() const refFromCreateRef = createRef() if (!refFromUseRef.current) { refFromUseRef.current = renderIndex } if (!refFromCreateRef.current) { refFromCreateRef.current = renderIndex } return ( <> <p>Current render index: {renderIndex}</p> <p> <b>refFromUseRef</b> value: {refFromUseRef.current} </p> <p> <b>refFromCreateRef</b> value:{refFromCreateRef.current} </p> <button onClick={() => setRenderIndex(prev => prev + 1)}> Cause re-render </button> </> ) } 复制代码
由于一直都存在 refFromUseRef.current,因此并不会改变值。
那么,为何要赋予 useRef 这种特性,在什么场景下咱们须要这种特性呢?
一个经典案例:
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState() function handleAlertClick() { setTimeout(() => { alert(`Yout clicked on ${count}`) }, 3000) } return ( <div> <p>You click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 复制代码
当咱们更新状态的时候, React 会从新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并从新渲染一个 handleAlertClick 函数. 每个 handleAlertClick 里面都有它本身的 count。
你会发现,count 的值并不可以实时的显示更新的数据,这个是因为 JS 中一值就存在的闭包机制致使的,当点击显示弹窗的按钮时,此时的 count 的值已经肯定,而且传入到了alert
方法的回调中,造成闭包,后续值的改变不会影响到定时器的触发。
而若是在类组件中,若是咱们使用的是this.state.count
,获得的结果又会是实时的,由于它们都是指向的同一个引用对象。
在函数组件中,咱们可使用 useRef 来实现实时获得新的值,这就是 useRef 的另一种用法,它还至关于 this , 能够存听任何变量。useRef 能够很好的解决闭包带来的不方便性。
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState(0) const lastestCount = useRef() lastestCount.current = count function handleAlertClick() { setTimeout(() => { alert(`You clicked on ${lastestCount.current}`) // 实时的结果 }, 3000) } return ( <div> <p>Yout click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 复制代码
要值得注意的是,若是咱们在 useRef 中传入参数(通常 useRef 中传值就用在这里),使用下面这种方法来访问值,结果又会不一样:
import React, { useRef, useState } from 'react'
function App() { const [count, setCount] = useState(0) const lastestCount = useRef(count) // 直接传入count function handleAlertClick() { setTimeout(() => { alert(`You clicked on ${lastestCount.current}`) }, 3000) } return ( <div> <p>Yout click {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div> ) } export default App 复制代码
点击的时候咱们会发现弹出来的值永远是0,正如咱们所说,useRef 返回的都是相同的引用,参数在第一个传入进去的时候已经赋值给了 current 属性,返回了一个实例回来,后续由于已经有了实例了,因此会直接将原来的实例返回,传入的参数也就再也不起做用了。
forwardRef((props, ref) => {
// dosomething return ( <div ref={ref}></div> ) }) 复制代码
forwardRef 准确来讲不是 hooks 中的内容,可是若是咱们要使用 useImperativeHandle,就须要使用它来进行搭配。
该方法的做用是:引用父组件的 ref 实例,成为子组件的一个参数,能够引用父组件的 ref 绑定到子组件自身的节点上。
该方法能够看作是一个高阶组件,自己 props 只带有 children 这个参数,它能将从父组件拿到的 ref 和 props 传入给子组件,由子组件来调用父组件传入的 ref。
传入的组件会接收到两个参数,一个是父组件传递的 props,另外一个就是 ref 的引用。
// 咱们可使用三层组件嵌套,把传入forwardRef的函数当作传值的中间层
function InputWithLabel(props) { // 这里的myRef为经过外部打入的父级ref节点 const { label, myRef } = props const [value, setValue] = useState("") const handleChange = e => { const value = e.target.value setValue(value) } return ( <div> <span>{label}:</span> <input type="text" ref={myRef} value={value} onChange={handleChange} /> </div> ) } // 这里用forwardRef来承接获得父级传入的ref节点,并将其以参数的形式传给子节点 const RefInput = React.forwardRef((props, ref) => ( <InputWithLabel {...props} myRef={ref} /> )) // 调用该RefInput的过程 function App() { // 经过useRef hook 得到相应的ref节点 const myRef = useRef(null) const handleFocus = () => { const node = myRef.current console.log(node) node.focus() } return ( <div className="App"> <RefInput label={"姓名"} ref={myRef} /> <button onClick={handleFocus}>focus</button> </div> ) } 复制代码
useImperativeHandle(ref, () => ({
a:1, b:2, c:3 })) 复制代码
官方建议useImperativeHandle和forwardRef同时使用,减小暴露给父组件的属性,避免使用 ref 这样的命令式代码。
useImperativeHandle 有三个参数:
function Example(props, ref) {
const inputRef = useRef() useImperativeHandle(ref, () => ({ // 父组件能够经过this.xxx.current.focus的方式使用子组件传递出去的focus方法 focus: () => { inputRef.current.focus() } })) return <input ref={inputRef} /> } export default forwardRef(Example) 复制代码
class App extends Component {
constructor(props){ super(props) this.inputRef = createRef() } render() { return ( <> <Example ref={this.inputRef}/> <button onClick={() => {this.inputRef.current.focus()}}>Click</button> </> ) } } 复制代码
MemoComponent = memo(Component)
复制代码
咱们都知道,对于类组件来讲,有 PureComponent 能够经过判断父组件传入的 props 是否进行改变来优化渲染性能。因此,在函数式组件中,React 也有一个相似 PureComponent 功能的高阶组件 memo,效果同 PureComponent,都会判断父组件传入的 props 是否发生改变来从新渲染当前组件。
使用方法很简单:
import React, { memo } from 'react'
function Demo(props){ return ( <div>{props.name}</div> ) } export default memo(Demo) 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
复制代码
useMemo 是 React 推出用于优化函数式组件性能的 hooks,它能够传入两个参数:
import React, { useState, useMemo } from 'react'
function Child({ color }) { // color值不发生改变不会打印console,可是依旧会触发从新渲染,若是连这个函数都不执行,在最外层加上memo const actionColor = useMemo(() => { console.log('color update') return color }, [color]) return <div style={{ actionColor }}>{actionColor}</div> } function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child color={color} /> </div> ) } export default MemoCount 复制代码
上面的例子其实并非 useMemo 最经常使用的场景,就像以前说的,在 props 发生改变的时候才会触发被 memo 的组件的从新渲染,可是若是只是 props 的引用对象发生改变,实际的值并无发生改变,组件仍是会被从新渲染。就像下面这样:
import React, { useState, memo } from 'react'
const Child = memo(({ config }) => { console.log(config) return <div style={{ color:config.color }}>{config.text}</div> }) function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') const config = { color, text:color } return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child config={config} /> </div> ) } export default MemoCount 复制代码
当咱们改变 count 值的时候,咱们发现这其实和 config 对象是无关的,可是 Child 组件依旧会从新渲染,由于因为父组件的从新渲染,config 被从新赋值了新的对象,虽然新的对象里面的值都是相同的,但因为是引用类型对象,因此依旧会改变值,要改变这种情况,咱们须要:
// 使用useMemo
import React, { useState,useMemo, memo } from 'react' const Child = memo(({ config }) => { console.log(config) return <div style={{ color:config.color }}>{config.text}</div> }) function MemoCount() { const [count, setCount] = useState(0) const [color, setColor] = useState('blue') // 只会根据color的改变来返回不一样的对象,不然都会返回同一个引用对象 const config = useMemo(()=>({ color, text:color }),[color]) return ( <div> <button onClick={() => { setCount(count + 1) }} > Update Count </button> <button onClick={() => { setColor('green') }} > Update Color </button> <div>{count}</div> <Child config={config} /> </div> ) } export default MemoCount 复制代码
这样,当 count 的值发生改变时,子组件就不会再从新渲染了。
const memoizedCallback = useCallback(
() => { doSomething(a, b) }, [a, b], ) 复制代码
useCallback 的用法和 useMemo 相似,是专门用来缓存函数的 hooks,也是接收两个参数,同时,咱们第一个参数传入额回调函数就是要缓存的函数。
注意:第二个参数目前只用于指定须要判断是否变化的参数,并不会做为形参传入回调函数。建议回调函数中使用到的变量都应该在数组中列出。
要在回调函数中传入参数,咱们最好使用高阶函数的方法,useCallback 会帮咱们缓存这个高阶函数,如上所示。
能够看出,都是当依赖项方式改变时,才触发回调函数。所以,咱们能够认为:useCallback(fn, inputs)
等同于 useMemo(() => fn, inputs)
// useCallback的实现原理
let memoizedState = null function useCallback(callback, inputs) { const nextInputs = inputs !== undefined && inputs !== null ? inputs : [callback] const prevState = memoizedState; if (prevState !== null) { const prevInputs = prevState[1] if (areHookInputsEqual(nextInputs, prevInputs)) { return prevState[0] } } memoizedState = [callback, nextInputs] return callback } // useMemo的实现原理 function useMemo(callback, inputs){ return useCallback(callbak(),inputs) } 复制代码
更多状况,useCallback通常用于在 React 中给事件绑定函数并须要传入参数的时候:
// 下面的状况能够保证组件从新渲染获得的方法都是同一个对象,避免在传给onClick的时候每次都传不一样的函数引用
import React, { useState, useCallback } from 'react' function MemoCount() { const [count, setCount] = useState(0) memoSetCount = useCallback(()=>{ setCount(count + 1) },[]) return ( <div> <button onClick={memoSetCount} > Update Count </button> <div>{color}</div> </div> ) } export default MemoCount 复制代码
useDebugValue(value)
// or useDebugValue(date, date => date.toDateString()); 复制代码
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
useDebugValue 接收两个参数,根据传入参数数量的不一样有不一样的使用方式:
直接传 debug 值
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null); // ... // 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; } 复制代码
延迟格式化 debug 值
const date = new Date()
useDebugValue(date, date => date.toDateString()) 复制代码
自定义 Hook 是一个函数,其名称以use
开头,函数内部能够调用其余的 Hook
// myhooks.js
// 下面自定义了一个获取窗口长宽值的hooks import React, { useState, useEffect, useCallback } from 'react' function useWinSize() { const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) const onResize = useCallback(() => { setSize({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) }, []) useEffect(() => { window.addEventListener('resize', onResize) return () => { window.removeEventListener('reisze', onResize) } }, [onResize]) return size } export const useWinSize 复制代码
import { useWinSize } from './myhooks'
function MyHooksComponent() { const size = useWinSize() return ( <div> 页面Size:{size.width}x{size.height} </div> ) } export default MyHooksComponent 复制代码
本文使用 mdnice 排版