最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    正文概述 掘金(杜帅在掘金)   2021-05-22   939

    任何逻辑,都可以拆分成这样的模型:

    事件引起状态的变化,状态的变化又发起新的事件

    “当 xxxx 发生时,xxxx 的数据会发生改变”,事件在前,状态在后,它是什么样的形式呢?

    React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    引用 cycle.js 的说明文档,事件到状态的依赖箭头,在事件那里

    事件主动发起,状态被动承受,注意这个主被动关系

    但是,我们换个描述

    “监听到 xxxx 数据变化时,执行 xxxx”,状态在前,事件在后,它就会变成这种形式:

    React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    如图,依赖关系在 Bar,状态所有者,可以 主动控制 流程的执行

    这种主被动的转换,就是 响应式编程 的核心

    那么我们来看看 React 那些带依赖数组的 api,哪些是主动的?而哪些又是被动的呢?

    useEffect,useLayoutEffect 是主动 API

    useEffect(()=>{
        if(a){
         // ...
        }
    },[a,b,c,d])
    

    控制结构在 useEffect 中,所以 useEffect 是主动的,是 响应式 API

    这就意味着,无论这个结构在哪里,它都能实现独立自主,也就是实现了 低耦合,同时融合了 React 的调度机制,在处理异步方面也有天然的优势

    useMemo,useCallback,甚至 useState 是 被动 API

    useCallback 无法决定何事变化, useMemo 只是跟随 state,而 useState 也无法决定自己如何变化

    useMemo 无所谓,因为数据不会自发地触发事件

    但是,当你在 useEffect 中使用 useCallback 的返回值时,巨大的陷阱就出现了:

    const cb = useCallback(()=>{/* ... */},[/* ... */])
    useEffect(()=>{
      // 耦合 cb,如果 cb 不在本作用域内声明,你将很难确定依赖,控制调度
    },[cb])
    

    于是会有很多神奇的死循环 effect 诞生,既 effect(主动)->callback(被动?主动?)

    useEffect 中的依赖数组,是主动的,在 effect 中作为事件发起者,应该作用于 useState 的 setter,而这里却写入了一个 被动控制的 useCallback,一旦 callback 的依赖与 effect 耦合,就会出现回环,死循环诞生

    const cb = useCallback(()=>{ 
       setA('') // 这里写入 a
    },[a,b,c,d])
    useEffect(()=>{
    },[cb,a]) // 这里传入 a
    

    如果需要避免出现回环,很简单 ——

    尽量不要在 useEffect 中调用 带依赖callback

    如何保证无依赖 callback 呢?

    很简单,我们将所有这些 事件->状态->事件->状态 的流程,都找出来,然后将它变换成:

    不就可以了么?

    MVI 模型

    React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    我们将 IO 进行高度抽象,不难发现,只是一个如此的流程:

    而其中,处理输入部分,设计为 事件,而输出部分,设计为 状态,计算部分,设计为 state + effect 集合

    则可以轻松实现建模,而这个模式,被称作 MVI 模型:

    React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    其中 intent 部分,就是 useCallback 发力的地方,既 ——

    我们用 useCallback 将 事件转化为状态

    那你需要为 useCallback 添加依赖么?

    const [a,setA] = useState()
    const [b,setB] = useState()
    
    const intentCb = useCallback(()=>{
      setA('')
      setB('')
    },[])
    

    基于这个目的的 useCallback 使用,不会带有任何依赖,可以放心地进行传递,组合:

    const refresh = useCallback(()=>{},[])
    
    useEffect(
      ()=>{
          refresh()
      }
    ,[refresh,a,b,c,d,e])
    
    

    既:

    容易被人忽略的一点,是 view 层

    MVI 中,用 DOM SINK 描述 view 层,即从调度上来讲,不是每个用户操作,都会让每个数据进行变化,因此,需要做一次类似过滤一样的工作

    但是,开发者们总是忘记这一部分,比如代码中很容易习惯性直接 return 一个 element

    function SomeCompo(){
      // ...
      return <div>
        {/* state 好说,变换容易控制,callback 的变化很难控制,因为 function 总是新值 */}
        <SomeExpenssiveCompo value={[state,someCb]}/>
      <div>
    }
    

    React 官方解法是 用 useReducer 向下传递回调,并分开写 context

    const [state,dispatch] = useReducer(()=>{},{})
    return <State.Provider value={state}>
      <Dispatcher.Provider value={dispatch}>
        {/* ... */}
      </Dispatch.Provider>
    </State.Provider>
    

    我认为这很蠢!且不说原本属于同一上下文逻辑的的部分需要拆开,写成嵌套的上下文,让依赖注入结构更加复杂

    最主要的是, useReducer 很难用上各种第三方 Hooks 生态,比如 react-use,swr 等,本身异步支持也不够

    当然,有模块能力的 useReducer 都如此,更不要说全局统一上下文,无拆分初始化能力的 Redux 等统一状态管理库了

    这里的问题,我认为真正的解法是 —— 头疼医头脚疼医脚

    1. 无依赖的 callback 替代 dispatch (本来就是无依赖,不会变化)

    2. 为 jsx 加上 useMemo

    是的,你的目的就是对组件调度进行控制,既 DOM sink(沉降)

    你应该直接作用于消费阶段(JSX Element)(将这个逻辑提前到 model 阶段,是个非常傻的做法)

    function SomeCompo(){
      return <div>
        {/* 只在 a,b,c 变化时,刷新这个 component */}
        useMemo(()=> <SomeExpenssiveCompo value={[state,someCb]}/>, [a,b,c])
      <div>
    }
    

    很简单就能解决的问题,为何要上 useReducer 呢?

    我个人非常反对在 js/ts 中使用 reducer 这种状态机制,js/ts 没有模式识别,没有完善的协变类型,很多时候是靠开发者自己来控制代码行文,用字符串模拟协变类型,用if/switch 语句强行模式识别,这样的函数式状态机,真的没有存在的必要

    当然,我反对在 js/ts 中使用 reducer,并不影响我认为它在 reasonml 中很美~

    React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    不过我没有 flow + react 的开发经历,或许校验类型系统能够给这种开发方式带来更好的体验,但是基于 js/ts 的环境,还是不报太大希望

    没错,大家注意到,我在说明 主被动/响应式编程 和 MVI 模型的时候,都是用的 cycle.js 的文档,而 cycle.js 又是基于或者借鉴 Rxjs 的

    没错,这种基于 MVI 模型 + 主被动响应式开发的思想,就是 ——

    1. 流程上,将一次 IO 抽象为 intent,model,view,intent 发起,model 处理,view 沉降

    2. 逻辑上,区分主被动响应模式,如果全部更改为主动模式,即是 事件驱动流 (rx,xstream),如果全部切换为被动模式,即是 数据驱动流(react主要模式)

    事件 -> 事件 -> 事件数据 -> 数据 -> 数据

    换句话说,如果你的应用逻辑部分主要采用 useCallback + state 的模式,就是事件驱动模型(全员被动),如果你的应用逻辑部分主要采用 useEffect + state 的模式,就是数据驱动模型(全员主动)

    React 更加适合 数据驱动模型,为何?

    哈哈哈,因为 React 没有将事件进行完全代理啊(用过 ng zone 的同学,可以站出来,科普一下何为完全暴力事件代理)

    换言之,并非所有的事件,react 都能够感知到,合成事件处,你可以直接调用 useCallback,非合成事件呢?(setTimeout,promise,socket,web worker,media stream?)

    你可能需要在 useEffect 中 调用 useCallback 了吧?

    const handleTimeout = useCallback(()=>{
       // timeout
    },[])
    useEffect(()=>{
      setTimeout(handleTimeout)
    },[handleTimeout])
    

    每次这些事件的处理,都必须小心翼翼,胆战新机

    不过,有一种特殊的模式,可以用在这些地方:

    action 模式 - 按照 React 调度执行函数

    const [action,disaptch] = useState(()=>()=>'')
    
    useEffect(()=>{
        // 函数参数
        const params = action()
        // 函数逻辑
    },[action])
    
    // 在某一处
    dispatch(()=>'new param')
    

    原理很简单,只是用 useEffect 的写法,来写一个 useCallback,不过,这个所谓函数的调度,完全是由 react 控制,即:

    1. 异步进行调用
    2. 同一事件循环,只会调用一次

    第二个问题可以使用参数集合的形式解决,而第一个问题,基本误解(useCallback 的话,你也无解,useEffect 处理逻辑必须按照 react 调度进行,毕竟这也是没有办法的事)

    这样的话,setTimout 等异步事件,就非常好解决了

    const [action,dispatch] = useState(()=>()=>{})
    
    useEffect(()=>{
      console.log('timeout')
    },[action])
    
    useEffect(()=>{
      setTimeout(()=>{ dispatch(()=>{}) })
    },[])
    

    数据驱动 - 笨办法,解决复杂问题

    数据驱动没有像事件驱动那样,有那么多的超高度抽象的工具,比如 switchMap,merge,combine,debounce(是的,数据驱动你甚至不用 debounce,一个 timer + 另一个标识即可),但是他却高效稳定得可怕

    不难理解为何很多 hooks 工具,会这样封装:

    /* swr */ const {data,error,loading} = useSwr(key,fetcher) `/* react-use */ const [state, doFetch] = useAsyncFn(async ()=>{},[])

    但是,这里就不得不说 React 的一大缺点了,即 ——

    数据驱动,并不适合函数式

    没错,数据驱动并不适合函数式

    • 一个函数调用有调用和返回值两个状态,还有调用次数和错误,同步多次调用的话,还有参数集合等数据
    • 一个异步函数调用,除了函数都有的状态,还有loading,为了方便调度控制,还有历史 loading 时间,被调用时间,发起时间以及他们的列表 等
    • 一个不断触发的事件,更是有数不清的数据和相关的描述信息

    这让状态数量极具扩张,因此,如何管理状态,就是一个非常棘手的问题

    用函数加返回值?既函数模拟类(无继承,继承本身应该被抛弃),效果非常差 —— 没有附加描述信息,没有自解释性

    应该用类似 Golang interface 或者 直接使用 贫血类 的方式,来管理封装状态

    而 React 这方面的支持,不能说没有,只能说很差(要知道 class 有自解释性,对于庞大的 state 集合,没有自动生成文档,自动生成图形统计,这些 state 只能让你的开发体验直线下降)

    再加上 React 并没有完整的事件驱动支持(即没有代理全部事件,保证用户代码在 model - view 范畴内,实现全被动),你如果采用 事件驱动 Rxjs 等工具辅助开发,体验也不会好(思维负担加重)

    这算是 React 最大的缺点吧,不过原因也很正常,reason react 才是 react 嘛,哈哈哈


    起源地下载网 » React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元