前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Hooks】:[译]React hooks是怎么工作的

【Hooks】:[译]React hooks是怎么工作的

作者头像
WEBJ2EE
发布2021-02-26 16:08:01
9760
发布2021-02-26 16:08:01
举报
文章被收录于专栏:WebJ2EEWebJ2EE
代码语言:javascript
复制
目录
1. 什么是闭包
2. 在函数式组件中使用
3. 之前的闭包
4. 模块中的闭包
5. 复制 useEffect
6. 仅仅是数组
7. 理解 Hooks 的原则
8. 总结

从根本上说,hooks 是一种相对简单的方式去封装状态行为和用户行为。React 是第一个使用 hooks 的,然后广泛地被其他框架(比如:Vue、Svelte)所接受。但是,hooks 函数式的设计需要对 javascript 的闭包有一个深刻的理解。

这里,我们通过实现一个简单的 hooks,重新介绍下闭包。主要2个目标:保证闭包的有效使用;展示怎么通过29行js代码实现一个 hooks。最后会介绍下自定义 hooks。

提示:你不需要为了理解 hooks 而去做下面的这些事情。通过这些练习会帮助你提升js基础能力。不用担心,不是很难。

1. 什么是闭包

hooks 的一个卖点是可以避免类的复杂性和高阶组件。但是,有人觉得,我们只是用一个问题替代了另一个问题。我们不用再担心 context 的边界问题,但是需要去担心闭包。就像 Mark Dalgleish 很准确的总结。

闭包是 js 的一个基本原则。但是,很多新的js开发者对闭包很困惑。《You Don't Know JS》的作者 Kyle Simpson 这样定义闭包:闭包使得一个函数能够记住和访问它的词法作用域,即使这个函数是在作用域外执行。

他们很明显和词法作用域的原则关联了起来,在 MDN 是这样定义的:当函数嵌套时,一个解析器怎么解析变量名。为了更好的理解,让我们来看个例子:

代码语言:javascript
复制
// Example 0
function useState(initialValue) {
  var _val = initialValue // _val is a local variable created by useState
  function state() {
    // state is an inner function, a closure
    return _val // state() uses _val, declared by parent funciton
  }
  function setState(newVal) {
    // same
    _val = newVal // setting _val without exposing _val
  }
  return [state, setState] // exposing functions for external use
}
var [foo, setFoo] = useState(0) // using array destructuring
console.log(foo()) // logs 0 - the initialValue we gave
setFoo(1) // sets _val inside useState's scope
console.log(foo()) // logs 1 - new initialValue, despite exact same call

这里,我们简单实现了 React 的 useState hook。函数中有2个内部函数,state 和 setState。state 返回一个本地变量 _val,setState 将变量赋值给传进来的参数(比如:newVal)。

这里 state 是一个 getter 函数(当然还不是很完美),我们会稍微修改下。重要的是,我们能通过 foo 和 setFoo,获取和控制内部变量 _val。他们能获取 useState 的作用域,这种引用关系叫做闭包。在 React 或其他框架的上下文中,这就是 state。

2. 在函数式组件中使用

让我应用一下新创建的 useState 函数。我们将创建一个 Counter 组件。

代码语言:javascript
复制
// Example 1
function Counter() {
  const [count, setCount] = useState(0) // same useState as above
  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 }

这里,我们选择简单打印 state,而不是渲染到具体的 dom。我们也只是手动调用了一下 Counter,而不是绑定到一个事件处理器上。这样,我们可以模拟组建渲染和响应用户行为。当代码开始执行,通过 getter 获取 state 并不是真正的 React.useState hook。让我们优化一下。

3. 之前的闭包

如果我们想匹配真实的 React API,我们的 state 必须是一个变量,而不是一个函数。但是如果我们简单的暴露 _val,会发现有一个bug:

