# React 源码解读(五) hooks

# 什么是 hooks

它可以让你再不编写 class 的情况下使用

# 一些基本的 hooks

# useState

import React, { useState } from "react";

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times </p>
      <button onClick={setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;
1
2
3
4
5
6
7
8
9
10
11
12
  • count 就是一个 state,setCount 就是来控制对应的 state 的方法
  • useState() 里可以设置初始值
  • useState 的初始值如果是对象的话,需要注意的一点就是,每次 setCount 的时候都要传入完整的对象,否则少传的对象就会丢失

# useEffect

import React, { useState, useEffect } from "react";

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    // 这个callback没有参数
    console.log("componentDidMount");
  }, []);

  useEffect(
    () => {
      // // 不能改变依赖数组的state
      console.log("change count");
      let timer = setInterval(() => {
        console.log("每秒执行");
      }, 1000);

      return () => {
        clearInterval(timer);
      };
    },
    [count]
  );

  return (
    <div>
      <p>You clicked {count} times </p>
      <button onClick={setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  • useEffect 有两个参数,callback 和 依赖
  • useEffect 的执行时机,在第一次调用 Example 函数组件的时候, useEffect 的 callBack 执行,在依赖 dependencies 的值, 发生变化的时候, callBack 执行
  • 如果第二个参数为空,那么每当有 state 改变时,都会执行一次
  • 如果第二个参数是空数组[],那么只在第一次调用组件的时候执行一次
  • 如果第二个参数是某个 state 值,那么就会在这个依赖值变化的时候执行 callback
  • 可以在 useEffect 最后 return 一个函数,这个函数会在组件卸载的时候执行,比如清除定时器
  • 有个坑!不要在 callback 里更新自己依赖的 state!!!
  • useEffect的目的是去处理副操作,所以useEffect会在dom渲染结束再去执行

# useCallback

+当我们定义点击事件的时候,如果直接定义的话,每次组件状态改变重新执行的时候都要重新定义一次,很不好,所以出现了 useCallback

import React, { useState, useCallback } from "react";

function Example() {
  const [count, setCount] = useState(0);
  const dealCount = useCallback(
    () => {
      // 还是之前缓存的0
      setCount({
        value: 1
      });
      // stateRef.current = count+1
      // 只是添加了一个update, 还没有更新最终的值
      setTimeout(() => {
        // 用的还是之前的值
        console.log("timer count", count);
        // console.log('timer stateRef', stateRef.current)
      }, 2000);
      // setCount((count) => {
      //     return count+1
      // })
    },
    [count]
  );

  return (
    <div>
      <p>You clicked {count} times </p>
      <button onClick={dealCount}>Click me</button>
    </div>
  );
}
export default Example;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  • useCallback 相当于把点击事件缓存了,这样在每次重新执行组件的时候就不会再次定义
  • useCallback 的第二个参数目的是将所需要的 state 值传进去,如果不填的话,第一次的 state 值就会被一起缓存,后面怎么点击都不会变
  • 还有一种解决方法,就是第二个参数为[],setCount 里面传入函数,也可以获取到最新的 state
const dealCount = useCallback(() => {
  setCount(count => {
    return count + 1;
  });
}, []);
1
2
3
4
5

这里有个坑,capture value,快照变量

const dealCount = useCallback(
  () => {
    // 还是之前缓存的0
    setCount(count + 1);
    // 只是添加了一个update, 还没有更新最终的值
    setTimeout(() => {
      console.log("timer count", count);
    }, 2000);
  },
  [count]
);
1
2
3
4
5
6
7
8
9
10
11
  • 当点击触发 dealCount 方法时,经过两秒延迟输出 0 ╮(╯▽╰)╭
  • setCount 执行只是把新状态添加到了 update 队列中,但是并没有更新最终值,而 setTimeout 中缓存了之前的旧值
  • 换句话解释,setTimeout 是通过闭包缓存的上一轮的变量,而 setCount 之后重新执行创建了新的变量,所以就不一样了

# useMemo

import React, { memo, useState, useMemo } from "react";
function App() {
    const [value, setValue] = useState(0);
    const increase = useMemo(() => {
        if(value > 2) return value + 1;
    }, [value]);
    return (
        <div>
            <Child value={value} />
            <button
                type="button"
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                value:{value},increase:{increase || 0}
            </button>
        </div>
    );
}
const Child = memo(function Child(props) {
    console.log('Child render')
    return <h1>value:{props.value}</h1>;
});
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • useMemo()是定义一段函数逻辑是否重复执行,来处理开销较大的计算,最后返回一个值
  • 传入 useMemo 的函数会在渲染期间执行,不要在这个函数内部执行与渲染无关的操作
  • 如果在useMemo中执行useState之类的会产生死循环,并警告,因为useMemo是在渲染中进行的,你在其中操作DOM后,又会导致触发memo

# useRef

import React, { Component, useEffect, useRef } from 'react';
function App() {
  const childRef = useRef();
  useEffect(() => {
    console.log('useRef')
    console.log(childRef.current)
    childRef.current.handleLog();
  }, [])
  return (
    <div>
      <h1>Hello World!</h1>
      <Child ref={childRef} count="1"/>
    </div>
  )
}
// 因为函数组件没有实例,如果想用ref获取子组件的实例,子组件组要写成类组件
class Child extends Component {
  handleLog = () => {
    console.log('Child Component');
  }
  render() {
    const { count } = this.props;
    return <h2>count: { count }</h2>
  }
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 获取DOM元素的节点
  • 获取子组件的实例
  • 渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)

# memo

使用 React.memo 把组件包起来,可以避免不必要的刷新,类似 PureComponent

使用 memo + useCallback 能有效的调高性能

export default React.memo(Example);
1

# hooks 的结构

  • memorizedState 保存 state 的值
  • queue 缓存队列,储存多次更新行为
  • 执行下一个 useState 对应的 hooks 对象

# memorizedState

简版的 useState 和 useEffect

let memorizedState = []; // hooks存放在这个数组
let cursor = 0; // 当前 memorizedState 下标

function useState(initialValue) {
  memorizedState[cursor] = memorizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memorizedState[currentCursor] = newState;
    render();
  }
  return [memorizedState[cursor++], setState]; // 返回当前state,并把cursor加1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memorizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memorizedState[cursor] = depArray;
  }
  cursor;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let memorizedState = []; // hooks存放在这个数组

function useReducer(reducer, initialArg, init) {
  let initState = void 0;
  if (typeof init !== "undefined") {
    initState = init(initialArg);
  } else {
    initState = initialArg;
  }
  function dispatch(action) {
    memorizedState = reducer(memorizedState, action);
    render();
  }
  memorizedState = memorizedState || initState;
  return [memorizedState, dispatch];
}
function useState(initState) {
  return useReducer((oldState, newState) => newState, initState);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19