Vue3 学习笔记之 watchEffect

最近在看 Vue3 的一些新 feature,顺道学习了一些 hooks 编程的思想,感觉挺有启发的。今天就以 watchEffect 这个很小的 case 为例,开启我的 Vue3 学习笔记。

Vue2 v.s. Vue3

对所有初学者来说,Vue2 到 Vue3 最直观的改变就是 Composition API——几乎所有的 Vue2 options 方法都被放到了 setup 函数里:

+ import { onMounted, reactive, watchEffect } from 'vue'

export default {
  name: "App",

+  setup( props ) {
+    const state = reactive({ /*...*/ });
+    onMounted(() => { /*...*/ });
+    watchEffect(() => { /*...*/ });
+    return { state };
+  },

-  data: () => ({ state: /*...*/ }),
-  mounted(){ /*...*/ },
-  watch: { /*...*/ },
};

这是一个比较大的风格转变,通俗来说,就是从基于对象的编程(OOP)转向了函数式编程(FP)。

函数式编程

初学者可能分辨不清 OOP 和 FP 的区别。大家注意看 onMountedwatchEffect 方法的参数——箭头函数,大致能体会到不同之处了。

OOP 的特点是:对象(或 class)是数据(variable)和逻辑(methods)的封装。在 Vue2 时代,我们经常写如下代码:

// vue2
export {
  data: () => ({count: 1}),
  methods: {
    message: (prefix) => `${prefix} ${this.count}`,
  },
  watch: {
    count() {
      console.log( this.message('Count is') );
    };
  }
}

Vue2 的内部实现比较复杂,不过对外表现的编程模式基本就是:对象调用自己的数据和方法——this + . 操作。所以在 Vue2 时代,我们通常会把相关的数据和操作写在同一个对象里。但是到了 Vue3 的 setup 里,你几乎不会用到 this 了;变成了让函数来调用对象或是另一个函数——就是 FP 的特点了。

// Vue3
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(1);
    const message = (prefix) => `${prefix} ${count.value}`;

    watchEffect(() => {
      console.log(message("Count is"));
    });
    return { count, message };
  },
};

纯函数和负作用

本文不想过多介绍函数式编程,但是既然 Vue3 的风格转向了 FP,我们得遵守 FP 的规则——函数只应该做一件事,就是返回一个值。下面的一个 vue 组件就可以看做一个函数,通过 props 传入一个参数 name,返回一个 html。

<template>
  <h1>{{ name }}</h1>
</template>

<script>
  export default {
    props: {
      name: String,
    },
  };
</script>

上面这个函数有什么特点呢?

  1. 相同的输入产生相同的输出
  2. 不能有语义上可观察的函数副作用

这个就是经典的纯函数(pure function)。

pure v.s. impure

不过现实中一个 Vue 组件可能还要做其他很多事,如:

  • 获取数据
  • 事件监听或订阅
  • 改变应用状态
  • 修改 DOM
  • 输出日志

这些其他改变就是所谓的副作用(side effect)。在 FP 的世界里,我们不能向 Vue2 那样简单地调用全局插件了(this.$tthis.$routerthis.$store……);而是通过间接的手段——即通过其他函数调用——包含副作用。Vue3 就提供了一个通用的副作用钩子(hook)叫做 watchEffect(从名字上也可见一斑),就是我们今天的主角了。

watchEffect

兜兜转转,我们再来介绍一下 watchEffect 的用法,借助 typescript,我们可以很清晰地看到该函数的定义:

类型定义

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle;

interface WatchEffectOptions {
  flush?: "pre" | "post" | "sync";
  onTrack?: (event: DebuggerEvent) => void;
  onTrigger?: (event: DebuggerEvent) => void;
}

interface DebuggerEvent {
  effect: ReactiveEffect;
  target: any;
  type: OperationTypes;
  key: string | symbol | undefined;
}

type InvalidateCbRegistrator = (invalidate: () => void) => void;

type StopHandle = () => void;

第一个参数

watchEffect 自己是函数,它的第一个参数——effect——也是函数(函数是一等公民,可以用在各个地方)。effect,顾名思义,就是包含副作用的函数。如下代码中,副作用函数的作用是:当 count 被访问时,旋即在控制台打出日志。

// Vue3
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(0);
    const effect = () => console.log(count.value);
    watchEffect(effect);

    setTimeout(() => count.value++, 1000);

    return { count };
  },
};

如上代码会打印出010是出于 Vue 响应式设计,在响应式元素(count)依赖收集阶段会运行一次 effect 函数;1是来自 setTimeout 里对 count 加一操作。

清除副作用(onInvalidate )

大家注意到没有?watchEffect 的第一个参数——effect函数——自己也有参数:叫onInvalidate,也是一个函数,用于清除 effect 产生的副作用。(而且 onInvalidate 的参数也是函数,哈哈!)

*p.s. FP 就是这样,函数嵌套函数;初学者可能有点晕,习惯就好*

onInvalidate 被调用的时机很微妙:它只作用于异步函数,并且只有在如下两种情况下才会被调用:

  1. effect 函数被重新调用时
  2. 当监听器被注销时(如组件被卸载了)

如下代码中,onInvalidate 会在 id 改变时或停止侦听时,取消之前的异步操作(asyncOperation):

import { asyncOperation } from "./asyncOperation";

const id = ref(0);

watchEffect((onInvalidate) => {
  const token = asyncOperation(id.value);

  onInvalidate(() => {
    // run if id has changed or watcher is stopped
    token.cancel();
  });
});

返回值(停止侦听)

副作用是随着组件加载而发生的,那么组件卸载时,就需要清理这些副作用。watchEffect 的返回值——StopHandle依旧是一个函数——就是用在这个时候。如下 stopHandle 可以在 setup 函数里显式调用,也可以在组件被卸载时隐式调用。

setup() {
  const stopHandle = watchEffect(() => {
    /* ... */
  });

  // 之后
  stopHandle();
}

第二个参数

watchEffect 还有第二个参数叫 options,类型是WatchEffectOptions,一个很复杂的接口。虽然很少能被用到吧,但也在这里快速提一下。

第二个参数的主要作用是指定调度器,即何时运行副作用函数。比如,你希望副作用函数在组件更新前发生,可以将 flush 设为 'pre'(默认是 'post')。还有 WatchEffectOptions 也可以用于 debug:onTrackonTrigger 选项可用于调试一个侦听器的行为(当然只开发阶段有效)。

// fire before component updates
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: "pre",
    onTrigger(e) {
      debugger;
    },
  }
);

注意点

watchEffect 会在 Vue3 开发中大量使用,这里说几个注意点:

  1. 如果有多个负效应,不要粘合在一起,建议写多个 watchEffect

    watchEffect(() => {
      setTimeout(() => console.log(a.val + 1), 1000);
      setTimeout(() => console.log(b.val + 1), 1000);
    });
    

    这两个 setTimeout 是两个不相关的效应,不需要同时监听 a 和 b,分开写吧:

    watchEffect(() => {
      setTimeout(() => console.log(a.val + 1), 1000);
    });
    
    watchEffect(() => {
      setTimeout(() => console.log(b.val + 1), 1000);
    });
    
  2. watchEffect 也可以放在其他生命周期函数内

    比如你的副作用函数在首次执行时就要调用 DOM,你可以把他放在 onMounted 钩子里:

    onMounted(() => {
      watchEffect(() => {
        // access the DOM or template refs
      });
    }
    

小结

watchEffect 基本上是现象级拷贝了 React 的 useEffect;这里倒不是 diss Vue3,只是说 watchEffect 和 useEffect 的设计都源自于一个比较成熟的编程范式——FP。大家在看 Vue3 文档时,也不要只盯着某些 api 的用法,Vue 只是工具,解决问题才是终极目标;我们还是要把重点放在领悟框架的设计思想上;悟到了,才是真正掌握了解决问题的手段。最后以独孤求败的一句名人名言结尾:

重剑无锋,大巧不工,四十岁前持之横行天下;四十岁后,不滞于物,草木竹石均可为剑。

文章同步自an-Onion 的 Github。码字不易,欢迎点赞。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容