hooks自定义实现

# 设计理念

# 基本原理

# useState

/**第一版*/
function useState(initVal) {
    let val = initVal;
    function setVal(newVal) {
        val = newVal;
        render(); // 修改val后 重新渲染页面
    }
    return [val, setVal];
}
/**第二版*/
let val; // 放到全局作用域
function useState(initVal) {
    val = val|| initVal; // 判断val是否存在 存在就使用
    function setVal(newVal) {
        val = newVal;
        render(); // 修改val后 重新渲染页面
    }
    return [val, setVal];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# useEffect useState

/**第一版:不考虑第二个参数*/
function useEffect(fn){
    fn();
}

/**第二版:不考虑第二个参数*/
let watchArr; // 为了记录状态变化 放到全局作用域
function useEffect(fn,watch){
    // 判断是否变化 
    const hasWatchChange = watchArr?
    !watch.every((val,i)=>{ val===watchArr[i] }):true;
    if( hasWatchChange ){
        fn();
        watchArr = watch;
    }
}

/**第三版:解决同时调用多个 useState useEffect的问题*/
let memoizedState  = [];// 通过数组维护变量
let currentCursor = 0;
function useState(initVal) {
    memoizedState[currentCursor] = memoizedState[currentCursor] || initVal;
    function setVal(newVal) {
        memoizedState[currentCursor] = newVal;
        render(); 
    }
    // 返回state 然后 currentCursor+1
    return [memoizedState[currentCursor++], setVal]; 
}

function useEffect(fn, watch) {
  const hasWatchChange = memoizedState[currentCursor]
    ? !watch.every((val, i) => val === memoizedState[currentCursor][i])
    : true;
  if (hasWatchChange) {
    fn();
    memoizedState[currentCursor] = watch;
    currentCursor++; // 累加 currentCursor
  }
}
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
33
34
35
36
37
38
39
40

# 图解Hook原理

image-20201217193920755

如上图我们根据调用hook顺序,将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState。

image-20201217193900783

上面状态更新图,我们可以看到执行setCount(count + 1)或setData(data + 2)时,先将旧数组memoizedState中对应的值取出来重新复值,从而生成新数组memoizedState。对于是否执行useEffect通过判断其第二个参数是否发生变化而决定的。

image.png

function FunctionalComponent () {
  const [state1, setState1] = useState(1);
  const [state2, setState2] = useState(2);
  const [state3, setState3] = useState(3);
}
1
2
3
4
5

image-20201214201026210

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# 自定义实现

# useState

  • useState 就是一个 Hook
  • 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
  • useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
  • useState 唯一的参数就是初始 state
  • 返回一个 state,以及更新 state 的函数
    • 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
    • setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列
const [state, setState] = useState(initialState);
1

# 1: 计数器

Class方式与函数方式的比较

import React,{useState} from 'react';
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0
    };
  }
  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={() => this.setState({ number: this.state.number + 1 })}>
          + </button>
      </div>
    );
  }
}
function Counter2(){
  const [number,setNumber] = useState(0);
  return (
    <>
    <p>{number}</p>
    <button onClick={()=>setNumber(number+1)}>+</button>
    </>
  )
}
export default Counter2;
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

# 2: 每次渲染都是独立的闭包

  • 每一次渲染都有它自己的 Props and State
  • 每一次渲染都有它自己的事件处理函数
  • alert会“捕获”我点击按钮时候的状态。
  • 组件函数每次渲染都会被调用,但是每一次调用中number值都是常量,并且它被赋予了当前渲染中的状态值
  • 在单次渲染的范围内,props和state始终保持不变
  • making-setinterval-declarative-with-react-hooks (opens new window)
function Counter2(){
  const [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      alert(number);
    },3000);
  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}
function Counter() {
    const [number, setNumber] = useState(0);
    const savedCallback = useRef();
    function alertNumber() {
        setTimeout(() => {
            alert(savedCallback.current);
        }, 3000);
    }
    return (
        <>
            <p>{number}</p>
            <button onClick={() => {
                setNumber(number + 1);
                savedCallback.current = number + 1;
            }}>+</button>
            <button onClick={alertNumber}>alertNumber</button>
        </>
    )
}
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
33
34

# 3: 函数式更新

  • 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值
