React的函数组件及Hooks
本章主要介绍了React函数组件的创建和各类Hook的使用以及使用技巧。
函数组件
-
函数组件的创建
// 传统函数 function Hello(props){ return (<div>Hello world</div>) } // 箭头函数 const Hello = (props)=>{ return (<div>Hello world</div>) } // 仅返回模板时,箭头函数适当简写 const Hello = props => <div>Hello world</div>
函数组件的创建要注意2点:传props和return的内容用()包裹起来。
-
函数组件使用props、state及内部函数声明
请回看React组件基础。
-
函数组件与类组件的对比
比起类组件,函数组件的代码量更小,代码更简洁,更简单,容易阅读。更重要的是,使用函数组件能够避免复杂的this指向的问题。
那函数组件有缺点吗?在ReactV16.8.0之前,函数组件的致命缺点在于:没有state和生命周期。但是在eactV16.8.0之后,React推出了Hooks,开发者利用Hooks就能够实现state和生命周期的功能。
下面就让我们深入学习各个Hooks的使用吧。
React Hooks
Hooks的引入
如果引入了React那么就可以直接使用React.hooksName
来调用hooks。同时我们也能直接引入特定的Hooks,直接调用。
// 以使用useState为例
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
// 引入useState,直接调用useState
const [user,setUser] = useState({name:'Frank', age: 18})
// 引入React,通过React来调用useState
const [user2,setUser2] = React.useState({name:'Jack', age: 18})
return ( <div className="App"></div> );
}
useState
-
作用:
useState主要是用于创建state的读、写的接口。
-
使用useState创建state:
function App() { // 初始值传入一个简单类型 const [n,setN] = useState(0) // 初始值传入一个对象 const [user,setUser] = useState({name:'Frank', age: 18}) // 传入一个函数,用函数的返回值作为初始值 const [m,setM] = useState( ()=>{return 0 }) return ( <div className="App"></div> ); }
-
使用细节:
-
初次渲染,useState返回的状态 (
state
) 与传入的第一个参数 (initialState
) 值相同。而在后续的渲染中,useState
返回的第一个值将始终是更新后最新的 state。 -
写接口(setState)不能局部更新数据,只能完全覆盖原来的state对象。而且不像类组件一样,函数组件不会合并属性。我们使用写接口更新数据时,要记得通过展开语法先引入原来的所有state数据,再进行数据的修改,如:
setState({...state,name:'jack'})
。 -
在使用写接口时,我们可以直接传入数据,同时我们还能够通过传入函数来修改state,传入的函数接收先前的 state ,并返回一个更新后的值 。这两个的区别在于:通过传入函数来修改state,我们可以连续使用多个setState来对state进行多次的修改。推荐尽量通过传入函数来使用setState。
// 每个按钮点击一次,结果 n=2,m=3 function App() { const [n, setN] = useState(0); const [m, setM] = useState(0); const onClick = () => { setN(n + 1); console.log(n); // 0 setN(n + 2); }; const onClick2 = () => { setM(i => i + 1); setM(i => { console.log(i); // 1 return i + 2; }); }; return ( <div className="App"> <h1>n: {n}</h1> <button onClick={onClick}>+2</button> <h1>m: {m}</h1> <button onClick={onClick2}>+2</button> </div> ); }
针对上面的例子:n为什么结果为2不等于3?m的结果为3?
归根结底,是因为setState参数传入的时机的问题。
首先,setState()执行后,会马上将组件的一次重新渲染加入队列。但是setState并不会马上执行,而是加入执行队列,等待主线程的代码执行完毕后才会执行。
setN
加入执行队列的时候,参数就已经传入了,实际上先执行了setN(0+1)
后执行了setN(0+2)
,而第2次setN
的结果覆盖了第1次的结果。而setM
加入执行队列的时候,参数是一个函数,这个参数具体的值只有在对应的setM
真正调用的时候才会确定。第1次setM
的时候,m
为0,所以传入的state为0。而第2次setM
的时候,m
为1,所以实际上执行的是setM(1=>1+2)
。 -
使用写接口更新对象类型的数据时,一定要注意更新对象的地址,否则React会认为state并没有变化。因此React不会触发组件的重新渲染。React检查state中某对象的数据变没变,是通过这个对象的地址是否改变来决定的(复杂类型看地址,简单类型看值)。
function App() { const [user,setUser]= React.useState({ name:'jack'}) const onClick = ()=>{ user.name = 'Ben'; setUser(user); } }
上面的代码执行
onClick
后不会触发组件的重新渲染,你会发现页面中的渲染的user.name
仍然是'jack'。
useReducer
-
作用:
作用和useState很像,是useState的替代方法。useReducer 可以将函数内部声明的state和操作state的方法抽离出来,单独声明集中处理。它接收一个形如
(state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的dispatch
方法。 -
使用:
下面是一个点击按钮,+1、+2的实例:
// 这里我们通过一个函数来存储需要初始化的state的值,后续会传给useReducer // 当然,也可以省略这个步骤,直接在useReducer中声明一个对象 const initial = { n: 0 }; // 声明reducer,后续会传给useReducer const reducer = (state, action) => { // 通过action.type 类型的不同选择触发不同的处理函数,对state进行不同的操作并返回 // 同时还能在调用dispatch时动态传入参数,这里先声明 action.number,后续调用时才传入具体的值 if (action.type === "add") { return { n: state.n + action.number }; } else if (action.type === "multi") { return { n: state.n * 2 }; } else { throw new Error("unknown type"); } }; function App() { // 使用useReducer,传入声明好的 reducer 和 initial // 并用 [state, dispatch] 接受返回的当前的state和dispatch const [state, dispatch] = useReducer(reducer, initial); const { n } = state; const onClick = () => { // 调用dispatch,操作state // action.type必须传入,其它的参数可以根据使用情况选择性传入 dispatch({ type: "add", number: 1 }); }; const onClick2 = () => { dispatch({ type: "add", number: 2 }); }; return ( <div className="App"> <h1>n: {n}</h1> <button onClick={onClick}>+1</button> <button onClick={onClick2}>+2</button> </div> ); }
上面的例子中比较关键的地方在于声明reducer时第2个参数action的使用。action是一个对象,通过action中的type属性,可以选择触发的函数,对state进行不同的操作。同时action中还能声明其它属性,如:
action.number
,在后续的调用时动态传入具体的值,根据不同的要求传入不同的值,实现一个函数,多重效果。 -
使用细节:
- 虽然useReducer使用起来比useState复杂,但是在一些复杂的情况下,会更具性价比,以及代码会更加清晰。
- useReducer()返回的内容,一般都用 [state, dispatch] 接受,当然也可以使用其他名字,但不推荐。
- useReducer的执行只能写在函数组件的内部,可以在组件外部声明函数调用useReducer,但是useReducer调用的时机一定是在函数组件内部。
- 可以使用useReducer来替代Redux。
useContext
-
作用:
局部实现一个类似全局作用域的东西,在该区域中的所有组件都能够通过useContext直接使用预设好的一些变量,不需要传入。
-
使用:
// 创建上下文 const C = React.createContext(null); function App() { const [n, setN] = useState(0); return ( // <C.Provider>圈定作用域范围,并传入需要全局使用的value <C.Provider value={{ n, setN }}> <div className="App"> <Baba /> </div> </C.Provider> ); } function Baba() { // 使用 useContext 来获取需要操作的数据 const { n, setN } = useContext(C); return ( <div> <!-- 使用获取的数据n --> 我是爸爸 n: {n} <Child /> </div> ); } function Child() { // 使用 useContext 来获取需要操作的数据 const { n, setN } = useContext(C); const onClick = () => { // 使用获取的方法 setN setN(i => i + 1); }; return ( <div> <!-- 使用获取的数据n --> 我是儿子 我得到的 n: {n} <button onClick={onClick}>+1</button> </div> ); }
- 使用
const ContextName = React.createContext(defaultValue)
创建上下文。React.createContext
支持传入一个默认值,如果不需要则可以传入null
。 - 使用
<ContextName.Provider value={...}>
圈定作用域,并通过value
传入值。圈定的作用域内部的所有组件,都能通过useContext(ContextName)
来获取value中的值。如果不使用<ContextName.Provider value={...}>
圈定作用域,并通过value
传入值的话,组件通过useContext(ContextName)
则可以获取到创建 ContextName 时传入的默认值。 - 作用域内部的组件通过
useContext(ContextName)
获取传递的值或默认值。
- 使用
-
使用细节:
- useContext并非响应式的。即,我们传入的需要全局使用的数据,如果被某个组件内部修改了,其他组件并不会知晓这个变化。
useEffect
-
作用:
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。而useEffect就是为了完成这些带有副作用的操作。
-
使用:
function App(){ const [n,setN] = useState(0) const [m,setM] = useState(0) useEffect(()=>{ const time = setTimeout(()=>{ console.log(n) },3000) return ()=>{ clearTimeout(time) } },[n]) }
useEffect()的2个参数:
第一个参数:类型为函数。在满足一定条件下执行的函数,该函数还可以返回一个函数。React称之为effect。
第二个参数:可选,类型为包含某些变量的数组。数组中的每个变量都是effect的依赖。
effect的执行:
- effect默认当组件初次渲染后执行。
- 当组件再次渲染后,effect会根据第二个参数中的依赖选择性执行。
- 不传参数,effect默认监听组件中的所有变量,即组件后续的每次渲染后effect都会执行。
- 参数为
[]
,即不监听任何变量,effect只会在初次渲染执行,组件后续的渲染effect都不会执行。 - 参数为
[n]
,即监听staten
,组件后续的渲染effect只会在n
改变时执行。
- effect返回的函数会在组件销毁前执行。
-
使用技巧:
-
effect的执行是在组件渲染完毕之后才执行。
-
一个组件内可以存在多个useEffect,执行顺序则按照代码顺序执行。
-
可以使用useEffect来模拟类组件中的:componentDidMount,componentDidUpdate,componentWillUnmount 三个生命周期。
-
不要在useEffect的effect中操作DOM。
-
-
useLayoutEffect
-
作用:
和useEffect类似,但是effect的执行时机和useEffect不同。useLayoutEffect的effect一般会操作DOM元素,那么页面渲染时就不会出现闪烁的效果。
-
使用:
所有参数都和useEffect相同。
const BlinkyRender = () => { const [value, setValue] = useState(0); useLayoutEffect(() => { document.querySelector('#x').innerText = `value: 1000` }, [value]); return ( <div id="x">value: {value}</div> ); };
-
使用细节:
-
effect的执行时机:
-
在使用上,一般来说在操作DOM元素(Layout)时才会使用useLayoutEffect。但是在实际使用上一般很少回去操作DOM,所以 useLayoutEffect使用频率较少,因为会对UI的渲染造成延迟。
-
如果组件中同时存在useLayoutEffect与useEffect,那么useLayoutEffect总是先执行。
-
尽量使用useEffect而不是useLayoutEffect。
-
useMemo
-
作用:
useMemo会根据传入的函数返回一个 memoized 值,这个 memoized 可以是任何类型的数据,这个 memoized 会缓存下来,只有依赖项改变后才会重新计算。如过不传递依赖项,则 memoized 中任何的变动都会触发重新计算。可以参考Vue的computed。通常会用于减少子组件的无意义渲染。
-
使用:
代码示例:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo接受一个具有返回值的函数,以及memoized依赖项的数组,数组中的依赖项改变后会重新计算memoized。如过不传递依赖项,则 memoized 中任何的变动都会触发重新计算。
function App() { const [n, setN] = React.useState(0); const [m, setM] = React.useState(0); const onClick = () => { setN(n + 1); }; return ( <div className="App"> <div> <button onClick={onClick}>update n {n}</button> </div> <Child data={m} number={1} /> <Child2 data={m} number={2} /> </div> ); } function Child(props) { console.log(`child ${props.number}执行了`); console.log("假设这里有大量代码"); return <div>child: {props.data}</div>; } const Child2 = React.memo(Child);
上面的实例中,useMemo接受的是一个函数组件(Child),并使用Child2接收。实例中点击按钮,触发
setN(n+1)
,父组件App重新渲染,此时正常情况下子组件也会重新渲染。但是我们使用了useMemo对其中一个子组件进行了缓存,并存储在Child2变量中,由于缓存的子组件中的依赖(props,state,事件函数)并没有改变,所以组件Child2并不会触发再次渲染。点击按钮,在控制台中log了child 1执行了
,假设这里有大量代码
,这就说明了只有第一个Child组件触发了重新渲染。 -
使用技巧:
-
useMemo能够优化组件没有意义的渲染,从而提高性能。
-
先编写在没有 useMemo 的情况下也可以执行的代码 , 之后再在你的代码中添加 useMemo,以达到优化性能的目的。
-
React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。
-
我们传过来的props不仅仅是简单类型,也有可能是对象,比如说:传过来一个数组,或者一个函数。此时当父组件重新执行后,这些对象虽然值还是原来的值,但是地址已经改变了,那么 memoized 也会重新计算。如:
<Child2 onclick={clickMe}>
,由于父组件再次渲染时声明的clickMe
内容没有改变,但是地址已经改变,所以Child2组件会触发再次渲染。对此我们可以将函数或者其他对象缓存起来:
const [m, setM] = React.useState(0); const onClickChild = useMemo(() => { const fn = () => { console.log("on click child, m: " + m); }; return fn; }, [m]); // 只有当依赖的 m 变化后才会从新计算函数
-
useCallback
-
作用:
useMemo的语法糖,用于优化缓存一个函数时的代码书写。
-
使用:
使用useMemo缓存一个函数时,我们需要通过函数来返回函数,而useCallback优化了这个写法:
// useMemo 通过函数返回函数 const myFn = useMemo( ()=> (x)=>log(x) , [m]) // useCallback 直接传入需要缓存的函数 const myFn = useCallback( (x)=>log(x) , [m] )
useRef
-
作用:
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。 ref可以保存任何可变值,甚至可以用于保存DOM节点。 -
使用:
代码示例:
const refContainer = useRef(initialValue);
常用方式命令式地访问子组件:
如果你将 ref 对象以
<div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的.current
属性设置为相应的 DOM 节点,这是常见的访问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> </> ); }
-
使用细节:
useRef
保存的数据的变化不会触发React组件的重新渲染。但是,我们可以借助setState来实现监听ref从而实现更新ref自动渲染,即当ref.current
改变时调用setState()
。useRef()
和自建一个{current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref 对象。
forwardRef
-
作用:
用于传递props无法传递的属性ref。
-
使用:
通过
React.forwardRef(props,ref)
来声明组件:function App() { const buttonRef = useRef(null); const onclick = ()=>{ console.log(buttonRef.current) } return ( <div className="App"> <!-- 这里的“按钮”,相当于value通过props传递过去了 --> <Button3 ref={buttonRef}>按钮</Button3> <button onClick={onclick}>log buttonRef</button> </div> ); } // 这里不是使用 function Button3(props){} 声明组件 const Button3 = React.forwardRef((props, ref) => { // {...props}是JSX语法 return ( <> <div>button3</div> <button/> <button style={{ color: "red" }} ref={ref} {...props} /> </> ); });
上面的实例中,我们将创建的ref(buttonRef)传递到了
<button className="red" ref={ref} {...props} />
上。当点击父组件的 log buttonRef 按钮时,会log出<button style="color: red;">按钮</button>
,说明我们已经获取到了这个<button>
的引用。
useImperativeHandle
-
作用:
useImperativeHandle
可以让你在使用ref
时自定义暴露给父组件的实例值。useImperativeHandle
应当与forwardRef
一起使用。 -
使用:
function App() { const buttonRef = useRef(null); useEffect(() => { console.log(buttonRef.current); }); return ( <div className="App"> <Button2 ref={buttonRef}>按钮</Button2> <button className="close" onClick={() => { console.log(buttonRef); buttonRef.current.x(); }} > x </button> </div> ); } // 使用了useImperativeHandle const Button2 = React.forwardRef((props, ref) => { const realButton = useRef(null); // 自定义暴露给父组件的实例值 useImperativeHandle(ref, () => { return { x: () => { realButton.current.remove(); }, realButton: realButton }; }); return <button ref={realButton} {...props} />; }); // 不使用useImperativeHandle const Button2 = React.forwardRef((props, ref) => { return <button ref={ref} {...props} />; });
上面的2种情况,我们通过父组件log
buttonRef.current
:- 使用了useImperativeHandle的结果:
Object {x: function x(), realButton: Object}
- 没有使用的结果则为:
<button>按钮</button>
- 我们甚至可以通过
buttonRef.current.x()
调用子组件暴露给父组件的方法。
- 使用了useImperativeHandle的结果:
自定义hooks
这是React Hooks最棒的地方,能够自由组合实现契合项目需求的hooks。
下面是一个自定义hooks实例:
使用这个自定义hook能够在组件渲染时获取list数据,以及处理list数据的2个方法。
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []);
return {
list: list,
addItem: name => {
setList([...list, { id: Math.random(), name: name }]);
},
deleteIndex: index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}
};
};
export default useList;