关于React Hook
React Hook 对于React来说无疑是一个伟大的特性,它将React从类组件推向了函数组件,从而让人们对于JavaScript的理解不再去可以理解晦涩的JS中的类,以及难以琢磨的this。在《你不知道的JavaScript》上卷中,作者就对JavaScript中的类,继承,面向对象做了一定的解释,总的来说就是,在JavaScript中生搬硬套用面向对象,得不偿失,很容易造成学习和理解负担。
在React16之前没有Hook的时候,必须在类组件去维护组件状态,因此必须理解JS中this的工作机制,并且在给元素绑定事件的时候总是需要绑定this。在组件之间复用状态逻辑比较困难,官方提供的render props和高阶组件确实很好用,但是整个用起来感觉很重,具体关于对类组件的吐槽可以参考React官网Hook简介这部分内容。
当使用React Hook去写React应用后,会发现再也不想用类组件了。。。
官方是这么介绍Hook的:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
在React中提供了以下几种Hook
基础Hook
- useState 给函数组件添加内部state
- useEffect 可以让你在函数组件中执行副作用操作
- useContext 组件间数据共享更轻松
额外Hook
- useReducer 替代useState统一管理数据状态
- useRef
useRef
返回一个可变的 ref 对象 - useCallback
- useMemo
useState
示例:
import React, {useState} from 'react';
import './style.less';
function Login(){
const [num, setNum] = useState(0);
return (
<>
<div>{num}</div>
<div><button onClick={() => {setNum(num+1)}}>加:</button></div>
</>
)
}
export default Login;
将useState这个钩子从react中导入,在函数组件Login中利用解构赋值的方式声明两个常量num
,setNum
作为useState(0)
的返回值,useState钩子返回一个数组,第一个参数是我们声明的state变量
,第二个参数是一个方法
,用来手动改变第一个参数。
语法:const [num, setNum] = useState(0)
useState接受参数为基本类型如数字字符串等,也可以是引用类型对象数组等。用来作为初始值。
import React, {useState} from 'react';
import './style.less';
function Login(){
const [person, setPerson] = useState({name: 'lucy', age: 18});
return (
<>
<div>{person.name}</div>
<div>{person.age}</div>
<div><button onClick={() => {setPerson({name: 'lily', age: 22})}}>加:</button></div>
</>
)
}
export default Login;
关于 useState返回值的第二个参数
由于其是一个修改第一个参数的方法,因此一般默认我们给它起名字的时候前面加上set提升语义化。
默认每次调用此方法都会重新渲染组件(当接收参数与定义state相等时不会重新渲染)。
set方法接受参数为一个新的state,或者一个函数,函数的第一个参数为上一次的state。setPeople( prev => prev.name="hasaki");
-
与class组件的
setState
方法不同,setState
默认是参数和原来的state进行合并,而useState则是使用参数替换掉之前的值。比如:在上面代码中如果setPerosn的参数为setPerson({name: 'lily'})}
当没有age属性时,那么div中的person.name
在点击button之后最终为undefined。如果只想修改name而不想修改age那么可以采用setPerson中传入函数参数,获取之前的属性进行合并使用。import React, {useState} from 'react'; import './style.less'; function Login(){ const [person, setPerson] = useState({name: 'lucy', age: 18}); return ( <> <div>{person.name}</div> <div>{person.age}</div> <div> <button onClick={() => {setPerson((prev) => { return { ...prev, name: 'hasaki' }; })}}>加:</button> </div> </> ) } export default Login;
注意如上setPeople方法,将name改为hasaki,age属性复用前一个状态下的age属性。
useEffect
示例:
import React, {useState, useEffect} from 'react';
import './style.less';
function Login(){
const [count, setCount] = useState(0);
useEffect(() => {
let timer = setInterval(() => {
setCount(count+1);
console.log(count);
}, 1000);
return () => {
clearInterval(timer);
}
})
return (
<>
<div>{count}</div>
<div>
<button onClick={() => {setCount(count+1)}}>加:</button>
</div>
</>
)
}
export default Login;
官网这么说:如果你熟悉 React class 的生命周期函数,你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
useEffect接收一个函数
为第一个参数。
关于useEffect 这个Hook,它的形式有以下几种:
-
只有一个函数参数时:相当于是
componentDidMount
和componentDidUpdate
useEffect(() => { console.log('hello'); })
也就是说在这种情况下,组件第一次渲染执行一次‘helo’的输出,当组件再次渲染时(比如调用了setCount等方法)钩子里的‘hello’会再输出一次。
-
组件中的状态一旦发生改变组件就会重新渲染,useEffect中的副作用就会重新执行一次,这样势必会造成性能的浪费,因此,useEffect还会接收第二个参数为一个数组。
const [count, setCount] = useState(0); const [name, setName] = useState('lucy'); useEffect(() => { console.log('world'); }, [count])
在这种情况下,组件第一次渲染执行一次‘world’输出,当在组件内调用setCount方法导致count发生变化时,'world'才会再次输出。无论name怎么变,‘world’是不会再次输出的。
当useEffect第二个参数为一个
空数组[]
时,就相当于componentDidMount,此Effect Hook就只在组件第一次渲染时执行一次。-
useEffect还可以有返回值,它的返回值是一个函数,相当于
componentWillUnmount
useEffect(() => { let timer = setInterval(() => { console.log(count); }, 1000); return () => { clearInterval(timer); } })
如上代码所示,在组件渲染的时候,添加了一个定时器或其他副作用,当组件卸载的时候如果不去清除定时器,那么定时器会一直执行影响应用性能,因此这个时候需要在组件卸载前进行清除。useEffect的返回值函数,会在组件卸载前处理这一类副作用。
说了这么多那到底什么是副作用?
React官网是这样解释的:数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
也就是说,当函数在运行的过程中对外部环境造成影响,或者与外部环境发生交互。比如操作DOM,发起请求,设置订阅这种,都属于副作用。同时有些副作用需要清除,比如订阅解绑,定时器延时器清除,有些不需要清除,比如数据获取,操作DOM。
那什么样的函数没有副作用呢?纯函数是不会有副作用的。
在使用useEffect hook的时候可以使用多个Effect分离关注点:
import React, {useState, useEffect} from 'react';
import './style.less';
function Login(){
const [count, setCount] = useState(0);
// 1. 进行DOM操作
useEffect(() => {
document.title = `You clicked ${count} times`;
})
// 2. 定时器,延时器处理 事件监听、解绑
useEffect(() => {
let timer = setInterval(() => {
setCount(count+1);
}, 1000);
return () => {
clearInterval(timer);
}
})
// 3. 数据获取
useEffect(() => {
// axios.post...
})
return (
<>
<div>{count}</div>
<div>
<button onClick={() => {setCount(count+1)}}>加:</button>
</div>
</>
)
}
export default Login;
useContext
举一个场景来说下:弹窗场景。在父组件中点击按钮打开弹窗,在弹窗内部点击关闭按钮关闭弹窗。
此时控制弹窗打开与关闭的只能是一个状态isOpen,此时这个isOpen状态就需要在字符组件中共享。
import React, {useState, createContext, useContext} from 'react';
import './style.less';
const popContext = createContext({}); // 创建context
function Login(){
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div>Father
<button onClick={() => {setIsOpen(!isOpen)}}>打开弹窗</button>
{ isOpen ?
<popContext.Provider value={{isOpen, setIsOpen}}>
<Poper></Poper>
</popContext.Provider>
: ''
}
</div>
</>
)
}
function Poper(){
const oprator:any = useContext(popContext); // 使用context
const {isOpen, setIsOpen} = oprator;
return (
<>
<div>我是弹窗</div>
<button onClick = {() => {
setIsOpen(!isOpen);
}}>点击关闭</button>
</>
)
}
export default Login;
使用isOpen控制弹窗的显示与隐藏,在父组件中调用setIsOpen方法,打开弹窗,在弹窗组件中使用useContext共享的父组件中的方法setIsOpen关闭弹窗。
注:父子组件一般不会存在一个文件中,需要将popContext导出再在子组件中导入使用。
useReducer
官网这样解释:useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
示例:
import React, { useState, useReducer } from 'react';
import './style.less';
function reducer(state: any, action: any) {
switch (action.type) {
case 'add':
return {
...state,
num: state.num + 1,
};
case 'min':
return {
...state,
num: state.num - 1,
};
default:
return state;
}
}
const init = { name: 'lucy', num: 0 };
function Login() {
const [state, dispatch] = useReducer(reducer, init);
return (
<>
<div>{state.name}</div>
<div>{state.num}</div>
<div>
<button onClick={() => { dispatch({ type: 'add' })}}>加</button>
<button onClick={() => { dispatch({ type: 'min' })}}>减</button>
</div>
</>
);
}
export default Login;
useReducer第一个参数是一个reducer函数,第二个参数是初始化状态,在reducer函数中根据不同的type对state进行不同的处理。类似于redux中的reducer。
useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
这个看来有两种用法:
第一种就是命令式操作DOM元素
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
第二种是关于其另外一个特性变更 .current
属性不会引发组件重新渲染
import React, { useState, useRef, useEffect } from 'react';
import './style.less';
function Login() {
const myRef = useRef(0) // 每次组件重新渲染都将返回同一个myRef对象
const [count,setCount] = useState(0);
myRef.current++;
useEffect(() => {
console.log(myRef.current);
})
return(
<div>
<button onClick={()=>{setCount(count+1)}}>+</button>
</div>
)
}
export default Login;
使用myRef.count
来统计组件渲染次数。每一次组件重新渲染的时候,都将返回同一个myRef
对象,并且,myRef
对象发生变化时,并不会导致组件渲染,这样的特性可以用来处理一些特殊场景下的需求。
当自定义一个myRef对象时,每次组件重新渲染都将返回一个新的对象。
import React, { useState, useRef, useEffect } from 'react';
import './style.less';
function Login() {
const myRef = {current: 0}; // 每次组件重新渲染都将返回一个新的myRef对象
const [count,setCount] = useState(0);
myRef.current++;
useEffect(() => {
console.log(myRef.current);
})
return(
<div>
<button onClick={()=>{setCount(count+1)}}>+</button>
</div>
)
}
export default Login;
useCallback & useMemo
这两个Hook可以用来做优化,比如以下例子,有name和age两个状态,子组件只需要在name发生变化时重新渲染,而在age发生变化时不需要重新渲染。
示例:
import React, { useState, useEffect, useMemo, useCallback} from 'react';
import './style.less';
function Login() {
const [name, setName] = useState('lucy');
const [age, setAge] = useState(18);
// const memorized = useMemo(() => <Child/>, [name]);
const memorized = useCallback<any>(<Child/>, [name]);
return (
<>
<div>
<div>{age}</div>
{memorized}
<div>
<button onClick={() => {setName('lisa')}}>修改name</button>
<button onClick={() => {setAge(28)}}>修改age</button>
</div>
</div>
</>
)
}
function Child(){
useEffect(() => {
console.log('hello');
})
return (
<>
<div>{Date.now()}</div>
</>
)
}
export default Login;
useMemo第一个参数接收一个函数第二个参数接收一个数组 useMemo(()=>fn, [])
,数组里面是依赖,只有数组里面的依赖发生变化,函数才会执行。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
React 官方也在极力推动Hook的发展,并且近期也有了React准备重写文档,更新后的文档针对Hook的内容肯定会更多,而且Vue在3.0版本出来后也使用了类似于React Hook的机制Composition API,这也是一个趋势。
参考阅读:
最近在用webpack4+react16+ts4做自己的一个移动端小博客,功能正在完善中,主要是想学习使用一下React技术栈,期间发现React Hook确实非常好用,因此做一些记录。
博客 github 地址:https://github.com/Mstian/mobile-react-blog
线上地址:http://m.tianleilei.cn(开发中)
useEffect和 useLayoutEffect 的区别
useEffect 会在浏览器渲染结束后执行(异步不阻塞浏览器渲染),useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行(同步阻塞浏览器渲染)