function Counter2(){
  const [number,setNumber] = useState(0);
  let numberRef = useRef(number);
  numberRef.current = number;
  function alertNumber(){
    setTimeout(()=>{
      alert(numberRef.current);
    },3000);
  }
+  function lazy(){
+    setTimeout(()=>{
+      setNumber(number+1);
+    },3000);
+  }
+  function lazyFunc(){
+    setTimeout(()=>{
+      setNumber(number=>number+1);
+    },3000);
+  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={lazy}>lazy+</button>
          <button onClick={lazyFunc}>lazyFunc+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}
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

# 4: 惰性初始 state

  • initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
  • 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
  • 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
function Counter3(){
  const [{name,number},setValue] = useState(()=>{
    return {name:'计数器',number:0};
  });
  return (
      <>
          <p>{name}:{number}</p>
          <button onClick={()=>setValue({number:number+1})}>+</button>
      </>
  )
}
1
2
3
4
5
6
7
8
9
10
11

# 5: 性能优化

# 1) Object.is
  • 调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

    function Counter4(){
    const [counter,setCounter] = useState({name:'计数器',number:0});
    console.log('render Counter')
    return (
        <>
            <p>{counter.name}:{counter.number}</p>
            <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
            <button onClick={()=>setCounter(counter)}>-</button>
        </>
    )
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
# 2) 减少渲染次数
  • 内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
  • 创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
function Child({onButtonClick,data}){
  console.log('Child render');
  return (
    <button onClick={onButtonClick} >{data.number}</button>
  )
}
Child = memo(Child);
function App(){
  const [number,setNumber] = useState(0);
  const [name,setName] = useState('samy');
  const addClick = useCallback(()=>setNumber(number+1),[number]);
  const  data = useMemo(()=>({number}),[number]);
  return (
    <div>
      <input type="text" value={name} onChange={e=>setName(e.target.value)}/>
      <Child onButtonClick={addClick} data={data}/>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3) useState+useCallback+useMemo实现原理

自己实现 useState+useCallback+useMemo

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

let hookStates = [];//放着此组件的所有的hooks数据
let hookIndex = 0;//代表当前的hooks的索引

function useState(initialState){
  //如果有老状态,则使用老状态,如果没有则使用默认值
  //hookStats[0]=0;
  hookStates[hookIndex]=hookStates[hookIndex]||initialState;
  let currentIndex = hookIndex;
  function setState(newState){
    hookStates[currentIndex]=newState;
    render();
  }
  return [hookStates[hookIndex++],setState];
} 
//let lastCallback,lastCallbackDeps;
function useCallback(callback,deps){
  if(hookStates[hookIndex]){
    let [lastCallback,lastCallbackDeps] = hookStates[hookIndex];
    let same = deps.every((item,index)=>item === lastCallbackDeps[index]);
    if(same){//如果老依赖和新的依赖都相同,则直接返回老的,如果不一相同,则返回新的
      hookIndex++;
      return lastCallback;
    }else{
      hookStates[hookIndex++]=[callback,deps];
      return callback;
    }
  }else{
    hookStates[hookIndex++]=[callback,deps];
    return callback;
  }
}
//let lastMemo,lastMemoDeps;
function useMemo(factory,deps){
  if(hookStates[hookIndex]){
    let [memo,lastDeps] = hookStates[hookIndex];
    let same = deps.every((item,index)=>item === lastDeps[index]);
    if(same){//如果老依赖和新的依赖都相同,则直接返回老的,如果不一相同,则返回新的
      hookIndex++;
      return memo;
    }else{
      let newMemo = factory();
      hookStates[hookIndex++]=[newMemo,deps];
      return newMemo;
    }
  }else{
    let newMemo = factory();
    hookStates[hookIndex++]=[newMemo,deps];
    return newMemo;
  }
}
function Child(props){
  console.log('Child render');//渲染子组件
  return (
    <div>
      <button onClick={props.add1} >{props.data1.number}</button>
      <button onClick={props.add2} >{props.data2.number}</button>
    </div>
  )
}
//memo备记录 如果你想让一个函数组件有一个功能,如果属性不变,就不要刷新

function memo(OldComponent){
  return class extends React.Component{
    shouldComponentUpdate(nextProps, nextState){
      if(nextProps === this.props){
        return false;
      }
      if(Object.keys(this.props).length !== Object.keys(nextProps).length){
        return true;
      }
      if(this.props == null || nextProps == null){
        return true;
      }
      for(let key in this.props){
        if(this.props[key] !== nextProps[key]){
          return true;
        }
      }
      return false;
      //return nextProps!==this.props;
     // return !(Object.is(nextProps,this.props) &&  Object.is(nextState,this.state))
    }
    render(){
      return <OldComponent {...this.props}/>
    }
  }
}
let MemoChild = memo(Child);
//在源码里也是在进入一个新的组件的时候设置的过
function App(){
  hookIndex =0;
  //如果一旦进入函数组件,索引先归为0 
  //每调用一个hook,索引加会+1
  let [number,setNumber]=useState(0);//0
  let [name,setName] = useState('zhufeng');//hookIndex=1
  const add1 = useCallback(()=>setNumber(number+1),[number]);//hookIndex=2
  const add2 = useCallback(()=>setNumber(number+2),[number]);//hookIndex=3
  //useMemo 缓存函数的返回值 {number:0}
  //第2个参数是依赖数组,依赖变量化才会重新计算number值 空数组则意味不依赖任何变量
  const data1 = useMemo(()=>({number:number}),[number]);//hookIndex=4
  const data2 = useMemo(()=>({number:-number}),[number]);//hookIndex=5
  return (
    <div>
      <input type="text" value={name} onChange={event=>setName(event.target.value)}/>
      <MemoChild add1={add1} add2={add2}  data1={data1} data2={data2}/>
    </div>
  )
}
function render(){

  ReactDOM.render(
    <App/>,
    document.getElementById('root')
  );
}
render();
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

# 6: 注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用

    import React, { useEffect, useState, useReducer } from 'react';
    import ReactDOM from 'react-dom';
    function App() {
      const [number, setNumber] = useState(0);
      const [visible, setVisible] = useState(false);
      if (number % 2 == 0) {
          useEffect(() => {
              setVisible(true);
          }, [number]);
      } else {
          useEffect(() => {
              setVisible(false);
          }, [number]);
      }
      return (
          <div>
              <p>{number}</p>
              <p>{visible && <div>visible</div>}</p>
              <button onClick={() => setNumber(number + 1)}>+</button>
          </div>
      )
    }
    ReactDOM.render(<App />, document.getElementById('root'));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

# useReducer

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

# 基本用法

const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = 0;

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'add'})}>+</button>
          <button onClick={() => dispatch({type: 'minus'})}>-</button>
        </>
    )
}
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

# useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
const CounterContext = React.createContext();

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function Counter(){
  let {state,dispatch} = useContext(CounterContext);
  return (
      <>
        <p>{state.number}</p>
        <button onClick={() => dispatch({type: 'add'})}>+</button>
        <button onClick={() => dispatch({type: 'minus'})}>-</button>
      </>
  )
}
function App(){
    const [state, dispatch] = useReducer(reducer, {number:0});
    return (
        <CounterContext.Provider value={{state,dispatch}}>
            <Counter/>
        </CounterContext.Provider>
    )

}
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

# useEffect

  • 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
  • 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
useEffect(didUpdate);
1

# 1 通过class实现修标题

class Counter extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        number: 0
      };
    }

    componentDidMount() {
        document.title = `你点击了${this.state.number}`;
    }

    componentDidUpdate() {
        document.title = `你点击了${this.state.number}`;
    }

    render() {
      return (
        <div>
          <p>{this.state.number}</p>
          <button onClick={() => this.setState({ number: this.state.number + 1 })}>
            +
          </button>
        </div>
      );
    }
  }
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

在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行

# 2 通过effect实现

import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        // 使用浏览器的 API 更新页面标题
        document.title = `你点击了${number}`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。

# 3 跳过 Effect 进行性能优化

  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
function Counter(){
  const [number,setNumber] = useState(0);
  // 相当于componentDidMount 和 componentDidUpdate
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number=>number+1);
     },1000);
  },[]);
  return (
      <>
          <p>{number}</p>
      </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4 清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
  • React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:
import React, { useEffect, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
    const [number, setNumber] = useState(0);
    useEffect(() => {
        console.log('开启一个新的定时器')
        const $timer = setInterval(() => {
            setNumber(number => number + 1);
        }, 1000);
        return () => {
            console.log('销毁老的定时器');
            clearInterval($timer);
        }
    });
    return (
        <>
            <p>{number}</p>
        </>
    )
}
function App() {
    let [visible, setVisible] = useState(true);
    return (
        <div>
            {visible && <Counter />}
            <button onClick={() => setVisible(false)}>stop</button>
        </div>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));
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
  • useEffect的执行时机
  • 同步才是理解effects的心智模型
import React,{useEffect,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log('setInterval',count);
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('clearInterval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>{count}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 干掉对count的依赖

    function Counter() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
      const id = setInterval(() => {
        setCount(count=>count + 1);
      }, 1000);
      return () => {
        clearInterval(id);
      };
    }, []);
    
    return <h1>{count}</h1>;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

# 5 useEffect+useReducer

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 假如我们不想在step改变后重启定时器,我们该如何从effect中移除对step的依赖呢?
import React, { useEffect, useReducer } from 'react';
import ReactDOM from 'react-dom';
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state = initialState, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      return state;
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => dispatch({ type: 'step' ,step:Number(e.target.value)})} />
    </>
  );
}
function render() {
  ReactDOM.render(
    <Counter />,
    document.getElementById('root')
  );
}
render();
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
33
34
35
36
37
38
39
40
41
42
43

