useState实现
最简单的useState
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
console.log('run')
const [n, setN] = React.useState(0);
console.log(`n:${n}`)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 首次渲染render
- 调用App,得到虚拟DIV,根据虚拟div创建真实div
- 用户点击Button,调用setN(n+1),再次render
- 调用App(),得到虚拟DIV,DOM Diff更新真实div
- 每次调用App(),都会运行useState()
- 每次点击button,都会打印run
- 每次点击button,打印出来的n都变化,那么他是怎么做到运行同样的代码,打印出来的东西不同的呢?
问几个问题
- 执行setN的时候发生了什么?n会变吗?app会重新执行吗?
- n不会变
- app会重新执行
- 通过console.log得到,每次运行n的值不同
- 如果App()会重新执行?
- n每次的值不同
分析几个问题
- setN
- setN一定会修改数据x,将n+1存入x
- setN一定会触发重新渲染(re-render)
- useState
- useState会从x读取n的最新值
- x
- 每个数组都有自己的数据x,我们将其命名为state
尝试实现React.useState
- let _state; // 定义在外面
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state; // 定义在外面
function myUseState(initialValue) {
_state = _state === undefined ? initialValue : _state;
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
// 教学需要,不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
const [n, setN] = myUseState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
如果一个组件用了2个useState怎么办?
改进思路:
- 把_state设计为一个对象
- 比如_state={n:0,m:1}
- 不行,因为useState(0),并不知道变量是n还是m
- 把_state设计为一个数组
- 比如_state = [0,0]
- 貌似可以
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
let index = 0;
function myUseState(initialValue) {
const currentIndex = index;
index += 1;
_state[currentIndex] = _state[currentIndex] || initialValue;
const setState = newState => {
console.log(currentIndex)
_state[currentIndex] = newState;
render();
};
return [_state[currentIndex], setState];
}
//,不用在意 render 的实现
const render = () => {
index = 0;
ReactDOM.render(<App />, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
console.log(_state);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
<p>{m}</p>
<p>
<button onClick={() => setM(m + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
_state数组方案缺点
- useState调用顺序
- 若第一次渲染时n是第一个,m是第二个,k是第三个
- 则第二次渲染时必须保证顺序完全一致
- 所以react不允许出现在if for里
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
let index = 0;
function myUseState(initialValue) {
const currentIndex = index;
index += 1;
_state[currentIndex] = _state[currentIndex] || initialValue;
const setState = newState => {
_state[currentIndex] = newState;
render();
};
return [_state[currentIndex], setState];
}
//不用在意 render 的实现
const render = () => {
index = 0;
ReactDOM.render(<App />, rootElement);
};
function App() {
const [n, setN] = React.useState(0);
let m, setM;
if (n % 2 === 1) {
[m, setM] = React.useState(0);
}
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
<p>{m}</p>
<p>
<button onClick={() => setM(m + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
总结
- 每个函数组件对应一个react节点(FiberNode)
- 每个节点保存着state(memorizedState)和index
- useState会读取state[index]
- index由useState出现顺序决定
- setState会修改state,并处触发更新
n的分身
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const [n, setN] = React.useState(0);
const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
两种操作:
- 先+1再log:1,1(无bug)
- 先log再+1:0,1(bug)
- 为什么第二种 情况会log出旧的n
- 每次都有一个新的n
- 现在我就想有一个贯穿始终的状态,应该怎么做?
- 全局变量
- window.xxx
- useRef
- useRef不仅可以用来引用div,还可以用于任何数据
- 例子见下文
- 强制更新的例子见下文
- useContext
- useContext不仅能贯穿始终,还能贯穿不同组件
- useContext例子
useRef
- 代码
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0); // {current:0}
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => (nRef.current += 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 这是不管先log后log,都是1.因为只有一个n
- 发现这是展示出的n不能实时更新,因为使用useRef不会让app重新渲染
- 如何强制更新呢?不推荐
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0);
const update = React.useState()[1];
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => ((nRef.current += 1), update(nRef.current))}>
+1
</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
useContext
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
// 1. 初始化上下文
const themeContext = React.createContext(null);
function App() {
const [theme, setTheme] = React.useState("red");
return (
// 2. context作用域,不管多深都可以
<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);
- 初始化上下文
const themeContext = React.createContext(null);
- provider: 上下文的作用域从这里开始,这个范围里的,不管多深,都可以用
- 子组件使用
const { setTheme } = React.useContext(themeContext);
总结
- 每次中心渲染,组件函数就会执行
- 对应的所有state都会出现分身
- 如果不希望出现分身
- 使用useRef,useContext