• 【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'))
    
  • 相关阅读:
    LeetCode 83. Remove Duplicates from Sorted List (从有序链表中去除重复项)
    LeetCode 21. Merge Two Sorted Lists (合并两个有序链表)
    LeetCode 720. Longest Word in Dictionary (字典里最长的单词)
    LeetCode 690. Employee Importance (职员的重要值)
    LeetCode 645. Set Mismatch (集合不匹配)
    LeetCode 500. Keyboard Row (键盘行)
    LeetCode 463. Island Perimeter (岛的周长)
    115.Distinct Subsequences
    55.Jump Game
    124.Binary Tree Maximum Path Sum
  • 原文地址:https://www.cnblogs.com/fe-linjin/p/11412419.html
Copyright © 2020-2023  润新知