zoukankan      html  css  js  c++  java
  • 【React hooks】你不得不知道的闭包问题

    需求分析

    我们实现了一个这样的功能

    • 点击 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,
       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给更改了并渲染出来,然后才清除。

    顺序大概是这样的:
    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 改变。

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

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

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    const buttonStyles = {
      border: '1px solid #ccc',
      background: '#fff',
      fontSize: '2em',
      padding: 15,
      margin: 5,
       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'))
    
  • 相关阅读:
    O(n^2)的排序方法
    99乘法表
    excel 转 csv
    批量关闭 excel
    tomcat 加入服务
    文件打包 zip
    字符串转换
    List数组种删除数据
    mybatis 批量上传
    sql server 查询表字段及类型
  • 原文地址:https://www.cnblogs.com/fe-linjin/p/11412419.html
Copyright © 2011-2022 走看看