vue3 watch深度监听,vue watcher原理

  vue3 watch深度监听,vue watcher原理

  在平时的开发工作中,我们经常使用监听器来帮助我们观察一些数据的变化,然后执行一条逻辑。在Vue.js2.x中,可以通过watch选项初始化一个名为watcher的监听器。本文将详细介绍listener的实现原理,有需要的可以参考。

  

目录

   watch API用法watch API实现原理标准化源码构造回调函数创建调度器创建效果返回销毁函数设计异步任务队列创建异步任务队列执行检测循环更新优化:只使用一个变量watchEffect注册无效回调函数摘要在平时的开发工作中,我们经常使用监听器帮助我们观察一些数据的变化,然后执行一段逻辑,

  在Vue.js 2.x中,可以通过watch选项初始化一个名为watcher的监听器:

  导出默认值{

  观察:{

  一个(新瓦尔,旧瓦尔){

  console.log(新:%s,00旧:%s ,newVal,oldVal)

  }

  }

  }

  当然,您也可以通过$watch API创建一个监听器:

  const unwatch=vm。$watch(a ,function(newVal,oldVal) {

  console.log(新:%s,旧:%s ,newVal,oldVal)

  })

  与watch选项不同,由$watch API创建的监听器观察器将返回一个unwatch函数,您可以随时执行该函数来阻止这个观察器监听数据。对于由watch option创建的监听器,它会随着组件的销毁而停止监听数据。

  在Vue.js 3.0中,虽然仍然可以使用watch选项,但是对于Composition API,Vue.js 3.0提供了watch API来实现监听器效果。本文分析了watch API的实现原理。

  

watch API 的用法

  我们先来看看手表API在Vue.js 3.0中的用法。

  1.watch API可以是侦听一个 getter 函数,但是必须返回一个响应对象。当响应对象被更新时,相应的回调函数将被执行。

  从“vue”导入{反应式,观察式}

  常量状态=反应性({ count: 0 })

  watch(()=state.count,(count,prevCount)={

  //当state.count更新时,会触发这个回调函数

  })

  2 .手表API也可以直接侦听一个响应式对象。当响应对象被更新时,相应的回调函数将被执行。

  从“vue”导入{ ref,watch }

  const count=ref(0)

  手表(计数,(计数,前计数)={

  //当count.value更新时,会触发这个回调函数

  })

  3 .手表API也可以直接侦听多个响应式对象。在任何响应对象被更新后,相应的回调函数将被执行。

  从“vue”导入{ ref,watch }

  const count=ref(0)

  const count2=ref(1)

  watch([count,count2],([count,count2],[prevCount,prevCount2])={

  //当count.value或count2.value更新时,会触发这个回调函数。

  })

  

watch API实现原理

  监听器的含义是,当它监听的对象或函数发生变化时,它会自动执行一个回调函数,这与副作用函数效果非常相似。它的内在实现是否依赖于效果?带着这个问题,我们来探讨一下watch API的具体实现:

  功能表(信号源、cb、选项){

  if ((process.env.NODE_ENV!==制作)!isFunction(cb)) {

  warn(‘watch(fn,options?)`签名已移动到单独的API。`

  `使用` watchEffect(fn,options?)`反而。“现在只看”

  支持手表(信号源、cb、选项?)签名。`)

  }

  返回doWatch(源、cb、选项)

  }

  函数Dow watch(source,cb,{ immediate,deep,flush,onTrack,onTrigger }=EMPTY_OBJ) {

  //规范源码

  //构造applyCb回调函数

  //创建一个调度程序顺序执行函数

  //创建效果副作用函数

  //返回侦听器销毁函数

  }

  从代码中可以看出,watch函数在内部调用了doWatch函数。在调用之前,它会确定第二个参数cb是否是一个非生产环境中的函数。如果不是,它会给出一个警告,告诉用户应该使用watchEffect(fn,options) API,这也是一个与监听器相关的API。后面我们会详细介绍。

  让我们来看看doWatch函数是做什么的。

  

