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

相关推荐
web147862107239 分钟前
C# .Net Web 路由相关配置
前端·c#·.net
m0_7482478010 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖14 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案121 分钟前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_7482548826 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.37 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营42 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui