写在前面
阅读hook的官方文档 可以看到它的一些使用限制
。
平时开发的时候,我们会遵循这些使用规则,但是通常我们不太理解其中的原因,当我们尝试在循环
或者条件语句
中调用hook的时候,
浏览器就会抛出类似的错误。
接下来 ,让我们尝试手动实现以下useState和useEffect这两个Hook。通过理解其设计原则
来理解其使用规则
.
useState实现
参考如下计数器
setState需要返回一个[变量,函数]元组,并且在每次 调用函数时,自动调用render方法更新视图
function App() {
const [num, setNum] = useState < number > 0;
return (
<div>
<div>num: {num}</div>
<button onClick={() => setNum(num + 1)}>+ 1</button>
</div>
);
}
// Example 1 这么做是有bug的!
function useState(initialValue) {
var _val = initialValue
function setState(newVal) {
_val = newVal
render()
}
return [_val, setState] // 直接对外暴露_val
}
var [foo, setFoo] = useState(0)
console.log(foo) // logs 0 不需要进行函数调用
setFoo(1) // 在useState作用域内给_val赋值
console.log(foo) // logs 0 - 糟糕!!
这里实现的setState每次更新state后,并不能拿到最新的state,因为组件内部的state值,已经state的值等于解构赋值时的初始值,后续没有重新赋值给解构出来的变量,因此不会更新。
为了达到更新state的目的,而这个state又必须是一个变量而不是一个函数,我们考虑把useState本身放在一个闭包中。
// Example 2
const MyReact = (function() {
let _val // 将我们的状态保持在模块作用域中
return {
render(Component) {
const comp = Component()
comp.render()
return comp
},
useState(initialValue) {
_val = _val || initialValue // 每次运行都重新赋值
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()
function Counter(){
var [a,b] = MyReact.useState(0)
return {
click : ()=>{b(a+1)},
render:()=> {console.log(a)}
}
}
let App = MyReact.render(Counter)
App.click()
App = MyReact.render(Counter)
MyReact是允许用来渲染react组件的一个对象,当通过setState更新状态后,每次渲染都会重新执行MyReact.useState ,以此拿到MyReact闭包内的最新数据。更新state到视图
此时我们的state只支持一个usestate 我们通过将存放状态的变量_val
改造成数组和使用当前所在的usestate索引curr
使其支持多个useState
// Example 3
const MyReact = (function() {
let _val = [] // 将我们的状态保持在模块作用域中
let curr = 0
return {
render(Component) {
// 每次更新视图都需要重置curr
curr = 0
const comp = Component()
comp.render()
return comp
},
useState(initialValue) {
// 注意这里需要保存curr到本地变量currenCursor,否则当用户setState时,curr已经不是我们所需要的值
const currenCursor = curr;
_val[currenCursor] = _val[currenCursor] || initialValue; // 检查是否渲染过
function setState(newVal) {
_val[currenCursor] = newVal
}
++curr;
return [_val[currenCursor], setState]
}
}
})()
function Counter(){
var [a,b] = MyReact.useState(0)
var [c,d] = MyReact.useState(6)
return {
click : ()=>{b(a+1),d(c+1)},
render:()=> {alert(a + '+' +c)}
}
}
let App = MyReact.render(Counter)
App.click()
App = MyReact.render(Counter)
至此 这个功能更像React中的useState Hook了
那么如何解释,不能再循环或者条件语句中使用useState呢
假设有如下代码
let tag = true;
function App() {
const [num, setNum] = useState < number > 0;
// 只有初次渲染,才执行
if (tag) {
const [unusedNum] = useState < number > 1;
tag = false;
}
const [num2, setNum2] = useState < number > 2;
return (
<div>
<div>num: {num}</div>
<div>
<button onClick={() => setNum(num + 1)}>加 1</button>
<button onClick={() => setNum(num - 1)}>减 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setNum2(num2 * 2)}>扩大一倍</button>
<button onClick={() => setNum2(num2 / 2)}>缩小一倍</button>
</div>
</div>
);
}
第一次循环时 拿到的state&对应的cursor如下
state - setState中的cursor
num -0
unusedNum - 1
num2 - 2
第二次循环时获得的对应关系如下
state - setState中的cursor
num - 0
num2 - 1
此时的setNum2指向的对象不正确,自然update后返回的state也不正确。
实际上,react使用的是单向链表
维护hook的先后顺序和内容,但原因是一致的,我们可以看看react实际源码
ReactFiberHooks.js中定义了初始化时, firstWorkInProgressHook 和 workInProgressHook这两个全局变量,观察所有的hook实现
firstWorkInProgressHook指向第一个hook,workInProgressHook 指向当前正在处理的hook
每个hook的结构可以看开头的Hook type定义
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
考虑下面的代码
let condition = true;
const [state1,setState1] = useState(0);
if(condition){
const [state2,setState2] = useState(1);
condition = false;
}
const [state3,setState3] = useState(2);
初始化时 执行了以下流程
- 初始时,组件还未渲染时,firstWorkInProgressHook = workInProgressHook = null;
- firstWorkInProgressHook = workInProgressHook = Hook{state1}
- firstWorkInProgressHook = workInProgressHook = Hook{state2}
- firstWorkInProgressHook = workInProgressHook = Hook{state3}
可以用下图理解这个结构
memoizedState 存储当前Hook的结果,next指向下一个hook,每个hook对象中保存了当前hook的状态 ,和指向下一个hook的属性。
第二次渲染时,condition不符合,只剩下两个hook 渲染调用update方法,update方法对每个hook分别执行了updateWorkInProgressHook()
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
currentHook 指向当前代码中正在处理的hook,
workInProgressHook 指向之前维护的链表中对应的当前正在处理的hook
firstWorkInProgressHook 对应维护的链表结构对应的hook头
更新时 第一个currentHook指向的next为原先的第三个hook,并且赋值给链表结构中的第二个workInProgressHook对象,导致返回的state和setstate对象都是错误
的!
useEffect实现
useEffect 称为组件的副作用,他相对于直接将逻辑写在函数组件顶层的优点是,可以在依赖未更新的时候不重复执行。
在Example3 的基础上加上useeffect的部分
_val 中 useEffect对应的内容为传入的依赖数组
每次执行时 对比之前的依赖和现在的值是否不同,并且更新依赖值,
如果更新 执行callback,并且更新依赖的最新值,否则什么都不做
// Example 4
const MyReact = (function() {
let _val = [] // 将我们的状态保持在模块作用域中
let curr = 0
return {
render(Component) {
curr = 0
const comp = Component()
comp.render()
return comp
},
// + effect部分
useEffect(callback, depArray) {
const currenCursor = curr;
const hasNoDeps = !depArray
const deps = _val[currenCursor] // type: array | undefined
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
if (hasNoDeps || hasChangedDeps) {
callback()
_val[currenCursor] = depArray
}
curr++ // 本hook运行结束
},
// - effect部分
useState(initialValue) {
const currenCursor = curr;
_val[currenCursor] = _val[currenCursor] || initialValue; // 检查是否渲染过
function setState(newVal) {
_val[currenCursor] = newVal
}
++curr;
return [_val[currenCursor], setState]
}
}
})()
function Counter(){
var [a,b] = MyReact.useState(0)
MyReact.useEffect(() => {
alert('effect:' + a)
}, [a])
return {
// 触发effect
click : ()=>{b(a+1)},
// 不触发effect
noop: () => b(a),
render:()=> {alert(a)}
}
}
let App = MyReact.render(Counter)
App.click()
App = MyReact.render(Counter)
App.noop()
App = MyReact.render(Counter)
以上的逻辑解释了,为什么useeffect在不传参数的时候,会在每次rerender时执行,而在传[]时,只在初始化时执行一次。
总结
- 本文使用数组 + cursor维护多个hooks的状态,使用模块闭包实现状态值的更新。
- 文章中使用的demo是简化版的实现,并且在实际react中使用的是单向链表维护的hooks,demo为最简单实现,方便理解,useState 和 useEffect 特性洗
- 欢迎批评 欢迎指正 🐶
参考文章