React之useState、useEffect原理解析

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;

分析:来看下面这行代码:

const [count, setCount] = useState(0)

可以发现:

  • 调用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"));
}

但是效果如下:
React之useState、useEffect原理解析_第1张图片

出现这种情况的原因分析:

  1. 首先,可以看到控制台上有所输出,说明:自定义的setState函数执行了。
  2. 但由于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];
}

此时,代码修改后的页面效果才是正常的:
React之useState、useEffect原理解析_第2张图片

1.1 惰性初始化state

我们可以注意到,在使用useState的时候,允许我们传入一个参数作为该状态变量的默认值。我们将这个参数先命名为initValue该参数只会在组件初次渲染的时候起生效,在后续渲染的时候则会被忽略。

同时,倘若这个初始值需要经过计算获得,那么这种情况我们大致分为这么2种方式:

  1. useEffect()函数中去调用一个伪代码getUserInfo(),然后通过返回值去调用setState函数赋值。
  2. 第一种不在本环节讨论范围内,那么第二种就是用惰性初始化state的方式。useState中不仅可以传入一个参数作为默认值,还可以传入一个函数,在函数中计算并返回初始的state即可。

例如(伪代码):

const [state, setState] = useState(() => {
  const userName= getUserInfo();
  return userName;
});

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

二. 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"));
}

但是页面效果如下,可以发现每点击一次按钮,就重新渲染一次,而每次渲染则调用一次useEffect()函数。
React之useState、useEffect原理解析_第3张图片
那么再来看看有两个参数版本的自定义useEffect()函数,这个函数需要做到:

  • 根据第二个参数来判断是否执行useEffect()函数。
let watchArr;
function useEffect(fn, watch) {
  const hasWactchChange = watchArr
    ? !watch.every((val, i) => val === watchArr[i])
    : true;
  if (hasWactchChange) {
    fn();
    watchArr = watch;
  }
}

完整案例如下(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"));
}

页面效果如下:可以见到,useEffect()函数就执行了一次,因为第二个参数中我们传入了一个空数组。
React之useState、useEffect原理解析_第4张图片

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"));
}

页面效果如下:
React之useState、useEffect原理解析_第5张图片

原因分析:以useState为例:

  1. 所有调用useState方法的地方,都会共享一个全局变量val
  2. 因此在组件中多次调用,就会引起变量冲突问题。

2.2 变量冲突解决方案

代码改进:

  1. 通过一个全局的数组来维护变量。
  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"));
}

页面效果如下:
React之useState、useEffect原理解析_第6张图片
从上述代码中,我们可以发现每次调用render()函数,都要将对应的全局下标重置为0,这个操作我刚开始看到就觉得匪夷所思,想了半天我才明白是为什么:

  1. 因为我们是根据Hook的调用顺序,来依次将变量存储在数组中的。
  2. 而我们useEffect函数的第二个参数是数组的原因,也是因为我们变量的存储也是以数组形式来存在。

备注:

我们这里的代码是个简化版的,官方的useStateuseEffect函数肯定是要更完善的。希望大家引以区分。

这里在对上述案例中的输出做一个解释,我们以第一次输出为例:(此时点击按钮1
React之useState、useEffect原理解析_第7张图片
结果分析:
React之useState、useEffect原理解析_第8张图片
那么此时的数组值也就对应了控制台中输出的内容:
在这里插入图片描述
我们也可以注意到代码中:

  1. 每次调用setXXX函数的时候,都会从数组memoizedState中对应的位置去取值,并重新赋值,从而获得一个全新的数组memoizedState之所以能够把索引位置对得上,是因为调用render函数的时候把全局下包也重置为0了
  2. 那么对于useEffect()函数,则通过第二个参数是否发生变化来决定其是否调用。

三. 拓展小知识

在日常开发当中,我们往往会给一个按钮添加一个onChange事件或者onClick事件,那么就以onClick事件为例:

  1. 假设按钮A绑定了onClick事件,里面肯定是调用我们自定义的onClick函数。
  2. 由于其他组件的原因我们触发了组件的渲染,那么每次render的时候,都会重新产生新的onClick函数。
  3. 这会造成不必要的渲染而引起性能浪费。

伪代码如下:

class Demo extends Component{
    render() {
        return 
        <div>
            <Button onClick={ () => { console.log('Hello World!!'); }}  />
        </div>;
    }
}

因此我们在类式组件开发过程中,往往会这么改写代码,来避免性能浪费问题:

class Demo extends Component{
    constructor(){
        super();
        this.buttonClick = this.buttonClick.bind(this);
    }
    render() {
        return 
        <div>
            <Button onClick={ this.buttonClick }  />
        </div>;
    }
}

那如果在函数式组件中开发,如何写呢?

回答:采用ReactHook中的useCallback()函数:

function Demo(){
    const buttonClick = useCallback(() => { 
    	console.log('Hello World!!')
    },[])
    return(
        <div>
            <Button onClick={ buttonClick }  />
        </div>
    )
}

作用:useCallback函数会生成一个记忆函数,这样更新时就能保证这个函数不会发生渲染。 从而达到规避性能浪费的目标。

你可能感兴趣的:(React)