(本文内容是参考此篇英文原版,整理了里面的主要内容,属于精简版。)
hooks 是利用了闭包原理,在内部声明了变量,return 出来,在外部可是访问。 实现个基础版 useState hook。
// demo1
function useState(initialValue) {
// 创建一个内部变量,用来存状态,相当于 getter
var _val = initialValue
function state(){
return _val
}
// 设置状态的函数, 相当于 setter
function setState(newVal){
_val = newVal
}
return [state, setState]
}
var [foo, setFoo] = useState(0)
console.log(foo()) // 0
// 对它的作用域的访问,该引用称为“闭包”
setFoo(1)
console.log(foo()) // 1
// 在组件中使用
function Counter(){
const [count, setCount] = useState(0)
return {
click: ()=>setState(count()+1),
render: console.log('render:',{count:count()})
}
}
// 调用组件
const C = Counter()
C.render() // render: {count: 0}
C.click()
C.render() // render: {count: 1}
上面虽然实现了useState的功能,但是获取状态时候,使用的函数调用,我们想要匹配真是的ReactApi,状态必须是变量,下面来进一步修整。
// demo2
function useState(initialVal){
var _val = initialVal
function setState(newVal){
_val = newVal
}
return [_val, setState]
}
var [foo, setFoo] = useState(0)
console.log(foo) // logs 0
setFoo(1)
console.log(foo) // logs 0
上面直接返回_val会出现状态不更新的问题。
当 foo 从 useState 中解构出来时,将始终指向初始状态,并且不会被改变。
显然这种做法不是我们期望的...
我们希望组件状态始终指向当前的状态,是个变量而不是函数调用。
继续修整...
// demo3
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() {
const [count, setCount] = MyReact.useState(0)
return {
click: () => setCount(count + 1),
render: () => console.log('render:', {count})
}
}
let App
App = MyReact.render(Counter) // render: {count: 0}
App.click()
App = MyReact.render(Counter) // render: {count: 1}
上面将闭包写在模块(Module)中,模拟微型React,跟踪组件状态,可解决demo2中的问题。
目前为止,我们已经介绍了useState,这是第一个基本的ReactHook。下一个最重要的Hook是 useEffect。
与 useState 不同,useEffect 是异步执行,意味着有更多机会遇到闭包。
下面对demo3中建立的React微型模型进行扩展。
// demo4
const MyReact = (function(){
// 声明状态和依赖栈
var _val, _deps
return {
render(Component){
const Comp = Component()
Comp.render()
return Comp
},
useState(initialValue){
_val = _val || initialValue
function setState(newVal){
_val = newVal
}
return [_val, setState]
},
useEffect(callback, depArray){
// 没有添加执行依赖
const hasNoDeps = !depArray
const hasChangedDeps = _deps ? !depArray.every((el,i)=>el===_deps[i]):true
// 如果没有依赖或依赖有变更都执行callback
if(hasNoDeps || hasChangedDeps) {
callback()
_deps = depArray
}
}
}
})()
// usage
function Counter(){
const [count, setCount] = MyReact.useState(0)
MyReact.useEffect(()=>{
console.log('effect', count)
}, [count])
return {
click: () => setCount(count+1),
noop: () => setCount(count),
render:() => console.log('render', {count})
}
}
let App
App = MyReact.render(Counter)
// effect 0
// render {count: 0}
App.click()
App = MyReact.render(Counter)
// effect 1
// render {count: 1}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1}
App.click()
App = MyReact.render(Counter)
// effect 2
// render {count: 2}
上面虽然实现了useState和useEffect,但两者都是单例。
为了跟踪依赖关系(因为当依赖改变后effect便会再次执行),我们再增加一个变量进一步跟踪。
hooks 本质是个数组~
// demo5
const MyReact = (function(){
var hooks = [],
currentHook = 0
return {
render(Component){
const Comp = Component()
Comp.render()
return Comp
},
useState(initialValue){
Hooks[currentHook] = Hooks[currentHook] || initialValue
const setStateHookIndex = currentHook // for setState's closure!
const setState = (newVal) => (Hooks[setStateHookIndex] = newVal)
return [Hooks[currentHook++], setState]
},
useEffect(callback, depArray){
const hasNoDeps = !depArray
const deps = Hooks[currentHook]
const hasChangeDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
if (hasNoDeps || hasChangedDeps) {
callback()
hooks[currentHook] = depArray
}
currentHook++ // done with this hook
}
}
})()
注意在setStateHookIndex这里的用法,该用法似乎没有做任何事情,但用于防止setState关闭currentHook变量!
如果您将其取出,则setState由于已关闭currentHook而过期,因此再次停止工作。往下看
function Counter() {
const [count, setCount] = MyReact.useState(0)
const [text, setText] = MyReact.useState('foo') // 2nd state hook!
MyReact.useEffect(() => {
console.log('effect', count, text)
}, [count, text])
return {
click: () => setCount(count + 1),
type: txt => setText(txt),
noop: () => setCount(count),
render: () => console.log('render', { count, text })
}
}
let App
App = MyReact.render(Counter)
// effect 0 foo
// render {count: 0, text: 'foo'}
App.click()
App = MyReact.render(Counter)
// effect 1 foo
// render {count: 1, text: 'foo'}
App.type('bar')
App = MyReact.render(Counter)
// effect 1 bar
// render {count: 1, text: 'bar'}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}
因此,基本直觉是拥有一个数组hooks和一个索引,该索引在调用每个钩子时递增,并在呈现组件时重置。
我们可以在单个组件中使用多个 State Hook 或 Effect Hook
那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。
Hook 的调用顺序在每次渲染中都是相同的
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部,否则 Hook 的调用顺序就容易变更。
参考:Hooks规则