vaakian / vaakian.github.io

some notes
https://vaakian.github.io
3 stars 0 forks source link

自定义hooks(custom hooks): useCamera #38

Open vaakian opened 2 years ago

vaakian commented 2 years ago

React之:自定义hooks[useCamera]

尝试用浏览器API封装一个读取摄像头视频流的useCamera自定义hooks,一步一步优化,总结一下得到目前为止的最佳实践。 首先,摄像头读取API需要传入最基本的参数constraints,通过promise方式得到stream后会展示到video标签上,那么useCamera应该接受一个能够读取到video标签的参数,那么首选ref,得到如下第一版代码:

function useCamera(constraints, videoRef) {
  const storedStream = useRef(null)
  // 当stream改变时,创建新的stop函数
  const stop = useCallback(() => {
    storedStream.current.getTracks().forEach(track => track.stop())
  }, [storedStream.current])

  // 当constraints/videoRef改变时,创建新的start函数
  const start = useCallback(() => {
    navigator.mediaDevices.getUserMedia(constraints)
      .then(stream => {
        console.log('setting stream')
        videoRef.current.srcObject = stream
        storedStreamRef.current = stream
      })
      .catch(err => {
        console.error(err)
      })
  }, [constraints, videoRef])
 // constraints改变时,stop上一次的媒体流并重新请求
  useEffect(() => {
    start()
    return stop
  }, [constraints])
  return [start, stop]
}

然后在组件中调用创建的hooks,功能上运行正常。

function App() {
    const videoRef = useRef(null)
    const [start, stop] = useCamera({ video: true, audio: false }, videoRef)
    return (
    <div className="App">
      <video ref={videoRef} autoPlay></video>
      <button onClick={stop}>stop</button>
      <button onClick={start}>start</button>
    </div>
  )
}

依赖变化:对象深比较

但发现当页面re-render的时候,video视频标签会出现闪屏,明显是video标签的视频流被重新设置了。但useEffectconstraints是一个字面量,怎么会导致依赖变化然后执行side effect呢? 后来经过仔细调试发现是因为字面量每次都会重新创建,虽然内容一样,但实际上并不是同一个对象,而useEffect是浅比较,也就是“===”号比较,所以导致判定依赖发生变化。解决办法有很多种:

function useDeepCompareEffect(callback, dependencies) {}

优化hooks设计逻辑

useCamera需要接受一个ref来访问video标签,这其实增加了的耦合性,更好的方式是将媒体流通过hooks返回,交给开发者自由使用。其二,访问摄像头需要用户同意,那么也应该返回摄像头访问权限状态:请求中、同意、拒绝、已停止和媒体设备情况(摄像头、麦克风)。

那么代码如下

function App() {
  const videoRef = useRef(null)
  const [stream, status, start, stop] = useCamera({ video: true, audio: false })

  useEffect(() => {
    if (status === 'success') {
      videoRef.current.srcObject = stream
    }
    return () => {
      videoRef.current.srcObject = null
    }
  }, [status])
  return (
    <div className="App">
      <div>status: {status}</div>
      <video ref={videoRef} autoPlay></video>
      <button onClick={stop}>stop</button>
      <button onClick={start}>start</button>
    </div>
  )
}

hooks实现,那些变量需要用到useState?哪些方法需要用到useCallback? 显然,选择status最佳,当status变化时,页面需要作出不同的响应。

如果不用useState来存储status,那么当status变化时,不会触发rerender,进而useEffect不会比较status并作出响应。

function useCamera(constraints) { const storedStream = useRef(null) const [status, setStatus] = useState('pending') // 当stream改变时,创建新的stop函数 const stop = useCallback(() => { storedStream.current.getTracks().forEach(track => track.stop()) setStatus('stoped') }, [storedStream.current])

// 当constraints/videoRef改变时,创建新的start函数 const start = useCallback(() => { setStatus('pending') navigator.mediaDevices.getUserMedia(constraints) .then(stream => { console.log('setting stream') storedStream.current = stream setStatus('success') }) .catch(err => { setStatus('error') console.error(err) }) }, [constraints])

useDeepCompareEffect(() => { start() return stop }, [constraints]) return [storedStream.current, status, start, stop] }



_**未完待续**_
vaakian commented 2 years ago

先导致rerender,才会有新的compare,从而出现一系列effect。