这里是Feniast这个人类记录些东西的地方。一个微不足道的存在平日里挣扎生存的一些痕迹。仅此而已。
feniast / notes Goto Github PK
View Code? Open in Web Editor NEW记录些东西,有用无用
记录些东西,有用无用
Disclaimer: 不保证此思路和实现完全正确。
需要一个全局的store来存储本应用路由的历史historyRoutes
。
1.监听route变化
当进入一个页面时,将当前路由存到route
, 之前的route
就存到prevRoute
。
如果不是back的,将prevRoute
加入historyRoutes
。反之跳过这个步骤。
然后将backFlag
置为false。
2. 定义回退方法
点击应用中的回退按钮,设置backFlag
为true,同时判断historyRoutes
是否为空,为空则push到首页,否则跳到historyRoutes
中第一条,同时将historyRoutes
中第一条移除。
为什么要定义这么一个东西,理论上只要是全局的store都可以,这里选择了redux。
// action.ts
import { createAction, Action } from 'redux-actions';
import { ThunkAction } from 'redux-thunk';
import { RootState } from '@/store';
export const SET_ROUTE = 'SET_ROUTE';
export const SET_BACK_FLAG = 'SET_BACK_FLAG';
export const SET_HISTORY_ROUTES = 'SET_HISTORY_ROUTES';
export const setRoute = createAction(SET_ROUTE);
export const setBackFlag = createAction(SET_BACK_FLAG);
export const setHistoryRoutes = createAction(SET_HISTORY_ROUTES);
export const appendHistoryRoute = (): ThunkAction<
void,
RootState,
unknown,
Action<any>
> => (dispatch, getState) => {
const { historyRoutes, backFlag, prevRoute } = getState().router;
if (!backFlag) {
if (historyRoutes[0] !== prevRoute) { // 判断历史中第一条和要推入历史的这条记录是否一致,不一致则推入
const newHistoryRoutes = [prevRoute, ...historyRoutes];
dispatch(setHistoryRoutes(newHistoryRoutes));
}
}
};
// reducer.ts
import { handleActions, Action } from 'redux-actions';
import {
SET_ROUTE,
SET_BACK_FLAG,
SET_HISTORY_ROUTES,
} from '../actions/router';
import { Reducer } from 'redux';
export interface RouterState {
prevRoute?: string;
route?: string;
backFlag?: boolean;
historyRoutes?: string[];
}
const routerReducer: Reducer<RouterState, Action<any>> = handleActions<RouterState, any>(
{
[SET_ROUTE]: (state: RouterState, action: Action<string>) => ({
...state,
prevRoute: state.route,
route: action.payload,
}),
[SET_BACK_FLAG]: (state: RouterState, action: Action<boolean>) => ({
...state,
backFlag: action.payload,
}),
[SET_HISTORY_ROUTES]: (state: RouterState, action: Action<string[]>) => ({
...state,
historyRoutes: action.payload,
}),
},
{
prevRoute: null,
route: null,
backFlag: false,
historyRoutes: [],
}
);
export default routerReducer;
这里定义了一个组件,完成思路中的1。用useEffect去绑定监听器。
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import Router from 'next/router';
import {
setRoute,
appendHistoryRoute,
setBackFlag,
} from '@/store/actions/router';
const RouteAware: React.FC = (props) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(setRoute(Router.pathname)); // 记录应用刚挂载完的路由,因为此时是没有事件监听的
}, []);
useEffect(() => {
const onComplete = (url: string) => {
dispatch(setRoute(url));
dispatch(appendHistoryRoute());
dispatch(setBackFlag(false));
};
Router.events.on('routeChangeComplete', onComplete);
return () => {
Router.events.off('routeChangeComplete', onComplete);
};
}, [dispatch]);
return <>{props.children}</>;
};
export default RouteAware;
然后需要把这个组件放到层级较高的位置,不能在Page组件里,同时需要在redux Provider里,放到_app.tsx即可
<Provider store={reduxStore}>
<RouteAware>
<Component {...pageProps} />
</RouteAware>
</Provider>
此处用hooks实现,然后需要调用的地方使用这个hook的返回方法即可
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { setBackFlag, setHistoryRoutes } from '@/store/actions/router';
import { useRouter } from 'next/router';
import { useSelector } from '@/store';
const useRouteBack = () => {
const dispatch = useDispatch();
const router = useRouter();
const historyRoutes = useSelector(state => state.router.historyRoutes);
const goBack = useCallback(() => {
dispatch(setBackFlag(true));
if (historyRoutes.length > 0) {
router.back();
dispatch(setHistoryRoutes(historyRoutes.slice(1)));
} else {
router.push('/');
}
}, [dispatch, router, historyRoutes]);
return goBack;
};
export default useRouteBack;
// const goBack = useRouteBack();
// goBack();
很可能说错,因为有些地方没有深入了解。
今天看到这位大神的animate-css-grid就想了解一下css grid动画怎么实现的,拜读源码之后发现逻辑其实不难,核心就是通过MutationObserver触发FLIP动画。所谓FLIP,就是First, Last, Invert, Play这四个词的缩写
animate-css-grid
里就对这点做了要求)。想了解FLIP的,可以参考
https://css-tricks.com/animating-layouts-with-the-flip-technique/
https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd
https://aerotwist.com/blog/flip-your-animations/
[接下来分析一下animate-css-grid具体怎么是实现的(稍微复杂的例子就不说了,比如React路由动画,我曾经用过这位大神的react-flip-toolkit尝试过一下,https://lr2o4qwl7z.csb.app/books)。
BoundingRect
,放到缓存中(WeakMap),这个信息非常重要,因为是之后应用动画的依据,即First阶段的信息。这个操作记为recordPositions
。recordPositions
,更新子元素的相关信息。基本是这么个流程。仿着写了个简单的例子https://7gphw.csb.app/,为了方便在动画阶段用了gsap,感觉在invert子元素的scale上似乎还是能明显看到抖动的。可能用tween的动画更好一些,每一帧都应用1 / scale。
由于之前对typescript不熟悉,所以记录一下流程
yarn add typescript
yarn add @types/react @types/react-dom
touch tsconfig.json
yarn dev # 此处是样例,输入你的启动服务命令
nexts.js会自动配置tsconfig.json并创建一个next-env.d.ts文件
example:
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
paths下的路径都是以baseUrl为基础解析,所以@/开头的路径都会到src目录下去解析。baseUrl必须配置。
先安装依赖
yarn add @types/styled-components
声明主题文件, 按照自己的theme对象包含的属性修改。e.g.:
// import original module declarations
import 'styled-components'
// and extend them!
declare module 'styled-components' {
export interface DefaultTheme {
borderRadius: string
colors: {
main: string
secondary: string
}
}
}
所有class的属性必须声明在类代码块中, 根据需要选择修饰符private
or public
e.g.
interface AInitOptions = {
a?: string;
b?: number;
}
class A {
private a: string;
public b: number;
constructor(options: AInitOptions) {
this.a = options.a;
this.b = options.b;
}
//......
}
const composeEnhancers: <StoreExt, StateExt>(
...funcs: Array<StoreEnhancer<StoreExt>>
) => StoreEnhancer<StoreExt> =
process.env.NODE_ENV !== 'production' ? composeWithDevTools : compose;
const myReducerEnhancer: StoreEnhancer = (
createStore: StoreEnhancerStoreCreator
): StoreEnhancerStoreCreator => <S = any, A extends Action = AnyAction>(
reducer: Reducer<S, A>,
initialState: PreloadedState<S>
) => {
// do something
return createStore(reducer, initialState);
};
export default function configureStore(initialState: any) {
const middlewares = [thunkMiddleware];
const middlewareEnhancer = applyMiddleware(...middlewares);
const enhancers: StoreEnhancer[] = [middlewareEnhancer];
if (process.env.NODE_ENV === 'development') {
enhancers.unshift(myReducerEnhancer);
}
const composedEnhancers = composeEnhancers(...enhancers);
return createStore(rootReducer, initialState, composedEnhancers);
}
https://stackoverflow.com/a/57441122
import { NextPage, NextPageContext } from 'next';
const MyComponent: NextPage<MyPropsInterface> = props => (
// ...
)
interface Context extends NextPageContext {
// any modifications to the default context, e.g. query types
}
MyComponent.getInitialProps = async (ctx: Context) => {
// ...
return props
}
否则可能会报'SomeComponent' refers to a value, but is being used as a type here.
microsoft/TypeScript#15713 (comment)
在泛型后面加一个逗号
const f = <T,>(arg: T): T => {...}
import {
useSelector as useReduxSelector,
TypedUseSelectorHook,
} from 'react-redux'
export type RootState = ReturnType<typeof rootRuducer>;
export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector;
引入ThunkAction作为返回类型, 需要定义四个泛型类型<R, S, E, A>
R是最后ThunkAction的返回类型,S是State,E是ThunkAction额外参数(这里没有用到),A是ThunkDispatch即dispatch
参数接受的action类型,可以是普通action也可以是ThunkAction。
export const someThunkAction = (...args: any[]): ThunkAction<
void,
RootState,
unknown,
Action<any>
> => (dispatch, getState) => {
const { historyRoutes, backFlag, prevRoute } = getState().router;
if (!backFlag) {
if (historyRoutes[0] !== prevRoute) {
const newHistoryRoutes = [prevRoute, ...historyRoutes];
dispatch(setHistoryRoutes(newHistoryRoutes));
}
}
};
xxx is not assignable to type xxx
interface Props {
// ......
children: React.ReactNode
}
声明Props的定义
interface IProps {
propA: number;
propB: string;
style: CSSProperties;
// ……
}
函数式组件
const Comp: React.FC<IProps> = (props) => {
<div>Hello</div>
};
Class组件
interface IState {
stateA: number;
}
class Comp extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
stateA: 0
};
}
// ……
}
hash.ts主要是对缓存的key进行hash, key为数组,返回一个字符串。如果数组中某个元素是基础类型,则将它转化为字符串,为了与其他类型转换后的值区分,字符串类型的值会用引号再包裹一下。非基础类型的引用值会存在一个Weakmap中,用一个自增的计数器来作为它的hash值。
// libs/hash.ts
const table = new WeakMap()
let counter = 0
function hash(args: any[]): string {
if (!args.length) return ''
let key = 'arg'
for (let i = 0; i < args.length; ++i) {
let _hash
if (args[i] === null || typeof args[i] !== 'object') {
if (typeof args[i] === 'string') {
_hash = '"' + args[i] + '"'
} else {
_hash = String(args[i])
}
} else {
if (!table.has(args[i])) {
_hash = counter
table.set(args[i], counter++)
} else {
_hash = table.get(args[i])
}
}
key += '@' + _hash
}
return key
}
缓存的接口如下
interface CacheInterface {
get(key: keyInterface): any
set(key: keyInterface, value: any, shouldNotify?: boolean): any
keys(): string[]
has(key: keyInterface): boolean
delete(key: keyInterface, shouldNotify?: boolean): void
clear(shouldNotify?: boolean): void
serializeKey(key: keyInterface): [string, any, string]
subscribe(listener: cacheListener): () => void
}
可以从方法名推断出来每个方法的作用,就不详细解释。
这个缓存类内部包含了一个Map, get, set, has, delete, clear等方法都是对这个内部Map进行操作。
get, set, has, delete都接收一个名为key的keyInterface类型的参数,而这些方法的内部都先对这个key进行了序列化,调用了serializeKey这个方法。接下来看看这个方法是如何进行序列化的。
serializeKey(key: keyInterface): [string, any, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}
if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
return [key, args, errorKey]
}
先判断key是否为函数类型,是则先调用,把结果再赋给key。如果key为数组,则调用上一节分析的hash函数,不是则直接转换成字符串。最后返回一个数组,key为存放data的key,args为调用fetcher时传入的参数,errorKey为存放error的key。
// cache
const cache = new Cache()
// state managers
const CONCURRENT_PROMISES = {} // 保存fetcher调用后的promise,去重(dedupe)用
const CONCURRENT_PROMISES_TS = {} // 保存fetcher调用的时间,去重(dedupe)用
const FOCUS_REVALIDATORS = {} // 窗口获得焦点后的数据重新验证回调
const CACHE_REVALIDATORS = {} // hooks内部的数据重新验证回调,会先更新state,再根据是否shouldRevalidate决定是否发起数据重新验证
const MUTATION_TS = {} // 记录缓存的修改时间,同时通过忽略在此时间点之前的fetcher结果,起到去重和避免脏数据的作用
……
先声明了一个缓存,同时声明了几个状态管理器,具体的作用已在注释中说明。数据重新验证或数据重验(revalidate)本质上是重新调用fetcher,更新状态。
// error retry
function onErrorRetry(
_,
__,
config: ConfigInterface,
revalidate: revalidateType,
opts: RevalidateOptionInterface
): void {
if (!isDocumentVisible()) {
// if it's hidden, stop
// it will auto revalidate when focus
return
}
if (config.errorRetryCount && opts.retryCount > config.errorRetryCount) {
return
}
// exponential backoff
const count = Math.min(opts.retryCount || 0, 8)
const timeout =
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval
setTimeout(revalidate, timeout, opts)
}
默认的错误重试函数,根据重试次数得到指数延时,大于配置的错误重试次数后不再重试
const slowConnection =
typeof window !== 'undefined' &&
navigator['connection'] &&
['slow-2g', '2g'].indexOf(navigator['connection'].effectiveType) !== -1
// config
const defaultConfig: ConfigInterface = {
// events
onLoadingSlow: () => {},
onSuccess: () => {},
onError: () => {},
onErrorRetry,
errorRetryInterval: (slowConnection ? 10 : 5) * 1000,
focusThrottleInterval: 5 * 1000,
dedupingInterval: 2 * 1000,
loadingTimeout: (slowConnection ? 5 : 3) * 1000,
refreshInterval: 0,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshWhenHidden: false,
refreshWhenOffline: false,
shouldRetryOnError: true,
suspense: false,
compare: deepEqual
}
默认配置,会根据网络连接情况调整部分值
let eventsBinded = false
if (typeof window !== 'undefined' && window.addEventListener && !eventsBinded) {
const revalidate = () => {
if (!isDocumentVisible() || !isOnline()) return
for (let key in FOCUS_REVALIDATORS) {
if (FOCUS_REVALIDATORS[key][0]) FOCUS_REVALIDATORS[key][0]()
}
}
window.addEventListener('visibilitychange', revalidate, false)
window.addEventListener('focus', revalidate, false)
// only bind the events once
eventsBinded = true
}
绑定了窗口重新获得焦点后的事件回调,触发数据重验
首先理解一下key,key是这个hook的关键,规定了这个hook的作用范围,缓存和其他的状态管理器,包括请求去重的判断,都是和这个key绑定的。理论上说key相同,fetcher函数也应该是相同的。如果请求内容发生了变化,不论是url还是参数,key一定是不同的。理解了这点,能够避免在使用中产生bug。
先分析几个定义在外部的函数
const trigger: triggerInterface = (_key, shouldRevalidate = true) => {
// we are ignoring the second argument which correspond to the arguments
// the fetcher will receive when key is an array
const [key, , keyErr] = cache.serializeKey(_key)
if (!key) return
const updaters = CACHE_REVALIDATORS[key]
if (key && updaters) {
const currentData = cache.get(key)
const currentError = cache.get(keyErr)
for (let i = 0; i < updaters.length; ++i) {
updaters[i](shouldRevalidate, currentData, currentError, i > 0)
}
}
}
const broadcastState: broadcastStateInterface = (key, data, error) => {
const updaters = CACHE_REVALIDATORS[key]
if (key && updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](false, data, error)
}
}
}
trigger
函数使用缓存中的值触发所有对应key的hooks进行状态更新,默认会在状态更新后触发数据重验(shouldRevalidate=true)。
broadcastState
函数是将某个hook中revalidate
的结果广播给拥有相同key的其他hooks,触发它们进行更新,不做数据重验。通常来说,只有非去重的那个请求在完成后会去调用这个函数,因为去重的只是在等待结果(即CONCURRENT_PROMISES
),本质上没有发出请求。
const mutate: mutateInterface = async (
_key,
_data,
shouldRevalidate = true
) => {
const [key] = cache.serializeKey(_key)
if (!key) return
// if there is no new data, call revalidate against the key
if (typeof _data === 'undefined') return trigger(_key, shouldRevalidate)
// update timestamp
MUTATION_TS[key] = Date.now() - 1
let data, error
if (_data && typeof _data === 'function') {
// `_data` is a function, call it passing current cache value
try {
data = await _data(cache.get(key))
} catch (err) {
error = err
}
} else if (_data && typeof _data.then === 'function') {
// `_data` is a promise
try {
data = await _data
} catch (err) {
error = err
}
} else {
data = _data
}
if (typeof data !== 'undefined') {
// update cached data, avoid notifying from the cache
cache.set(key, data, false)
}
// update existing SWR Hooks' state
const updaters = CACHE_REVALIDATORS[key]
if (updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](!!shouldRevalidate, data, error, i > 0)
}
}
// throw error or return data to be used by caller of mutate
if (error) throw error
return data
}
mutate
的作用就是更新缓存,然后触发对应key的所有hooks进行状态更新,从而触发重新渲染。可以传入函数或promise或数据作为data。举例来说,可以在更新用户信息之后,直接先将前端的信息修改成新的数据进行展示,同时再发起一个请求去获取新的用户信息,可以认为是用户体验的一种优化策略吧(?)。
const initialData = cache.get(key) || config.initialData
const initialError = cache.get(keyErr)
// if a state is accessed (data, error or isValidating),
// we add the state to dependencies so if the state is
// updated in the future, we can trigger a rerender
const stateDependencies = useRef({
data: false,
error: false,
isValidating: false
})
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
const rerender = useState(null)[1]
let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
stateRef.current[k] = payload[k]
if (stateDependencies.current[k]) {
shouldUpdateState = true
}
}
if (shouldUpdateState || config.suspense) {
rerender({})
}
}, [])
dispatch
即触发state更新的方法,stateDependencies
为依赖收集,最后在hook的返回结果里会使用定义getter的形式来对data
, error
, isValidating
三个值做依赖收集。意味着只有访问了某个值且它发生变化了,才会触发rerender
。
接下来分析关键函数revalidate
,关键点基本都注释了,
主要的流程就是
注意:revalidate
参数是根据key变化的,意味着你的hook只修改了fetcher函数,它是无法感知到的,它依然会使用原先的fetcher发起请求。如果你的请求需要改变,请一定要修改key,或者说把请求的参数当作key数组的一部分。具体可以参考官方文档。
const revalidate = useCallback(
async (
revalidateOpts: RevalidateOptionInterface = {}
): Promise<boolean> => {
// ……………………
// 去重判断
let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
try {
dispatch({
isValidating: true
})
let newData
let startAt
if (shouldDeduping) {
// 去重,等待已经存在的fetcher promise完成
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
// 可能发生数据竞争,所以标记了MUTATION_TS的时间,用于后续的判断
// req1------------------>res1
// req2-------->res2
if (CONCURRENT_PROMISES[key]) {
// we can mark it as a mutation to ignore
// all requests which are fired before this one
MUTATION_TS[key] = Date.now() - 1
}
// 无缓存值时触发加载过慢回调
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading) config.onLoadingSlow(key, config)
}, config.loadingTimeout)
}
// 并发请求占位
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = Date.now()
newData = await CONCURRENT_PROMISES[key]
// 去重间隔过了之后,清除占位
setTimeout(() => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}, config.dedupingInterval)
// 请求成功回调
config.onSuccess(newData, key, config)
}
// MUTATION优先,避免过时请求产生脏数据
if (MUTATION_TS[key] && startAt <= MUTATION_TS[key]) {
dispatch({ isValidating: false })
return false
}
// 更新缓存
cache.set(key, newData, false)
cache.set(keyErr, undefined, false)
// 新的state赋值
const newState: actionType<Data, Error> = {
isValidating: false
}
if (typeof stateRef.current.error !== 'undefined') {
// we don't have an error
newState.error = undefined
}
if (config.compare(stateRef.current.data, newData)) {
// deep compare to avoid extra re-render
// do nothing
} else {
// data changed
newState.data = newData
}
// 触发state更新
dispatch(newState)
if (!shouldDeduping) {
// 将新数据广播给其他hooks
broadcastState(key, newData, undefined)
}
} catch (err) {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
cache.set(keyErr, err, false)
if (stateRef.current.error !== err) {
// 原数据保存,只更新error
dispatch({
isValidating: false,
error: err
})
if (!shouldDeduping) {
// 将错误广播给其他hooks
broadcastState(key, undefined, err)
}
}
// 错误回调,重试
config.onError(err, key, config)
if (config.shouldRetryOnError) {
// when retrying, we always enable deduping
const retryCount = (revalidateOpts.retryCount || 0) + 1
config.onErrorRetry(
err,
key,
config,
revalidate,
Object.assign({ dedupe: true }, revalidateOpts, { retryCount })
)
}
}
loading = false
return true
},
[key]
)
然后是key值相关的一些事件绑定了。修改或省去了部分代码。
useEffect(() => {
// ……略
const latestKeyedData = cache.get(key) || config.initialData
// 只会在key或缓存变了触发data更新
if (
keyRef.current !== key ||
!config.compare(currentHookData, latestKeyedData)
) {
dispatch({ data: latestKeyedData })
keyRef.current = key
}
// 软更新(去重)
const softRevalidate = () => revalidate({ dedupe: true })
// 没有初始数据,触发数据重验
if (!config.initialData) {
softRevalidate()
}
// 订阅焦点事件
let onFocus
if (config.revalidateOnFocus) {
// throttle: avoid being called twice from both listeners
// and tabs being switched quickly
onFocus = throttle(softRevalidate, config.focusThrottleInterval)
if (!FOCUS_REVALIDATORS[key]) {
FOCUS_REVALIDATORS[key] = [onFocus]
} else {
FOCUS_REVALIDATORS[key].push(onFocus)
}
}
// 数据更新订阅(外部触发)
const onUpdate: updaterInterface<Data, Error> = (
shouldRevalidate = true,
updatedData,
updatedError,
dedupe = true
) => {
// update hook state
const newState: actionType<Data, Error> = {}
let needUpdate = false
// newState和needUpdate赋值,略……
if (needUpdate) {
dispatch(newState)
}
if (shouldRevalidate) {
revalidate({ dedupe })
}
return false
}
// add updater to listeners
if (!CACHE_REVALIDATORS[key]) {
CACHE_REVALIDATORS[key] = [onUpdate]
} else {
CACHE_REVALIDATORS[key].push(onUpdate)
}
// 重连事件
let reconnect = null
if (!IS_SERVER && window.addEventListener && config.revalidateOnReconnect) {
window.addEventListener('online', (reconnect = softRevalidate))
}
return () => {
// 清理 FOCUS_REVALIDATORS, CACHE_REVALIDATORS, online事件绑定
// 具体代码略
}
}, [key, revalidate])
剩下的就简要描述一下
轮询polling,使用useEffect,默认窗口不可见或离线不做轮询
Suspense,没有数据时,调用revalidate, throw promise
返回结果中data, error, isValidating用getter做依赖收集,同时返回revalidate和mutate
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.