标准化source

  我们先来看手表函数的第一个参数来源。

  我们从上一篇文章中知道source 可以是 getter 函数,也可以是响应式对象甚至是响应式对象数组,所以需要规范来源。这是标准化源代码的过程:

  //当来源不合法时会给出警告

  const warinvalidsource=(s)={

  warn(`无效的监视源: `,s,`监视源只能是getter/effect函数,ref,`

  `反应性对象或这些类型的数组。`)

  }

  //当前组件实例

  常量实例=当前实例

  让getter

  if (isArray(source)) {

  getter=()=source.map(s={

  if (isRef(s)) {

  返回s值

  }

  else if (isReactive(s)) {

  返回导线

  }

  else if(is function)){

  返回callWithErrorHandling(s,instance,2 /* WATCH_GETTER */)

  }

  否则{

  (process.env.NODE_ENV!==production )警告无效源

  }

  })

  }

  else if (isRef(source)) {

  getter=()=source.value

  }

  else if (isReactive(source)) {

  getter=()=source

  深度=真实

  }

  else if (isFunction(source)) {

  if (cb) {

  //带cb的getter

  GETTER=()=callWithErrorHandling(source,instance,2 /* WATCH_GETTER */)

  }

  否则{

  //观看效果的逻辑

  }

  }

  否则{

  getter=NOOP

  (process.env.NODE_ENV!== production )warnivalidsource(source)

  }

  if (cb深度){

  const baseGetter=getter

  getter=()=traverse(baseGetter())

  }

  其实源码标准化主要是基于源码的类型,变成了一个getter函数。具体来说:

  如果源是ref对象,则创建一个getter函数来访问source.value。如果源是一个反应性对象,创建一个访问源的getter函数,并将deep设置为true(后面我会讲到deep的作用)。如果源是函数,它将进一步确定第二个参数cb是否存在。对于watch API,cb必须存在,并且是回调函数。在这种情况下,getter是一个封装了源函数的简单函数。如果来源不满足上述条件,在非生产环境下会给出警告,表示来源类型不合法。

  我们来看看最终的标准化getter函数,它将返回一个responsive对象,这个对象将用于后续创建effect runner副作用函数。每次执行runner时,getter函数返回的responsive对象都会作为watcher的求值结果。我们后面会详细分析效果跑者的创建过程,这里不需要深入了解。

  最后,我们来关注一下deep为真的情况。此时,我们会发现生成的getter函数会被traverse函数包装。遍历函数的实现非常简单,就是递归访问value的每个子属性。那么,为什么要递归访问每个子属性呢?

  其实deep属于watcher的一个配置选项,Vue.js 2.x也支持,表面含义是深度侦听,实际上是通过遍历对象的每一个子属性来实现。例如,你会明白:

  从“vue”导入{反应式,观察式}

  恒定状态=反应({

  计数:{

  答:{

  乙:1

  }

  }

  })

  watch(state.count,(count,prevCount)={

  console.log(计数)

  })

  state.count.a.b=2

  这里我们用reactive API创建一个深嵌套层次的响应式对象状态,然后调用watch API监听state.count的变化接下来我们修改内部属性state.count.a.b的值你会发现执行watcher的回调函数,为什么要执行?

  原则上,对于代理实现的响应式对象,只有先访问对象的属性并触发依赖关系集合,然后修改属性,才能通知相应的依赖关系更新。从上面的业务代码来看,我们修改的时候并没有访问state.count.a.b的值,但是触发了watcher的回调函数。

  根本原因是当我们执行 watch 函数的时候,我们知道如果侦听的是一个 reactive 对象,那么内部会设置 deep 为 true,然后执行遍历来递归访问对象的深层子属性。此时会访问state.count.a.b触发依赖关系收集。这里收集的依赖是watcher内部创建的效果运行器。所以当我们再次修改state.count.a.b时,会通知这个效果,所以最终会执行watcher的回调函数。

  当我们监听通过反应式API创建的响应对象时,遍历函数将在内部执行。如果对象非常复杂,比如嵌套层次很深,那么递归遍历会有一定的耗时性能。因此,如果我们需要监听这个复杂的响应对象内部的特定属性,我们可以找到一种方法来减少遍历带来的性能损失。

  比如刚才的例子,我们可以直接听state.count.a.b的变化:

  watch(state.count.a,(newVal,oldVal)={

  console.log(newVal)

  })

  state.count.a.b=2

  这可以减少内部执行遍历的次数。你可能会问,直接听state.count.a.b可以吗?答案是否定的,因为state.count.a.b已经是基本的数字类型,不符合source要求的参数类型,所以在非生产环境下会给出警告。

  那么有没有办法优化一下,让traverse不执行呢?答案是肯定的。我们可以听一个getter函数:

  watch(()=state.count.a.b,(newVal,oldVal)={

  console.log(newVal)

  })

  state.count.a.b=2

  这样函数会在内部访问并返回state.count.a.b,不会执行一次traverse,仍然可以监听其变化来执行watcher的回调函数。

  

构造回调函数

  在处理完watch API的第一个参数源之后,接下来处理第二个参数cb。

  Cb是一个回调函数,有三个参数:第一个newValue代表新值;第二个oldValue表示旧值。关于invalid的第三个参数,后面会介绍。

  这个API设计其实很好理解,就是监听一个值的变化,如果值发生变化,就执行一个回调函数,在这个回调函数中可以访问新值和旧值。

  接下来,我们来看看构造回调函数的处理逻辑:

  让清理

  //注册无效的回调函数

  const onInvalidate=(fn)={

  clean up=runner . options . on stop=()={

  callWithErrorHandling(fn,instance,4 /* WATCH_CLEANUP */)

  }

  }

  //旧值的原始值

  设oldValue=isArray(source)?[] : INITIAL_WATCHER_VALUE /*{}*/

  //回调函数

  const applyCb=cb

  ?()={

  //如果组件被破坏,就直接返回。

  if(instance instance . isun mounted){

  返回

  }

  //找到新值

  const newValue=runner()

  if (deep hasChanged(newValue,oldValue)) {

  //执行清洗功能

  if(清理){

  清理()

  }

  callwithasynchrorhandling(CB,instance,3 /* WATCH_CALLBACK */,[

  新价值,

  //第一次更改时传递的旧值未定义

  old VALUE===INITIAL _ WATCHER _ VALUE?未定义:旧值,

  onInvalidate

  ])

  //更新旧值

  旧值=新值

  }

  }

  :void 0

  OnInvalidate函数用于注册无效的回调函数。我们暂时不需要去关注它。我们需要专注于applyCb。这个功能实际上是对cb的一层封装。当监听值改变时,将执行applyCb方法。我们来分析一下它的实现。

  首先,watch API与组件实例相关,因为通常我们在组件的设置函数中使用。当组件被销毁时,回调函数cb不应该被执行而是直接返回。

  接下来执行runner获取新值,实际上就是执行前面创建的getter函数获取新值。

  最后,做个判断。如果很深或者新旧值都有变化,执行回调函数cb,传入参数newValue和oldValue。注意,旧值的初始值在第一次执行时是空数组或未定义的。在执行回调函数cb之后,oldValue被更新为newValue,用于下一次比较。

  

