ReactWheels07-02useState原理

useState原理

useState用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
console.log("app 运行了");
const [n, setN] = useState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>onAdd</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

脑补过程

  • 首次渲染 render(<App/>)
  • 调用 App 函数 => 得到 虚拟div,然后创建真实的div
  • 用户点击 button,调用 setN(n+1)
  • 再次 render(<App/>)
    • 调用 App函数 ,
    • 得到 虚拟div
    • DOM Diff 更新真实 div
  • 每次都调用 App(),都会运行 useState(0)
    • 第一次的时候 n = 0
    • 之后每次 n = n+1

分析

setN

  • setN 一定会修改数据 x,将 n+1 存入 x
  • setN 一定会触发 <App/> 重新渲染 re-render

useState

  • useState 肯定会从 x 读取 n的最新值

x

  • 每个组件有自己的数据x,我们将它命名为 state

自己实现一个 useState

最简单的 useState 实现

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
import React from "react";
import ReactDOM from "react-dom";

let _state;
const myUseState = (initValue) => {
console.log("myUseState run");
_state = _state === undefined ? initValue : _state;
const setState = (newValue) => {
_state = newValue;
render();
};
return [_state, setState];
};
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}

function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
</div>
);
}
render();
  • 存在问题 ,App内 有两个变量的时候咋办 m,n 那样就会使用两次 useState

改进思路

把 _state 做成一个对象

  • _state = {n:0,m:0}
  • 不行,因为 useState(0) 的时候 不知道变量 叫做 n 还是 m

把 _state 做成数组

  • 比如 _state = [0,0]
  • 貌似可以

解决多个 值 setState问题

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
import React from "react";
import ReactDOM from "react-dom";

let _state = [];
let index = 0;
const myUseState = (initValue) => {
const currentIndex = index;
_state[currentIndex] =
_state[currentIndex] === undefined ? initValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};
function render() {
// 每次重新渲染的时候 重置 index
index = 0;
ReactDOM.render(<App />, document.getElementById("root"));
}

function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>n:{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
<h1>m:{m}</h1>
<button onClick={() => setM(m + 1)}>onAddM</button>
</div>
);
}

render();

useState数组方案缺陷

注意事项:外面不能加 if

因为上面的实现我们知道了,实际state是一个[],你加了 if的时候 ,这个 index索引就乱了

  • 第一次调用 n 是第一个,第二次调用 m 是第二个
  • 必须保证他们的顺序是一致的
  • 所以不能这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
let m,setM
if(n%2 === 1){
[m, setM] = myUseState(0);
}
console.log("n:" + n);
return (
<div className="App">
<h1>n:{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
<h1>m:{m}</h1>
<button onClick={() => setM(m + 1)}>onAddM</button>
</div>
);
}
  • vue3克服了这个问题

现在存在的问题

  • App 用了 _state 和 index,其他组件咋办

    • 解决办法给每个组件创建一个 _state 和 index
  • 放在全局变量重名了咋办?

    • 放在组件对应的 虚拟dom节点身上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
开始的时候 render(<App/>)

生成虚拟dom
vnode = {
tag:'div',
type:'App'
_state:[],
index:0
}

第一次渲染的时候 触发 useState(0)
渲染 0
第二次 onAdd的时候
产生新的 vnode2 同时 测试的 n = n+1

diff算法 分析差异 ,patch打补丁,更新 <App/>
渲染更新后的 vnode2,n = 1

总结

  • 每个函数组件对应一个 React节点, 虚拟dom
  • 每个节点保存着 state和 index
    • _state 真实名称为 memoizedState
    • index的实现用到了 链表
  • useState 会读取 state[index]
  • index 由 useState 出现的次数 依次递增
  • setState 会修改 state,并触发更新

useState的错误理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
const [n, setN] = useState(0);
const log = () => {
setTimeout(() => {
console.log(`n:${n}`);
}, 3000);
};
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>+1</button>
<button onClick={log}>log</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

先 log 在 +1

  • 打印 0

先+1 在 log

  • 打印 n+1

错误理解就是:setN会改变n

  • setN 不会改变 n
  • 每次+1都会有一个新的n
  • n有分身

能不能一个 n贯穿始终

  • 推荐你用 vue3
  • 因为react推行 函数式,每次都是一个迭代,原来的已经不复存在了

贯穿始终

  • 全局变量
    • window.xxx 即可 太low
  • useRef
    • useRef不仅可用于 div,还能用于 任意数据
    • 但是 n+1 后 ,不会触发UI更新,我就想让它更新咋办? 用vue3
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import React from "react";
      import ReactDOM from "react-dom";

      function App() {
      const nRef = React.useRef(0)
      const log = () => {
      setTimeout(() => {
      console.log(`n:${nRef.current}`);
      }, 3000);
      };
      return (
      <div className="App">
      <h1>{nRef.current}</h1>
      <button onClick={()=>(nRef.current+=1)}>+1</button>
      <button onClick={log}>log</button>
      </div>
      );
      }

      const rootElement = document.getElementById("root");
      ReactDOM.render(<App />, rootElement);

变通的触发UI更新,只要你不觉得自己蛋疼

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
import React from "react";
import ReactDOM from "react-dom";

function App() {
const nRef = React.useRef(0)
// 通过 跟初始值不同,来update
// 但是非常的蛋疼这样写。还是用 vue3吧
const update = React.useState(null)[1]
const log = () => {
setTimeout(() => {
console.log(`n:${nRef.current}`);
}, 3000);
};
return (
<div className="App">
<h1>{nRef.current}</h1>
<button onClick={()=>{
nRef.current+=1;
update(nRef.current)
}}>+1</button>
<button onClick={log}>log</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
  • vue3 的 ref 变更后会触发 UI更新,同时这个 ref贯穿始终

useContext来实现贯穿始终

  • 代码

  • 不管你组件嵌套多少层都可以

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
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");

const themeContext = React.createContext(null);

function App() {
const [theme, setTheme] = React.useState("red");
return (
<themeContext.Provider value={{ theme, setTheme }}>
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
</themeContext.Provider>
);
}

function ChildA() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("red")}>red</button>
</div>
);
}

function ChildB() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("blue")}>blue</button>
</div>
);
}

ReactDOM.render(<App />, rootElement);