在讲解之前我们先回忆下闭包,什么是闭包呢?
闭包是一个特殊的对象 它由两部分组成,执行上下文A以及在A中创建的函数B。
当B执行时,如果访问了A中的变量对象,那么闭包就会产生。
为什么要谈到闭包呢?
答:闭包是react hooks的核心。
首先看下我们怎么使用hooks的呢?
useEffect(()=>{
console.log('useEffect')
},[])
const ref = useRef(null);
手写一个简易版 useState 的代替品 useMyState
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
// 在当前模块中定义变量_state
let _state;
const myUseState = (initState) => {
// 第一次调用时没有初始值,因此使用传入的初始值赋值
_state = ( _state === undefined ? initState : _state);
const setState = (newState) => {
_state = newState
// 此方法能触发页面渲染
render()
}
return [_state,setState]
}
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
// myUseState在App组件内执行,访问了_state变量,这里产生一个闭包
const [n, setN] = myUseState(0);
// 根据闭包的特性,_state变量会持久存在,当App组件再次触发渲染时候,依然能够获取上一次_state值
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
但是这样的代码也会遇到问题,如果除了 n 还有个 m 呢?
useMyState 方法进阶版
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
let index = 0;
const myUseState = (initState) => {
let currentIndex = index;
_state[currentIndex] = ( _state[currentIndex] === undefined ? initState : _state[currentIndex]);
const setState = (newState) => {
_state[currentIndex] = newState
index = 0
render()
}
index += 1
return [_state[currentIndex],setState]
}
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(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>
);
}
在上面的useMyState中,我们使用了数组来存放多个hook,然后react源码中使用了更高级的存储结构。
react用了链表这种数据结构来存储 函数组建里面的 hooks
baseQueue: null, // 当前 update
baseState: 'hook1', // 初始值,即 useState 入参
memoizedState: null, // 当前状态(更新时表示上一次的状态)
queue: null, // 待执行的更新队列(queue.pending)
next: { // 下一个 hook
baseQueue: null,
baseState: null,
memoizedState: 'hook2',
next: null
queue: null
}
}
**hooks 以链表的形式存储在fiber节点的memoizedState属性上 **
举例说明,下面我们书写了几个hook
const [count, setCount] = useState(1)
const [name, setName] = useState('chechengyi')
useEffect(()=>{
}, [])
const text = useMemo(()=>{
return 'ddd'
}, [])
}
分析:
1、在组件第一次渲染的时候,为每个hooks都创建了一个对象, 最终形成了一个链表.
2、在组件更新的过程中,hooks函数执行的顺序是不变的,就可以根据这个链表拿到当前hooks对应的Hook对象,函数式组件就是这样拥有了state的能力。
注意:所以,知道为什么不能将hooks写到if else语句中了把?因为这样可能会导致顺序错乱,导致当前hooks拿到的不是自己对应的Hook对象。
闭包陷阱问题 在上面实现useState中,使用了闭包来存储state值,这就难免会引发一系列闭包问题,如内存溢出、闭包陷阱等。
举例什么是闭包陷阱?
const [count, setCount] = useState(1);
useEffect(()=>{
setInterval(()=>{
console.log(count)
}, 1000)
}, [])
}
上面例子中中,我们持续触发setCount函数+1处理10次,等待1秒后打印结果还是1,那么为什么呢
每一次 render 都会生成一个闭包,每个闭包都有自己的 state 和 props(所以在异步函数中访问hooks的state值拿到的是当前的闭包值并不是最新的state值),所以打印出来的是1;
如何解决这个问题呢? 第一,对useEffect添加依赖count
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
alert("You clicked on: " + count);
}, 3000);
}, [count])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
第二,使用 useRef 解决闭包陷阱的问题
const [value, setValue] = useState(1)
const countRef = useRef(value)
useEffect(() => {
countRef.current = value
}, [value])
const log = useCallback(
() => {
setTimeout(() => {
alert(countRef.current)
}, 1000);
},
[value],
)
return (
<div>
<p>FunctionComponent</p>
<div>value: {value}</div>
<button onClick={() => setValue(value + 1)}>add</button>
<br/>
<button onClick={log}>alert</button>
</div>
)
}
原理:useRef 每次 render 时都会返回同一个引用类型的对象,我们设置值和读取值都在这个对象上处理,这样就能获取到最新的 value 值了。
注意:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。