# 6 竞态

  • 请求更早但返回更晚的情况会错误地覆盖状态值
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
const API = {
  async fetchArticle(id){
    return new Promise((resolve)=>{
        setTimeout(()=>{
          resolve({id,title:`title_${id}`});
        },1000*(5-id));
    });
  }
}
function Article({ id }) {
  const [article, setArticle] = useState({});
  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }
    fetchData();
    return () => {
      didCancel = true;
    };
  }, [id]);
  return (
    <div>
      <p>{article.title}</p>
    </div>
  )
}
function App(){
  let [id,setId] = useState(1);
  return (
    <div>
      <p>id:{id}</p>
       <Article id={id}/>
       <button onClick={()=>setId(id+1)}>改变id</button>
    </div>
  )
}
function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}
render();
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 5 useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
  • 返回的 ref 对象在组件的整个生命周期内保持不变
const refContainer = useRef(initialValue);
1
# 1) useRef
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));
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
# 2) forwardRef
  • 将ref从父组件中转发到子组件中的dom元素上
  • 子组件接受props和ref作为参数
function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3) useImperativeHandle
  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
function Child(props,ref){
  const inputRef = useRef();
  useImperativeHandle(ref,()=>(
    {
      focus(){
        inputRef.current.focus();
      }
    }
  ));
  return (
    <input type="text" ref={inputRef}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    console.log(inputRef.current);
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}
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

# 6 effect回调里读取最新的值

  • 有时候你可能想在effect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法是使用refs

index.js

import React,{useEffect,useRef,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    latestCount.current = count;
    setTimeout(() => {
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  return (
    <div>
      <p>{count}</p>
      <button onClick={()=>setCount(count+1)}>+</button>
    </div>
  )
}
function render(){
  ReactDOM.render(
    <Counter/>,
    document.getElementById('root')
  );
}
render();
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

# useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • useEffect不会阻塞浏览器渲染,而 useLayoutEffect 会浏览器渲染
  • useEffect会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行

# 1 事件循环

    1. 从宏任务队列中取出一个宏任务执行
    1. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,则会继续执行新的微任务
    1. 进入更新渲染阶段,判断是否需要渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的,一般为60帧/秒
    1. 如果确定要更新会进入下面的步骤,否则本循环结束
      1. 如果窗口大小发生了变化,执行监听的resize事件
      1. 如果页面发生了滚动,执行scroll方法
      1. 执行帧动画回调,也就是 requestAnimationFrame 的回调
      1. 重新渲染用户界面
    1. 判断是否宏任务和微任务队列为空则判断是否执行requestIdleCallback的回调函数

img

# 2 使用

import React, {useRef } from 'react';
import ReactDOM from 'react-dom';
let lastDependencies;
function useEffect(callback,dependencies){
    if(lastDependencies){
        let changed = !dependencies.every((item,index)=>item==lastDependencies[index]);
        if(changed){
            setTimeout(callback)
            lastDependencies=dependencies;
        }
    }else{
        setTimeout(callback)
        lastDependencies=dependencies;
    }
}

let lastLayoutDependencies;
function useLayoutEffect(callback,dependencies){
    if(lastLayoutDependencies){
        let changed = !dependencies.every((item,index)=>item==lastLayoutDependencies[index]);
        if(changed){
            queueMicrotask(callback);
            lastLayoutDependencies=dependencies;
        }
    }else{
        Promise.resolve().then(callback);
        lastLayoutDependencies=dependencies;
    }
}
const Animate = ()=>{
    const ref = useRef();
    useLayoutEffect(()=>{
        ref.current.style.WebkitTransform = `translate(500px)`;
        ref.current.style.transition  = `all 500ms`;
    });
    let style = {
        width:'100px',
        height:'100px',
        backgroundColor:'red'
    }
    return (
        <div>
            <div style={style} ref={ref}></div>
        </div>
    )
}
function render(){
    ReactDOM.render(<Animate/>,document.getElementById('root'));
}
render();
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

#

上次更新: 2022/04/15, 05:41:29
×