halo 大家好,我是 132,我又来了::>_<::今天给大家带啦的是一篇有关于 fre(react)中对于 effect、ref 等的处理以及测试
事件的起因是 mindplay-dk 提出的两个 issue:
内容里比较大,但是其实不是很麻烦,我们一一道来
异步的 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 地址,欢迎来一起讨论: