Hooks 逐一解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
简介: Hooks 逐一解析

Hooks 逐一解析


参考React文档

Hooks:useState、useEffect、useLayoutEffect、useContext、useReducer、useMemo、React.memo、callCallback、useRef、useImperativeHandle、自定义Hook、useDebugValue

进一步学习以及复习 PowerPoint 演示文稿

网络异常,图片无法展示
|

useState(最常用)

在React的函数组件里,默认只有属性,没有状态。

使用状态

//数组第1项是读接口,第2项是写接口,初始值0 
const [n,setN] = React.useState(0) //数字
const [user,setUser] = React.useState({name:'F'}) //对象

注意事项

1.不可局部更新

如果state是一个对象,是不能部分 setState 的。 因为setState不会帮我们合并属性。所以当只更新部分属性时,未更新的属性就会消失。

那怎么解决"未更新的属性会消失"的问题?

import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
    name: 'Jack'
  })
}
return (
  <div className="App">
    <h1>{user.name}</h1>
    <h2>{user.age}</h2>
    <button onClick={onClick}>Click</button>
  </div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

网络异常,图片无法展示
|

当点击按钮

网络异常,图片无法展示
|

...拷贝之前所有的属性,然后再覆盖属性。

import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
  const [user,setUser] = useState({name:'Frank', age: 18})
  const onClick = ()=>{
    setUser({
      ...user, //拷贝user的所有属性
      name: 'Jack' //覆盖name
    })
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

题外话:useReducer也不会合并属性,React新版的所有东西都不会帮你合并,它认为这是你自己要做的事。

2.地址要变

setState(obj) ,如果obj地址不变,那么 React 就认为数据没有变化。

useState 可接受函数

当初始值比较复杂时,可采用。

const [state,setState] = useState(()=>{
    return initialState
})

该函数返回初始 state ,且只执行一次。

setState 接受函数

点击button后你会发现n=1而不是2,因为当你setN(n+1)时,n不会变。 不管你做多少次计算,只有最后一次有用。

解决方法: 改成函数

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
  //setN(n+1) 第1次计算
  //setN(n+1) 第2次计算,也是最后1次计算
    setN(n => n + 1) //形式化的操作
    setN(n => n + 1)// 你会发现 n 不能加 2
    // setN(i=>i+1)
    // setN(i=>i+1)
  }
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+2</button>
    </div>
  );
}

JS语法有问题:对象必须加()。(JS的bug)

总结:对state进行多次操作时,优先使用函数。

useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法

useReducer4步走:

1.创建初始值initicalState

const initical = { n:0 }

2.创建所有操作reducer(state,action)

reducer接受2个参数:旧的状态state操作的类型action(一般是类型),最后返回新的state。

怎么得到新的state?

看下动作的的类型是什么

规则和useState一样,必须返回新的对象。 (不能直接操作n)

const reducer=(state,action)=>{
  if(action.type==='add'){
    return { n:state.n+1 } //return新对象
  }else if(action.type==='mult'){
    return { n:state.n*2 }
  }else{
    console.log("unknown type")
  }
}

3.传给useReducer,得到读和写API

(1)需要导入useReducer或者直接使用全称React.useReducer

(2)useReducer接收2个参数:所有操作reducer初始状态initical

(3)你将得到读API、写API写API一般叫dispatch,因为你必须通过reducer才能setState,所以叫dispatch。

import React,{useReducer} from "react"
function App(){
  const [state,dispatch]=useReducer(reducer,initical)
}

拿出属性n的2种方法:

1' {state.n} 2'const {n}=state然后{n}

4.调用 写({type:'操作类型'})

const onClick=()=>{
  dispatch({
    type:'add' //调用reducer的add操作
  })
}

相当于useState,只不过把所有操作聚拢在一个函数里,这样的好处是:调用的代码简短了。

调用传参:+2时传了参数number:2,那么reducer里的1就可以变成一个参数。因为dispatch()里传的对象就是action。

if (action.type === "add") {
//return { n: state.n + 1 };
  return { n: state.n + action.number };
}
...
const onClick2 = () => {
//dispatch({type:'add'})
  dispatch({type:'add',number:2}) //里面的对象就是action
}

这就是useReducer对useState的升级操作,总的来说useReducer是useState的复杂版。好处是用来践行React社区一直推崇的flux/Redux思想。随着hooks的流行这个思想会退化。完整代码

