• home > webfront > ECMAS > vue3 >

    vue3中的memo/watchEffect和react中useEffect/useMemo看设计理念差异

    Author:zhoulujun Date:

    vue3里面的watchEffect watchPostEffect watchSyncEffect与v-memo,对比react的useEffect 与Memo useMemo 看一看

    vue通过依赖收集与管理对数据做到了更细致的监听,精准实现组件级别的更新(vue2通过数据冻结,vue3 Ref ShallowRef精准控制,同时watch也能精确到具体属性)。

    react当组件调用setState或props变化的时候,组件内部render会重新渲染,子组件也会随之重新渲染,可以通过shouldComponentUpdate或者PureComponent可以避免不必要的重新渲染。

    为什么避免重复渲染,所欲要处理副作用!

    什么是副作用

    在编程语言中,副作用是指一个函数除了返回值之外,还会对外部的状态进行修改的行为。通俗来讲,就是一个函数在执行过程中除了返回结果之外还会对程序或者系统造成其他的影响。副作用可以是很多种类型的,比如说:修改变量值、调用其他函数、与服务器交互等等。

    日常生活中的副作用,比如:我原本要治疗感冒,服用了 含马兜铃酸的中成药(灵龙感冒胶囊、咽炎含片、龙胆泻肝丸、耳聋丸、甘露消毒丸、连翘败毒丸、通便清火丸、炎见宁片),副作用是  肾损伤(马兜铃酸是不分剂量的)

    image.png


    Vue 3 中的副作用函数

    在 Vue 3 中,副作用函数有两种形式:watchEffect 和 onMounted,分别用于处理响应式变化和组件挂载时执行的副作用函数。

    副作用函数的原理

    vue3中的effect函数是响应式的核心,它被称作副作用函数!

    在 Vue 3 中,副作用函数的实现原理是通过 effect 函数和 reactive 函数实现的。在 watchEffect 中,effect 函数会监听传入函数中的所有响应式状态,并在这些状态变化时触发传入函数的执行。在 onMounted 中,effect 函数会在组件挂载时执行传入函数。

    具体来说,effect 函数会在函数执行时,通过 track 函数监听所有读取的响应式状态,并把这些状态加入到一个依赖列表中。当这些响应式状态发生变化时,effect 函数会通过 trigger 函数触发副作用函数的执行,并把依赖列表传递给副作用函数。

    具体查看:vue3中的effect函数到底是什么 https://juejin.cn/post/7241461049780944956

    React副作用分离

    本质上useState是一个有副作用的api但是react内部帮你抽离了让你在使用层面上看起来像一个纯函数。

    useEffect函数中我们可以不用去关心可能它在count改变后,对fiber渲染所做的事情,他在内部已经给你处理了。其实到这不关心源码的朋友们已经可以结束了。

    const [count, setCount] = useState(0)
    useEffect(() => {
      // 随便做点啥
    }, [count])

    useEffect会创建一个effect环形链表并保存在fiber.updateQueue和hook.memoizedState中,当我们渲染开始的时候会将useEffect放入调度中,等到flushPassiveEffectsImpl去异步执行它。此时会有一些副作用留存到fiber上。

     try {
        const create = effect.create;
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          fiber.mode & ProfileMode
        ) {
          try {
            startPassiveEffectTimer();
            effect.destroy = create();
          } finally {
            recordPassiveEffectDuration(fiber);
          }
        } else {
          // 执行
          effect.destroy = create();
        }
        } catch (error) {
        invariant(fiber !== null, 'Should be working on an effect.');
        captureCommitPhaseError(fiber, error);
        }

    等到我们更新的时候,这些副作用又会派上用场,依次循坏。

    function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
      // 获取当前hook
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      let destroy = undefined;
      // 分析依赖
      if (currentHook !== null) {
        // 留存下来的`effect`链表
        const prevEffect = currentHook.memoizedState;
        // 继续使用先前effect.destroy
        destroy = prevEffect.destroy;
        if (nextDeps !== null) {
          const prevDeps = prevEffect.deps;
          // 浅比较依赖是否变化
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            如果依赖不变, 新建effect(tag不含HookHasEffect)
            pushEffect(hookFlags, create, destroy, nextDeps);
            return;
          }
        }
      }
      // 如果依赖改变, 更改fiber.flag, 新建effect
      currentlyRenderingFiber.flags |= fiberFlags;
    
      hook.memoizedState = pushEffect(
        HookHasEffect | hookFlags,
        create,
        destroy,
        nextDeps,
      );
    }


    useEffect vs watchEffect

    watchEffect:

    立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

    watchEffect 类似于 watch immediate,立即执行一个函数,依赖改变时重新执行。 

    watch 和 watchEffect 

    • watch 需要明确监听哪些属性,且可访问属性之前的值。监听可以配置

    • watchEffect 会根据其中的属性,自动监听其变化

      它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听(或者手动停止)。

    import { watch, watchEffect } from 'vue';
    
    const obj = reactive({name:'zs'});
    const stopWatch = watch(()=>obj.name,()=>{console.log('watch name:',obj.name)})
    const stopWatchEffect = watchEffect(() => {console.log('watchEffect name:',obj.name)})
    const stop = () => {
      console.log('停止监听')
      stopWatch();
      stopWatchEffect();
    }


    flush

    watch/watchEffect 函数的第三个参数是可选的配置项,其中一个配置项是flush,它控制何时运行watch的回调函数。flush有三个选项:"pre"、"post"和"sync"。        

    • pre:在侦听器的回调函数运行之前立即运行更新函数,即在dom渲染完毕前调用回调函数(此时获取不到DOM!)。这是默认值。

    • post在侦听器的回调函数运行之后立即运行更新函数,即在下一次DOM更新之后执行。

    • sync在更改被触发时立即运行侦听器的回调函数和更新函数,这是非常明确和强制的选项。

    在大多数情况下,不需要指定flush选项,因为默认的行为通常足够了。但是,对于一些需要更精确控制的场景,flush可以是一个非常有用的配置项

    onTrack和onTrigger

    onTrack和onTrigger用于跟踪和调试响应式对象属性的访问和修改,可以帮助我们更好地理解和追踪响应式数据的读取和修改操作,更好地进行调试和开发。可以帮助我们更好地理解和追踪响应式数据的读取和修改操作,更好地进行调试和开发。

    • onTrack:选项会在我们读取响应式数据时被触发,我们可以在该函数中做一些记录或者打印日志的操作。

    • onTrigger:onTrigger选项会在响应式数据被修改时被触发,我们可以在该函数中做一些特殊的处理。



    watchEffect 清除副作用

    简单来说副作用就是未完成的异步任务(包括setTimeout等)。

    因此清除副作用就是清除掉未完成的异步任务进而调用下一次的watchEffect——就跟防抖里面的清除定时器变量一个道理。

    onInvalidate是watchEffect回调函数里面的参数,作用就是清除副作用

    const searchQuery = ref('');
    setTimeout(() => searchQuery.value = 'new query', 1000);// 更新状态值
    let cancel;
    watchEffect((onInvalidate) => {// 使用 watchEffect 监听 searchQuery 的变化
      if (cancel) cancel();// 取消上一次的请求
      cancel = axios.CancelToken.source();// 发起新的请求
    
      axios.get('/api/search', { cancelToken: cancel.token }).then(response => {});
      onInvalidate(() => { // 注册清理函数
        cancel.cancel('Operation canceled.');
      });
    });

     从vue3.2.0开始后,watchPostEffect、watchSyncEffect别名

    ReactiveEffectOptions

    effect函数

    export function effect<T = any>(
      fn: () => T,
      options?: ReactiveEffectOptions,
    ): ReactiveEffectRunner<T> {}

    ReactiveEffectOptions参数

    export interface DebuggerOptions {
      onTrack?: (event: DebuggerEvent) => void
      onTrigger?: (event: DebuggerEvent) => void
    }
    
    export interface ReactiveEffectOptions extends DebuggerOptions {
      scheduler?: EffectScheduler
      allowRecurse?: boolean
      onStop?: () => void
    }

    参数解释

    • scheduler:默认是 undefined。则 effect 会同步的调用,也就是说,响应式数据改变,函数立即执行。

      • 这个参数还是挺重要的,场景也挺丰富。后续会好好讲一下。

    • allowRecurse: 递归,effect 函数内部,继续调用自己。默认是 false,不让自己递归自己。

    • onStop:是一个回调函数,默认是 undefined。

      • 作用:会在 effect 停止追踪响应式数据 或被 手动停止时调用。

      • 用于清理工作。


    useEffect 

    useEffect 用于在函数组件中的副作用操作,组件每次渲染后执行。

    该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

    在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

    使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

    默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

    useEffect 第一个参数是一个函数,要执行的副作用函数,第二个参数是一个数组,指定副作用函数的依赖项,当依赖项发生变化时,副作用函数才会执行。如果第二个参数为空,副作用函数在组件初始渲染后执行一次,后面组件重新渲染就不会执行。

    function Welcome(props) {
      useEffect(() => {
        document.title = `Hello, ${props.name}`;
      }, [props.name]);
      return <h1>Hello, {props.name}</h1>;
    }





    useMemo与 v-memo

    react的具体查看《ReactHook详解:memo/useMemo/useCallback等钩子细讲

    v-memo

    Vue3 中的 v-memo 是一种高效的优化组件重渲染的指令,用于性能至上场景中的微小优化!

    它可以阻止组件元素在没有必要的情况下进行重新渲染,从而提高应用程序的性能

    <template>
        <div v-memo="[valueA, valueB]">
            ...
        </div>
    </template>

    缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。

    正确指定缓存数组很重要,否则应该生效的更新可能被跳过。v-memo 传入空依赖数组 (v-memo="[]") 将与 v-once 效果相同

    v-memo应用场景

    最常见的情况可能是有助于渲染海量 v-for 列表 (长度超过 1000 的情况):

    <template>
        <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
            <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
            <p>...more child nodes</p>
        </div>
    </template>

    假设上面是一个很大的数据量的列表,它会显示当前 item 是否被选中(单选)。

    当组件的 selected 状态改变,默认会重新创建大量的 vnode,即使绝大部分都没有变化(selected 状态没有改变,仍然为 false)。Vue 会将这些新的 vnode 跟上一个状态的 vnode 进行比对,找到它们的差异,然后进行更新。

    由于只有少部分差异,但由于 vnode 数量巨大,会消耗非常多的性能用于查找差异,这种场景下使用 v-memo 就非常的合适

    因为v-memo不适合大量的状态变化场景!因为 缓存其实是大量失效的,这时候的性能提升效果就不会太明显,还可能有 v-memo 的依赖设置错误导致更新被跳过的风险!



    React与Vue的思想理念差异

    Vue组件实现为一个有模板(template)、有数据(props)的对象, React组件则实现为一个输入数据(props)、输出html片段(JSX)的函数。

    • React:映射思维体现,先有js数据,再有html模板。能解决复杂问题。

      Vue写起来既像模板引擎一样易于理解,又比模板引擎方便很多。开发思路差不多就是“写模板-填模板”的套路,与jQuery时代一脉相承。

    • Vue:模板思维体现,mvvm 数据和视图分开,重点在解耦。清晰简单。

    阐明模板思维与映射思维的不同

    • 模板思维认为是先有html,再有js。正如上面图的左半部分所示,模板本身已经是一个接近完整的网页了,只不过其中几个变量没法确定,需要一点逻辑来动态填充,所以找了js这么个小弟来打打下手,把这几个变量给补上去。因此,在模板思维的世界观里,html居于主导地位,js服从于html的需要。

    • 映射思维却相反,认为js里的数据才是第一性的,html只不过是数据层向视图层的单向映射或者说投影,如上图右半部分所示,一个个数据的投影片段拼起来,组成了完整的html。在这个世界观里,数据是本体,视图是现象,他们之间的关系就像一棵树与它的影子一样,要想改变影子,就必须改变树,而不能影子变了树没变。明白了这一点,React那些乍看之下有些奇怪的设定(如阻止input标签直接响应的用户输入),以及React社区对单向数据流的偏好(如Redux),就不难理解了。

    在模板思维看来,html模板和js数据都是实质,二者是并列的关系;在映射思维的世界观里,js数据是唯一的实质。

    函数有什么特别呢?

    从数学上讲,函数本质上就是表示一种映射关系。React受函数式编程思想的影响,将html视为数据映射的结果。一个数据映射出一个html片段,所有的html片段拼起来,就形成了完整的页面DOM树。当然,React组件还可以是class形式,但只是为了更好地操作数据,最终render函数会完成映射这一步。这是一种截然不同于模板填充的思维方式,姑且称之为“映射思维”。相应的,将变量绑定到模板上的思维方式就称之为“模板思维”。理解这两种思维的对立非常重要,因为这是React与Vue等其他框架的核心差异所在。


    学会了thinking in React,可以在编写复杂页面的时候,仍然保持逻辑清晰和极度舒适。


    Vue3函数编程副作用处理

    vue3 composition API + TSX,也是可以实现react一样的编程效果。

    effectScope作用

    在没有effectScope之前,vue实例本身应该会跟踪这些副作用,然后在组件销毁(unmounted)的时候,自动销毁这些副作用的,但是随着composition API的使用越来越广泛,应用的结构也变得越来越灵活,原有的副作用跟踪机制在某些场景下显得不够灵活和直观,或者用起来不够顺畅,因此,使用effectScope来解决这个问题,它允许开发者显示地组织和管理副作用!

    // realTimeUpdates.js
    import { ref, effectScope, watchEffect } from 'vue'
    const useRealTimeUpdates = () => {
      const isEnabled = ref(false)
      const scope = effectScope()
      const startUpdates = () => {
        if(!isEnabled.value){
          isEnabled.value = true
          scope.run(() => {
            watchEffect(() => {
              console.info('实时更新数据...')
            })
          })
        }
      }
      const stopUpdates = () => {// 停止容器内的所有副作用
        if(isEnabled.value){
          isEnabled.value = false
          scope.stop()
        }
      }
      return { startUpdates, stopUpdates }
    }

     而且在vue3中除了effectScope()API之外,还提供了另外两个API:

    • getCurrentScope(): 获取当前scope,scope.stop() // 取消所有侦听管家监听

      比如一个页面中存在多个管家,而我们不想一个个取消每个管家的监听,可以通过此方式批量取消监听。

    • onScopeDispose(fn: () => void): void: 当当前的scope被销毁的时候,自动执行的回调方法!

      这是一个回调事件。当执行getCurrentScope().stop()时,或者组件注销时触发。

    在组件component内部通过onUnmounted()来销毁资源的,那么对于在非组件层面(比如composable function)中,所创建出来的响应式副作用则可以通过onScopeDispose()来进行管理销毁回调操作

    <script setup>
        import {
            ref,
            computed,
            watch,
            watchEffect,
            effectScope,
            getCurrentScope,
            onScopeDispose
        } from 'vue'
    
        const counter = ref(2)
    
        // 定义第一个侦听管家
        const scope = effectScope()
        scope.run(() => {
            const doubled = computed(() => counter.value * 2)
            watch(doubled, () => console.log(doubled.value))
            watchEffect(() => console.log('Count: ', doubled.value))
        })
        //scope.stop()  // 调用它,可以取消scope内的侦听。执行这个不会触发onScopeDispose事件
    
        //  定义第二个侦听管家
        const scope2 = effectScope()
        scope2.run(() => {
            const doubled2 = computed(() => counter.value * 3)
            watch(doubled2, () => console.log(doubled2.value))
            watchEffect(() => console.log('Count: ', doubled2.value))
        })
        //scope2.stop() // 调用它,可以取消scope2内的侦听。执行这个不会触发onScopeDispose事件
    
        // 获取当前侦听实例
        const allScope = getCurrentScope()
        // 执行 allScope.stop()时会触发 onScopeDispose 事件
        // 当前页面或组件注销时会触发 onScopeDispose 事件
        onScopeDispose(() => {
            console.log('已停止所有侦听');
            // to do...
        })
      
        // 5秒后停止所有侦听,此时会触发 onScopeDispose 事件
        setTimeout(() => {
            allScope.stop()
        }, 5000)
    </script>



    参考文章:

    Vue v-memo 指令的使用与源码解析 https://juejin.cn/post/7197046540040273976

    【Vue3】利用watchEffect的清除副作用实现一个防抖函数 https://blog.csdn.net/qq_24719349/article/details/121681523

    React与Vue的思想理念差异之处 https://www.cnblogs.com/zhulimin/p/13168623.html

    react中的副作用? https://juejin.cn/post/7175843021752598589


    转载本站文章《vue3中的memo/watchEffect和react中useEffect/useMemo看设计理念差异》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue3/9224.html