【框架实现】vue3的监听器watch

源码阅读与调试

watch文档

watch和computed有些相似的地方,但作用却与computed大有不同。watch可以监听响应式数据的变化,从而触发指定的函数;

js 复制代码
watch(
  () => obj.name,
  (value, oldValue) => {
    console.log('watch 监听被触发')
    console.log('oldValue', oldValue)
    console.log('value', value)
  },
  {
    immediate: true,
    deep: true
  }
)

上面的代码接收3个参数:     
1.监听的响应式对象;    
2.回调函数cd;   
3.配置对象:options   
  1.immediate:watch初始化完成后被立刻触发一次;   
  2.deep:深度监听;  

基础的watch实例

js 复制代码
<script>
    const { reactive, watch } = Vue
    
    // 1. reactive构建了响应式数据
    const obj = reactive({
      name: '张三'
    })

    // 2. 执行了watch函数
    watch(obj, (value, oldValue) => {
      console.log('watch 监听被触发')
      console.log('value', JSON.stringify(value))
    })
    
    // 3. 两秒之后触发setter行为
    setTimeout(() => {
      obj.name = '李四'
    }, 2000)
</script>

reactive构建了响应式数据(前面文章详细写过)

执行watch函数

watch方法的源码在runtime-core/src/apiWatch.ts的路径下;

ts 复制代码
export function watch(){
  return doWatch(source as any, cb, options)
}

此时doWatch方法传入的3个参数如下图所示:

ts 复制代码
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  // currentInstance为null
  const instance = currentInstance
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  if (isRef(source)) {
    // 如果是一个ref,需要去获取.value
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // source是一个Reactive
    getter = () => source
    deep = true
  }

  if (cb && deep) {
    const baseGetter = getter
    // 暂时不用管
    getter = () => traverse(baseGetter())
  }
  // isMultiSource判断是不是数组,INITIAL_WATCHER_VALUE是一个空对象
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  // job函数非常重要,它是watch函数的核心
  // 此时是定义一个job,具体的暂时不用关注
  const job: SchedulerJob = () => {}

  // 声明了一个调度器
  let scheduler: EffectScheduler
  if (flush === 'sync') {
    // 略
  } else {
    // 调度器赋值,queuePreFlushCb中传人job
    scheduler = () => queuePreFlushCb(job)
  }
  // 用getter和scheduler生成effect
  const effect = new ReactiveEffect(getter, scheduler)

  if (cb) {
    if (immediate) {
      // immediate为true,直接触发job
      job()
    } else {
      // 当前oldValue是Proxy {name: '张三'}
      oldValue = effect.run()
    }
  }

  return () => {
    // watch会被停止,effect监听被停止
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
}

SetTimeout两秒之后触发setter行为

测试实例执行完watch函数之后,两秒后会触发Setter行为;Setter行为会触发triggerEffects方法;

triggerEffects方法中effects如下图所示,effects是有3个ReactiveEffect,它的fn此时其实返回的是source;scheduler是一个() => traverse(baseGetter())方法:

triggerEffects方法=>triggerEffect方法=>effect.scheduler方法=>scheduler = () => queuePreFlushCb(job)=>queueCb方法=>queueFlush方法

queuePreFlushCb方法中传入的cb就是之前doWatch方法中定义的job;

queueFlush方法是关键

ts 复制代码
function queueFlush() {
  // 此时isFlushing和isFlushPending都为false
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    // resolvedPromise是Promise.resolve(),flushJobs都变成异步微任务;
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

此时同步任务已经全部完成,剩下的就是异步微任务了,也就是flushJobs方法;

ts 复制代码
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true

  flushPreFlushCbs(seen)
}

flushJobs方法=>flushPreFlushCbs方法=>通过任务队列的形式触发job;job方法的触发,也就是watch方法的触发;

ts 复制代码
export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  // 此时pendingPreFlushCbs有一个job
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    // 此时pendingPreFlushCbs的length置为空
    pendingPreFlushCbs.length = 0
   
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      // job函数执行
      activePreFlushCbs[preFlushIndex]()
    }
  }
}

接着我们job函数中都做了什么?

ts 复制代码
 const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // newValue是Proxy {name: '李四'}
      const newValue = effect.run()
      if () {
        // callWithAsyncErrorHandling对fn函数进行了try catch,从而能够统一处理所有的错误
        // cb是(value, oldValue) => {}
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }

SchedulerJob方法=>callWithAsyncErrorHandling方法=>callWithErrorHandling方法; callWithErrorHandling方法就是对fn函数进行了try catch,从而能够统一处理所有的错误;

ts 复制代码
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