import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";
const initial = { n: 0};
const reducer = (state, action) => {
  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() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    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>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

如何选择 使用useReducer还是useState?

事不过三原则

如果你发现有几个变量应该放一起(对象里)这时候就用useReducer对对象进行整体的操作。useReducer的常用例子

const initFormData = {
  name: "",
  age: 18,
  nationality: "汉族"
};
function reducer(state, action) {
  switch (action.type) {
    case "patch": //更新
//把第1个对象的所有属性和第2个对象的所有属性全部放到第3个空对象里,这就是更新
      return { ...state, ...action.formData }; 
    case "reset": //重置,返回最开始的对象
      return initFormData;
    default:
      throw new Error("你传的啥 type 呀");
  }
}
function App() {
  const [formData, dispatch] = useReducer(reducer, initFormData);
  // const patch = (key, value)=>{
  //   dispatch({ type: "patch", formData: { [key]: value } })
  // }
  const onSubmit = () => {};
  const onReset = () => {
    dispatch({ type: "reset" });
  };
  return (
    <form onSubmit={onSubmit} onReset={onReset}>
      <div>
        <label>
          姓名
          <input value={formData.name} onChange={e => dispatch(
            {type:"patch", formData:{ name: e.target.value }})
            }
          />
        </label>
      </div>
      <div>
        <label>
          年龄
          <input value={formData.age} onChange={e =>dispatch(
            {type:"patch",formData: { age: e.target.value }})
            }
          />
        </label>
      </div>
      <div>
        <label>
          民族
          <input value={formData.nationality} 
            onChange={e => dispatch({type:"patch",
              formData:{nationality: e.target.value}})
            }
          />
        </label>
      </div>
      <div>
        <button type="submit">提交</button>
        <button type="reset">重置</button>
      </div>
      <hr />
      {JSON.stringify(formData)}
    </form>
  );
}

网络异常,图片无法展示
|

用户一旦输入就会触发onChange事件。用户输入即更新,因为内容不一样了嘛。 每次更新,App都会render遍。

如何用useReducer代替Redux ?

前提:你得知道Redux是什么 用React的reducer+context即可代替Redux。

网络异常,图片无法展示
|
例子

模块化的实现

useContext(常用)

概念

上下文就是你运行一个程序所需要知道的所有其它变量(全局变量)。

全局变量是全局的上下文,所有变量都可以访问它。

上下文是局部的全局变量,context只在<C.Provider>内有用,出了这个范围的组件是用不到这个contextde。

使用方法:

一.使用C = createContext(initical)创建上下文

二.使用<C.provider value={}>初始化并圈定作用域

三.在作用域内的组件里使用useContext(C)来获取上下文

import React, { createContext } from "react";
const C = createContext(null)
<C.Provider value={}>
  ...
</C.Provider>

value的初始值可以是任何值,一般我们会给一个读写接口.

<C.Provider>内的所有组件都可以用上下文C

例子

+1操作的不是本身的state,而是从App那里得到的读、写接口。 App也可以不用state,用reducer: const [n, setN] = useState(0);,context不管你用啥,它只是告诉你n、setN可以共享给你的子代的任何组件的,范围就是由<C.Provider>圈定的。

useContext注意事项

不是响应式的

你在一个模块将C里面的值改变,另一个模块不会感知到这个变化。

更新的机制并不是响应式的,而是重新渲染的过程。

比如,当我们点击+1时:setN去通知useState,useState重新渲染App,发现n变了,于是问里面的组件<Baba />有没有用到n?没有,就继续问<Child />有没有用到n?用到了,这时候儿子就知道要刷新了,是一个从上而下逐级通知的过程,并不是响应式的过程。

Vue3是你改n时,它就知道n变了,于是它就找谁用到了n,它就把谁直接改变了。它不会从上而下整体过一遍,没有这么复杂,因为它是一个响应式的过程。

总结: useContext的更新机制式是自顶向下,逐级更新数据。 而不是监听这个数据变化,直接通知对应的组件。

useEffect & useLayoutEffect

useEffect

副作用

  • 对环境的改变即为副作用,如修改 document.title
  • 不一定非要把副作用放在 useEffect 里
  • 实际上叫做 afterRender 可能更好,每次 render 后执行

用途

  • 作为componentDidMount 使用,[]作第二个参数
  • 作为 componentDidUpdate 使用, 可指定依赖
  • 作为componentWillUnmount 使用,通过 return
  • 以上三种用途可以同时存在

特点

如果同时存在多个 useEffect ,会按照出现次序执行。

useLayoutEffect

useEffect 在浏览器渲染完成后执行

useLayoutEffect 在浏览器渲染前执行

特点

  • useLayoutEffect 总是比 useEffect 先执行
  • useLayoutEff 里的任务最好影响了 Layout

为了用户体验,优先使用 useEffect

useEffect和useLayoutEffect的本质区别:

useEffect在浏览器渲染完成后执行,useLayoutEffect在浏览器渲染完成前执行。

特点

1.useLayoutEffect总是比useEffect先执行。

下面的代码打印2和3,再打印1。

useEffect(()=>{ 
   if(time.current){ console.log("1") },[])
 }
 useLayoutEffect(()=>{ 
   if(time.current){ console.log("2") },[])
 }
 useLayoutEffect(()=>{ 
   if(time.current){ console.log("3") },[])
 }  

2.useLayoutEffect里的任务最好影响了Layout

如果没有改变屏幕外观Layout,就没必要放浏览器渲染前,占时间。

经验: 为了用户体验,优先使用useEffect(优先渲染)

useMemo

要理解React.useMemo需要先了解React.memo

useCallback是useMemo的语法糖React.memo

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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}/>
      {/* <Child2 data={m}/> */}
    </div>
  );
}
function Child(props) {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React默认有多余的render,点击按钮,Child() 执行了,但Child 依赖数据并没有改变,此时,可使用React.memo(Child)代替 Child。

如果props不变,就没必要再执行一个函数组件。

但是,React.memo有个 bug

const onClickChild = ()=>{}

把一个监听函数传给这个组件时,即使监听函数什么也不做,每次当外部组件数据改变重新渲染时,这个组件也会执行。

这是因为每次重新执行 App() ,都会生成一个新的监听函数,和之前的监听函数地址不同,所以会导致这个组件也执行。

使用useMemo可以解决这个问题

const onClickChild = useMemo(()=>{ return console.log(m) },[m])

useMemo特点

  • 第一个参数是 ()=> value
  • 第二个参数是依赖[m,n]
  • 只有当依赖变化时,才会计算出新的 value,如果依赖不变,那么就重用之前的value

注意

如果你的 value 是个函数,那么就要写成 useMemo(()=> ()=> console.log(x))

这是一个返回函数的函数,很难用,于是有了 useCallback

useCallback

用法

useCallback(x=>log(x),[m]) 等价于 useMemo(()=>x=>log(x),[m])

useRef

目的

  • 如果需要一个值,在组件不断 render 时保持不变
  • 初始化: const count=useRef(0)
  • 读取:count.current
  • 为什么需要current ,为了保证两次useRef是同一个值(只有引用能做到)

forwardRef

props无法传递ref属性。

import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}
const Button3 = React.forwardRef((props, ref) => {
  return <button className="red" ref={ref} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useImperativeHandle

用于自定义 ref 的属性

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
  const setRef = useImperativeHandle;
  setRef(ref, () => {
    return {
      x: () => {
        realButton.current.remove();
      },
      realButton: realButton
    };
  });
  return <button ref={realButton} {...props} />;
});

