初识hooks

389 阅读8分钟

React Hooks

一、什么是 Hooks

React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有 Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以直接在现有的函数组件中使用 Hooks 凡是 use 开头的 React API 都是 Hooks

二、Hooks 解决的问题

1. 类组件的不足

状态逻辑难复用: 在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余 趋向复杂难以维护: 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )

this 指向问题: 父组件给子组件传递函数时,必须绑定 this

三、Hooks 优势

1:能优化类组件的三大问题 2:能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks ) 3:能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) 4:副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。

四、useState、useEffect、useMemo、useContext的使用例子

useState

const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数,在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

例子

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
  <View>
      Count: {count}
      <Button onClick={() => setCount(initialCount)}>Reset</Button>
      <Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
      <Button onClick={() => setCount(prevCount => prevCount - 1)}>-</Button>
  </View>
  );
}

假如一个组件有多个状态值怎么办?

useState是可以多次调用的,所以我们完全可以这样写:

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

useState无论调用多少次,相互之间是独立的

react是怎么保证多个useState的相互独立的?

react是根据useState出现的顺序来定的。

useState原理

let memoizedStates = []	
let index = 0	
function useState (initialState) {	
  memoizedStates[index] = memoizedStates[index] || initialState	
  let currentIndex = index	
  function setState (newState) {	
    memoizedStates[currentIndex] = newState	
    render()	
  }	
  return [memoizedStates[index++], setState]	
}

useEffect

一个至关重要的 Hooks API,顾名思义,useEffect是用于处理各种状态变化造成的副作用,也就是说只有在特定的时刻,才会执行的逻辑。

我们写的有状态组件,通常会产生很多的副作用,比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有某些值改变的时候才执行。

如何使用useEffect?

useEffect(() => {
    // 更新文档的标题,再每次重新渲染的时候就会执行
    document.title = `helloworld!`;
});

如何只让useEffect只执行一次

useEffect(() => {
    // 更新文档的标题
    document.title = `helloworld!`;
},[]);

useEffect的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数

useEffect怎么解绑一些副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。 怎么清除呢?让我们传给useEffect的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [],
);

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

一个常见的用例便是命令式地访问子组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <View>
      <Input ref={inputEl} type="text" />
      <Button onClick={onButtonClick}>Focus the input</Button>
    </View>
  );
}

useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 JavaScript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值

useMemo 也能针对传入子组件的值进行缓存优化

我们来看一个反例

import React from 'react';
 
 
export default function WithoutMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    function expensive() {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }
 
    return <div>
        <h4>{count}-{val}-{expensive()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:

使用useMemo

export default function WithMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive = useMemo(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count]);
 
    return <div>
        <h4>{count}-{expensive}</h4>
        {val}
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。

useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

别忘记 useContext 的参数必须是 context 对象本身

import React, { useState ,,useContext, createContext} from 'react';
import './App.css';

// 创建一个 context
const Context = createContext(0)



//  useContext 写法
function Item3 () {
  const count = useContext(Context);
  return (
    <div>{ count }</div>
  )
}

function App () {
  const [ count, setCount ] = useState(0)
  return (
    <div>
      点击次数: { count } 
      <button onClick={() => { setCount(count + 1)}}>点我</button>
      <Context.Provider value={count}>
        <Item3></Item3>
      </Context.Provider>
    </div>
    )
}

export default App;

useContext结合mobx使用

demoStore.js

import React, { createContext } from 'react'
import { observable, action, configure, runInAction } from 'mobx'
configure({ enforceActions: 'observed' })
class ManagementStore {
  @observable current = 0
  @action setCurrent = (value) => {
    this.current = value
  }
}

export default createContext(new ManagementStore)

组件内使用

const managementStore = useContext(_managementStore)