咱们实现了一个这样的功能
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'))