React hooks详解
此篇文章仅是对hooks入门的总结,老鸟略过吧~
React从16.8.X以后增加了一个新特性,react hooks 让我们看看这个新特性又带来了哪些惊喜呢~以下内容我们采取不同方式创建组件来进行对比总结
组件的创建方式:
用过react的都了解,传统react创建组件提供了两种方式,函数式与类(class)
class创建无状态组件
class App extends React.Component { constructor(props) { super(props); } render() { return <div> <p>{this.props.name}</p> </div> } } function renderApp() { let appProps = { name: 'dqhan' } ReactDOM.render( <App {...appProps} />, document.getElementById('app') ) } renderApp();
添加状态管理
class App extends React.Component { constructor(props) { super(props); this.state = { name: props.name }; this.handleChangeName = this.handleChangeName.bind(this); } handleChangeName() { this.setState({ name: '我变了' }) } render() { return ( <React.Fragment> <p> {`hello~${this.state.name}`} </p> <button onClick={this.handleChangeName}></button> </React.Fragment> ); } }
我们通过class可以实现无状态组件以及常规组件通过setState的状态管理。
函数式创建组件
function App(props) { return <p>{`hello! ${props.name}`}</p> } function renderApp() { let appProps = { name: "dqhan" }; ReactDOM.render(<App {...appProps} />, document.getElementById("app")); }
函数式创建组件通常是无状态组件,这种方式没有办法在内部对状态统一管理,如果我们非要添加状态管理呢,那就只能借助redux啦~或者我们自己利用观察者模式实现一个发布订阅(emmmm比较勉强吧,毕竟实际开发中我们不可能这么做)
那么如果我们非要这么做呢,正题来了,React版本在16.8.X以后增添了一个新特性就是hooks。
hooks涉及API有useState、 useEffect、 useCallback、 useRef、 useMemo、 React.memo、 useReducer等,具体可以参考官方文档,我们来看一下hooks怎么用
React Hooks创建组件
无状态组件
function App(props) { return <div> <p>{`hello~${props.name}`}</p> </div> }
哈哈,跟函数式一样,毕竟我们要在函数式组件里添加状态管理嘛
1.useState
作用:添加状态管理
function App() { let [name, setName] = useState('dqhan'); return <div> <p>{`hello~${name}`}</p> <button onClick={() => setName('我变了')}>Click</button> </div>; }
react hook可以管理自己的状态,有自己的函数钩子,这点相比要函数式显然效果更好,不需要借助redux,这就是我们为啥要在前面提到函数式编程涉及状态管理问题,就是要在这里跟react hook做个比较。
到这里我们知道了这个useState,多了一个useState让函数式创建类有了自己的持久状态。那么在函数式里面我们如何做到class组件中的setState呢?
function App(props) { let [name, setName] = useState('dqhan'); let handleChangeName = useCallback(() => { setName(preState => { let updatedValues = { newValue: '我变了' } return { ...{ preState }, ...updatedValues } }) }) function click1(params) { setName('我变了1') } return <div> <p>{`hello~${name}`}</p> <button onClick={handleChangeName}>Click1</button> <button onClick={click1}>Click2</button> </div>; }
这中方式已经实现了状态整合,但是我们如果模拟一个state呢,来统一管理state呢,我们可以这么实现
function App(props) { let [state, setState] = useState({ name: 'dqhan' }); let handleChangeName = useCallback(() => { setState(preState => { let updatedValues = { name: '我变了' } return { ...preState, ...updatedValues } }) }) return <div> <p>{`hello~${state.name}`}</p> <button onClick={handleChangeName}>Click</button> </div>; }
到目前为止,已经知道了react hook中如何使用state,那么周期函数呢,那么就涉及另一个钩子useEffect
2.useEffect
作用:周期函数
useEffect涉及三个周期函数 componentDidMount 、componentDidUpdate、 compinentWillUmount 我们看一个简单的实例
function App() { var [count, setCount] = useState(0); useEffect(() => { console.log(`update--${count}`); }, [count]); return ( <div> <button onClick={() => setCount(count + 1)}>Click</button> </div> ); }
function App() { let [count, setCount] = useState(0); useEffect( () => { //默认每一次渲染都需要执行的方法 console.log('didmount') //如果需要实现componentWillComponent,则return 一个函数即可 return function unmount() { console.log('unmount') } } ) let handleSetCount = useCallback(() => { setCount((preCount) => { let updatedCount = preCount + 1; return updatedCount; }); }) return <React.Fragment> {console.log('render')} <p>{`${count}`}</p> <button onClick={handleSetCount}>Click</button> </React.Fragment> }
我们可以看一下执行周期
第一次渲染时候执行 render didmount
点击事件执行顺序 render unmount didmount
不难看出,每一次渲染我们都会执行render进行渲染,然后清除掉上一次的useEffect,然后渲染完成之后重新执行useEffect
这样通过一个useEffec可以默认执行两个周期函数,也就是当我们需要对组件添加一些需要当组件卸载时候清除掉的功能时候,这个是很方便的,常见的就是setTimeout setIntrval等定时器
但是比如一个component渲染之后我们通常会发送一个请求来请求数据,然后重写渲染这个组件,这样会造成死循环怎么办,我们可以在useEffect后添加第二个参数
阻止useEffect每一次都要执行
function App() { let [count, setCount] = useState(0); useEffect( () => { //默认每一次渲染都需要执行的方法 console.log('didmount') //如果需要实现componentWillComponent,则return 一个函数即可 return function unmount() { console.log('unmount') } }, [setCount] ) let handleSetCount = useCallback(() => { setCount((preCount) => { let updatedCount = preCount + 1; return updatedCount; }); }) return <React.Fragment> {console.log('render')} <p>{`${count}`}</p> <button onClick={handleSetCount}>Click</button> </React.Fragment> }
当传入第二个参数得值不变得时候就会跳过useEffect函数执行
如何模拟componentDidMount与componentWillUmount,第二个参数我们传一个空数组,这样就可以实现仅当组件渲染跟组件卸载得时候执行
function App() { let [count, setCount] = useState(0); useEffect( () => { //默认每一次渲染都需要执行的方法 console.log('didmount') //如果需要实现componentWillComponent,则return 一个函数即可 return function unmount() { console.log('unmount') } }, [] ) let handleSetCount = useCallback(() => { setCount((preCount) => { let updatedCount = preCount + 1; return updatedCount; }); }) return <React.Fragment> {console.log('render')} <p>{`${count}`}</p> <button onClick={handleSetCount}>Click</button> </React.Fragment> }
不过,这隐藏了一个问题:传递空数组容易出现问题。如果咱们添加了依赖项,那么很容易忘记向其中添加项,如果错过了一个依赖项,那么该值将在下一次运行useEffect时失效,并且可能会导致一些奇怪的问题。
常见得就是当我们想用父组件调用子组件时候使用得ref,或者我们要获取dom焦点时
function App() { let [count, setCount] = useState(0); let [value, setValue] = useState('') const inputRef = useRef(); useEffect( () => { //默认每一次渲染都需要执行的方法 console.log('didmount') //如果需要实现componentWillComponent,则return 一个函数即可 return function unmount() { console.log('unmount') } }, [inputRef] ) let handleSetCount = useCallback(() => { setCount((preCount) => { let updatedCount = preCount + 1; return updatedCount; }); }) let handleSetValue = function (e) { setValue(e.target.value); } return <React.Fragment> {console.log('render')} <p>{`${count}`}</p> <input ref={inputRef} value={value} onChange={handleSetValue} ></input> <button ref={inputRef} onClick={handleSetCount} >Click</button> </React.Fragment> }
下面我们实现一个请求实例
请求实例
function App() { let [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { const result = await axios(config); setData(result); }; fetchData(); }, []); return <div></div>; }
利用hook可以做到分离接口
function useFetchHook(config, watch) { let [data, setData] = useState(null); let [status, setStatus] = useState(0); useEffect( () => { const fetchData = async () => { try { const result = await axios(config); setData(result); setStatus(0); } catch (e) { setStatus(1); } fetchData(); }; }, watch ? [...watch] : [] ); return { data, status }; }
现在我们整体上知道了useState useEffect怎么使用了,我们来自己实现一个简易的
实现useState
var val; function useState(initVal) { let resultVal = val || initVal; function setVal(newVal) { resultVal = newVal; render(); } return [resultVal, setVal] }
实现useEffect
var watchArr = []; function useEffect(fn, watch) { var hasWatchChange = true; hasWatchChange = watchArr && watch.every((val, i) => val === watchArr[i]) if (hasWatchChange) { fn(); watchArr = watch; } }
hooks里面最常用的两个API就是useState与useEffect,现在是不是已经了解了呢,下面我们介绍一些其他API
3.useContext
作用:越级别获取组件内容
类组件中我们也常用context,类组件实现方式
class App extends React.Component { constructor(props) { super(props) } render() { return ( <AppContext.Provider value="dark"> <Target /> </AppContext.Provider> ); } } class Target extends React.Component { //通过定义静态属性 contextType 来订阅 //没有定义是获取不到的 static contextType = AppContext; render() { console.log(this.context); return <div></div>; } }
Hooks实现方式
const AppContext = React.createContext('target'); function App() { useEffect( () => { }, [] ); return <AppContext.Provider value="dark"> <Target /> </AppContext.Provider>; } function Target() { const value = useContext(AppContext); console.log(value); return <div></div>; }
在需要订阅多个 context 的时候,就更能体现出useContext的优势。
传统的实现方式
function App() { return <CurrentUser.Consumer> { user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } </Notifications.Consumer> } </CurrentUser.Consumer> }
hooks实现
function App() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
是不是比传统的要简单的多
4.useReducer
作用:复杂状态管理,跟redux本质上是一样的
函数式组件如果涉及到状态管理,我们需要借助redux,那么hooks需要吗,答案也是一样的,简单的状态管理我们可以通过useState来进行管理,如果比较复杂的状态管理呢,react hook给我们提供了方法 useReducer
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> </> ); }
5.useCallback
作用:提升性能,缓存事件,减少没必要的渲染
当我们使用类组件创建时,我们会怎么绑定事件呢
class App extends React.Component { constructor(props) { super(props); } render() { return <div> <p>{`hello~${name}`}</p> <button onClick={() => { console.log('click') }}>Click</button> </div> } }
这样写会导致什么结果呢,就是当渲染的时候react会认为每一次绑定的事件都是新的,从而从新进行计算
改进如下
class App extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('click') } render() { return <div> <p>{`hello~${name}`}</p> <button onClick={this.handleClick}>Click</button> </div> } }
我们讲触发函数绑定在this上,来缓存这个方法
hooks
function App() { let [count, setCount] = useState(0); return <div> <button onClick={() => setCount(1)} ></button> </div> }
同样的问题这么写也是存在的,改进如下
function App() { let [count, setCount] = useState(0); let handleSetCount = useCallback(() => { setCount(1); }) return <div> <button onClick={handleSetCount} ></button> </div> }
我们通过useCallback来缓存这个事件达到优化效果
6.useMemo
作用:提升性能,选择性的渲染变化组件
function App(target, target2) { const target = useMemo(() => { return <Target /> }, [target]) const target2 = useMemo(() => { return <Target2 /> }, [target2]) return <div> {target} {target2} </div> }
当target变化仅渲染Target组件,同理也作用与Target2组件
React.memo
作用:提升性能
如果想实现class中的shouldComponentUpdate方法呢 ,区别是它只能比较 props,不会比较 state:
const App = React.mome((target, target2) => { const target = useMemo(() => { return <Target /> }, [target]) const target2 = useMemo(() => { return <Target2 /> }, [target2]) return <div> {target} {target2} </div> })
7.useRef
作用:获取dom依赖关系
类组件实现方式
class App extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount() { this.myRef.current.focus(); } render() { return <input ref={this.myRef} type="text" />; } }
hooks
function App() { let [value, setValue] = useState('') const inputRef = useRef(); useEffect( () => { }, [inputRef] ) let handleSetValue = function (e) { setValue(e.target.value); } return <React.Fragment> <input ref={inputRef} value={value} onChange={handleSetValue} ></input> </React.Fragment> }
好了hooks的基本使用方式就介绍完了,现在你对hook多少能了解了一些吧~