谈谈 effect、ref 和 异步测试

1,381 阅读5分钟

halo 大家好,我是 132,我又来了::>_<::今天给大家带啦的是一篇有关于 fre(react)中对于 effect、ref 等的处理以及测试

事件的起因是 mindplay-dk 提出的两个 issue:

github.com/132yse/fre/…

github.com/132yse/fre/…

内容里比较大,但是其实不是很麻烦,我们一一道来

异步的 effects

我们都知道 useEffect 是异步的,它发生在 dom 操作之后

我们怎样知道 dom 操作完了呢?很明显,就是我们常常说的 nextTick

它可以这样被模拟

setTimeout(fn, 0)

但是事实上 setTimeout 太晚了,它的最小延时是 4ms 而且容易受各种奇葩的因素影响,这对于业务代码没什么问题,但是对于框架,何况是时间切片的框架而言,这些不准确的时间浪费尤为可贵

所以解决办法也很简单,requestAnimationFrame 就可以啦

所以综上所述,fre 中的 effects 实在 rAF 中执行的,rAF 的执行时机是下一帧重绘之前,也可以理解为 macrotask 的队头

然后 effects 有个特殊的行为,就是 cleanup

useEffect(()=>{
    console.log('effect'),
    return ()=>{
        console.log('cleanup')
    }
})

return 一个 cleanup 函数,它会被挂起,然后在下次 effect 之前触发

所以上面的代码如果应该打印出以下:

1.
effect
2.
cleanup
effect

如果是删除,那么只会执行 cleanup,因为这个 cleanup 是上次遗留的

看上去仿佛没有问题…………但是结合 ref 看的话,emmm

异步的 ref

ref 和 effect 是类似的,它也是几乎类似的执行时机,fre 中同样放到 rAF 中

看这一段代码:

<p ref={n => {
    if(n){
        doSometing()
    }else{
        doCleanup()
    }
}} />

我们通过判断 dom 参数来判断是否需要进行清理工作,所以当一个元素被删除的时候,我们应该将 ref 断言为 null

但是这里还有更多的细节,和 effect 不同的是,ref 是针对于单个元素的,看这个代码:

<p>
  <span ref={t}></span>
</p>

当父元素被删除的时候,也就是 p 标签,我们是否需要执行 span 标签的 ref 呢?

答案是肯定的……当父亲被删除,它的所有孩子的 ref 引用都需要执行 cleanup

react 的行为是再次遍历它们,就是父亲删除,孩子仍然进行遍历更新行为,然后执行 ref

我认为这很智障,所以 fre 做了收集工作,第一次遍历的时候对 ref 进行收集,第二次删除后仍然遍历上次的 ref

这和 effect 的处理十分类似

那么问题又来了………………孩子应该是什么执行顺序呢?

<p ref={two}>
  <span ref={one}></span>
</p>

是的,你没有看错,倒序执行,先执行 span 再执行 p

所以 fre 的收集数组是个 栈

到这里,基本上 ref 就结束了………………

才怪。

<p ref={
  c ? n => doSomething(n)
  : n => doSomethingElse(n)
} />

看这个…………我们怎样对 ref 进行更改呢?

其实这个是很难更改的,因为我们的 ref 其实是一个变量替换,我们之后拿到 oldRef 然后和 newRef 对比,看看是不是同一个函数,然后再替换

然而很不幸,怎么比较两个函数相同呢?难就一个字。

所以 react 想了个办法,每次都执行两次 ref,第一次断言为 null,这样第二次就可以愉快替换了

<p ref={
  (dom)=> console.log(dom)
} />

更新阶段它会打印两次:

null
<p></p>

所以 fre 中其实这个栈也遍历了两次

呼,讲了这么多,最后一个问题,effect 和 ref 的顺序如何?

答案是——

当组件渲染或者更新时,ref 在 effect 之前执行

当组件删除时,ref 的清理工作在 effect 的清理之后

综上所述,真的可以把人绕晕,这些东西在平时写业务的时候很难注意到

我也是花了很长时间才捋顺这些,最后 fre 也给出了正确的实现

异步测试

上面我们介绍了两个异步的 API,effect 和 ref

接下来我来说说,fre 框架如何用 jest 去测试它们的(包括异步渲染)

首先,fre 整个的渲染行为是异步的,我们如何测异步渲染呢?正常思路是 promise,等渲染结束,resolve 渲染结果

事实上 fre 也是这么做的

  new Promise(resolve => {
    document.body.innerHTML = ''
    render(jsx, document.body, () => resolve([...document.body.childNodes]))
  })

传入一个 render callback 然后等待它渲染完成

正常的 dom 都可以拿到,但是很不幸,当渲染完成后,rAF 还不知道有没有执行完

那咋办………………node 中没有 raf 这一说,jest 模拟的 raf 太鸡肋了

也就是说,我们实在没办法,在宏任务队列执行完毕后去 resolve

所以只好上手动延迟大发了

const defer = fn => {
  let start = Date.now()
  while(Date.now() < start + 16){}
}

每次 expect 之前,手动延迟 16ms,也就是延迟到下一帧

这样一来就可以拿到受 effect 影响的 dom 了

对于非 dom 内容,我们也可以通过多 render 一次的方式,拿到上一次的内容

终于,fre 将 effect 和 ref 重构后,跑通所有测试,现在测试覆盖率降低了 1%,但是这都不是事儿

江湖路远,来日方长!

最后放一下 fre 的 github 地址,欢迎来一起讨论:

github.com/132yse/fre