到这里watch的逻辑就处理完了;watch函数被触发,本质上都是job函数被触发了;

总结

整体分为4块:

  1. watch函数本身;
  2. reactive的setter;
  3. flushJobs;
  4. job;

代码实现watch

初步实现watch

新建packages/packages/runtime-core/src/apiWatch.ts文件;

ts 复制代码
import { isReactive, ReactiveEffect } from "@vue/reactivity";
import { queuePreFlushCb } from "./scheduler";
import { EMPTY_OBJ, hasChanged } from "@vue/shared";

export interface WatchOptions<immediate = boolean> {
  immediate?: immediate;
  deep?: boolean;
}

export function watch(source, cb: any, options?: WatchOptions) {
  return doWatch(source as any, cb, options);
}

function doWatch(
  source: any,
  cb: Function,
  { immediate, deep }: WatchOptions = EMPTY_OBJ
) {
  let getter: () => any;

  if (isReactive(source)) {
    getter = () => source;
    deep = true;
  } else {
    getter = () => {};
  }
  if (cb && deep) {
    // TODO
    const baseGetter = getter;
    getter = () => baseGetter();
  }
  let oldValue = {};

  const job: Function = () => {
    if (cb) {
      const newValue = effect.run();
      if (deep || hasChanged(newValue, oldValue)) {
        cb(newValue, oldValue);
        oldValue = newValue;
      }
    } else {
      effect.run();
    }
  };

  let scheduler = () => queuePreFlushCb(job);

  const effect = new ReactiveEffect(getter, scheduler);
  if (cb) {
    if (immediate) {
      job();
    } else {
      oldValue = effect.run();
    }
  } else {
    effect.run();
  }
  return () => {
    effect.stop();
  };
}

修改packages/reactivity/src/reactive.ts;

ts 复制代码
export const enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}

export function isReactive(r: any) {
  return !!(r && r.__v_isReactive === true);
}
function createReactiveObject(){
 proxy[ReactiveFlags][IS_REACTIVE] = true
 return proxy
}

在shared/src/index.ts中增加:

ts 复制代码
export const EMPTY_OBJ: { readonly [key: string]: any } = {};

export const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue);

记得将watch在两个地方export; 创建一个测试实例:

js 复制代码
const { reactive, watch } = Vue;

// 1. reactive构建了响应式数据
const obj = reactive({
  name: "张三",
});

// 2. 执行了watch函数
watch(
  obj,
  (value, oldValue) => {
    console.log("watch 监听被触发");
    console.log("value", JSON.stringify(value));
  }
);

// 3. 两秒之后触发setter行为
setTimeout(() => {
  obj.name = "李四";
}, 2000);

watch啥也没打印出来!why?!!!依赖是没有被收集吗?

【源码学习】watch如何进行依赖收集?

doWatch方法中有一段代码:

ts 复制代码
function doWatch(){
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
}

export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

这个traverse是递归来取value,目的是来做依赖收集;

【框架实现】实现watch的依赖收集

apiWatch.ts文件中:

ts 复制代码
function doWatch(){
  if (cb && deep) {
    // 用来依赖收集
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }
}
// 递归获取value,触发getter,进行依赖收集
export const traverse = (value: any) => {
  if (!isObject(value)) {
    return value;
  }
  for (const key in value) {
    traverse((value as Object)[key]);
  }
  return value;
};

此时log被打印出来了;

相关推荐
萌萌哒草头将军14 分钟前
尤雨溪强烈推荐的这个库你一定要知道 ⚡️⚡️⚡️
前端·vue.js·vite
2401_8784545319 分钟前
Vue 核心特性详解:计算属性、监听属性与事件交互实战指南
前端·vue.js·交互
1024小神1 小时前
uniapp+vue3+vite+ts+xr-frame实现ar+vr渲染踩坑记
前端
测试界清流1 小时前
基于pytest的接口测试
前端·servlet
知识分享小能手1 小时前
微信小程序入门学习教程,从入门到精通,自定义组件与第三方 UI 组件库(以 Vant Weapp 为例) (16)
前端·学习·ui·微信小程序·小程序·vue·编程
trsoliu2 小时前
多仓库 Workspace 协作机制完整方案
前端
啦工作呢2 小时前
数据可视化 ECharts
前端·信息可视化·echarts
NoneSL2 小时前
Uniapp UTS插件开发实战:引入第三方SDK
前端·uni-app
trsoliu2 小时前
Claude Code Templates
前端·人工智能
wangpq2 小时前
使用rerender-spa-plugin在构建时预渲染静态HTML文件优化SEO
前端·javascript·vue.js