代码语言:javascript
复制
// Example 0, revisited - this is BUGGY!
function useState(initialValue) {
  var _val = initialValue
  // no state() function
  function setState(newVal) {
    _val = newVal
  }
  return [_val, setState] // directly exposing _val
}
var [foo, setFoo] = useState(0)
console.log(foo) // logs 0 without needing function call
setFoo(1) // sets _val inside useState's scope
console.log(foo) // logs 0 - oops!!

这是之前的闭包问题的一种。当我们在 useState 外面重新设置 foo 时,foo 指向的是 useState 初始化时的那个 _val,并且永远不会再改变。这个不是我们想要的,当用变量代替方法调用时,我们通常需要我们的组件状态会响应当前的 state。这2个目标好像完全相反了。

4. 模块中的闭包

我们可以通过将闭包放到另一个闭包里来解决 useState 的这个问题。

代码语言:javascript
复制
// Example 2
const MyReact = (function() {
  let _val // hold our state in module scope
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue // assign anew every run
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

这里我们选择使用模块化的方式去克隆一个小的React。像 React,他会跟踪组件的状态。这个设计允许 MyReact 去‘渲染’你的函数组件,也允许每次闭包执行时去设置内部的 _val。

代码语言:javascript
复制
// Example 2 continued
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 }

现在看起来更像是有 hooks 能力的 React。

5. 复制 useEffect

目前为止,我们已经实现了React Hook 里最基础的一个hook useState。第2个重要的 hook 是 useEffect。不同于 useState,useEffect 是异步执行的,所以它更有可能出现闭包问题。

我们把之前的代码扩展一下。

代码语言:javascript
复制
// Example 3
const MyReact = (function() {
  let _val, _deps // hold our state and dependencies in scope
  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。

6. 仅仅是数组

我们用函数的方式实现了 useState 和 useEffect,但是不太好的是,2个都是单例的。为了愉快的做任何事情,我们需要大量的创建 state 和 effects。幸运的是,就像 Rudi Yardley 写的,React Hooks 并不死魔法,仅仅是数组。因此,我们需要一个 hooks 数组。然后把 _val 和 _deps 都放到数组里,因为他们不会有交集:

代码语言:javascript
复制
// Example 4
const MyReact = (function() {
  let hooks = [],
    currentHook = 0 // array of hooks, and an iterator!
  return {
    render(Component) {
      const Comp = Component() // run effects
      Comp.render()
      currentHook = 0 // reset for next render
      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++ // done with this hook
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue // type: any
      const setStateHookIndex = currentHook // for setState's closure!
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()

注意 setStateHookIndex 的使用,看起来好像啥也没干,但是其实它是为了避免 setState 受 currentHook 变量的影响。如果你注意到,因为 currentHook 是旧的值,setState 会出现问题。

代码语言:javascript
复制
// Example 4 continued - in usage
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  const [text, setText] = MyReact.useState('foo') // 2nd 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)
// // no effect run
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}

因此,第一直觉是我们需要一个 hooks 数组,和一个指针,它会随着 hook 被调用而递增,随着组件的渲染而重置。

你可以看一个自定义 hook 的例子。

代码语言:javascript
复制
// 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 不是魔法 - 不管是 React 的原生 hooks,还是我们之前创建的 hooks,自定义 hooks 都很容易脱离成独立的 hook。

7. 理解 Hooks 的原则

看了上文,你很容易理解 React Hooks 的第一条原则:只能在最上层调用 hooks。我们也明确指明了 React 在调用 hooks 的顺序依赖了 currentHook 变量。你可以通读文章 the entirety of the rule’s explanation,再结合我们这篇文章对 Hooks的解读,就可以完全理解了。

第二条原则:只能在函数式组件中调用 hooks,在我们的实现中,这条原则是非必须的,但是对于明确划分哪些代码模块依赖状态逻辑,这很明显是一个很好的实践。(还有一点好处,这一条原则也很容易让你能够编写工具去保证第一条原则得到实施。)

8. 总结

到这里,你已经延伸了你的能力范围。希望你已经加深了对闭包的理解,掌握了 React Hooks 是怎么运行的。

参考:

Deep dive: How do React hooks really work?: https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档