【框架实现】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被打印出来了;

相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^3 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic4 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test6 小时前
js下载excel示例demo
前端·javascript·excel
Yaml46 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理