i007.cc

i007.cc

优先队列-降维打击

05.价值资料

React之useState、useEffect原理解析

一. useState的实现
1.1 惰性初始化state
1.2 Object.is算法
二. useEffect的实现
2.1 变量冲突问题
2.2 变量冲突解决方案
三. 拓展小知识
参考文章:React Hook的实现原理和最佳实践
一. useState的实现
首先,我们来看一个简单的useState()的使用案例:
import ‘./App.css’;
import { useEffect, useState } from ‘react’
function App() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log(`update—${count}`)
  }, [count])
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        {`当前点击次数:${count}`}
      </button>
    </div>
  );
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
分析:来看下面这行代码:
const [count, setCount] = useState(0)
1
可以发现:
调用useState()函数,会返回一个变量(count)以及一个函数(setCount)。
useState()函数中可以传入一个参数,也就是该变量的初始值。
那么根据上述发现的2点,我们来自定义一个函数(创建个react脚手架,在index.js文件中修改):
import React from “react”;
import ReactDOM from “react-dom”;
function useState(initVal) {
  let val = initVal;
  function setVal(newVal) {
    val = newVal;
    // 修改变量后,调用render函数,重新渲染页面
    render();
  }
  return [val, setVal];
}
function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => { console.log(count); setCount(count + 1); }}>
        {`当前点击次数:${count}`}
      </button>
    </div>
  )
}
// 初次渲染用
render();
function render() {
  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
但是效果如下:
出现这种情况的原因分析:
首先,可以看到控制台上有所输出,说明:自定义的setState函数执行了。
但由于let val = initVal;这行代码是在函数内部被声明的,也因此每次调用useState函数的时候,都会重新声明val变量,从而导致其状态无法被保存。
因此我们对上述代码进行一个修改,将val变量放到全局作用域中:
// 全局作用域
let val;
function useState(initVal) {
// 判断val是否存在 存在就使用
    val = val|| initVal;
    function setVal(newVal) {
        val = newVal;
        // 修改变量后,调用render函数,重新渲染页面
        render();
    }
    return [val, setVal];
}
1
2
3
4
5
6
7
8
9
10
11
12
此时,代码修改后的页面效果才是正常的:
1.1 惰性初始化state
我们可以注意到,在使用useState的时候,允许我们传入一个参数作为该状态变量的默认值。我们将这个参数先命名为initValue,该参数只会在组件初次渲染的时候起生效,在后续渲染的时候则会被忽略。
同时,倘若这个初始值需要经过计算获得,那么这种情况我们大致分为这么2种方式:
useEffect()函数中去调用一个伪代码getUserInfo(),然后通过返回值去调用setState函数赋值。
第一种不在本环节讨论范围内,那么第二种就是用惰性初始化state的方式。useState中不仅可以传入一个参数作为默认值,还可以传入一个函数,在函数中计算并返回初始的state即可。
例如(伪代码):
const [state, setState] = useState(() => {
  const userName= getUserInfo();
  return userName;
});
1
2
3
4
1.2 Object.is算法
React中,通过Object.is算法来比较状态变量的不同,其判别的标准如下(摘自官网):Object.is(A,B)只要满足下列条件任意一条,就代表这两个值相同。
两个都 undefined.
两个都是null。
两个都是true/false。
两个长度相同且字符相同且顺序相同的字符串。
两者都是同一个对象。
两个数字和:
两个都 +0。
两个都 -0。
两个都 NaN。
或两者都不为零且两者均不NaN相同,且两者的值相同。
Object.is(25, 25);                // true
Object.is(‘foo’, ‘foo’);          // true
Object.is(‘foo’, ‘bar’);          // false
Object.is(null, null);            // true
Object.is(undefined, undefined);  // true
Object.is(window, window);        // true
Object.is([], []);                // false
var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);              // true
Object.is(foo, bar);              // false
// Case 2: Signed zero
Object.is(0, -0);                 // false
Object.is(+0, -0);                // false
Object.is(-0, -0);                // true
Object.is(0n, -0n);               // true
// Case 3: NaN
Object.is(NaN, 0/0);              // true
Object.is(NaN, Number.NaN)        // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
二. useEffect的实现
我们知道useEffect()函数会在第一次渲染之前调用,并且有两个参数:
参数1:执行的函数。
参数2:数组(可选参数),useEffect()函数根据第二个参数中是否有变化,来判断是否执行第一个参数的函数。
也因此,我们在开发过程中,对于只希望其执行一次的useEffect()函数,我们往往写入第二个参数为一个空数组(否则可能引起无限渲染的BUG)。
实现1:useEffect传入一个函数参数,里面调用即可。
import React from “react”;
import ReactDOM from “react-dom”;
let val;
function useState(initVal) {
  val = val || initVal;
  function setVal(newVal) {
    val = newVal;
    render(); // 重新render页面
  }
  return [val, setVal];
}
// 自定义的useEffect
function useEffect(fn) {
  fn();
}
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`自定义useEffect调用–count:${count}`);
  });
  return (
    <div>
      <button onClick={() => { console.log(count); setCount(count + 1); }}>
        {`当前点击次数:${count}`}
      </button>
    </div>
  )
}
// 初次渲染用
render();
function render() {
  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
31
32
33
34
35
36
37
但是页面效果如下,可以发现每点击一次按钮,就重新渲染一次,而每次渲染则调用一次useEffect()函数。
那么再来看看有两个参数版本的自定义useEffect()函数,这个函数需要做到:
根据第二个参数来判断是否执行useEffect()函数。
let watchArr;
function useEffect(fn, watch) {
  const hasWactchChange = watchArr
    ? !watch.every((val, i) => val === watchArr[i])
    : true;
  if (hasWactchChange) {
    fn();
    watchArr = watch;
  }
}
1
2
3
4
5
6
7
8
9
10
完整案例如下(index.js文件):
import React from “react”;
import ReactDOM from “react-dom”;
let val;
function useState(initVal) {
  val = val || initVal;
  function setVal(newVal) {
    val = newVal;
    render(); // 重新render页面
  }
  return [val, setVal];
}
// 自定义的useEffect
let watchArr;
function useEffect(fn, watch) {
  const hasWactchChange = watchArr
    ? !watch.every((val, i) => val === watchArr[i])
    : true;
  if (hasWactchChange) {
    fn();
    watchArr = watch;
  }
}
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`自定义useEffect调用–count:${count}`);
  }, []);
  return (
    <div>
      <button onClick={() => { setCount(count + 1); }}>
        {`当前点击次数:${count}`}
      </button>
    </div>
  )
}
// 初次渲染用
render();
function render() {
  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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
页面效果如下:可以见到,useEffect()函数就执行了一次,因为第二个参数中我们传入了一个空数组。
2.1 变量冲突问题
在上面,我们初步实现了useState和useEffect函数,并成功调用,但是上述案例都是在一个变量的情况下发生的,那么倘若有两个变量的情况下,依旧采用上述的自定义代码,会发生什么?
案例如下:
import React from “react”;
import ReactDOM from “react-dom”;
let val;
function useState(initVal) {
  val = val || initVal;
  function setVal(newVal) {
    val = newVal;
    render(); // 重新render页面
  }
  return [val, setVal];
}
// 自定义的useEffect
let watchArr;
function useEffect(fn, watch) {
  const hasWactchChange = watchArr
    ? !watch.every((val, i) => val === watchArr[i])
    : true;
  if (hasWactchChange) {
    fn();
    watchArr = watch;
  }
}
function App() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(0);
  useEffect(() => {
    console.log(`自定义useEffect调用–count:${count}`);
  }, [count]);
  useEffect(() => {
    console.log(`自定义useEffect调用–data:${data}`);
  }, [data]);
  return (
    <div>
      <button onClick={() => { setCount(count + 1); }}>
        {`按钮1:当前点击次数:${count}`}
      </button>
      <hr />
      <button onClick={() => { setData(data + 1); }}>
        {`按钮2:当前点击次数:${data}`}
      </button>
    </div>
  )
}
// 初次渲染用
render();
function render() {
  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
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
页面效果如下:
原因分析:以useState为例:
所有调用useState方法的地方,都会共享一个全局变量val。
因此在组件中多次调用,就会引起变量冲突问题。
2.2 变量冲突解决方案
代码改进:
通过一个全局的数组来维护变量。
通过一个全局的下标用来定位对应的状态变量存储的位置。
代码如下:
import React from “react”;
import ReactDOM from “react-dom”;
let memoizedState = [];
let currentIndex = 0;
function useState(initVal) {
  memoizedState[currentIndex] = memoizedState[currentIndex] || initVal;
  const cursor = currentIndex;
  function setVal(newVal) {
    memoizedState[cursor] = newVal;
    render();
  }
  // 返回state 然后 currentIndex+1
  return [memoizedState[currentIndex++], setVal];
}
// 自定义的useEffect
function useEffect(fn, watch) {
  const hasWatchChange = memoizedState[currentIndex]
    ? !watch.every((val, i) => val === memoizedState[currentIndex][i])
    : true;
  if (hasWatchChange) {
    fn();
    memoizedState[currentIndex] = watch;
    currentIndex++; // 累加 currentIndex
  }
}
function App() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(0);
  useEffect(() => {
    console.log(`自定义useEffect调用–count:${count}`);
  }, [count]);
  useEffect(() => {
    console.log(`自定义useEffect调用–data:${data}`);
  }, [data]);
  return (
    <div>
      <button onClick={() => { setCount(count + 1); }}>
        {`按钮1:当前点击次数:${count}`}
      </button>
      <hr />
      <button onClick={() => { setData(data + 1); }}>
        {`按钮2:当前点击次数:${data}`}
      </button>
    </div>
  )
}
// 初次渲染用
render();
function render() {
  console.log(memoizedState); // 执行hook后 数组的变化
  currentIndex = 0; // 重新render时需要设置为 0
  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
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
页面效果如下:
从上述代码中,我们可以发现每次调用render()函数,都要将对应的全局下标重置为0,这个操作我刚开始看到就觉得匪夷所思,想了半天我才明白是为什么:
因为我们是根据Hook的调用顺序,来依次将变量存储在数组中的。
而我们useEffect函数的第二个参数是数组的原因,也是因为我们变量的存储也是以数组形式来存在。
备注:
我们这里的代码是个简化版的,官方的useState和useEffect函数肯定是要更完善的。希望大家引以区分。
这里在对上述案例中的输出做一个解释,我们以第一次输出为例:(此时点击按钮1)
结果分析:
那么此时的数组值也就对应了控制台中输出的内容:
我们也可以注意到代码中:
每次调用setXXX函数的时候,都会从数组memoizedState中对应的位置去取值,并重新赋值,从而获得一个全新的数组memoizedState(之所以能够把索引位置对得上,是因为调用render函数的时候把全局下包也重置为0了)
那么对于useEffect()函数,则通过第二个参数是否发生变化来决定其是否调用。
三. 拓展小知识
在日常开发当中,我们往往会给一个按钮添加一个onChange事件或者onClick事件,那么就以onClick事件为例:
假设按钮A绑定了onClick事件,里面肯定是调用我们自定义的onClick函数。
由于其他组件的原因我们触发了组件的渲染,那么每次render的时候,都会重新产生新的onClick函数。
这会造成不必要的渲染而引起性能浪费。
伪代码如下:
class Demo extends Component{
    render() {
        return
        <div>
            <Button onClick={ () => { console.log(‘Hello World!!’); }}  />
        </div>;
    }
}
1
2
3
4
5
6
7
8
因此我们在类式组件开发过程中,往往会这么改写代码,来避免性能浪费问题:
class Demo extends Component{
    constructor(){
        super();
        this.buttonClick = this.buttonClick.bind(this);
    }
    render() {
        return
        <div>
            <Button onClick={ this.buttonClick }  />
        </div>;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
那如果在函数式组件中开发,如何写呢?
回答:采用ReactHook中的useCallback()函数:
function Demo(){
    const buttonClick = useCallback(() => {
    console.log(‘Hello World!!’)
    },[])
    return(
        <div>
            <Button onClick={ buttonClick }  />
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10
作用:useCallback函数会生成一个记忆函数,这样更新时就能保证这个函数不会发生渲染。 从而达到规避性能浪费的目标。

发表回复