「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」
在本文中,我们通过构建 React Hooks 来重新引入闭包。这将有两个目的——展示闭包的有效使用,以及展示如何仅用 29 行可读的 JS 来构建 Hooks。最后,我们了解自定义 Hooks 是如何产生的。你不需要为了理解 Hooks 做任何这些。如果你完成这个练习,它可能只会帮助你的 JS 基础知识。别担心,没那么难!
什么是闭包?
闭包是 JS 中的一个基本概念。尽管如此,它们还是因让许多特别是新开发人员感到困惑而臭名昭著。You Don't Know JS 的Kyle Simpson将闭包定义为:
闭包是指函数能够记住并访问其词法范围,即使该函数在其词法范围之外执行。
它们显然与词法作用域的概念密切相关,MDN 将其定义为“当函数嵌套时解析器如何解析变量名”。让我们看一个实际例子来更好地说明这一点:
// Example 0
function useState(initialValue) {
var _val = initialValue
function state() {
return _val
}
function setState(newVal) {
_val = newVal
}
return [state, setState]
}
var [foo, setFoo] = useState(0)
console.log(foo())
setFoo(1)
console.log(foo())
在这里,我们正在创建 ReactuseState
钩子的原始克隆。在我们的函数中,有 2 个内部函数,state
和setState
. state
返回一个_val
上面定义的局部变量并将局部变量setState
设置为传递给它的参数(即newVal
)。
我们在state
这里的实现是一个 getter 函数,它并不理想,但我们稍后会修复它。重要的是,使用foo
and setFoo
,我们能够访问和操作(又名“关闭”)内部变量_val
。它们保留对useState
的范围的访问,该引用称为闭包。在 React 和其他框架的上下文中,这看起来像状态,而这正是它的本质。
在函数组件中的使用
让我们useState
在熟悉的环境中应用我们新创建的克隆。我们将制作一个Counter
组件!
// Example 1
function Counter() {
const [count, setCount] = useState(0)
return {
click: () => setCount(count() + 1),
render: () => console.log('render:', { count: count() })
}
}
const C = Counter()
C.render()
C.click()
C.render()
在这里,我们没有渲染到 DOM,而是选择了console.log
退出我们的状态。我们还为我们的 Counter 公开了一个编程 API,因此我们可以在脚本中运行它,而不是附加一个事件处理程序。通过这种设计,我们能够模拟我们的组件渲染和对用户操作的反应。
虽然这有效,但调用 getter 来访问状态并不是真正React.useState
钩子的 API 。让我们解决这个问题。
模块中的闭包
我们可以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]
}
}
})()
现在这看起来更像是 React with Hooks!
复制 useEffect
到目前为止,我们已经介绍了useState
,这是第一个基本的 React Hook。下一个最重要的钩子是useEffect
. 与 不同setState
,useEffect
异步执行,这意味着有更多机会遇到闭包问题。
我们可以扩展我们迄今为止建立的 React 的微型模型,以包含以下内容:
// Example 3
const MyReact = (function() {
let _val, _deps
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useEffect(callback, depArray) {
const hasNoDeps = !depArray
const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true
if (hasNoDeps || hasChangedDeps) {
callback()
_deps = depArray
}
},
useState(initialValue) {
_val = _val || initialValue
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()
// 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}
为了跟踪依赖关系(因为useEffect
依赖关系改变时重新运行),我们引入了另一个变量来跟踪_deps
。
所以基本的直觉是有一个数组hooks
和一个索引,当每个钩子被调用时递增,并在组件呈现时重置。
// Example 4, revisited
function Component() {
const [text, setText] = useSplitURL('www.netlify.com')
return {
type: txt => setText(txt),
render: () => console.log({ text })
}
}
function useSplitURL(str) {
const [text, setText] = MyReact.useState(str)
const masked = text.split('.')
return [masked, setText]
}
let App
App = MyReact.render(Component)
// { text: [ 'www', 'netlify', 'com' ] }
App.type('www.reactjs.org')
App = MyReact.render(Component)
// { text: [ 'www', 'reactjs', 'org' ] }}
推导出 Hooks 的规则
请注意,从这里您可以轻松理解Hooks 规则的第一条:仅在顶层调用 Hooks。我们已经用我们的currentHook
变量明确地模拟了 React 对调用顺序的依赖。您可以在考虑到我们的实施的情况下通读规则的全部解释,并完全理解正在发生的一切。
另请注意,第二条规则“仅从 React 函数调用钩子”也不是我们实现的必要结果,但明确划分代码的哪些部分依赖于有状态逻辑当然是一个好习惯。(作为一个很好的副作用,它还可以更轻松地编写工具以确保您遵循第一条规则。通过在循环和条件内包装命名为常规 JavaScript 函数的有状态函数。以下规则 2 可帮助您遵循规则 1。)