背景
React项目,是由N个大大小小的组件组合而成的,React创建组件的方式一种是类组件(class),一种是函数组件 (function),因为类组件性能消耗比较大,而函数组件是个纯函数,没有自身的状态,生命周期一说,因为对于复杂的状态共享等问题,React在16.8引入除render-props和HOC(高阶组件)之外,另一种解决办法React Hooks
例:该怎样实现组件的逻辑复用
const List = (props) => {
return (
{
props.list.map((item,index) => <div key={index}>{item.name}</div>)
}
)
}
const Content = ({isLogin,list=['张三','李四','王五']}) => {
return (
<>
{isLogin ? <List list={list} />:<p>请登录...</p>}
</>
)
}
-
render-props
-
功能:将一个组件内的state作为props传递给子组件,调用者可以动态决定如何渲
-
此问题实现方法:
class Toggle extends Component{ constructor(props){ super(props) this.state = { status:props.initialStatus, list:[] } } toggleStatus = () =>{ let {status} = this.state this.setState({ status:!status },()=>{ if(this.state.status){ this.getList() } }) } getList = () => { this.setState({ list:['Alex','Bob','Cooper','Diann'] }) } render(){ const {status,list} = this.state return ( <div status={status} list={list} toggleStatus={this.toggleStatus} > {this.props.children} </div> ) } } function App(){ return ( <Toggle initialStatus={false}> {({status,list,toggleStatus}) => ( <> <button onClick={toggleStatus}>{status?'退出登录':'登录'}</button> <Content status={status} list={list} /> </> )} </Toggle> ) }
-
-
hoc
- 定义:hoc(high-order-component)译为高阶组件,其本质是一个高阶函数,接受一个组件组为参数,返回一个新的组件,在这个新的组件中的状态共享,通过pops传给原来的组件
const Toggle = inititalStatus => WrapperComponent => { return calss extends Component { constructor(props){ super(props) this.state = { status:props.inititalStatus, list:[] } } toggleStatus = () =>{ let {status} = this.state this.setState({ status:!status },()=>{ if(this.state.status){ this.getList() } }) } getList = () => { this.setState({ list:['Alex','Bob','Cooper','Diann'] }) } render(){ const {status,list} const newProps = { ...this.props, status:status, list:list, toggleStatus:this.toggleStatus }0 return <WrapperComponent ...newProps /> } } } //装饰器模式 只能装饰类 @Toggle(false) class App extends Component{ const {status,list,toggleStatus} = this.props return ( <> <button onClick={toggleStatus}>{status?'退出登录':'登录'}</button> <Content status={status} list={list} /> </> ) } 不论是render-props 还是hoc都有各自的优缺点,render-props和hoc都容易引起地域回调 ,而且hoc固定的props可能容易被子组件劫持覆盖,hooks因此油然而生从而完美解决了以上问题
Hooks
-
定义:hooks意为’钩子‘,react hooks鼓励我们组件尽量写成纯函数,如果需要外部功能状态或副作用就用hooks调用,常见的钩子有以下几种
-
useState
- 保存组件状态,接受状态的初始值作为参数,返回一个数组,其中数组第一项为一个变量,指向状态的当前值,第二项是一个函数,用来更新状态,约定有set为前缀+状态的变量名为函数名
-
//实现一个简单的计数器 Class Interval extends Component { constructor(props){ super(props) this.state = { count:1 } } add = val => { this.setState({ count: this.state.count += 1 }) } reduce = val => { this.setState({ count: this.state.count -= 1 }) } render() { const {count} = this.state return ( <> <button onClick={reduce(1)}>-</button> <div>{count}</div> <button onClick={add(1)}>+</button> </> ) } } //使用hooks import React ,{useState} from 'react' function Interval(){ const [count, setCount] = useState(0) return ( <> <button onClick={() => setCount(count-=1)}>-</button> <div>{count}</div> <button onClick={() => setCount(count+=1)}>+</button> </> ) }
-
useEffect
- 副作用钩子,类似componentDidMount和compoonentDidUpdate结合体,useEffect第一个参数接收一个函数,用来做一些''副''作用(异步请求,修改外部参数等行为),第二个参数是一个数组,如果数组中的值变化才会触发useEffect第一个参数中的函数,如果第二个参数为空数组,则指挥在组件加载进入dom后才会触发副作用,如果不传第二个参数,则会在每次渲染时候都会触发副作用
-
import React,{useState,useEffect} from 'React' function Interval(){ const [count,setCount] = useState(0) UseEffct(()=>{ document.title='当前count值为:'+ count }) return ( <> /*********/ </> ) }
-
useContext
- 跨组件共享状态狗子,在组件间共享状态,接收一个Context对象,并返回该context的当前值, useContext 的参数必须是 context 对象本身
-
import React, {useState,createContext,useContext} from 'react' impoft {Button} from 'antd' const typeList = ['primary','default','text','ghost','default','dashed'] const GlobalContext = createContext({type:'primary'}) const BtnA = () => { const {type} = useContext(GlobalContext) return <Button type={type}>按钮A</Button> } const btnB = ()=> { return <Button type={type}>按钮B</Button> } function BtnGroup(){ const [type,setType] = useState('primary') const toggelType = () => { let index = Math.floor(Math.random() *6) setType(typeList[index]) } return ( <> <Button onClick={toggelType}>切换按钮类型</Button> <GlobalContext.Provider value = {{type}})> <BtnA /> <BtnB /> </GlobalContext.Provider> </> }
-
useReducer
-
状态管理action钩子,hooks提供了useReducer功能,给function组件提供类似redux功能,引入useReducer后,useReducer接受一个reducer(纯函数)和一个初始state值根据reducer中的action来更新state,然后返回一个数组,第一个值是状态(state),第二个值是用来调用发布时间来更新state(dispatch)
-
import React, {useReducer} from 'react' //老样子,实现一个计数器 function Interval(){ const [count,dispatch] = useReducer((state,action)=> { //眼熟不? switch(action.type) { case 'increment': return state + action.num case 'decrement': return state - action.num default: return state } },0) return ( <div> <button onClick={()=>{dispatch({type:'decrement',num:3})}}>减3</button> <p>{count}</p> <button onClick={()=>dispatch({type:'increment',num:2}) }>加2</button> </div> ) } export default Interval -
useReducer和useContext careateContext混合使用实现一个局部的redux功能
//Context.js import React ,{createContext}from 'react' export const myContext = createContext(null)//App.js import React, { useReducer } from "react"; import { myContext } from "./Context/myContext"; import { listReducer } from "./reducer"; import List from "./components/List"; import Form from "./components/Form"; const { Provider } = myContext; function App() { const [state, dispatch] = useReducer(listReducer, { list: [], name: "", }); return ( <Provider value={{ state, dispatch }}> <Form /> <List /> </Provider> ); } export default App;//reducer.js export const listReducer = (state, action) => { var { list } = state; switch (action.type) { case "increment": if (action.name.length) { list.push(action.name); } return { ...state, list, name: "", }; case "decrement": let index = list.findIndex((item) => item === action.name); let newList = list.slice() newList.splice(index, 1) return { ...state, list: newList }; case "setName": return { ...state, name: action.name, }; default: return state; } };//List.js import React, { useContext } from "react"; import { myContext } from "./../../Context/myContext"; function List() { const { state, dispatch } = useContext(myContext); return ( <ul> {state.list.map((item, index) => { return ( <li key={index}> <span> {item} </span> {state.list.length ? ( <button onClick={() => dispatch({ type: "decrement", name: item })} > 删除 </button> ) : null} </li> ); })} </ul> ); } export default List;import React, { useContext } from "react"; import { myContext } from "./../../Context/myContext"; function Form() { const { state, dispatch } = useContext(myContext); const handleInputChange = (el) => { dispatch({type:'setName',name:el.target.value}) } return ( <div className="form-box"> <span>姓名:</span> <input value={state.name} placeholder="请输入姓名" onChange={handleInputChange} /> <button className="add-btn" onClick={() => dispatch({ type: "increment", name: state.name })}> 新增 </button> </div> ); } export default Form;
-
-
useCallback
-
记忆函数钩子,该函数又两个参数,1.useCallback会固定该函数的引用,只要依赖项没有发生改变,则之中返回之前函数的地址,2数组,记录依赖项
-
场景重现:
function Parents(props){ const handleMthdos = () => { ////// } rturn( <div> <Child handleMthdos={handleMthdos} /> </div> ) } 如上 如果Parent组件的props改变了,那么Parents组件就会重新渲染,Child组件也会重新渲染,即使Child没有使用到props,这是不需要重新渲染的,可以使用useCallback稍稍优化一下:
import React,{useCallback} from 'react' function Parents(props) { const handleMethods = useCallback(()=> { ////// },[]) }第二个参数传入一个数组,数组中的每一项值或者引用发生改变,useCallback就会重新返回一个新的记忆函数提供给Child组件进行渲染,空数组表示无论什么情况下该函数都不回发生改变
-
-
useMemo
- 记忆组件钩子,和useCallback功能类似,不过useCallback不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并返回执行 的结果
- 上面的例子也可改成
function Parents { const count = useMemo(()=> { ////// return **** },[]) retuen ( <div> <Child count={count} /> </div> ) }
-
useRef
- 保存引用值钩子,返回一个可变的ref对象,该对象只有一个current属性,初始值为传入的参数
- 返回的ref对象在组建的整个生命周期内保持不变
- 与setState不同的是,useRef更新current的值并不会重新渲染
- 更新useRef一般写在useEffect里面或者事件监听器里面
- 类似于class组件的this
function App() { const [count, setCount] = useState(0); const lastCount = useRef(count); console.log(lastCount) useEffect(() => { lastCount.current = count; }); function handleClick() { console.log(count); setTimeout(() => { alert("You clicked on:" + lastCount.current); }, 3000); } return ( <div> <div>you clicked {count} times</div> <button onClick={() => { setCount(count + 1); }} > click me </button> <button onClick={handleClick}>show alert</button> </div> ); }
-
useImperativeHandle
- 透传ref:通过useImperativeHandle用于让父组件获取子组件内的索引
import React, { useRef, forwardRef } from "react"; import Child from "./components/Child"; const ChildInput = forwardRef(Child); function App() { const inputRef = useRef(null); const handleClick = () => { inputRef.current.focus(); }; return ( <div className="app-page"> <div className="app-title">父组件</div> <ChildInput ref={inputRef} /> <button onClick={handleClick}>聚焦</button> </div> ); } //Child.js import React, { useRef, useImperativeHandle } from "react"; function Form(props, ref) { const inputRef = useRef(null); useImperativeHandle(ref, () => inputRef.current); const handleFocus = () => { console.log("从外界聚焦"); }; return ( <div className="child"> <div className="title">子组件</div> <input ref={inputRef} onFocus={handleFocus} /> </div> ); }
- 透传ref:通过useImperativeHandle用于让父组件获取子组件内的索引
-
useLayoutEffect
- 其函数签名与useEffect相同,但是调用时机不同。useLayoutEffect会在dom更新之后马上同步调用代码,可能会阻塞页面渲染,而useEffect会在整个页面渲染完才会调用代码
- 为避免阻塞视觉更新,尽量使用useEffect
useEffect(()=> { moveTo(ref.count,300) //使用useEffect会出现滑动的动画过程 }) useLayoutEffect(()=> { moveTo(ref.count,300) //使用useLayoutEffect会直接出现在移动后的位置 }) const moveTo = (dom,delay,x) => { domm.style.transform = 'translate('+ x + 'px)' dom.style.transition = 'left 3s' } return ( <div> <div ref={ref}></div> </div> )
总结
-
hooks可以解决的问题
- 组件之间的逻辑状态难以复用
- 大型复杂的组件很难拆分
- class语法的使用不友好
-
hooks优缺点
- 优点
- 没有破坏性改动,完全可选
- 更容易复用代码
- 代码量少
- 容易拆分
- 缺点
- 状态不同步,函数的运行是独立的,每个函数都有一份独立的闭包作用域
- 使用useState无法直接更改数组对象
- useEffect依赖引用类型可能会触发死循环
- 不能再循环,条件中或者嵌套函数中调用hooks,必须在react函数顶层使用hooks,因为react需要利用调用顺序来正确更新相应的状态,如果顺序打乱了,可能会出现预料不到的后果
- 优点
-
useEffect会根据第二个参数来决定会不会处理副作用
- 不传第二个参数,代表不监听任何参数变化,每次渲染dom都会执行useEffect中的函数
- 如果第二个参数为空数组,则只会在组件初始化或者组件被销毁时触发
-
useMemo和useCallback
- 接受参数一样,执行机制一样,只有当第二个参数发生变化时,才会执行第一个参数里面的回调,才会重新计算结果,也起到缓存的作用
- useMemo计算结果是return回来的值,主要用于缓存计算结果的值,useCallback计算结果是函数,主要用于缓存函数
-
ref,useRef,createRef,forwardRef
- ref可以拿来直接调用,但是不能在函数式组件中使用的,因为函数式组件是没有实例的
- createRef每次渲染会返回一个新的引用
- useRef每次都会返回相同的引用
- fordwardRef会创建一个react组件,这个组件能够将其接受的ref属性转发到组件数下的另一个组件中,如果子组件不想暴露全部方法,就可以使用useImperativeHandle来自定义暴露给父组件的实例值
-
useEffect和useLayoutEffect
-
useEffect是异步执行的,useLayoutEffect是同步执行的
-
useEffect是浏览器执行渲染完成之后,useLayoutEffect是浏览器把内容渲染到界面之前和componentDidMount等价,会导致视觉阻塞
-