一、组件更新过程
function App() {
const [n,setN] = React.useState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}+1</button>
</p>
</div>
);
ReactDOM.render(<App />, rootElement);
过程:
- 首次渲染,render(<App />)
- render会调用App函数,得到虚拟DIV,创建真实DIV
- 用户点击Button,调用setN(n+1),render函数被再一次调用
- render进一步调用App函数,得到虚拟DIV,Diff,更新真实DIV
- 每一次setN都会再次调用render,进而调用App
问题就来了每次setN触发的App函数,都会运行useState(0),每次运行区别是什么?
二、简单实现useState
- setN会触发re-render
- useState会读取新的数据值,不然的话每次重新执行App都会被初始化,每一次读的都是同一个变量很容易联想到闭包
function render() {
ReactDOM.render(<App />, root);
}
function myUseState(initialValue) {
let state = initialValue;
function setState(newValue) {
state = newValue;
render();
}
return [state,setState]
}闭包隐藏并保护一个变量,每次调用setN改变的是同一个变量
这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。
自然我们就能想到,把 state 提取出来,存在 useState 外面。
var _state; // 把 state 存储在外面
function useState(initialValue) {
_state = _state === undefined ? initialValue : _state;
// 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}核心就在于setN改变的变量是函数外部的所以函数会记住上次的值
有一个很大的问题:它只能使用一次,因为只有一个 _state,数组完美解决
let _state = [];
let index = 0;
function useState(initialValue) {
const currentIndex = index;
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state;
function setState(newState) {
_state[currentIndex] = newState;
render();
}
index += 1;
return [_state[currentIndex], setState];
}有一个很大的问题:每次render()重新执行App函数都会执行useState,然后产生新下标。
let _state = [];
let index = 0;
function useState(initialValue) {
const currentIndex = index;
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state;
function setState(newState) {
_state[currentIndex] = newState;
index = 0 //重置
render();
}
index += 1;
return [_state[currentIndex], setState];
}React的数据更新依赖的还是useState这个函数返回的第一个值,setN只是修改第一个参数而已。
每次调用useState,index就会被固定数值成为currentIndex,之后setN只会修改这个n,因为下标不会变化,setN改变的数据是固定的(闭包保护了setN不会篡改currentIndex)。
根据上面代码发现useState的顺序是在太重要了,每次re-render的useState的顺序不能改变,所以不能出现在if语句中。每个组件都有自己的_state和index
三、更新原理
React会维护一个虚拟DOM树(始终存在),以及在页面存在的真实DOM树,
App() => useState(0) ===> App1(虚拟Dom)
||
setN(n+1) Diff算法(把差别做成对象patch,开始更新虚拟DOM树)
re-render()
||
App() => useState(0) ===> App2(虚拟Dom)
- 每个函数组件对应一个React节点
- 每个节点保存着state和index
- useState会读取state[index] -把state存储在useState函数外
- index由useState出现的顺序决定
- setState会修改state,并触发更新-index存储在setState函数外部
四、始终如一的数据
function App() {
const [n, setN] = React.useState(0);
const log = () => {
setTimeout(() => {
console.log(n);
}, 1000);
};
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={()=>{setN(n+1)}}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
} function setState(newState) {
_state[currentIndex] = newState;
index = 0 //重置
render();
}先点击log再点击setN,打印出的是旧数据
let n;
for (n = 0; n++; n < 6) {
setTimeout(() => {
console.log(n);
},1000);
}会打出6个6
for (let n = 0; n++; n < 6) {
setTimeout(() => {
console.log(n);
},1000);
}会打出123546,
因此我们推断每次re-render的state并不是同一个,而是新作用域创建的新变量
- 使用window.xxx 全局的变量来保证你的数据是始终如一的
- useRef
const refContainer = useRef(initialValue);useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
我们经常使用 ref 方式来访问 DOM 。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。
- useContext
如果说ref只是一个组件始终如一,你们context是所有组件始终如一,也就是全局变量
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}表示这个变量的作用范围
使用useContext来读取
五、useEffect
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
cursor = 0;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}代码关键在于:
- 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
- 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
- useState,useEffect 和使用的不是同一个数据
- 核心就在于每次更新把cursor赋值为零,然后更新时按照hooks顺序,依次从 memoizedState 中把上次记录的值拿出来,useEffect接受useState(返回新值)和旧值进行比较
六、总结
每次重新渲染,组件函数就会执行
对应的所有state都会出现 分身,(新作用域)
如果没有类似于setimeout的引用就会被垃圾回收掉
为什么会出现分身,因为每次重新执行App函数都会重新const一个新的state数组(过时的闭包)
function App() {
const [n, setN] = React.useState(0);
useEffect(() => {
setInterval(function log() {
console.log(`Count is :${n}`);
}, 2000);
},[]);
return (
<div>
{n}
<button
onClick={() => {
setN(n + 1);
}}
>
+1
</button>
</div>
);
}上面代码就是过时的闭包,log函数和const [n, setN]组成了闭包,但是log函数里的n只是旧的n,点击+1之后已经是n的"分身"了,由于log还在引用旧n,所以暂时没有被垃圾回收掉。
这个是React的设计缺陷,不是为了对比两个虚拟DOM