- Published on
从闭包到 React Hooks
- Authors

- Name
- 薯仔
- @Henry_Yangs
闭包
闭包是非常基础的前端知识点,我们先复习一下,假如有如下代码:
let foo = 1
function add() {
foo = foo + 1
return foo
}
console.log(add())
console.log(add())
console.log(add())

但是很明显,全局变量foo对于一个公共库来说是很不友好的,即便是业务项目,也很容易被篡改,因此我们可以通过使用闭包来实现上述功能:
function getAdd() {
let foo = 1
return function () {
foo = foo + 1
return foo
}
}
const add = getAdd()
console.log(add())
console.log(add())
console.log(add())
执行后,我们同样可以得到一样的输出 
但是const add = getAdd()仍有些多余,可以使用IIFE自动声明函数
const add = (function getAdd() {
let foo = 1
return function () {
foo = foo + 1
return foo
}
})()
console.log(add())
console.log(add())
console.log(add())
到这里为止,闭包的知识点已经复习完毕,这也是我们对React Hooks建模所需要的全部基础知识,接下来可以实现一个React Hooks的心智模型
useState
首先,我们想一下useState是怎么使用的?接收一个初始值,然后返回一个数组,第一项是一个变量,第二项是改变这个变量的函数,那我们可以这样定义和使用:
function useState(initVal) {
let _val = initVal
let state = _val
const setState = (newVal) => (state = newVal)
return [state, setState]
}
const [count, setCount] = useState(1)
console.log(count)
setCount(2)
console.log(count)
我们看到这里两次输出的都是1,其实仔细看上述代码,setState已经形成了闭包,state = newVal已经成功了,但由于解构出来的count变量是一个数字(即值,而非引用),因此,在调用setState之后,count也没有改变。
这里可以暂时做一个比较tricky的操作,将useState函数做一个很小的改动:
function useState(initVal) {
let _val = initVal
let state = () => _val // 改成函数
const setState = (newVal) => (_val = newVal)
return [state, setState]
}
const [count, setCount] = useState(1)
console.log(count()) // 调用方式需要稍微改变一下
setCount(2)
console.log(count())
当然这是一个临时方案,React里面不是这样使用count的,我们也不会想这样使用,因此我们对useState做一次重构:
const React = (function () {
function useState(initVal) {
let _val = initVal
let state = () => _val
const setState = (newVal) => (_val = newVal)
return [state, setState]
}
function render(Component) {
const C = Component()
C.render()
return C
}
return { useState, render }
})()
function Component() {
const [count, setCount] = React.useState(1)
return {
render: () => console.log(count),
click: () => setCount(count + 1),
}
}
var App = React.render(Component)
App.click()
var App = React.render(Component)
在上面重构的代码中,我们先引入了React这个变量,它是一个立即执行函数,我们先将useState函数放进去并且返回给外部。
由于useState都是使用在组件中,我们新增一个函数叫做Component,在这个函数中使用useState。因为现在并没有和DOM相关的内容,所以先返回一个对象,里面模拟render和鼠标点击的click函数,其中render函数直接输出count的值,click函数模拟鼠标点击时修改count的值。
现在我们有了React和Component组件,如何告诉React渲染组件呢?我们在React中再增加一个render函数,这个函数接收一个函数(组件)作为参数,在render函数中,直接执行Component参数,在得到Component返回的对象后,执行这个对象中的render函数,最后将执行Component参数得到的对象返回。
最后,我们就可以将React和Component结合起来使用。

一个名为state的函数。这是因为我们在React.useState中,将内部变量state赋值成了函数,为了解决这个问题,现在再对React做一些修改
const React = (function () {
let _val // 将_val提升到useState外
function useState(initVal) {
let state = _val || initVal // 将_val赋值给内部变量state,且用initVal作为默认值
const setState = (newVal) => (_val = newVal)
return [state, setState]
}
function render(Component) {
const C = Component()
C.render()
return C
}
return { useState, render }
})()

