课程目标
- 掌握函数组件的创建与使用;
- 掌握 React 常见 Hooks 使用;
- 掌握如何自定义 Hook。
课程内容
函数式组件
- 函数式组件,本质就是一个常规函数,接收一个参数 props 并返回一个 reactNodes,即 return 该组件需要输出的视图;
- 函数式组件中没有 this 和生命周期函数;
- 使用函数式组件时,应该尽量减少在函数中声明子函数,否则组件每次更新时都会重新创建这个函数。
React Hooks(钩子)
-- React Hooks 是 React 16.8 中的新增功能,它们使您无需编写类即可使用状态和其它 React 功能。
常用 Hook
useState
const [state, setState] = useState(initialState);
- const [状态,修改该状态的方法] = useState(初始值);
- 在同一组件中可以使用 useState 定义多个状态;
- 注意 useState 返回的 setState 方法不会进行对象的浅合并,即 setState 方法只接收一个参数,该参数代表的就是更新完之后的新状态;
- 会受到批处理影响,多次调用 setState 会被合并只执行一次;
- 注意 useState 返回的 setState 方法同样是异步方法。
import { useState } from "react"; function Child(props) { const [count, setCount] = useState(5); const [num, setNum] = useState(0) console.log('render'); return ( <> <h3>Child:{props.info}</h3> <h3>Count: {count}</h3> <h3>Num: {num}</h3> <button onClick={ () => { setCount(count+1); setCount(count+1); setCount(count+1); setCount(count+1); setCount(count+1); console.log('setCount'); // 先打印 setCount,然后打印 render,count只加1 }} >Count递增</button> <button onClick={ () => { setTimeout(() => { setNum(num+1) setNum(num+1) setNum(num+1) setNum(num+1) setNum(num+1) console.log('setNum'); // 先打印两次 render,再打印 setNum,num只加1 }, 0); }} >Num递增</button> </> ); } export default Child;
- 只在 React 函数中调用 Hook:
- React 函数组件中;
- React 自定义 Hook 中;
- 只在最顶层使用 Hook:在 React 中要保证 hook 的执行顺序在组件更新前后一致,否则会报错
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
,原因且看下面 hook 实现原理:// useState 实现原理初版 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"></div> <script> let state=null; function useState(init){ let nowState = state === null?init:state; return [nowState,(newState)=>{ state=newState; render(); }]; } function render(){ let root = document.querySelector("#root"); let p = document.createElement("p"); let btn = document.createElement("button"); let [count,setCount] = useState(10); p.innerHTML = "count:" + count; btn.innerHTML = "递增"; root.innerHTML = ""; btn.onclick=()=>{ setCount(count+1); } root.appendChild(p); root.appendChild(btn) } render(); </script> </body> </html>
// hook 为什么要保持调用顺序 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"></div> </body> <script> let state = []; let index = 0; function useState(init) { let nowIndex = index++; if(state[nowIndex] === undefined) { state[nowIndex] = init; } return [state[nowIndex], val => { state[nowIndex] = val; console.log(nowIndex, val) render(); }] } function render() { index = 0; const div = document.createElement('div'); const button = document.createElement('button'); const root = document.querySelector('#root'); const [count, setCount] = useState(10); div.innerHTML = 'Count: ' + count; button.innerHTML = '递增'; button.onclick = function() { setCount(count + 1); } root.innerHTML = ''; root.appendChild(div); root.appendChild(button); const div2 = document.createElement('div'); const button2 = document.createElement('button'); const [num, setNum] = useState(20); div2.innerHTML = 'Count2: ' + num; button2.innerHTML = '递增2'; button2.onclick = function() { setNum(num + 1); } root.appendChild(div2); root.appendChild(button2); } render(); </script> </html>
useEffect
- 可以完成的作用等同于类组件中的生命周期函数:componentDidMount、componentDidUpdate 和 componentWillUnmount;
- 需要清除的副作用。
// 基本使用 import { useEffect, useState } from "react"; function Effect() { /** * useEffect 副作用钩子:用于在 React 函数中来处理副作用【数据请求、DOM 操作】 useEffect( () => { // effect 函数 return () => { // cleanUp 函数, 可选 } }, [依赖数据]) // 依赖函数也是可选的 挂载阶段: 从上向下一行行执行代码,如果碰到 useEffect,则将对应的 effect、cleanUp 函数分别存储到一个队列中,当组件挂在完成后,按添加顺序执行 effect 函数;cleanUp 函数不执行; 更新阶段: 从上向下一行行执行代码,如果碰到 useEffect,则将对应的 effect 函数存储到一个队列中;当组件更新完成之后,找到之前存储的 cleanUp 队列,依次执行;然后再按照添加顺序依次执行 effect 队列、获取 cleanUp 函数并存储到一个队列中;如果有依赖参数,则对应的依赖参数发生变化才会执行更新阶段的代码;如果没有依赖参数,则只要组件有更新就会执行更新阶段代码; 卸载阶段: 找到对应的 cleanUp 队列,依次执行。 依赖参数:(只对更新阶段有影响) 1、不写依赖参数 useEffect(()=>{}),组件有更新,就会执行 cleanUp 函数 和 effect 函数; 2、有具体依赖参数(1-多个)useEffect(()=>{},[a[,b,...]),则组件更新时,其依赖参数有变化,会执行其 cleanUp 函数和 effect 函数; 3、有具体依赖参数(0个)useEffect(()=>{},[]),则组建更新时,不会执行其 cleanUp 和 effect 函数,只有加载完成和卸载阶段会执行这里的代码。 */ const [count, setCount] = useState(5); useEffect( () => { console.log('effect-1'); return () => { console.log('cleanUp-1'); } }); useEffect( () => { console.log('effect-2'); return () => { console.log('cleanUp-2'); } }); console.log('render'); return ( <> <h3>Count: {count}</h3> <button onClick={()=>setCount(count+1)}>递增</button> </> ); } export default Effect;
- 和类组件的生命周期进行对应:能对应到的生命周期如下
- 挂在完成之后,做某事;
- 更新完成之后,做某事;
- 挂载完成和更新完成之后,做某事;
- 即将卸载时,做某事。
import { useEffect, useRef } from "react"; function EffectLife() { useEffect( () => { console.log('2--挂载完成之后'); return () => { console.log('即将卸载时做某事'); } }, []); useEffect( () => { console.log('3--更新、挂载完成之后都会做某事'); }); // 只在更新阶段做某事【需要使用到 useRef】 const isMount = useRef(false); useEffect( () => { if(isMount.current) { console.log('更新阶段做某事'); } else { isMount.current = true; } }) console.log('1--render'); return ( <> <h3>Effect Life</h3> </> ); } export default EffectLife;
useRef
- 用户关联原生 DOM 节点;
- 或者用来记录组件更新前的一些数据。
import { useEffect, useRef, useState } from "react"; function RefTest() { // const ref = useRef(init); // 1.关联节点实例 // 2.传递组件更新前的一些数据,当 useRef 中存储的是某项数据时,该数据并不会随着组件的更新而自动更新 const [count, setCount] = useState(5888); const ref = useRef(count); const countRef = useRef(); useEffect( () => { ref.current = count; console.log(ref, countRef.current.innerHTML); }) return ( <> <div ref={countRef}>Count: {count}</div> <button onClick={()=> { setCount(count+1); }} >递增</button> </> ); } export default RefTest;
useMemo
- const data = useMemo(cb, []); 类似于 useState,有一个依赖参数。
// 发现做任何操作,都会执行 maxValue 函数 import { useState } from "react"; function MemoTest() { const [count, setCount] = useState(1); const [num, setNum] = useState(5); const [val, setVal] = useState(''); const plusCount = () => { console.log('plusCount'); setCount(count+1); }; const minusCount = () => { console.log('minusCount'); setCount(count-1); }; const plusNum = () => { console.log('plusNum'); setNum(num+1); }; const minusNum = () => { console.log('minusNum'); setNum(num-1) }; const valueChange = ({target}) => { console.log('valueChange'); setVal(target.value) }; const maxValue = () => { console.log('maxValue'); return count>num?"count":"num"; }; return ( <> <h3>MemoTest</h3> <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div> <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div> <p>当前比较大的值为:{maxValue()}</p> <div><input value={val} onChange={valueChange} /></div> <p>{val}</p> </> ); } export default MemoTest;
import { useMemo, useState } from "react"; /** * useMemo(cb, []) * 不是函数,直接等于 cb 的返回值。 * 发现只有修改 count、num 的值才会打印 maxValue,在输入框中输入内容时不再打印 */ function MemoTest() { const [count, setCount] = useState(1); const [num, setNum] = useState(5); const [val, setVal] = useState(''); const plusCount = () => { console.log('plusCount'); setCount(count+1); }; const minusCount = () => { console.log('minusCount'); setCount(count-1); }; const plusNum = () => { console.log('plusNum'); setNum(num+1); }; const minusNum = () => { console.log('minusNum'); setNum(num-1) }; const valueChange = ({target}) => { console.log('valueChange'); setVal(target.value) }; const maxValue = useMemo(() => { console.log('maxValue'); return count>num?"count":"num"; }, [num, count]); return ( <> <h3>MemoTest</h3> <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div> <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div> <p>当前比较大的值为:{maxValue}</p> <div><input value={val} onChange={valueChange} /></div> <p>{val}</p> </> ); } export default MemoTest;
useCallback
- 是 useMemo 的一个进化体;
- 当 useMemo 返回一个函数时,可以使用 useCallback 来改进;
- 将上面的代码进行改进,修改 plusCount,只有在 count 进行更新时才会进行声明子函数:
import { useCallback, useMemo, useState } from "react"; /** * useMemo(cb, []) * 当 useMemo 返回值是一个函数时,可以使用 useCallback 来优化 */ function CallbackTest() { const [count, setCount] = useState(1); const [num, setNum] = useState(5); const [val, setVal] = useState(''); const plusCount = useCallback( () => { console.log('plusCount'); setCount(count+1); }, [count]); const minusCount = useCallback(() => { console.log('minusCount'); setCount(count-1); }, [count]); const plusNum = useCallback(() => { console.log('plusNum'); setNum(num+1); }, [num]); const minusNum = useCallback(() => { console.log('minusNum'); setNum(num-1) }, [num]); const valueChange = useCallback(({target}) => { console.log('valueChange'); setVal(target.value) }, [val]); const maxValue = useMemo(() => { console.log('maxValue'); return count>num?"count":"num"; }, [num, count]); return ( <> <h3>MemoTest</h3> <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div> <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div> <p>当前比较大的值为:{maxValue}</p> <div><input value={val} onChange={valueChange} /></div> <p>{val}</p> </> ); } export default CallbackTest;
- 使用 useCallback、useMemo,会更麻烦一点,但是性能会有很大提升。
Memo
- React.memo(Component, [areEqual(prevProps, nextProps]);
Hook 使用规则
- 只在 React 函数中调用 Hook;
- React 函数组件中;
- React 自定义 Hook 中;
- 只在最顶层使用 Hook。
自定义 Hook
- 常用来解决(与状态相关的)逻辑复用问题;
- 自定义 hook 就是一个普通函数,但是该函数必须以 use 开始命名;
import { useCallback, useMemo, useState } from "react"; function useCount(init) { const [count, setCount] = useState(init); const plusCount = useCallback(() => { setCount(count + 1); }, [count]); const minusCount = useCallback(() => { setCount(count - 1); }, [count]); return [count, plusCount, minusCount]; } function HookTest() { const [count, plusCount, minusCount] = useCount(1); const [num, plusNum, minusNum] = useCount(5); const [val, setVal] = useState(''); const valueChange = useCallback(({target}) => { console.log('valueChange'); setVal(target.value) }, [val]); const maxValue = useMemo(() => { console.log('maxValue'); return count>num?"count":"num"; }, [num, count]); return ( <> <h3>MemoTest</h3> <div>Count: {count}; <button onClick={plusCount}>+</button>, <button onClick={minusCount}>-</button></div> <div>Num: {num}; <button onClick={plusNum}>+</button>, <button onClick={minusNum}>-</button></div> <p>当前比较大的值为:{maxValue}</p> <div><input value={val} onChange={valueChange} /></div> <p>{val}</p> </> ); } export default HookTest;