Hooks取数swr源码.docx
- 文档编号:8961222
- 上传时间:2023-02-02
- 格式:DOCX
- 页数:11
- 大小:21.46KB
Hooks取数swr源码.docx
《Hooks取数swr源码.docx》由会员分享,可在线阅读,更多相关《Hooks取数swr源码.docx(11页珍藏版)》请在冰豆网上搜索。
Hooks取数swr源码
1引言
取数是前端业务的重要部分,也经历过几次演化:
•fetch的兼容性已经足够好,足以替换包括$.post在内的各种取数封装。
•原生用得久了,发现拓展性更好、支持ssr的同构取数方案也挺好,比如isomorphic-fetch、axios。
•对于数据驱动场景还是不够,数据流逐渐将取数封装起来,同时针对数据驱动状态变化管理进行了dataisLoadingerror封装。
•Hooks的出现让组件更Reactive,我们发现取数还是优雅回到了组件里,swr就是一个教科书般的例子。
swr在2019.10.29号提交,仅仅12天就攒了4000+star,平均一天收获300+star!
本周精读就来剖析这个库的功能与源码,了解这个ReactHooks的取数库的WhyHow与What。
2概述
首先介绍swr的功能。
为了和官方文档有所区别,笔者以探索式思路介绍这个它,但例子都取自官方文档。
2.1为什么用Hooks取数
首先回答一个根本问题:
为什么用Hooks替代fetch或数据流取数?
因为Hooks可以触达UI生命周期,取数本质上是UI展示或交互的一个环节。
用Hooks取数的形式如下:
importuseSWRfrom"swr";
functionProfile(){
const{data,error}=useSWR("/api/user",fetcher);
if(error)return
if(!
data)return
return
}
首先看到的是,以同步写法描述了异步逻辑,这是因为渲染被执行了两次。
useSWR接收三个参数,第一个参数是取数key,这个key会作为第二个参数fetcher的第一个参数传入,普通场景下为URL,第三个参数是配置项。
Hooks的威力还不仅如此,上面短短几行代码还自带如下特性:
1.可自动刷新。
2.组件被销毁再渲染时优先启用本地缓存。
3.在列表页中浏览器回退可以自动记忆滚动条位置。
4.tabs切换时,被focus的tab会重新取数。
当然,自动刷新或重新取数也不一定是我们想要的,swr允许自定义配置。
2.2配置
上面提到,useSWR还有第三个参数作为配置项。
独立配置
通过第三个参数为每个useSWR独立配置:
useSWR("/api/user",fetcher,{revalidateOnFocus:
false});
配置项可以参考文档。
可以配置的有:
suspense模式、focus重新取数、重新取数间隔/是否开启、失败是否重新取数、timeout、取数成功/失败/重试时的回调函数等等。
第二个参数如果是object类型,则效果为配置项,第二个fetcher只是为了方便才提供的,在object配置项里也可以配置fetcher。
全局配置
SWRConfig可以批量修改配置:
importuseSWR,{SWRConfig}from"swr";
functionDashboard(){
const{data:
events}=useSWR("/api/events");
//...
}
functionApp(){
return(
3000}}> ); } 独立配置优先级高于全局配置,在精读部分会介绍实现方式。 最重量级的配置项是fetcher,它决定了取数方式。 2.3自定义取数方式 自定义取数逻辑其实分几种抽象粒度,比如自定义取数url,或自定义整个取数函数,而swr采取了相对中间粒度的自定义fetcher: importfetchfrom"unfetch"; constfetcher=url=>fetch(url).then(r=>r.json()); functionApp(){ const{data}=useSWR("/api/data",fetcher); //... } 所以fetcher本身就是一个拓展点,我们不仅能自定义取数函数,自定义业务处理逻辑,甚至可以自定义取数协议: import{request}from"graphql-request"; constAPI="https: //api.graph.cool/simple/v1/movies"; constfetcher=query=>request(API,query); functionApp(){ const{data,error}=useSWR( `{ Movie(title: "Inception"){ releaseDate actors{ name } } }`, fetcher ); //... } 这里回应了第一个参数称为取数Key的原因,在graphql下它则是一段语法描述。 到这里,我们可以自定义取数函数,但却无法控制何时取数,因为Hooks写法使取数时机与渲染时机结合在一起。 swr的条件取数机制可以解决这个问题。 2.4条件取数 所谓条件取数,即useSWR第一个参数为null时则会终止取数,我们可以用三元运算符或函数作为第一个参数,使这个条件动态化: //conditionallyfetch const{data}=useSWR(shouldFetch? "/api/data": null,fetcher); //...orreturnafalsyvalue const{data}=useSWR(()=>(shouldFetch? "/api/data": null),fetcher); 上例中,当shouldFetch为false时则不会取数。 第一个取数参数推荐为回调函数,这样swr会catch住内部异常,比如: //...orthrowanerrorwhenuser.idisnotdefined const{data,error}=useSWR(()=>"/api/data? uid="+user.id,fetcher); 如果user对象不存在,user.id的调用会失败,此时错误会被catch住并抛到error对象。 实际上,user.id还是一种依赖取数场景,当user.id发生变化时需要重新取数。 2.5依赖取数 如果一个取数依赖另一个取数的结果,那么当第一个数据结束时才会触发新的取数,这在swr中不需要特别关心,只需按照依赖顺序书写useSWR即可: functionMyProjects(){ const{data: user}=useSWR("/api/user"); const{data: projects}=useSWR(()=>"/api/projects? uid="+user.id); if(! projects)return"loading..."; return"Youhave"+projects.length+"projects"; } swr会尽可能并行没有依赖的请求,并按依赖顺序一次发送有依赖关系的取数。 可以想象,如果手动管理取数,当依赖关系复杂时,为了确保取数的最大可并行,往往需要精心调整取数递归嵌套结构,而在swr的环境下只需顺序书写即可,这是很大的效率提升。 优化方式在下面源码解读章节详细说明。 依赖取数是自动重新触发取数的一种场景,其实swr还支持手动触发重新取数。 2.6手动触发取数 trigger可以通过Key手动触发取数: importuseSWR,{trigger}from"swr"; functionApp(){ return(
);
}
大部分场景不必如此,因为请求的重新触发由数据和依赖决定,但遇到取数的必要性不由取数参数决定,而是时机时,就需要用手动取数能力了。
2.7乐观取数
特别在表单场景时,数据的改动是可预期的,此时数据驱动方案只能等待后端返回结果,其实可以优化为本地先修改数据,等后端结果返回后再刷新一次:
importuseSWR,{mutate}from"swr";
functionProfile(){
const{data}=useSWR("/api/user",fetcher);
return(
Mynameis{data.name}.
);
}
通过mutate可以在本地临时修改某个Key下返回结果,特别在网络环境差的情况下加快响应速度。
乐观取数,表示对取数结果是乐观的、可预期的,所以才能在结果返回之前就预测并修改了结果。
2.8Suspense模式
在ReactSuspense模式下,所有子模块都可以被懒加载,包括代码和请求都可以被等待,只要开启suspense属性即可:
import{Suspense}from"react";
importuseSWRfrom"swr";
functionProfile(){
const{data}=useSWR("/api/user",fetcher,{suspense:
true});
return
}
functionApp(){
return(
);
}
2.9错误处理
onErrorRetry可以统一处理错误,包括在错误发生后重新取数等:
useSWR(key,fetcher,{
onErrorRetry:
(error,key,option,revalidate,{retryCount})=>{
if(retryCount>=10)return;
if(error.status===404)return;
//retryafter5seconds
setTimeout(()=>revalidate({retryCount:
retryCount+1}),5000);
}
});
3精读
3.1全局配置
在Hooks场景下,包装一层自定义Context即可实现全局配置。
首先SWRConfig本质是一个定制ContextProvider:
constSWRConfig=SWRConfigContext.Provider;
在useSWR中将当前配置与全局配置Merge即可,通过useContext拿到全局配置:
config=Object.assign({},defaultConfig,useContext(SWRConfigContext),config);
3.2useSWR的一些细节
从源码可以看到更多细节用心,useSWR真的比手动调用fetch好很多。
兼容性
useSWR主体代码在useEffect中,但是为了将请求时机提前,放在了UI渲染前(useLayoutEffect),并兼容了服务端场景:
constuseIsomorphicLayoutEffect=IS_SERVER?
useEffect:
useLayoutEffect;
非阻塞
请求时机在浏览器空闲时,因此请求函数被requestIdleCallback包裹:
window["requestIdleCallback"](softRevalidate);
softRevalidate是开启了去重的revalidate:
constsoftRevalidate=()=>revalidate({dedupe:
true});
即默认2s内参数相同的重复取数会被取消。
性能优化
由于swr的data、isValidating等数据状态是利用useState分开管理的:
let[data,setData]=useState(
(shouldReadCache?
cacheGet(key):
undefined)||config.initialData
);
//...
let[isValidating,setIsValidating]=useState(false);
而取数状态变化时往往data与isValidating要一起更新,为了仅触发一次更新,使用了unstable_batchedUpdates将更新合并为一次:
unstable_batchedUpdates(()=>{
setIsValidating(false);
//...
setData(newData);
});
其实还有别的解法,比如使用useReducer管理数据也能达到相同性能效果。
目前源码已经从unstable_batchedUpdates切换为useReducer管理
dispatch(newState);
3.3初始缓存
当页面切换时,可以暂时以上一次数据替换取数结果,即初始化数据从缓存中拿:
constshouldReadCache=config.suspense||!
useHydration();
//stale:
getfromcache
let[data,setData]=useState(
(shouldReadCache?
cacheGet(key):
undefined)||config.initialData
);
上面一段代码在useSWR的初始化期间,useHydration表示是否为初次加载:
letisHydration=true;
exportdefaultfunctionuseHydration():
boolean{
useEffect(()=>{
setTimeout(()=>{
isHydration=false;
},1);
},[]);
returnisHydration;
}
3.4支持suspense
Suspense分为两块功能:
异步加载代码与异步加载数据,现在提到的是异步加载数据相关的能力。
Suspense要求代码suspended,即抛出一个可以被捕获的Promise异常,在这个Promise结束后再渲染组件。
核心代码就这一段,抛出取数的Promise:
throwCONCURRENT_PROMISES[key];
等取数完毕后再返回useSWRAPI定义的结构:
return{
error:
latestError,
data:
latestData,
revalidate,
isValidating
};
如果没有上面throw的一步,在取数完毕前组件就会被渲染出来,所以throw了请求的Promise使得这个请求函数支持了Suspense。
3.5依赖的请求
翻了一下代码,没有找到对循环依赖特别处理的逻辑,后来看了官方文档才恍然大悟,原来是通过try/catch并巧妙结合React的UI=f(data)机制实现依赖取数的。
看下面这段代码:
const{data:
user}=useSWR("/api/user");
const{data:
projects}=useSWR(()=>"/api/projects?
uid="+user.id);
怎么做到智能按依赖顺序请求呢?
我们看useSWR取数函数的主体逻辑:
constrevalidate=useCallback(
async()=>{
try{
//设置isValidation为true
//取数、onSuccess回调
//设置isValidation为false
//设置缓存
//unstable_batchedUpdates
}catch(err){
//撤销取数、缓存等对象
//调用onError回调
}
},
[key]
)
useIsomorphicLayoutEffect(
()=>{
....
},
[key,revalidate,...]
)
每次渲染的时候,SWR会试着执行key函数(例如()=>"/api/projects?
uid="+user.id),如果这个函数抛出异常,那么就意味着它的依赖还没有就绪(user===undefined),SWR将暂停这个数据的请求。
在任一数据完成加载时,由于setState触发重渲染,上述Hooks会被重选执行一遍(再次检查数据依赖是否就绪)然后对就绪的数据发起新的一轮请求。
另外对于一些正常请求碰到error(shouldRetryOnError默认为true)的情况下,下次取数的时机是:
constcount=Math.min(opts.retryCount||0,8);
consttimeout=
~~((Math.random()+0.5)*(1< 重试时间基本按2的指数速度增长。 所以swr会优先按照并行方式取数,存在依赖的取数会重试,直到上游Ready。 这种简单的模式稍稍损失了一些性能(没有在上游Ready后及时重试下游),但不失为一种巧妙的解法,而且最大化并行也使得大部分场景性能反而比手写的好。 4总结 笔者给仔细阅读本文的同学留下两道思考题: •关于Hooks取数还是在数据流中取数,你怎么看呢? •swr解决依赖取数的方法还有更好的改进办法吗?
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Hooks swr 源码
![提示](https://static.bdocx.com/images/bang_tan.gif)