Git Product home page Git Product logo

notes's Introduction

Notes

这里是Feniast这个人类记录些东西的地方。一个微不足道的存在平日里挣扎生存的一些痕迹。仅此而已。

notes's People

Contributors

feniast avatar

Watchers

 avatar

notes's Issues

#3 关于Next.js中防止router.back跳到外站或者blank page

解决思路

Disclaimer: 不保证此思路和实现完全正确。
需要一个全局的store来存储本应用路由的历史historyRoutes

1.监听route变化
当进入一个页面时,将当前路由存到route, 之前的route就存到prevRoute
如果不是back的,将prevRoute加入historyRoutes。反之跳过这个步骤。
然后将backFlag置为false。
2. 定义回退方法
点击应用中的回退按钮,设置backFlag为true,同时判断historyRoutes是否为空,为空则push到首页,否则跳到historyRoutes中第一条,同时将historyRoutes中第一条移除。

定义router reducer

为什么要定义这么一个东西,理论上只要是全局的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;

定义next.js route变化的监听器

这里定义了一个组件,完成思路中的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();

简单了解FLIP动画

FLIP动画

很可能说错,因为有些地方没有深入了解。

今天看到这位大神的animate-css-grid就想了解一下css grid动画怎么实现的,拜读源码之后发现逻辑其实不难,核心就是通过MutationObserver触发FLIP动画。所谓FLIP,就是First, Last, Invert, Play这四个词的缩写

  • First 记录当前元素的尺寸和位置信息,用getBoundingClientRect就可以获取
  • Last 记录当前元素变化后的尺寸和位置信息,用相同的办法获取。(获取到这个值的时候可能元素的变化还没有绘制到屏幕上,比如React中你可以在ComponentDidMount或者ComponentDidUpdate中去做这个操作。useEffect就可能造成闪烁了,因为它在绘制之后执行)。
  • Invert 反转,就是应用transform的translate和scale让当前元素回到之前的尺寸和位置,造成一种该元素还在原位置的假象。接下来就可以开始应用动画效果了。(当然,这里有个问题就是使用了scale后,你必须对其子元素应用1 / scale的scale来保证子元素没有拉伸。所以这类元素最好使用单一dom元素作为它的唯一子节点,否则你就要对它所有的直接子节点应用scale变换了。animate-css-grid里就对这点做了要求)。
  • Play 应用动画。简单点,可以直接写上transform: none,那它自动就会回到Last状态。但是为了easing函数和子节点的invert scale能够无缝配合,可能需要JS介入。还有,你可能希望transform-origin为0,0,否则scale动画可能会显得奇怪(默认是50% 50%)。

想了解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)。

  1. 以一个container元素即应用了display: grid的元素为参照,计算其所有子元素(grid item)的位置和尺寸信息,记为BoundingRect,放到缓存中(WeakMap),这个信息非常重要,因为是之后应用动画的依据,即First阶段的信息。这个操作记为recordPositions
  2. 监听resize和container的scroll事件,事件回调触发recordPositions,更新子元素的相关信息。
  3. 使用MutationObserver对container元素进行监听,监听添加删除元素以及class属性的变化,因为这些mutation大概率会引起布局的变化,这就是应用动画的时机。
  4. MutationObserver的回调会做几件事
    • 检查此次mutation是否是关心的变化,比如class修改,添加删除元素
    • 如果子元素有动画执行,清除,将transform设置为空
    • 计算子元素的当前尺寸和位置,与缓存值比较,(没有缓存值的计算缓存值,不执行动画)过滤出有变化的。对应Last阶段
    • 利用简单的减法和除法,得到Last反转为First的translate和scale,将transform-origin设为0 0
    • 执行补间动画(tween),将相关translate和scale应用到子元素和子元素的子元素

基本是这么个流程。仿着写了个简单的例子https://7gphw.csb.app/,为了方便在动画阶段用了gsap,感觉在invert子元素的scale上似乎还是能明显看到抖动的。可能用tween的动画更好一些,每一帧都应用1 / scale。

react + next.js + typescript配置

react + next.js + typescript配置

由于之前对typescript不熟悉,所以记录一下流程

  1. 安装typescript和types依赖
yarn add typescript
yarn add @types/react @types/react-dom
  1. 在项目根目录下创建tsconfig.json
touch tsconfig.json
  1. 启动服务
yarn dev # 此处是样例,输入你的启动服务命令

nexts.js会自动配置tsconfig.json并创建一个next-env.d.ts文件

  1. tsconfig.json路径别名配置

example:

 "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }

paths下的路径都是以baseUrl为基础解析,所以@/开头的路径都会到src目录下去解析。baseUrl必须配置。

  1. styled-components配置

先安装依赖

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
    }
  }
}
  1. class定义

所有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;
    }
    
    //......
}
  1. redux create store

参考: https://stackoverflow.com/questions/50451854/trouble-with-typescript-typing-for-store-enhancers-in-redux-4-0

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);
}
  1. Next.js Page 组件

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
}
  1. react组件文件名必须以tsx结尾

否则可能会报'SomeComponent' refers to a value, but is being used as a type here.

  1. tsx文件函数泛型报错问题

microsoft/TypeScript#15713 (comment)
在泛型后面加一个逗号

const f = <T,>(arg: T): T => {...}
  1. react-redux中useSelector hook的state参数类型获取不到
    https://stackoverflow.com/questions/57472105/react-redux-useselector-typescript-type-for-state
import {
  useSelector as useReduxSelector,
  TypedUseSelectorHook,
} from 'react-redux'

export type RootState = ReturnType<typeof rootRuducer>;

export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector;
  1. redux-thunk action定义

引入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));
    }
  }
};
  1. React.forwardRef的Props类型需要带上children, 否则可能会报xxx is not assignable to type xxx
interface Props {
  // ......  
  children: React.ReactNode
}
  1. React组件定义

声明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
        };
    }
    
    // ……
}

#1 swr库源码阅读记录

SWR 源码阅读

  1. 工具模块

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
}
  1. 缓存cache.ts

缓存的接口如下

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。

  1. 配置config.ts
// 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
}

绑定了窗口重新获得焦点后的事件回调,触发数据重验

  1. 核心hook use-swr.ts

首先理解一下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,关键点基本都注释了,
主要的流程就是

  • 去重判断
  • 去重则等待之前的请求完成,非去重则标记MUTATION时间防止数据竞争,同时占位
  • 请求完成后判断是否过期,过期了直接返回,不过期则更新缓存,触发state更新,非去重则广播状态
  • 请求出现错误时,更新缓存和state,非去重则广播状态,然后重试

注意: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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.