到这里,useState基本上可以运行起来了,但接下来又有了新的问题,如果我有多个useState呢?
function Component() {
const [count, setCount] = React.useState(1)
const [text, setText] = React.useState('PCG')
return {
render: () => console.log({ count, text }),
click: () => setCount(count + 1),
type: (newText) => setText(newText),
}
}
var App = React.render(Component)
App.click()
var App = React.render(Component)
App.type('WXG')
var App = React.render(Component)

可以很清楚地看到,每次调用setXX函数是,会将所有的变量都置为同一个值,这是因为他们共用了React模块中的_val这个私有变量,要解决这个问题,我们需要将_val扩充:
const React = (function () {
let hooks = []
let idx = 0
function useState(initVal) {
let state = hooks[idx] || initVal
/**
* 由于setState是异步执行
* 每次执行时,idx已经被重置为0了
* 因此需要拿到idx的瞬时值
* 否则,每次调用render后,输出的结果都是一样的
*/
const _idx = idx
const setState = (newVal) => {
hooks[_idx] = newVal
}
idx++ // 在每次执行完hook之后,idx自增,这样下一个hook就可以正确执行
return [state, setState]
}
function render(Component) {
idx = 0 // 每次渲染重置idx
const C = Component()
C.render()
return C
}
return { useState, render }
})()

可以看到此时的输出结果已经正常了,我们可以分别管理count和text的值
同时也可以看出来,我们为什么不能在条件语句中使用hooks,例如:
if (Math.random() > 0.5) {
const [count, setCount] = React.useState(1)
}
const [text, setText] = React.useState('PCG')
此时,第一个useState有50%的可能不会执行,在是否执行的两种情况下,内部变量idx的值的变化时不同的,这会导致内部变量hooks数组中保存的值的变化也变得不可预知。因此,千万不要在条件语句中使用hooks。
useEffect
在有了useState之后,我们当然想要useEffect,同样根据useEffect的使用方式,我们来对其建模:
const React = (function () {
let hooks = []
let idx = 0
function useState(initVal) {
let state = hooks[idx] || initVal
const _idx = idx
const setState = (newVal) => {
hooks[_idx] = newVal
}
idx++
return [state, setState]
}
function useEffect(cb, deps) {
const oldDep = hooks[idx] // 临时保存依赖的旧值
let hasChanged = true // flag,判断依赖是否改变
if (oldDep) {
/**
* 如果旧依赖存在
* 在传入的依赖列表中
* 找到对应的旧值并判断是否相等
* 如果不相等,即为依赖发生了变化
*/
hasChanged = deps.some((dep, i) => !Object.is(dep, oldDep[i]))
}
if (hasChanged) cb() // 依赖变化,执行回调函数
hooks[idx] = deps // 将传入的依赖的新值保存
idx++ // 每次hooks执行后都需要自增idx
}
function render(Component) {
idx = 0
const C = Component()
C.render()
return C
}
return { useState, render, useEffect }
})()
function Component() {
const [count, setCount] = React.useState(1)
const [text, setText] = React.useState('PCG')
React.useEffect(() => {
console.log('Tencent!!!!')
}, []) // 可以自行增加依赖项或去掉第二个参数查看效果
return {
render: () => console.log({ count, text }),
click: () => setCount(count + 1),
type: (newText) => setText(newText),
}
}
var App = React.render(Component)
App.click()
var App = React.render(Component)
App.type('WXG')
var App = React.render(Component)

可以看到在最开始执行了一次useEffect,证明我们的useEffect是成功的。
到这里为止,一个简易的React模型就已经完成了。虽然暂时没有和DOM联系起来,但所需要的也仅仅是JSX解析函数和一个更完善的render函数,这两块可以留到下次分享。
最后,虽然上述的React模块看上去很像真正的React,但其实它并不是,它只能算是一个React的模型,具体实现中需要考虑更多的因素。Anyway,我们可以从这样模型出发,再去读React的源码,也许会对React有另外的认识。
参考资料:JSCONF Asia 2019