创建scheduler

  接下来,我们将分析创建调度程序的过程。

  调度器的作用是按照一定的调度方式执行一定的功能。在watch API中,主要影响回调函数的执行方式。我们来看看它的实现逻辑:

  const invoke=(fn)=fn()

  让调度程序

  if (flush===sync) {

  //同步

  调度程序=调用

  }

  else if (flush===pre) {

  调度程序=作业={

  如果(!instance instance.isMounted) {

  //进入异步队列,在更新组件前执行。

  队列作业(作业)

  }

  否则{

  //如果组件尚未安装,同步执行将确保在组件安装之前

  作业()

  }

  }

  }

  否则{

  //进入异步队列,组件更新后执行。

  scheduler=job=queuepostrendeffect(job,instance instance.suspense)

  }

  除了source和cb,API的参数还支持第三个参数选项。不同的配置决定了观察者的不同行为。我们之前也分析过deep为真的情况。除了source 为 reactive 对象时会默认把 deep 设置为 true,还可以主动传入第三个参数,将deep设置为true。

  这里调度器的创建逻辑受第三个参数选项中flush属性的值影响,不同的flush决定了watcher的执行时机。

  当flush为sync时,表示是同步观察器,即在数据发生变化时同步执行回调函数。当flush为pre时,回调函数在通过queueJob更新组件之前执行。如果组件还没有安装,同步执行确保回调函数在组件安装之前执行。如果未设置flush,则回调函数将在queuePostRenderEffect更新组件后执行。Job和queueJob后期渲染效果不是这里的重点,后面再介绍。总之,你现在应该还记得,watcher的回调函数是通过某种调度方式执行的。

  

创建effect

  在前面的分析中,我们提到了runner,其实就是watcher创建的效果函数。接下来,我们来分析一下它的逻辑:

  const runner=effect(getter,{

  //延迟执行

  懒:真的,

  //计算效果可以先于普通效果运行,比如组件渲染的效果。

  computed: true,

  onTrack,

  翁特里格,

  调度器:applyCb?()=调度程序(applyCb):调度程序

  })

  //在组件实例中记录此效果

  recordinstanceboundfeffect(runner)

  //第一次执行

  if (applyCb) {

  如果(立即){

  applyCb()

  }

  否则{

  //找到旧值

  oldValue=runner()

  }

  }

  否则{

  //没有cb的情况下

  转轮()

  }

  这段代码逻辑是整个watcher实现的核心部分,也就是通过效果API创建一个副作用函数runner。我们需要注意以下几点。

  runner 是一个 computed effect。由于computed effect可以先于普通效果(比如组件渲染的效果)运行,所以可以实现当flush配置为pre时,watcher的执行可以优先于组件更新。

  runner 执行的方式。runner比较懒,创建后不会立即执行。第一次手动执行runner将执行之前的getter函数,访问响应数据并收集依赖项。注意,此时的activeEffect是runner,这样在后面更新响应数据时,可以触发runner执行调度器函数,以调度的方式执行回调函数。

  runner 的返回结果。手动执行runner相当于执行之前标准化的getter函数。getter函数的返回值是watcher计算的值,所以第一次执行runner得到的值可以作为oldValue。

  配置了 immediate 的情况。当我们配置immediate时,watcher会在创建后立即执行applyCb函数,此时oldValue仍然是初始值。执行applyCb时,也会执行runner,然后执行之前的getter函数,收集依赖关系,获取新值。

  

返回销毁函数

  最后会返回监听器销毁函数,也就是watch API执行后返回的函数。我们可以通过调用它来停止watcher监听数据。

  return ()={

  停止(跑步者)

  if(实例){

  //删除组件效果对该转轮的引用

  移除(实例.效果,转轮)

  }

  }

  功能停止(效果){

  if (effect.active) {

  清理(效果)

  if (effect.options.onStop) {

  effect.options.onStop()

  }

  effect.active=false

  }

  }

  在destroy函数内部,将执行stop方法来停用runner,并清除runner的相关依赖项,从而可以停止对数据的拦截。并且,如果是在组件中注册的观察者,组件效果对这个runner的引用也会被删除。

  

