【React hooks】你不得不知道的闭包问题

需求分析

咱们实现了一个这样的功能
react

  • 点击 Start 开始执行 interval,而且一旦有可能就往 lapse 上加一
  • 点击 Stop 后取消 interval
  • 点击 Clear 会取消 interval,而且设置 lapse 为 0
import React from 'react'
import ReactDOM from 'react-dom'

const buttonStyles = {
  border: '1px solid #ccc',
  background: '#fff',
  fontSize: '2em',
  padding: 15,
  margin: 5,
  width: 200,
}
const labelStyles = {
  fontSize: '5em',
  display: 'block',
}

function Stopwatch() {
  const [lapse, setLapse] = React.useState(0)
  const [running, setRunning] = React.useState(false)

  React.useEffect(() => {
    if (running) {
      const startTime = Date.now() - lapse
      const intervalId = setInterval(() => {
        setLapse(Date.now() - startTime)
      }, 0)
      return () => {
        clearInterval(intervalId)
      }
    }
  }, [running])

  function handleRunClick() {
    setRunning(r => !r)
  }

  function handleClearClick() {
    setRunning(false)
    setLapse(0)
  }

  if (!running) console.log('running is false')

  return (
    <div>
      <label style={labelStyles}>{lapse}ms</label>
      <button onClick={handleRunClick} style={buttonStyles}>
        {running ? 'Stop' : 'Start'}
      </button>
      <button onClick={handleClearClick} style={buttonStyles}>
        Clear
      </button>
    </div>
  )
}

function App() {
  const [show, setShow] = React.useState(true)
  return (
    <div style={{textAlign: 'center'}}>
      <label>
        <input
          checked={show}
          type="checkbox"
          onChange={e => setShow(e.target.checked)}
        />{' '}
        Show stopwatch
      </label>
      {show ? <Stopwatch /> : null}
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

点击进入demo测试浏览器

 

问题描述

1.咱们首先点击start,2.而后点击clear,3.发现问题:显示的并非0ms闭包

 

问题分析

为何经过clear设置了值为0,却显示的不是0?

出现这样的状况主要缘由是:useEffect 是异步的,也就是说咱们执行 useEffect 中绑定的函数或者是解绑的函数,都不是在一次 setState 产生的更新中被同步执行的。啥意思呢?咱们来模拟一下代码的执行顺序:
1.在咱们点击来 clear 以后,咱们调用了 setLapse 和 setRunning,这两个方法是用来更新 state 的,因此他们会标记组件更新,而后通知 React 咱们须要从新渲染来。
2.而后 React 开始来从新渲染的流程,并很快执行到了 Stopwatch 组件。
3.先执行了Stopwatch组件中的同步组件,而后执行异步组件,所以经过clear设置的0被渲染,而后即将执行useEffect中的异步事件,因为在执行清除interval以前,interval还存在,所以它计算了最新的值,并把经过clear设置的0给更改了并渲染出来,而后才清除。dom

顺序大概是这样的:
useEffect:setRunning(false) => setLapse(0) => render(渲染) => 执行Interval => (clearInterval => 执行effect) => render(渲染)异步

 

问题解决

方法1:使用useLayoutEffect

useLayoutEffect 能够看做是 useEffect 的同步版本。使用 useLayoutEffect 就能够达到咱们上面说的,在同一次更新流程中解绑 interval 的目的。
useLayoutEffect里面的callback函数会在DOM更新完成后当即执行,可是会在浏览器进行任何绘制以前运行完成,阻塞了浏览器的绘制.函数

顺序大概是这样的:
useLayoutEffect: setRunning(false) => setLapse(0) => render(渲染) => (clearInterval =>执行effect)测试

 

方法2: 使用useReducer解决闭包问题

把 lapse 和 running 放在一块儿,变成了一个 state 对象,有点相似 Redux 的用法。在这里咱们给 TICK action 上加了一个是否 running 的判断,以此来避开了在 running 被设置为 false 以后多余的 lapse 改变。spa

那么这个实现跟咱们使用 updateLapse 的方式有什么区别呢?

最大的区别是咱们的 state 不来自于闭包,在以前的代码中,咱们在任何方法中获取 lapse 和 running 都是经过闭包,而在这里,state 是做为参数传入到 Reducer 中的,也就是不论什么时候咱们调用了 dispatch,在 Reducer 中获得的 State 都是最新的,这就帮助咱们避开了闭包的问题。pwa

import React from 'react'
import ReactDOM from 'react-dom'

const buttonStyles = {
  border: '1px solid #ccc',
  background: '#fff',
  fontSize: '2em',
  padding: 15,
  margin: 5,
  width: 200,
}
const labelStyles = {
  fontSize: '5em',
  display: 'block',
}

const TICK = 'TICK'
const CLEAR = 'CLEAR'
const TOGGLE = 'TOGGLE'

function stateReducer(state, action) {
  switch (action.type) {
    case TOGGLE:
      return {...state, running: !state.running}
    case TICK:
      if (state.running) {
        return {...state, lapse: action.lapse}
      }
      return state
    case CLEAR:
      return {running: false, lapse: 0}
    default:
      return state
  }
}

function Stopwatch() {
  // const [lapse, setLapse] = React.useState(0)
  // const [running, setRunning] = React.useState(false)

  const [state, dispatch] = React.useReducer(stateReducer, {
    lapse: 0,
    running: false,
  })

  React.useEffect(
    () => {
      if (state.running) {
        const startTime = Date.now() - state.lapse
        const intervalId = setInterval(() => {
          dispatch({
            type: TICK,
            lapse: Date.now() - startTime,
          })
        }, 0)
        return () => clearInterval(intervalId)
      }
    },
    [state.running],
  )

  function handleRunClick() {
    dispatch({
      type: TOGGLE,
    })
  }

  function handleClearClick() {
    // setRunning(false)
    // setLapse(0)
    dispatch({
      type: CLEAR,
    })
  }

  return (
    <div>
      <label style={labelStyles}>{state.lapse}ms</label>
      <button onClick={handleRunClick} style={buttonStyles}>
        {state.running ? 'Stop' : 'Start'}
      </button>
      <button onClick={handleClearClick} style={buttonStyles}>
        Clear
      </button>
    </div>
  )
}

function App() {
  const [show, setShow] = React.useState(true)
  return (
    <div style={{textAlign: 'center'}}>
      <label>
        <input
          checked={show}
          type="checkbox"
          onChange={e => setShow(e.target.checked)}
        />{' '}
        Show stopwatch
      </label>
      {show ? <Stopwatch /> : null}
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
相关文章
相关标签/搜索