React Hooks 调度大全

更新日期: 2021-08-10阅读: 1.4k标签: Hooks

react Hooks 将调度权限全权交于用户,因此,分享一下个人在处理 Hooks 调度时的经验,希望对大家有所帮组

先前情提要一下,state 是时间上的数组,且细化到了具体的每一个 state,你可以自由地组织自己的调度结构,将调度之类的逻辑同状态一起进行封装复用,很方便。我相信调度问题对于事件驱动的 angular 来说,属于呼吸一样简单,但是对于数据驱动的 React 来说,可能是较为困难的地方,就比如 "state 是时间上数组" 这个论述,别急,先脑袋里想象一根时间线,数据随着时间进行变化,这是阅读下文的前提

将一切读取加入依赖数组

所有读取 state 的操作,都需要加入 useEffect,useMemo依赖数组,是的,包括你想封装读取了 state 的函数,也是必须 useMemo (简化版 useCallback) 的

这是必须付出的代价

// 出现了读取
useEffect(()=>{
  console.log(state)
},[state])

// 出现了隐式读取
const handleTimeout = useCallback(()=>{
  console.log(state)
},[state])

useEffect(()=>{
  setTimeout(handleTimeout, 1000)
},[handleTimeout])

// createElement 或者 jsx
return <div>
  {useMemo(()=><Compo data={state}/>,[state])}
</div>

useEffect 处添加,和 useMemo 处添加,大家能理解,但是 setTimeout 函数?也得用 useCallback 封装?

return 的 jsx Element 也要 useMemo?

如果你知道不 useMemo 有什么后果,请自行判断,因为它是你的逻辑,你如果不 useMemo 带参数组件,意思便是希望每次当前组件有依赖变化,都重新运行 Compo 的函数,不是没有这种需求,但是一般情况下,依赖数组中的依赖, 只能比被读取的依赖多,不能更少,更少会导致获取不到

这是你获得 hooks 完美封装性的必然代价,愿不愿意付出,你可以做选择,选择付出,可以继续往下看,选择不付出,请不要触碰任何 hooks api,因为它会破坏 class 写法的调度一致性,现如今 class 和 hooks 只能二选一,若项目存在 class 和 hooks 混用,甚至在组件树的不同层级混用,且涉及调度逻辑,那就只能说节哀

决定好了么?决定好了,前戏结束,我们开始正文

ref state

ref state 是利用 ref 将 state 存储下来,保存每次调度的最新数据,如果你不想或者无法传递依赖数组依赖,这是个很好的方式:

const [a,setA] = useState(0)
  const handler = useCallback(() => {
    console.log("not dep provoided:", a);
    // 0
    // because a is not in deps list
  }, []);
  useEffect(() => {
    setTimeout(() => {
      setA(1);
    }, 500);
    setTimeout(handler, 1000);
  }, [handler]);
  // but if a is in handlers deps list
  // useEffect will be trigger twice
  // how to deal with it?

  const aRef = useRef(a);
  useEffect(() => {
    aRef.current = a;
  }, [a]);
  useEffect(() => {
    setTimeout(() => {
      console.log("ref state", aRef.current);
    }, 1000);
  }, []);

一旦处理异步,读取 state,很容易遇到这样的问题,即 异步 useCallback 中存在依赖 a,而一点在 useEffect 中调用 callback,callback 传入依赖数组,会导致死循环

我们希望 handler 不变,而传入 a 的做法,使得 handler 改变了,因此,利用 ref state 转存 a 的最新值,实现无依赖

但请不要滥用 ref state, ref state 只需要用在事件处理 (合成事件除外),即 React 捕获不到的调度(实际上被逼无奈使用 ref state 的本质,就是因为 React 不知道事件触发,你的将事件调度和 React 调度联系起来)

阻止去重

useState,useMemo,都是默认自带去重逻辑,即:

const [a, setA] = useState(0);
  useEffect(() => {
    console.log("a effected:", a);
  }, [a]);
  useEffect(() => {
    setTimeout(() => {
      setA(1);
    }, 100);
    setTimeout(() => {
      setA(1);
    }, 200);
  }, []);

a effected 只会打印两次,后面两次 setA(1) 因为值没有改变,所以无法触发

这个去重逻辑你不一定需要,比如在 大对象处理或者分发事件 的时候

你可以采用函数返回值的做法,保证每次都是全新的函数,即可实现阻止去重

const [action, setAction] = useState(()=>()=>'payload')

useEffect(()=>{
  const payload = action()
},[action])

const [bigObj,setBigObj] = useState(()=>()=>{/* large data */})

const changeBigObj = useCallback((value:any)=>{
  setBigObj(res=>{
    const current = res()
    current.value.value.value.value = value
    // immutable 喔~ 
    return ()=> current
  })
})

不同调度模式