异步任务队列的设计

  监听器的回调函数是以调度的方式执行的,尤其是flush不同步的时候,它会把回调函数执行的任务推到异步队列中执行。接下来,我们将分析异步执行队列的设计。在分析之前,我们先思考一下为什么需要异步队列。

  让我们简单地修改一下前面的例子:

  从“vue”导入{反应式,观察式}

  常量状态=反应性({ count: 0 })

  watch(()=state.count,(count,prevCount)={

  console.log(计数)

  })

  状态.计数

  状态.计数

  状态.计数

  这里我们修改了state.count三次,那么watcher的回调函数会执行三次吗?

  答案是否定的,其实count的值只输出一次,就是最后计算出来的值3。这在大多数场景下都是意料之中的,因为在一个Tick(宏任务执行的生命周期)中,即使监听值被多次修改,它的回调函数也只执行一次。

  组件的更新过程是异步的。我们知道模板中引用的响应式对象的值时,会触发组件的重新渲染被修改,但是在一个Tick内,即使你多次修改多个响应对象的值,组件的重新渲染也只进行一次。这是因为如果每次更新数据时都触发组件重新呈现,则重新呈现的次数和成本都太高。

  那么,这是怎么做到的呢?让我们从异步任务队列的创建开始。

  

异步任务队列的创建

  从前面的分析我们知道,在创建watcher时,如果flush配置为pre或者没有配置flush,那么watcher的回调函数就会异步执行。此时,回调函数分别被queueJob和queuePostRenderEffect推入异步队列。

  在不涉及悬念的情况下,queuePostRenderEffect等价于queuePostFlushCb。让我们看看它们的实现:

  //异步任务队列

  常量队列=[]

  //队列任务执行后执行的回调函数队列。

  const postFlushCbs=[]

  函数queueJob(job) {

  如果(!队列.包含(作业)){

  queue.push(作业)

  队列刷新()

  }

  }

  函数queuePostFlushCb(cb) {

  如果(!isArray(cb)) {

  postFlushCbs.push

  }

  否则{

  //如果是数组,就把它展平成一维

  postFlushCbs.push(.cb)

  }

  队列刷新()

  }

  Vue.js内部维护了一个队列数组和一个postFlushCbs数组,其中队列数组作为异步任务队列,postFlushCbs数组作为异步任务队列执行后的回调函数队列。

  执行queueJob会将这个任务Job添加到队列的末尾,而执行queuePostFlushCb会将这个Cb回调函数添加到postFlushCbs的末尾。添加后,它们都执行queueFlush函数。接下来让我们看看它的实现:

  const p=Promise.resolve()

  //异步任务队列是否正在执行

  设isFlushing=false

  //异步任务队列是否正在等待执行?

  设isFlushPending=false

  函数nextTick(fn) {

  返回fn?然后(fn) : p

  }

  函数queueFlush() {

  如果(!正在冲洗!isflushpinding){

  isFlushPending=true

  nextTick(刷新作业)

  }

  }

  如你所见,Vue.js还维护了isFlushing和isFlushPending变量,用于控制异步任务的刷新逻辑。

  第一次执行queueFlush时,isFlushing和ISFlushing都为false。此时,ISFlushing设置为true,调用nextTick(flushJobs)执行队列中的任务。

  由于isFlushPending的控制,即使多次执行queueFlush,也不会多次执行flushJobs。另外,Vue.js 3.0中nextTick的实现也非常简单。flushJobs通过promise . resolve()then异步执行。

  因为JavaScript是在单线程中执行的,所以这种异步设计使您可以在一个Tick内多次执行queueJob或queuePostFlushCb来添加任务,同时也确保在宏任务执行后的微任务阶段执行一次flushJobs。

  

