深度剖析React hooks工作原理
在本文中,我们通过构建 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。)
作者:so丶简单
链接:https://juejin.cn/post/7031074478977187853