咱們實現了一個這樣的功能
react
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閉包
出現這樣的狀況主要緣由是: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(渲染)異步
useLayoutEffect 能夠看做是 useEffect 的同步版本。使用 useLayoutEffect 就能夠達到咱們上面說的,在同一次更新流程中解綁 interval 的目的。
useLayoutEffect裏面的callback函數會在DOM更新完成後當即執行,可是會在瀏覽器進行任何繪製以前運行完成,阻塞了瀏覽器的繪製.函數
順序大概是這樣的:
useLayoutEffect: setRunning(false) => setLapse(0) => render(渲染) => (clearInterval =>執行effect)測試
把 lapse 和 running 放在一塊兒,變成了一個 state 對象,有點相似 Redux 的用法。在這裏咱們給 TICK action 上加了一個是否 running 的判斷,以此來避開了在 running 被設置爲 false 以後多餘的 lapse 改變。spa
最大的區別是咱們的 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'))