Hook 简介
Hook 入世之前 React 存在的问题
-
在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的路径(例如,把组件连贯到 store)。有一些解决此类问题的计划,比方 render props 和 高阶组件。然而这类计划须要从新组织你的组件构造,这可能会很麻烦,使你的代码难以了解。
-
简单组件变得难以了解
组件经常在
componentDidMount
和componentDidUpdate
中获取数据。然而,同一个componentDidMount
中可能也蕴含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount
中革除。互相关联且须要对照批改的代码被进行了拆分,而齐全不相干的代码却在同一个办法中组合在一起。如此很容易产生 bug,并且导致逻辑不统一。 -
难以了解的 class
class 是学习 React 的一大屏障。你必须去了解 JavaScript 中
this
的工作形式,这与其余语言存在微小差别。还不能遗记绑定事件处理器。没有稳固的语法提案,这些代码十分冗余。大家能够很好地了解 props,state 和自顶向下的数据流,但对 class 却束手无策。
Hook 带来的解决方案
- 你能够应用 Hook 从组件中提取状态逻辑,使得这些逻辑能够独自测试并复用。Hook 使你在无需批改组件构造的状况下复用状态逻辑。
- Hook 将组件中互相关联的局部拆分成更小的函数(比方设置订阅或申请数据),而并非强制依照生命周期划分。你还能够应用 reducer 来治理组件的外部状态,使其更加可预测。
- Hook 使你在非 class 的状况下能够应用更多的 React 个性。 从概念上讲,React 组件始终更像是函数。而 Hook 则拥抱了函数,同时也没有就义 React 的精力准则。Hook 提供了问题的解决方案,无需学习简单的函数式或响应式编程技术。
Hook API
useState
useState
是 react 自带的一个 hook 函数,它的作用就是用来申明状态变量。useState
这个函数接管的参数是咱们的状态初始值(initial state),它返回了一个数组,这个数组的第 [0]
项是以后以后的状态值,第 [1]
项是能够扭转状态值的办法函数。
初始化
// 返回一个 state,以及更新 state 的函数 setState(接管一个新的 state 值并将组件的一次从新渲染退出队列)
const [state, setState] = useState(initialState);
函数式更新
// 如果新的 state 须要通过应用先前的 state 计算得出,那么能够将函数传递给 setState。该函数将接管先前的 state,并返回一个更新后的值。function Counter({initialCount}) {const [count, setCount] = useState(initialCount);
return (
<>
Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
惰性初始 state
// 如果初始 state 须要通过简单计算取得,则能够传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);
return initialState;
});
跳过 state 更新
调用 State Hook 的更新函数并传入以后的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 应用 Object.is
比拟算法 来比拟 state。)
useEffect
咱们写的有状态组件,通常会产生很多的副作用(side effect),比方发动 ajax 申请获取数据,增加一些监听的注册和勾销注册,手动批改 dom 等等。咱们之前都把这些副作用的函数写在生命周期函数钩子里,比方 componentDidMount
,componentDidUpdate
和componentWillUnmount
。而当初的 useEffect 就相当与这些申明周期函数钩子的集合体。它以一抵三。
简略例子
import {useState, useEffect} from 'react';
function Example() {const [count, setCount] = useState(0);
// 相似于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的题目
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}
革除 effect
通常,组件卸载时须要革除 effect 创立的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个革除函数。以下就是一个创立订阅的例子:
useEffect(() => {const subscription = props.source.subscribe();
return () => {
// 革除订阅
subscription.unsubscribe();};
});
为避免内存透露,革除函数会在组件卸载前执行。另外,如果组件屡次渲染(通常如此),则 在执行下一个 effect 之前,上一个 effect 就已被革除。参考 前端进阶面试题具体解答
effect 的执行机会
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器实现布局与绘制 之后,传给 useEffect
的函数会提早调用。这使得它实用于许多常见的副作用场景,比方设置订阅和事件处理等状况,因而不应在函数中执行阻塞浏览器更新屏幕的操作。
effect 的条件执行
默认状况下,effect 会在每轮组件渲染实现后执行。这样的话,一旦 effect 的依赖发生变化,它就会被从新创立。在某些状况下,咱们不须要在每次组件更新时都创立新的订阅,而是仅须要在 source
prop 扭转时从新创立。要实现这一点,能够给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。
// 此时,只有当 props.source 扭转后才会从新创立订阅。(要实现 componentDidMount 性能只须要设置第二个参数为 [] 即可)
useEffect(() => {const subscription = props.source.subscribe();
return () => {subscription.unsubscribe();
};
},
[props.source],
);
useContext
能够深层组件传值,父组件传给子孙组件。接管一个 context 对象(React.createContext
的返回值)并返回该 context 的以后值。以后的 context 值由下层组件中距离以后组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件下层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并应用最新传递给 MyContext
provider 的 context value
值。即便先人应用 React.memo
或 shouldComponentUpdate
,也会在组件自身应用 useContext
时从新渲染
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {const theme = useContext(ThemeContext);
return (<button style={{ background: theme.background, color: theme.foreground}}>
I am styled by theme context! </button>
);
}
useReducer
useState
的代替计划,能够用于简单状态解决。它接管一个形如 (state, action) => newState
的 reducer,并返回以后的 state 以及与其配套的 dispatch
办法。(如果你相熟 Redux 的话,就曾经晓得它如何工作了。)
指定初始 state
有两种不同初始化 useReducer
state 的形式,你能够依据应用场景抉择其中的一种。将初始 state 作为第二个参数传入 useReducer
是最简略的办法:
//nst [state, dispatch] = useReducer(reducer, initialArg, init);
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
某些场景下,useReducer
会比 useState
更实用,例如 state 逻辑较简单且蕴含多个子值,或者下一个 state 依赖于之前的 state 等。并且,应用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你能够向子组件传递 dispatch
而不是回调函数。
const initialState = {count: 0};
function reducer(state, action) {switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();}
}
function Counter() {const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
惰性初始化
你能够抉择惰性地创立初始 state。为此,须要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么做能够将用于计算 state 的逻辑提取到 reducer 内部,这也为未来对重置 state 的 action 做解决提供了便当:
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: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
跳过 dispatch
如果 Reducer Hook 的返回值与以后 state 雷同,React 将跳过子组件的渲染及副作用的执行。(React 应用 Object.is
比拟算法 来比拟 state。)
useMemo
把“创立”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项扭转时才从新计算 memoized 值。这种优化有助于防止在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。memo
是浅比拟,意思是,对象只比拟内存地址,只有你内存地址没变,管你对象外面的值变幻无穷都不会触发 render。
你能够把 useMemo
作为性能优化的伎俩,但不要把它当成语义上的保障。未来,React 可能会抉择“忘记”以前的一些 memoized 值,并在下次渲染时从新计算它们,比方为离屏组件开释内存。先编写在没有 useMemo
的状况下也能够执行的代码 —— 之后再在你的代码中增加 useMemo
,以达到优化性能的目标。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项扭转时才会更新。当你把回调函数传递给通过优化的并应用援用相等性去防止非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将十分有用。
useMemo
与 useCallback
相似,都是有着缓存的作用,useMemo 是缓存值的,useCallback 是缓存函数的。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
const memoizedCallback = useCallback(() => {doSomething(a, b);
},
[a, b],
);
useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内放弃不变。
useEffect
外面的 state 的值,是固定的,这个是有方法解决的,就是用 useRef
,能够了解成useRef
的一个作用:就是相当于全局作用域,一处被批改,其余中央全更新。
实质上,useRef
就像是能够在其 .current
属性中保留一个可变值的“盒子”。你应该相熟 ref 这一种拜访 DOM 的次要形式。如果你将 ref 对象以 <div ref={myRef} />
模式传入组件,则无论该节点如何扭转,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。然而,useRef()
比 ref
属性更有用。它能够很不便地保留任何可变值,其相似于在 class 中应用实例字段的形式。
请记住,当 ref 对象内容发生变化时,useRef
并 不会 告诉你。变更 .current
属性不会引发组件从新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要应用回调 ref 来实现。
const Hook =()=>{const [count, setCount] = useState(0)
const btnRef = useRef(null)
useEffect(() => {console.log('use effect...')
const onClick = ()=>{setCount(count+1)
}
btnRef.current.addEventListener('click',onClick, false)
return ()=> btnRef.current.removeEventListener('click',onClick, false)
},[count])
return(
<div>
<div>
{count} </div>
<button ref={btnRef}>click me </button>
</div>
)
}
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
能够让你在应用 ref
时自定义裸露给父组件的实例值。在大多数状况下,该当防止应用 ref 这样的命令式代码。useImperativeHandle
该当与 forwardRef
一起应用:
function FancyInput(props, ref) {const inputRef = useRef();
useImperativeHandle(ref, () => ({focus: () => {inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染 <FancyInput ref={inputRef} />
的父组件能够调用 inputRef.current.focus()
。
自定义 Hook
自定义 Hook 是一个函数,其名称以“use
”结尾,函数外部能够调用其余的 Hook。
例如,上面的 useFriendStatus
是咱们第一个自定义的 Hook:
import {useState, useEffect} from 'react';
function useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);
useEffect(() => {function handleStatusChange(status) {setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
自定义一个当 resize 的时候 监听 window 的 width 和 height 的 hook
import {useEffect, useState} from "react";
export const useWindowSize = () => {const [width, setWidth] = useState()
const [height, setHeight] = useState()
useEffect(() => {const {clientWidth, clientHeight} = document.documentElement
setWidth(clientWidth)
setHeight(clientHeight)
}, [])
useEffect(() => {const handleWindowSize = () =>{const {clientWidth, clientHeight} = document.documentElement
setWidth(clientWidth)
setHeight(clientHeight)
};
window.addEventListener('resize', handleWindowSize, false)
return () => {window.removeEventListener('resize',handleWindowSize, false)
}
})
return [width, height]
}
应用:
const [width, height] = useWindowSize()
const isOnline = useFriendStatus(id);