这是一篇译文, 原文地址
做为一种改变组件状态、处理组件反作用的方式,Hooks这个概念最先由React提出,然后被推广到其余框架,诸如Vue、Svelte,甚至出现了原生JS库。可是,要熟练使用Hooks须要对JS闭包有比较好的理解。javascript
在这篇文章中,咱们经过造一个迷你React Hooks轮子来说解闭包的应用。这么作主要有2个目的:css
文章最后咱们会讲解下自定义Hooks是如何回事。html
⚠️你不须要跟着敲一遍代码就能理解Hooks,虽然这么作能帮你巩固下JS基础。别担忧,这一点都不难!
Hooks的主要卖点之一是能够避免复杂的Class组件和高阶组件。可是有些人以为使用Hooks有点像从一个坑里进到另外一个坑里。虽然不用担忧Class组件的this指向的问题,可是又得担忧闭包的引用,so sad~前端
尽管闭包是JS的基础概念,但仍有不少前端萌新对其似懂非懂。Kyle Simpson在他的著做《你不知道的JS》中这么定义闭包:vue
当代码已经执行到一个函数词法做用域以外,可是这个函数仍然能够记住并访问他的词法做用域,那么他就造成了闭包。
因此说闭包的概念和词法做用域是紧密联系的,这是MDN对其的定义。咱们来看一个例子:java
// Demo 0 function useState(initialValue) { var _val = initialValue // _val是useState建立的本地变量 function state() { // state是一个闭包 return _val // state() 使用了由外层函数定义的_val } function setState(newVal) { // 一样 _val = newVal // 赋值_val } return [state, setState] // 将函数暴露给外部使用 } var [foo, setFoo] = useState(0) // 数组解构 console.log(foo()) // 打印 0 - initialValue(初始值) setFoo(1) // 在useState做用域内设置_val的值 console.log(foo()) // 打印 1 - 虽然调用同一个方法,但返回新的 initialValue 复制代码
如今咱们有了第一版的React useState hook。在咱们的函数内部有2个函数,state和setState。state返回内部变量_val的值,setState将该内部变量设置为传参的值。react
咱们这里将state实现为一个getter函数,这不是很理想,但没有关系,咱们待会儿会改进他。重点是咱们经过保存对useState做用域的访问,可以读写内部变量_val,这个引用就叫作闭包。在React和其余的框架中,这看起来像state,其实,这就是state。git
若是你想更深刻的了解闭包,我推荐阅读MDN,DailyJS。若是只是为了理解这篇文章,那理解上面这个例子就够了。github
让咱们用咱们刚才实现的useState来实现一个Counter组件。npm
// Demo 1 function Counter() { const [count, setCount] = useState(0) // 和上文实现的同样 return { click: () => setCount(count() + 1), render: () => console.log('render:', { count: count() }) } } const C = Counter() C.render() // render: { count: 0 } C.click() C.render() // render: { count: 1 } 复制代码
咱们的render方法没有渲染DOM,而是简单的打印咱们的state。同时咱们暴露click方法来替代绑定点击事件。经过以上方法咱们模拟了组件的渲染和点击。
虽然代码能够运行,但把state设计为一个getter并不符合React.useState的表现,咱们来改造他!
若是要和实际的React API保持一致,咱们的state须要设计为一个变量而不是一个函数。但若是只是简单的返回_val,咱们会遇到一个bug:
// Demo 0, 有bug的版本 function useState(initialValue) { var _val = initialValue // 去掉state函数 function setState(newVal) { _val = newVal } return [_val, setState] // 直接返回 _val } var [foo, setFoo] = useState(0) console.log(foo) // 打印 0 setFoo(1) // sets 将useState做用域内的_val赋值为1 console.log(foo) // 打印 0 - 杯具!! 复制代码
这是一种闭包失效的问题。当咱们解构了useState的返回值,他引用了useState调用时的函数内部的_val,这是个值引用,因此不会再改变了!这并非咱们想要的。总的来讲,咱们但愿组件state可以实时反应最新的state,同时又不能是一个函数调用!这两个目标看起来是彻底相悖的。
要解决咱们面对的useState难题,咱们能够把咱们的闭包...放到另外一个闭包里嘛(认真脸)
// Demo 2 const MyReact = (function() { let _val // 在函数做用域内保存_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同样追踪组件的状态(在咱们的例子里,他只会追踪一个组件,内部在_val中保存state)。这样的设计使得咱们的迷你React能够“render”函数式组件,而且每次正确的为内部变量_val赋值。
// 继续 Demo 2 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 } 复制代码
如今看起来更像React Hooks了!
到目前为止,咱们实现了第一个基础hook——useState。让咱们来看看另外一个一样很重要的Hook——useEffect。不一样于useState,useEffect的执行是异步的,这意味着更有可能出现闭包的问题。
一块儿来扩展咱们的React吧:
// Demo 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] } } })() // 使用 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。
咱们很好的实现了useState和useEffect,但他们的单例模式实现的不太好(只能存在一个不然就会出bug),为了最终效果,咱们但愿迷你React能够接收任意数量的state和effect。幸运的是,React Hooks不是魔法,他仅仅是数组。因此,咱们将有一个hooks数组。同时,既然_val和_deps是独立的,咱们能够把他们存储在hooks数组里。
// Demo 4 const MyReact = (function() { let hooks = [], currentHook = 0 // hooks数组 和 当前hook的索引 return { render(Component) { const Comp = Component() // 执行 effects Comp.render() currentHook = 0 // 为下一次render重置hook索引 return Comp }, useEffect(callback, depArray) { const hasNoDeps = !depArray const deps = hooks[currentHook] // type: array | undefined const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true if (hasNoDeps || hasChangedDeps) { callback() hooks[currentHook] = depArray } currentHook++ // 当前hook处理完毕 }, useState(initialValue) { hooks[currentHook] = hooks[currentHook] || initialValue // type: any const setStateHookIndex = currentHook // 为了setState引用正确的闭包 const setState = newState => (hooks[setStateHookIndex] = newState) return [hooks[currentHook++], setState] } } })() 复制代码
注意咱们定义的setStateHookIndex变量,虽然看起来没有作任何事,但它能够阻止setState引用到currentHook。若是不这么作,setState会引用到错误的hook。(试试!)
// 继续 Demo 4 - 使用 function Counter() { const [count, setCount] = MyReact.useState(0) const [text, setText] = MyReact.useState('foo') // 第二个 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) // // 没有effect执行 // render {count: 1, text: 'bar'} App.click() App = MyReact.render(Counter) // effect 2 bar // render {count: 2, text: 'bar'} 复制代码
简单来讲,整个逻辑是经过一个hooks数组和一个每次调用hook都会递增的索引,并在每次组件render后都重置索引。
你也能够构造自定义hooks:
// 再提一次 Demo 4 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“不是魔法”的缘由——不论是在React中仍是咱们的迷你React,自定义hook只是脱离框架的原生JS函数。
如今你能够理解Hook规则的第一条:只在最顶层使用 Hook。经过currentHook变量咱们模拟了React对调用顺序的依赖。你能够通读Hook规则详细说明并回忆咱们的代码实现,相信你会彻底明白的。
同时也请注意下第二条规则,只在 React 函数中调用 Hook,在咱们的迷你React虽然不是必要的,但遵照这条规则能够确保组件的状态逻辑在代码中清晰可见(同时做为一个好的反作用,遵照规则二能够帮你更容易写出遵照规则一的自定义Hook。由于这使你不容易在循环、条件语句中写出像通常JS函数同样命名的有状态函数组件,遵照规则二帮助你遵照了规则一)。
咱们已经作了足够多的练习了。如今你能够试试一行代码实现 useRef,或实现一个接受JSX并渲染到DOM上的render函数,或者其余咱们的迷你React忽略的重要细节。但愿经过本文你了解了如何在上下文中使用闭包,以及React Hooks的工做原理。