浏览器中的调度模式,主要分为 同步调度 , 延迟(微任务)调度 , 事件循环调度 , raf 调度 ,还有个计时器调度,不过它衍生自事件循环调度

在 React 18+ 中,默认为 batchUpdate (同事件循环统一延迟调度),即事件循环内的所有变更,统一在事件循环结束之前调度:

const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  useEffect(() => {
    console.log(a, b);
  }, [a, b]);
  useEffect(() => {
    setTimeout(() => {
      setTimeout(() => {
        console.log("next ev");
      }, 0);
      Promise.resolve().then(() => console.log("start batchUpdate"));
      setA(1);
      setB(1);
      Promise.resolve().then(() => console.log("end batchUpdate"));
      // 0 0
      // start batchUpdate
      // end batchUpdate
      // 1 1
      // next ev

      // in current ev, after all promise in useEffect
    }, 1000);
  }, []);

同步调度为 flushSync :

useEffect(() => {
    setTimeout(() => {
      Promise.resolve().then(() => console.log("end flush sync"));
      flushSync(() => {
        setA(2);
      });
      console.log("a changed");
      flushSync(() => {
        setB(2);
      });
      console.log("b changed");
    }, 2000);
    // 2 1
    // a changed
    // 2 2
    // b changed
    // end flush sync

    // synchronized
  }, []);

Raf调度为 startTransition (需配合 isPending 使用):

useEffect(() => {
    setTimeout(() => {
      Promise.resolve().then(() => console.log("next micro"));
      setTimeout(() => {
        console.log("next ev");
      }, 0);
      startTransition(() => {
        setA(3);
      });
      startTransition(() => {
        setB(3);
      });
    }, 3000);

    // next micro
    // 3 3
    // next ev

    // requestAnimationFrame scheduler
    // run with animation frame when spare
  }, []);

事件循环调度不用多说,setTimeout,setInterval

你需要综合多个调度的特性,安排状态变化的先后顺序,实现更多复杂逻辑,在时间上组织代码

调度探针

使用 useRef,通过 ref 在不同调度系统中传递消息,可以实现调度探针,对调度进行精确判定:

const [a, setA] = useState(0);
  const firstEvEnded = useRef(false);
  const secondEvStarted = useRef(false);
  const compoDestroy = useRef(false);
  const preRef = useRef<number | undefined>();
  useEffect(() => {
    // only batchUpdates work
    Promise.resolve().then(() => (firstEvEnded.current = true));

    setTimeout(() => {
      secondEvStarted.current = true;
    }, 0);
    return () => {
      // only use in return cb of effect cb
      compoDestroy.current = true;
    };
  }, []);

  // get preA
  useEffect(() => {
    Promise.resolve().then(() => {
      preRef.current = a;
    });
  }, [a]);

这里实现了 探针 ,探查当前 effect 是否在 第一次 ev 结束后,是否在第二次 ev 开始前,记录了它的前值

同样,你也可以利用 useMemo,将 ref 绑定在 特定调度源 上:

// useMemo on ref
  // eslint-disable-next-line
  const preA = useMemo(() => preRef.current, [a]);

  useEffect(() => {
    console.log("pre a:", preA);
  }, [preA]);

这样,preA 也可以对调度进行驱动了

最后再补充一个 domRef 获取的问题:

const ref = useRef<htmlElement|null>()
// ref 总有依赖
{a?<div ref={ref}/>:null}
useLayoutEffect(()=>{
  // a 变化后,在渲染回调拿到结果
  console.log(ref.current)
},[a])

<div ref/>
useLayoutEffect(()=>{
  // 所有 use(Layout)Effect 的依赖为空,都意味着隐式依赖组件的显示依赖
  // 在组件之外
  // {data? <Compo/>: null}
  // 这种思维对于直接编写跨组件逻辑非常重要
  // 逻辑不能局限在组建以内,需求不是按照组件给的
  console.log(ref.current)
},[])

以上代码接在顶部 sandbox 链接,再来一个通过调度解决 io 死锁问题的例子:

当你能控制调度的时候,逻辑才可能被你完全封装,视图才能实现对你被动的响应

注意, hooks 不再计较生命周期 ,没有生命周期,你的所有事件,渲染发生,总是因为数据的改变,你不需要去响应视图,而是 视图来响应你

原文 https://zhuanlan.zhihu.com/p/398036702


链接: https://www.fly63.com/article/detial/10627

React将引入Hooks,你怎么看?

近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?

精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。

使用react hooks实现自己的context-redux

我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理

useEffect Hook 是如何工作的?

使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。

React Hooks 你真的用对了吗?

从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考

如何用 Hooks 来实现 React Class Component 写法?

Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的

React-Hooks

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

React Hooks与setInterval

Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?

React Hooks 底层解析[译]

对于 React 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例

React Hooks实践

9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!