异步任务队列的执行

  创建任务队列后,下一步是异步执行这个队列。让我们来看看flushJobs的实现:

  const getId=(job)=(job.id==null?无限:job.id)

  函数flushJobs(已见){

  isFlushPending=false

  isFlushing=true

  让工作

  if ((process.env.NODE_ENV!== production ){

  见过=见过新地图()

  }

  //组件的更新是父先于子。

  //如果在父组件更新期间卸载了某个组件,则应该跳过它自己的更新。

  queue.sort((a,b)=getId(a) - getId(b))

  while ((job=queue.shift())!==未定义){

  if (job===null) {

  继续

  }

  if ((process.env.NODE_ENV!== production ){

  checkRecursiveUpdates(seen,job)

  }

  callWithErrorHandling(job,null,14 /* SCHEDULER */)

  }

  flushPostFlushCbs(参见)

  isFlushing=false

  postFlushCb执行过程中会再次添加一些异步任务,递归flushJobs会全部完成。

  if(queue . length postflushcbs . length){

  刷新作业(已看到)

  }

  }

  如您所见,当flushJobs函数开始执行时,它会将is flush重置为false,并将is flush设置为true,以指示正在执行异步任务队列。

  对于异步任务队列,在遍历它们之前,它们将被从小到大排序。这是因为两个主要原因:

  创建组件的过程是从父到子的,所以创建组件副作用渲染功能也是先父后子。父组件的副作用渲染函数的效果id小于子组件的效果id,每次组件更新时,通过queueJob将效果推送到异步任务队列中。因此,为了保证先更新父组,再更新子组件,队列应该从小到大排序。如果一个组件在父组件更新过程中被卸载,它自己的更新应该被跳过。所以也要保证先更新父组件,再更新子组件,队列要从小到大排序。接下来就是遍历这个队列,依次执行队列中的任务。在遍历过程中,注意checkRecursiveUpdates的逻辑,它用于检测非生产环境中是否存在循环更新。我们将在后面提到它的功能。

  遍历队列后,将进一步执行flushPostFlushCbs方法,遍历并执行所有推送到postFlushCbs的回调函数:

  函数flushPostFlushCbs(见){

  if (postFlushCbs.length) {

  //制作副本

  const cbs=[.新集合(postFlushCbs)]

  postFlushCbs.length=0

  if ((process.env.NODE_ENV!== production ){

  见过=见过新地图()

  }

  for(设I=0;i cbs.lengthi ) {

  if ((process.env.NODE_ENV!== production ){

  checkRecursiveUpdates(参见cbs[i])

  }

  哥伦比亚广播公司[i]

  }

  }

  }

  这里注意,postFlushCbs的一个副本将通过const CBS=[.新设置(postFlushCbs)]。这是因为在遍历的过程中,一些回调函数的执行可能会再次修改postfushcbs,所以复制一个副本进行循环遍历不会受到postfushcbs修改的影响。

  遍历postFlushCbs后,isFlushing将被重置为false。由于有些PostFlushCBS在执行过程中可能会再次添加异步任务,所以需要继续判断队列或postFlushCbs队列中是否还有任务,然后递归执行flushJobs来完成全部。

  

检测循环更新

  如前所述,在遍历和执行异步任务和回调函数的过程中,会在非生产环境中执行checkRecursiveUpdates来检查是否存在循环更新。是用来解决什么问题的?

  让我们重写前面的例子:

  从“vue”导入{反应式,观察式}

  常量状态=反应性({ count: 0 })

  watch(()=state.count,(count,prevCount)={

  状态.计数

  console.log(计数)

  })

  状态.计数

  如果您运行这个示例,您将在控制台中看到输出值101次,然后报告错误:超过最大递归更新。这是因为我们更新了watcher回调函数中的数据,会再次进入回调函数。如果我们不添加任何控件,回调函数会一直执行到内存耗尽,导致浏览器装死。

  为了避免这种情况,Vue.js实现了checkRecursiveUpdates方法:

  const递归_限制=100

  函数checkRecursiveUpdates(seen,fn) {

  如果(!seen.has(fn)) {

  seen.set(fn,1)

  }

  否则{

  const count=seen.get(fn)

  if (count RECURSION_LIMIT) {

  抛出新错误(“超过了最大递归更新数。”

  您组件中可能有正在改变状态代码

  呈现函数或更新的挂钩或观察器源函数。)

  }

  否则{

  seen.set(fn,计数1)

  }

  }

  }

  从前面的代码中我们知道,flushJobs在开头创建了seen,这是一个Map对象,然后当checkRecursiveUpdates时,它会将任务添加到seen并记录引用计数,初始值为1。如果postFlushCbs再次添加相同的任务,则引用计数count增加1。如果计数大于我们定义的限制100,这意味着相同的任务已经被添加了超过100次。然后,Vue.js会抛出这个错误,因为在正常使用中,不应该出现这种情况,我们上面的错误例子会触发这个错误逻辑。

  

优化:只用一个变量

  至此,异步队列的设计已经介绍完毕。你可能对isFlushing和ISflushing有些疑问。为什么需要两个变量来控制?

  语义上,isFlushing是用来确定你是否在等待下一个Tick执行flushJobs,而isflushing是一个队列,用来确定你是否在执行任务。

  在功能上,他们的作用是确保以下两点:

  您可以在一个时钟周期内多次向队列中添加任务,但是任务队列将在下一个时钟周期后执行。在执行任务队列的过程中,还可以向队列中添加新的任务,并在当前Tick中执行剩余的任务队列。但实际上,这里我们可以优化。在我看来,这里用一个变量就够了。让我们稍微修改一下源代码:

  函数queueFlush() {

  如果(!正在冲洗){

  isFlushing=true

  nextTick(刷新作业)

  }

  }

  函数flushJobs(已见){

  让工作

  if ((process.env.NODE_ENV!== production ){

  见过=见过新地图()

  }

  queue.sort((a,b)=getId(a) - getId(b))

  while ((job=queue.shift())!==未定义){

  if (job===null) {

  继续

  }

  if ((process.env.NODE_ENV!== production ){

  checkRecursiveUpdates(seen,job)

  }

  callWithErrorHandling(job,null,14 /* SCHEDULER */)

  }

  flushPostFlushCbs(参见)

  if(queue . length postflushcbs . length){

  刷新作业(已看到)

  }

  isFlushing=false

  }

  正如你所看到的,我们只需要一个isFlushing来控制和实现同样的功能。执行queueFlush时,如果is false为false,则将其设置为true,然后nextTick将执行flushJobs。在flushJobs函数执行结束时,也就是说,所有任务(包括后来添加的任务)都完成了,然后将isflush设置为false。

  了解了watch API和异步任务队列的设计之后,我们再来学习监听器提供的另一个API,—— Watch Effect API。

  

watchEffect

  watchEffect API的作用是注册一个副作用函数。可以在副作用函数内部访问响应对象,然后在内部响应对象更改后立即执行该函数。

  我们先来看一个例子:

  从“vue”导入{ ref,watchEffect }

  const count=ref(0)

  watch effect(()=console . log(count . value))

  计数.值

  结果,它依次输出0和1。

  watchEffect和之前的watch API有什么区别?主要有三点:

  侦听的源不同 。手表应用程序接口可以侦听一个或多个响应式对象,也可以侦听一个吸气剂函数,而watchEffect API侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象没有回调函数 。手表效果应用程序接口没有回调函数,副作用函数的内部响应式对象发生变化后,会再次执行这个副作用函数立即执行 。手表效果应用程序接口在创建好看守人后,会立刻执行它的副作用函数,而观察应用程序接口需要配置马上为没错,才会立即执行回调函数。对watchEffect API有大体了解后,我们来看一下在我整理的手表效果场景下,下载函数的简化版实现:

  函数手表效果(效果,选项){

  返回Dow watch(效果,空,选项);

  }

  函数Dow watch(source,cb,{ immediate,deep,flush,onTrack,onTrigger }=EMPTY_OBJ) {

  实例=当前实例

  让吸气剂

  if (isFunction(source)) {

  getter=()={

  if(实例instance。isun安装){

  返回;

  }

  //执行清理函数

  如果(清理){

  清理();

  }

  //执行来源函数,传入onInvalidate作为参数

  return callWithErrorHandling(source,instance,3 /* WATCH_CALLBACK */,[on invalidate]);

  };

  }

  让清理;

  const onInvalidate=(fn)={

  清理=跑步者。选项。在停止时=()={

  callWithErrorHandling(fn,instance,4/* WATCH _ clean up */);

  };

  };

  让调度器;

  //创建调度程序

  if (flush===sync) {

  调度程序=调用

  }

  else if (flush===pre) {

  调度程序=作业={

  如果(!instance instance.isMounted) {

  队列作业(作业);

  }

  否则{

  job();

  }

  };

  }

  否则{

  scheduler=job=queuepostrendeffect(job,instance instance.suspense

  }

  //创建跑步者

  const runner=effect(getter,{

  懒:真的,

  computed: true,

  onTrack,

  onTrigger,

  进程调度

  uler

   });

   recordInstanceBoundEffect(runner);

   // 立即执行 runner

   runner();

   // 返回销毁函数

   return () => {

   stop(runner);

   if (instance) {

   remove(instance.effects, runner);

   }

   };

  }

  可以看到,getter 函数就是对 source 函数的简单封装,它会先判断组件实例是否已经销毁,然后每次执行 source 函数前执行 cleanup 清理函数。

  watchEffect 内部创建的 runner 对应的 scheduler 对象就是 scheduler 函数本身,这样它再次执行时,就会执行这个 scheduler 函数,并且传入 runner 函数作为参数,其实就是按照一定的调度方式去执行基于 source 封装的 getter 函数。

  创建完 runner 后就立刻执行了 runner,其实就是内部同步执行了基于 source 封装的 getter 函数。

  在执行 source 函数的时候,会传入一个 onInvalidate 函数作为参数,接下来我们就来分析它的作用。

  

注册无效回调函数

  有些时候,watchEffect 会注册一个副作用函数,在函数内部可以做一些异步操作,但是当这个 watcher 停止后,如果我们想去对这个异步操作做一些额外事情(比如取消这个异步操作),我们可以通过 onInvalidate 参数注册一个无效函数。

  import {ref, watchEffect } from vue

  const id = ref(0)

  watchEffect(onInvalidate => {

   // 执行异步操作

   const token = performAsyncOperation(id.value)

   onInvalidate(() => {

   // 如果 id 发生变化或者 watcher 停止了,则执行逻辑取消前面的异步操作

   token.cancel()

   })

  })

  我们利用 watchEffect 注册了一个副作用函数,它有一个 onInvalidate 参数。在这个函数内部通过 performAsyncOperation 执行某些异步操作,并且访问了 id 这个响应式对象,然后通过 onInvalidate 注册了一个回调函数。

  如果 id 发生变化或者 watcher 停止了,这个回调函数将会执行,然后执行 token.cancel 取消之前的异步操作。

  我们来回顾 onInvalidate 在 doWatch 中的实现:

  const onInvalidate = (fn) => {

   cleanup = runner.options.onStop = () => {

   callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);

   };

  };

  实际上,当你执行 onInvalidate 的时候,就是注册了一个 cleanup 和 runner 的 onStop 方法,这个方法内部会执行 fn,也就是你注册的无效回调函数。

  也就是说当响应式数据发生变化,会执行 cleanup 方法,当 watcher 被停止,会执行 onStop 方法,这两者都会执行注册的无效回调函数 fn。

  通过这种方式,Vue.js 就很好地实现了 watcher 注册无效回调函数的需求。

  

总结

  侦听器的内部设计很巧妙,我们可以侦听响应式数据的变化,内部创建 effect runner,首次执行 runner 做依赖收集,然后在数据发生变化后,以某种调度方式去执行回调函数。

  相比于计算属性,侦听器更适合用于在数据变化后执行某段逻辑的场景,而计算属性则用于一个数据依赖另外一些数据计算而来的场景。

  以上就是深入了解Vue3中侦听器watcher的实现原理的详细内容,更多关于Vue3 侦听器watcher的资料请关注我们其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: