29行代码深刻React Hooks原理

这是一篇译文, 原文地址  

做为一种改变组件状态、处理组件反作用的方式,Hooks这个概念最先由React提出,然后被推广到其余框架,诸如Vue、Svelte,甚至出现了原生JS库。可是,要熟练使用Hooks须要对JS闭包有比较好的理解。javascript

在这篇文章中,咱们经过造一个迷你React Hooks轮子来说解闭包的应用。这么作主要有2个目的:css

  1. 展现闭包如何在Hooks内使用
  2. 展现一下如何用29行代码实现一个迷你React Hooks

文章最后咱们会讲解下自定义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个函数,statesetStatestate返回内部变量_val的值,setState将该内部变量设置为传参的值。react

咱们这里将state实现为一个getter函数,这不是很理想,但没有关系,咱们待会儿会改进他。重点是咱们经过保存对useState做用域的访问,可以读写内部变量_val,这个引用就叫作闭包。在React和其余的框架中,这看起来像state,其实,这就是stategit

若是你想更深刻的了解闭包,我推荐阅读MDNDailyJS。若是只是为了理解这篇文章,那理解上面这个例子就够了。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了!

实现useEffect

到目前为止,咱们实现了第一个基础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函数。

推导Hooks的规则

如今你能够理解Hook规则的第一条:只在最顶层使用 Hook。经过currentHook变量咱们模拟了React对调用顺序的依赖。你能够通读Hook规则详细说明并回忆咱们的代码实现,相信你会彻底明白的。

同时也请注意下第二条规则,只在 React 函数中调用 Hook,在咱们的迷你React虽然不是必要的,但遵照这条规则能够确保组件的状态逻辑在代码中清晰可见(同时做为一个好的反作用,遵照规则二能够帮你更容易写出遵照规则一的自定义Hook。由于这使你不容易在循环、条件语句中写出像通常JS函数同样命名的有状态函数组件,遵照规则二帮助你遵照了规则一)。

结论

咱们已经作了足够多的练习了。如今你能够试试一行代码实现 useRef,或实现一个接受JSX并渲染到DOM上的render函数,或者其余咱们的迷你React忽略的重要细节。但愿经过本文你了解了如何在上下文中使用闭包,以及React Hooks的工做原理。

相关文章
相关标签/搜索