自定义 Hook

封装数据操作新建目录hooks,新建文件useList.js

import { useState, useEffect } from "react";
const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    setList: setList
  };
};
export default useList;
function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" },
        { id: 3, name: "Alice" },
        { id: 4, name: "Bob" }
      ]);
    }, 2000);
  });
}

index.js

import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
  const { list, setList } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

总结

1.useState状态

2.useEffect(副作用)就是afterRender

3.useLayoutEffect就是比useEffect提前一点点。

但是很少用,因为会影响渲染的效率,除非特殊情况才会用。

4.useContext上下文,用来把一个读、写接口给整个页面用。

5.useReducer专门给Redux的用户设计的(能代替Redux的使用),我们甚至可以不用useReducer

6.useMemo(记忆)需要与React.Memo配合使用,useMemo不好用我们可以升级为更好用的useCallback(回调)

7.useRef(引用)就是保持一个量不变,关于引用还有个forwardRef,forwardRef并不是一个Hook,还有个useImperativeHandle就是setRef。

就是我支持ref时,可以自定义ref长什么样子,那就使用useImperativeHandle

8.自定义Hook

示例中的useList就是自定义Hook,非常好用。

有个默认的自定义HookuseDebugValue就是你在debugger时,可以给你的组件加上名字,很少用。

参考

little_ice

珍惜时间小李

相关文章
|
1月前
Turndown 源码解析:一、辅助函数
Turndown 源码解析:一、辅助函数
44 0
|
10月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段2
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段2
36 0
|
10月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段3
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段3
42 0
|
10月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段
前端学习笔记202307学习笔记第六十天-react源码-commit的第一个子阶段
35 0
|
10月前
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-callback函数内部this指向2
前端学习笔记202307学习笔记第五十九天-react源码-callback函数内部this指向2
32 0
|
8月前
|
JavaScript 算法
【Vue2.0源码学习】模板编译篇-模板解析阶段(优化阶段)
【Vue2.0源码学习】模板编译篇-模板解析阶段(优化阶段)
26 0
|
11月前
|
前端开发 API
react中hook的作用和用处
react中hook的作用和用处
jira学习案例54-用usememo解决循环依赖问题1
jira学习案例54-用usememo解决循环依赖问题1
54 0
jira学习案例54-用usememo解决循环依赖问题1
|
前端开发
#yyds干货盘点 【React工作记录七】如何对数组进行一个过滤操作map
#yyds干货盘点 【React工作记录七】如何对数组进行一个过滤操作map
88 0
#yyds干货盘点 【React工作记录七】如何对数组进行一个过滤操作map
|
JavaScript
vue3.0中为啥要删除过滤器功能,因为功能重复吧?
vue3.0中为啥要删除过滤器功能,因为功能重复吧?
106 0