React Hooks学习-1

277 阅读4分钟

一、Hooks介绍和使用

1.基本认识

class组件的缺点:

  • 相同业务逻辑分散到各个方法中,逻辑混乱(比如说在didmount的时候发送ajax获取数据,有可能在didupdate更新的时候还需要发送ajax获取数据,同样获取数据分散到didmount和didupdate两个方法中)
  • 复用组件逻辑复杂(HOC、Render Prop)

函数组件没有组件实例,没有生命周期,没有state和setState,只能接受props,为了增强功能需要Hooks

2.Hooks规范

命名规范:

  • 所有Hooks都用use开头,如useXxx
  • 自定义Hook也要以use开头
  • 非Hooks的地方,尽量不要用useXxx写法

使用规范:

  • 用于React的函数组件和自定义hook
  • 只能用于顶层代码,不能用于循环、判断条件中(可以通过eslint-plugin-react-hooks规范代码,create-react-app脚手架自身带有这个插件)

函数组件(纯函数),执行完就销毁,所以无论是初始化(render)还是组件更新(re-render),都会重新执行一次函数,获取最新的组件

render:初始化state的值,添加effect函数(useEffect)

re-render:恢复初始化的state的值,但不会重新设置新的值,要修改只能通过setState修改;替换effect函数(useEffect),effect内部的函数也会重新定义

以useState为例子,如果有多个useState是按顺序读取的,所以不能把useState放到条件判断中,不然如果条件判断不通过,useState读取的顺序会混乱,其他Hooks也是按顺序执行的

class组件有组件实例,执行完也不会销毁

二、State Hook(useState)

默认函数组件没有state,是一个纯函数,执行完即销毁,不能存储state,需要State Hook

1、基本用法

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  console.log('App运行')
  const [n, setN] = React.useState(0);
  console.log(`n:${n}`)
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  • 首次渲染,调用App函数,得到虚拟DOM,在页面创建真实DOM(调用App函数会运行useState(0))

  • 用户点击button,调用setN,再次调用App函数

  • 调用App函数,得到虚拟DOM,通过DOM Diff,更新真实DOM(调用App函数会运行useState(0))

  • 执行setN的时候,n不会马上变,会把n+1存入到数据x,并且会导致App函数重新执行,每次useState(0)的时候,从数据x读取n的最新值

  • 每个组件都会有自己的数据x,可以认定为state

2、实现一个简单的useState

2.1原始的myState

import React from 'react';
import ReactDOM from 'react-dom';

const root = document.getElementById('root')

let _state 
const myState = (initialValue)=>{
  _state = _state === undefined?initialValue:_state
  const setState = newValue =>{
     _state = newValue
     render()
  }
  return [_state,setState]
}
const render = ()=>{
  ReactDOM.render(<App/>,root)
}


const App = props =>{
  console.log('App运行了');
  const [n,setN] = myState(0)
  console.log(`n,${n}`)
  const addN = ()=>{
    setN(n+1)
  }
  return(
  <div>{n}
  <button onClick={addN}>+1</button>
  </div>)
}

export default App;
  • 存在的问题:如果一个组件里面用了两个myState,则_state会冲突
  • 将_state设置为数组

2.2改进版的myState

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

let _state = [];
let index = 0;

function myUseState(initialValue) {
  const currentIndex = index;//引入currentIndex,这样一来对index的+1操作不会影响后面的操作
  index += 1;
  _state[currentIndex] = _state[currentIndex] || initialValue;
  const setState = (newState) => {
    _state[currentIndex] = newState;
    render();
  };
  return [_state[currentIndex], setState];
}

const render = () => {
  index = 0;//App不断被调用,这样index会一直+1,要在改变数据更新UI前把index重置,再重新调用App
  ReactDOM.render(<App />, rootElement);
};

function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  return (
    <div className="App">
      <p>m:{m}</p>
      <p>
        <button onClick={() => setM(m + 1)}>+1</button>
      </p>
      <p>n:{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  • useState不要写在写在循环和条件中

  • 改进之处:还要给每个组件创建_state和index,并且还得放在组件对应的虚拟节点上

3、注意事项

3.1不可局部更新,要用...操作符或者Object.asign来实现合并

import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [user,setUser] = useState({name:'Frank', age: 18})
  const onClick = ()=>{
    setUser({
      ...user, //...操作符实现合并
      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);

3.2setState推荐用函数

如果要对setState进行多种操作,推荐写成函数

import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
    setN(n+1)
    setN(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>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

三、useRef

作用:

  • 是获取DOM节点

  • 如果需要一个值在render的时候保持不变,可以使用useRef

//初始化:
const count = useRef(0)

//读取:
count.current

注意:count.current值变化,不会自动render(Vue3可以实现ref变化自动render)

解决:监听ref,当ref.current变化的时候调用setX

一个例子:

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const [n, setN] = React.useState(0);
  const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  • 点击+1再点log,不存在bug
  • 如果点log再点+1,则打印n的值为旧值0

原因:

点击log的时候会三秒后打印n的值,此时n的值为0,然后再点+1,会产生新的值n+1

如果想要一个贯穿始终的状态,可以用useRef

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const nRef = React.useRef(0); //{current:0},本身变化并不能触发更新
  const update = React.useState()[1];//通过设置update来更新,update相当于setState
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  return (
    <div className="App">
      <p>{nRef.current} 这里并不能实时更新</p>
      <p>
        <button onClick={() => ((nRef.current += 1), update(nRef.current))}>
          +1
        </button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

四、useContext

作用:父子组件间通信,可以跨层传递值

import React, { useContext } from 'react'

// 主题颜色
const themes = {
    light: {
        foreground: '#000',
        background: '#eee'
    },
    dark: {
        foreground: '#fff',
        background: '#222'
    }
}

// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值

function ThemeButton() {
    //子组件通过useContext(xxx)来读取
    const theme = useContext(ThemeContext)

    return <button style={{ background: theme.background, color: theme.foreground }}>
        hello world
    </button>
}

function Toolbar() {
    return <div>
        <ThemeButton></ThemeButton>
    </div>
}

function App() {
    //父组件用xxx.Provider包裹起来
    return <ThemeContext.Provider value={themes.dark}>
        <Toolbar></Toolbar>
    </ThemeContext.Provider>
}

export default App