vue3的响应式原理学习

111 阅读20分钟

vue2的响应式原理缺陷

简单的描述一下vue2的响应式原理的核心:Object.defineProperty()

我们vue2的数据就是:

data() {
  return {
    obj: {
      name: 'test'
    }
  }
}

那我们要做的就是劫持obj这个对象,让它具有响应式,所以我们有个类似这样的方法来处理数据:

  1. objserve方法(这个是入口)
  2. defineReative方法(这里是处理响应式的地方)
function observe (target) {
  // 如果不是对象就直接结束了
  if (typeof target !== 'object' || target === null) {
    return
  }
  Object.entries(target).forEach([key, val] => {
    defineReative(target, key, val)
  })
}
function defineReative (target, key, val) {
  if (typeof val === 'object' && val !== null) {
    // 两个函数递归调用,确保劫持到所有的属性值
    observe(val)
  }
  Object.defineProperty(target, {
    enumerable: true, // 属性可枚举
    configurable: true, // 属性可删除
    get() { // get会返回值
      return val
    },
    set(newVal) { // set如果数据不一致就更新val
      if (newVal !== val) {
        val = newVal
      }
    }
  })
}

大概就是以上这么一个方法处理数据使得数据产生了响应式,当然省略了一些依赖收集和触发的代码,不过这个不重要,我们这次主要说一下这个vue2响应式的缺点,上面有几个缺点:

  1. 数组不能设置响应性,因为Object.defineProperty不可以设置数组
  2. 删除对象属性的时候不会响应式(无法触发set):const obj = { name: 'joe' }; delete obj.name;
  3. 直接在代码里添加属性也没有响应式(无法触发set):const obj = { }; obj.name = 'joe'

这个是天生的缺陷,当然vue2为了尽可能解决这些天然的缺陷做了很多修补:

  1. 重写了数组的很多方法,使得数组具有响应性
  2. 提供了vue.$set(对象,属性)的方法使得添加属性具有响应性

但是也只能尽量弥补,始终还是带来了一些不方便,那么vue3就完美的解决了这个问题

vue3响应式核心proxy和reflect

下面介绍一下完美解决vue2缺陷的核心api,proxyreflect

  • proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等
  • reflect在这里提供和proxy搭配使用的作用

proxy

先通过一个简单的例子来介绍,proxy怎么可以实现对象操作的各种监听

const obj = {
  name: 'joe',
  hobby: ['basketball', 'volleyball'],
  family: {
    mother: 'mama',
    father: 'baba',
  }
}
const proxyObj = new Proxy(obj, {
  get(target, key) {
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
    return true // api要求是这样的,修改成功要返回一个布尔值
  }
})
proxyObj.name // 触发get方法
proxyObj.name = 'lili' // 触发set方法

看了我们简单的例子,就看的出来我们是通过new Proxy把我们被代理对象传入进去,然后后面我们就直接操作代理对象就行,所有的修改会自动映射到被代理对象身上。

我们看到例子的obj是有个属性family,是一个对象,如果我们在点一层是怎么样呢,这样能不能代理到,这里要给代码说明一下

proxyObj.family // 这样很正常,get打印出来的key是【family】
proxyObj.family = 'xx' // 这样也很正常,可以正常触发set
proxyObj.family.mother // 这个时候get方法打印出来的key还是【family】,而不是【mother】
proxyObj.family.mother = 'xx' // 这样set就不会触发了

解释一下上面的代码,我们代理只会代理到第一个层级的内容,无论你点多少层级,get打印出来的都是你点的第一个层级的内容,所以无论是.family还是.fanmily.mother``get的时候的key都是family,也就是点多个层级的时候:

  1. .family 时候出发get返回对应的值返回出去也就是{mother: 'momo', father: 'baba'}
  2. 后面的.mother就在{mother: 'momo', father: 'baba'}这个基础上继续获取出mama

而如果set的时候设置了多个层级,set就直接不回触发了,因为你的第二个层级并没有代理,实际就是操作原对象了,也就是等于 obj.family.mother这样操作了就不会出发set了

所以我们一般都是需要递归也把所有的object类型的值都加上代理,就像这样

function deepProxy (obj) {
    return new Proxy(obj, {
        get(target, key) {
            const res = target[key]
            // 如果是对象,继续代理
            // 注意这里用了按需代理,你没用到的时候,我都不帮你代理
            if (typeof res === 'object' && res !== null) {
                return deepProxy(res)
            }
            return res
        },
        set(target, key, value) {
            target[key] = value
            return true
        }
    })
}

reflect

上面介绍了proxy了,那么reflect有什么用呢,reflect实现了很多的api,主要是对js对象操作的方法进行的升级,例如:

  • obj.name = 'joe'=> Reflect.set(obj, 'name', 'joe')
  • name in obj => Reflect.has(obj, 'name')
  • delete obj.name => Reflect.deleteProperty(obj, 'name')

等等很多的api升级,主要是处理了很多的边界情况带来了更合理的使用,而我们配合proxy做监听的就是:

  • Reflect.get(target, key, receiver)
  • Reflect.set(target, key, value, receiver)

target就是目标对象,key就是目标对象的属性,value就是设置的时候的值,receiver就是getter的时候this的值,可能有些不知道这个receiver是什么,其实proxygetset的时候也有这个入参,我们看看代码:

const obj = {
  name: 'joe',
}
const proxyObj = new Proxy(obj, {
  get(target, key, receiver) { // receiver就是指的当前的代理对象
    // return target[key]
    Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    // target[key] = value
    Rflect.set(target, key, value ,receiver)
    return true
  }
})

上面我们看到了在proxy的get、set里面都用了receiver,那具体有什么用,用了Reflect,加了receiver有什么区别呢,我们举例子来看看区别

const obj = {
  name: 'joe',
  get info() {
    return this.name
  }
}
// 不用Reflect
const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('触发')
    return target[key]
  },
  set(target, key, value, receiver) {
    target[key] = value
    return true
  }
})
// 用Reflect
const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('触发')
    Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    // set之后他会返回一个布尔值
    const res = Rflect.set(target, key, value ,receiver)
    return res
  }
})
// 不用Reflect:【触发】打印了1次  用了Reflect:【触发】打印了2次
proxyObj.info

我们来解析一下,当我们proxyObj.info的时候就会触发get info 这个方法返回的是this.name如果你没用Reflectreceiver来绑定代理对象,那么this就是指向obj,那么当访问this.name的时候就不会触发代理,若用了Reflectreceiverthis就是当前的代理对象,this.name其实就是proxyObj.name就会触发代理

  • 不用Reflect:触发info这个key的get(1次)
  • 用Reflect:触发info这个key的get,触发name这个key的get(2次)

所以我们是需要使用Reflect且要传入recevier来绑定get、set的时候this是当前的代理对象

vue3的Reactive,Effect实现

好了,解释完了基本原理,我们来说一下vue3的基本api:reactive,effect,

  • reactive是使得对象具有代理,是可以实现响应式的重要步骤
  • effect主要是收集依赖,追踪数据的变化,通知对象去触发响应

以上两个api是vue3里面最为基础和重要的api,所以他们是一起学习的,我们先通过代码简单看看他们的使用方式:

Reactive

<template>
  <div>
    <span id='test'></span>
  </div> 
</template>
<script setup>
  // 使得obj具有响应式功能
  const obj = reactive({
    name: 'joe',
    family: {
      mother: 'mama',
      father: 'baba',
    }
  })
  // 一开始effect会执行一次
  effect(() => {
    document.querySelector('#test').innerText = obj.name
  }) 
  // 此时obj.name发生了改变,effect监听了它函数里面的内容发生了变动,会再执行一次
  setTimeout(() => {
    obj.name = 'test'
  }, 2000)
</script>

接下来我们就要来实现一下vue3的reactive和effect了,看看它究竟是怎么让对象具有响应式的,实现和真正的源码会有不同(我做了代码耦合和忽略了很多边界情况),但是我为了尽量通过简单的代码去说明,先说reactive

const reactiveMap = new WeakMap()
const baseHandles = {
  get(target, key, receiver) {
    const result = Reflect(target, key, receiver)
    // 收集依赖
    track(target, key)
    // 这里如果get到的结果是对象,那么还要继续递归代理,这样深层次的对象才回具有响应性
    if (typeof result === 'object') {
      return reactive(result)
    }
    return result
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key, value)
    return result
  }
}

// 收集依赖
function track() { }
// 触发依赖
function trigger() { }

/**
 * @Description 获取响应式的代理
 * @author joex
 * @date 2025-03-27 17:37:29
 * @param {Object} target 被代理对象
 * @return {Object} proxy 代理对象
 **/
function reactive(target) {
  const existProxy = reactiveMap.get(target)
  if (existProxy) {
    return existProxy
  } else {
    const newProxy = new Proxy(target, baseHandles)
    reactiveMap.set(target, newProxy)
    return newProxy
  }
}

Effect

上面我们实现了reactive的部分,就是get的时候调用收集依赖,set的时候触发依赖,但是我们看到还没实现完成,track函数和trigger函数还没实现,我们实现effect顺便把track和trigger搞定了

// 这里是所有的变量的依赖存储的map(非常重要)
const targetMap = new WeakMap()
// 这个变量很重要,是用来建立起effect和reactive的联系
let activeEffect;
// 收集依赖
function track(target, key) {
  // 如果没有activeEffect说明当前没有任何东西依赖这个变量的属性,
  // 就不需要收集,就直接结束就行了
  if (!activeEffect) return
  // 看看全局依赖map是已经存在,若有则使用,无则设置一个新的子map
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  // 根据属性名获取子map的值,若有则使用,无则设置set结构
  let dep = depsMap.get(key)
  if (!dep) depsMap.set(key, (dep = new Set()))
  // 然后子map的值:set结构传递下去
  trackEffect(dep)
}
function trackEffect(dep) {
  // 把当前激活的effect存在set结构里面,这样依赖收集就完成了
  dep.add(activeEffect)
}
// 触发依赖
function trigger(target, key, newValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (!dep) return
  triggerEffect(dep)
}
// 触发effects(effect数组)
function triggerEffects(dep) {
  const effects = [...dep]
  for(const effect of effets) {
    triggerEffect(effect)
  }
}
// 触发每个具体的effect
function triggerEffect(dep) {
  effect.run()
}

class ReactiveEffect {
  // 构造设置fn变量,也就是effect传进来的函数
  constructor(fn) {
    this.fn = fn
  }
  // 这里执行函数
  run() {
    // 执行那个effect函数就把当前执行的设置到activeEffect
    activeEffect = this
    return this.fn()
  }
}
/**
 * @Description effect函数
 * @author joex
 * @date 2025-03-27 17:37:29
 * @param {Object} target 被代理对象
 * @return {Object} proxy 代理对象
 **/
function effect(fn) {
  // 会new一个对象,然后执行run方法
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

上面写了挺多代码的,先来解析一下,先说effect,核心就是ReactiveEffect类,这个类需要传入一个函数初始化,也就是:effect(() => { 我就是那个函数 }),这个类很重要的一个步骤就是:

  • 在执行传进来的函数fn的时候会把当前的执行的effect赋值给一个全局变量:activeEffect(代表当前激活的effect)
  • 如果fn里面有使用到响应式的变量,get的开始收集依赖
  • 而此时get怎么知道是哪个effect是在执行呢?就是通过activeEffect,他会把activeEffect收集到哪个target的哪个key里面,然后下次在set的时候就会触发对应的函数

画个示意图看看收集依赖的过程

就像示意图的结构,targetMap是存储所有的依赖的一个变量,存了依赖,当set的时候就根据对应的key找到对应的依赖执行即可,我们看到最后存储activeEffect的是一个set结构,因为我们的当前变量可能被多个effect所依赖,所以用set来存储多个,当触发的时候会遍历set里面的所有函数

vue3的Ref实现

ref是什么,也是一个把数据变成响应式的api,具体是怎么使用我们举个例子:

const name = ref('lili')
const obj = ref({
  name: 'joe'
})
name.value // lili
obj.value.name // joe

我们的ref和reactive还是有些不一样的,ref的变量每次使用的时候要通过.value才能获取它的值,那么这两个玩意有什么区别的:

  • ref需要.value,reactive不需要
  • ref可以包装基本数据类型和对象,reactive只能包装对象
  • ref可以替换整体对象:obj.value = {}还是可以保持响应性,但是如果reactive替换整体对象就不行了

那么我们来实现一下ref,看看为什么需要.value

// ref函数,主要提供一个入口,返回的是ref实例
function ref (value) {
  return new RefImpl(value)
}
// 主要的ref类型
class RefImpl {
  #_value // 私有属性:_value 这个是响应式的值
  #_rawValue // 私有属性: _rawValue(这个是原始值,要用来对比的)
  #dep // 私有属性:存储依赖的数组
  constuctor(value) {
    this.#_rawValue = value // 初始化
    this.#_value = toReactive(value) // 这里调用获取响应式方法
  },
  // .value的时候就收集依赖,和之前的reactive大同小异了,然后设置_value值
  get value() {
    trackRefValue(this)
    return this.#_value
  },
  // 判断如果有变化就触发依赖
  set value(newValue) {
    if (!Objiect.is(newValue, this.#_rawValue)) {
      this.#_rawValue = newValue
      this.#_value = toReactive(value)
      triggerRefValue(this)
    }
    
  }
}
function toReactive (value) {
  // 如果传进来的是对象,我们就调用reactive的方法了,就和之前介绍的reactive一样了
  // 如果不是对象就返回去就好
  return isObject(value) ? reactive(value) : value
}
// 收集依赖具体操作
function trackRefValue (ref) {
  if (activeEffect) {
    let dep = ref.dep || (ref.dep = new Set())
    // 这个逻辑之前写过了就不写了,可以返回去看Effect的实现那个
    trackEffects(dep)
  }
}
// 触发依赖具体操作
function triggerRefValue (ref) {
  if (ref.dep) {
    // 这个逻辑之前写过了就不写了,可以返回去看Effect的实现那个
    triggerEffects(ref.dep)
  }
}

以上就是我们的ref的代码,其实我们可以看到它的做法和reactive基本是一样的,只不过多包装了一层value:

  • get的时候收集依赖,看看activeEffect有没有,有就收集
  • set的时候触发依赖,遍历收集的activeEffect

vue3的computed和watch实现

接下来我们要介绍我们coomputed和watch了,这两个api可谓是我们的开发大帮手,但凡是用过vue开发的都会很熟悉了,但是他们具体是怎么实现的呢,让我们来看一下

computed

我们先说说computed就是我们的计算属性,他是接受一个函数或者对象,我们这里只讨论函数的形式,然后函数必须有返回值,就当成一个变量使用的computed,使用的时候也要.value,让我们看看使用的例子

<div id='app'></div>

const testReactive = { name: 'joe' }

const testComputed = computed(() => {
  return testReactive.name
})

effect(() => {
  // 执行两次,证明是有缓存的
   document.querySelector('#app').innerText = testComputed.name
   document.querySelector('#app').innerText = testComputed.name
})

setTimeOut(() => {
  testReactive.name = 'compunted'
}, 2000)

我们解释一下我们的例子:

  • 创建了一个reactive对象testReactive
  • 创建了一个computed对象,里面返回了reactive对象的值
  • 创建了effect函数,里面实现了app标签设置computed的值显示
  • 设置了定时器,2秒后修改testReactive,会触发computed的值变化,然后触发effect的值变化

所以整个的过程还是挺多步骤的,而且我们看到effect里面执行了两次获取computed,但是其实只会执行一次逻辑,这就是computed的缓存逻辑,如果值没有改变,他只会执行一次。这个computed涉及了之前的一些triggerEffects和triggerEffect的方法的修改,所以我会把之前写过的一些方法也都一并写一次在下面

let activeEffect

function computed (getterOrOptions) {
  let getter
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
  }
  const cRef = new ComputedRefOptions(getter)
  return cRef
}

class ComputedRefOptions {
  dep // dep存依赖的
  _value // 返回值的
  effect // 就是依赖的实例
  // 解释一下这个,就是标记是否需要脏了,如果脏检测为true就需要获取最新数据,否则返回旧的
  _dirty
  construtor(getter) {
    this._dirty = true
    // 构造时候先生成一个effect依赖,传进去computed的函数
    // 生成effect时候第二个参数就是scheduler调度器,后面触发依赖会调用
    this.effect = new ReactiveEffect(getter, () => {
      // 1. 调用scheduler判断是否脏检查为false,是的话就设置设置为true(告诉get需要获取新的)
      // 2. 然后触发依赖
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    // 为了后面区分出这个依赖是不是computed类型的依赖
    this.effect.computed = this
  },
  get value() {
    // 收集依赖
    trackRefValue(this)
    // 判断脏检测
    if (this._dirty) {
      // 执行effect实例的run函数
      this._dirty = false
      this._value = this.effect.run()
    }
    // 返回值
    return this._value
  }
  
}

// 判断是否函数
function isFunction (val) {
  return typeof fn === 'function'
}

class ReactiveEffect {
  // 改变1: 构造函数增加了scheduler的入参,是一个函数
  constructor(fn, scheduler) {
    this.fn = fn
    this.schedule = scheduler
  }
  run() {
    activeEffect = this
    return this.fn()
  }
}

// 收集依赖具体操作
function trackRefValue (ref) {
  if (activeEffect) {
    let dep = ref.dep || (ref.dep = new Set())
    trackEffects(dep)
  }
}
// 触发依赖具体操作
function triggerRefValue (ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

function triggerEffects (dep) {
  const effets = dep ? [...dep] : []
  // 改变2: 原本是一个for循环,改成2个,现在执行所有有computed的依赖,再执行别的
  for (const effect of dep) {
    if (effect.computed) {
      triggerEffect(effect)
    }
  }
  for (const effect of dep) {
    if (!effect.computed) {
      triggerEffect(effect)
    }
  }
}

function triggerEffect (effect) {
  // 改变3: 执行依赖的时候要判断是否有调度器scheduler,有的话直接执行调度器
  if (effect.scheduler) {
    effect.scheduler()
  } else {
    effect.run()
  }
}

可以看到我们的computed的实现主要是通过一个ComputedRefOptions的类来实现功能的,我们根据我们实现的例子来说一下相关的步骤:

  1. effect:run,activeEffect变成effect实例,然后执行effect的fn
  2. computed:testComputed.name触发获取computed的get事件,收集依赖此时computed收集到依赖是effect
  3. computed:dirty开始为true,继续执行get,dirty变成false
  4. computed:get事件调用自己的初始化的内部的effect实例的run方法,执行函数fn,设置activeEffect变成effect变成computed的内部的effect,返回testReactive.name,
  5. reactive:触发testReactive的get事件,收集到的依赖是computed的内部的effect
  6. computed:然后会再次执行步骤2,因为我们写了两个一样的testComputed.name,但此时收集的依赖不同,testComputed.name触发获取computed的get事件,收集依赖此时computed收集到依赖是computed的内部的effect
  7. computed:执行computed的get事件,因为dirty是false,不会收集依赖,直接返回之前记录的值
  8. reactive:等待2秒之后此时会改变testReactive.name,此时会触发它的set事件,触发依赖,也就是会执行computed的内部effect
  9. reactive:执行依赖会判断是否有scheduler,此时是有,那么就会执行scheduler
  10. scheduler:此时dirty为false,所以可执行,就会把dirty改成true,触发computed的依赖,也就是effect实例和computed的内部的effect
  11. computed:那我们执行到遍历依赖的时候我们是先执行computed的内部effect,会走9的路线,判断有scheduler,然后调用,但是此时的dirty是true,所以结束第一个依赖
  12. computed:第二个依赖是effect,执行就会走回到了第1步,然后一直到渲染新的变量就结束了

可以看到我们的步骤非常多,我们待会再画一下图来演示一下这个过程,但是我们这里还要加以解释一下之前代码的三大改变是为了什么:

  1. 增加了scheduler的入参:这是就为了后面调用的没什么好说
  2. 改变for循环:先循环computed的依赖,这样才不会造成死循环,不然会不断的自我调用然后爆栈
  3. 改变执行依赖:就为了调用scheduler

下面再图示一下这个的过程

watch

终于到了watch可比computed要难多了,主要里面涉及到schduler的部分,所以我们这里要分成两个来说:

  • scheduler:这个是实现watch机制重要的一部分会单独拎出来说一下
  • doWatch:这个就是实现watch功能的主要函数

scheduler

先说一下它实现后的效果是怎么样的:

const testReact = reactive({
        count: 1
    })
    effect(() => {
        console.log('count', testReact.count)
    }, { // 触发依赖会执行scheduler
        scheduler() {
            queuePreFlusCb(() => {
                console.log(testReact.count)
            })
        }
    })
    testReact.count = 2
    testReact.count = 3

// 结果
// count 1
// 3
// 3

为什么每次执行都是3呢,因为我们省略了中间的那次计算,两次触发都是最后一次的计算,而这一切的源头就是scheduler的把要执行的代码扔到了微任务队列实现的,那接下来看看它是怎么做到这件事的:

let isFlushing = false // 标记是否正在刷新队列
let peddingPreFlushCbs = [] // 队列

const resovedPromise = Promise.resolve() // 一个promise对象
let currentFlushPromise // 当前刷新的promise

// 刷新队列
export function queuePreFlusCb(cb) {
    queueCb(cb, peddingPreFlushCbs)
}

// 刷新队列
function queueCb(cb, peddingPreFlushCbs) {
    // 刷新队列添加回调函数
    peddingPreFlushCbs.push(cb)
    // 刷新队列
    queueFlush()
}
// 具体刷新队列
function queueFlush() {
    // 如果正在刷新队列,直接结束
    if (!isFlushing) {
        // 正在刷新队列
        isFlushing = true
        // 放进微任务执行刷新队列
        currentFlushPromise = resovedPromise.then(flushJobs)
    }
}
// 刷新队列
function flushJobs() {
    fulshPreFulshCbs()
    isFlushing = false
}
// 刷新队列
export function fulshPreFulshCbs () {
    // 如果有队列
    if (peddingPreFlushCbs.length) {
        // 去重换成数组(这里是伪实现,实际是通过设计id来实现去重的)
        let activePreFlushCbs = Array.from(new Set(peddingPreFlushCbs))
        // 清空队列 
        peddingPreFlushCbs.length = 0
        // 遍历队列
        for (let i = 0; i < activePreFlushCbs.length; i++) {
            // 执行队列
            activePreFlushCbs[i]()
        }
    }
}

我们来说一下schduler做了什么:

  1. 新建了一个任务队列数组,一个promise,一个是否正在执行的标识
  2. 把传入的函数塞到任务队列数组
  3. 判断标识,若没有正在执行则设置成执行然后把flushJobs放进promise的.then事件,这个操作很重要,这个实际就是把整个函数放到了微任务队列,若正在执行则直接结束
  4. 然后等到flushJobs触发的时候宏任务已经执行完成,此时我们cout已经变成3了,我们就会把任务队列去重然后执行

最重要的就是第三步,通过Promise.resove.then(这里面推到微任务)

doWatch

上面为什么先介绍了scheduler,因为watch里面就是使用了这个来实现,这样可以有效的节省性能,如果你连着:count = 1; count = 2; count = 3; count = 4 这几行代码,vue并不希望它不断的触发watch,这样很浪费性能,实际我们就只会被最后一个count = 4触发

接着让我们来看看watch的用法吧:

const testReactive = {
  count: 1
}
watch(testReactve, (val) => {
  console.log(val)
}, {
  
})
testReactive.count = 2

我们可以看到watch就是一个函数,然后传进去一个监听的对象,和一个回调函数,监听的对象如果有变化就会触发回调函数的内容



function watch (target, cb, options) {
  doWatch(target, cb, options)
}

function doWatch (target, cb, { immediate, deep } = options) {
  let getter
  if (isReative(target)) {
    getter = () => target
    deep = true
  } else {
    getter = () => {}
  }
  if (cb && depp) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  let oldValue = {}
  const job = () => {
    if (cb) {
      const newValue = effect.run()
      if (depp || hasChange(newValue, oldValue))
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  let scheduler = () => queuePreFlusCb(job)
  const effect = new ReactiveEffect(getter, scheduler)
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  }
}

function traverse (target) {
  if (!Object.is(target)) {
    return target
  }
  for (const key in value) {
    traverse(target[key])
  }
  return target
}

以上就是watch的实现,这么看起来还是比较简单的,我们来分析一下执行的步骤:

  1. 首先我们把监听的target变成一个函数:getter = () => target
  2. 然后设置深度deep为true
  3. 判断如果有回调函数和深度监听则设置getter成一个会遍历target再返回target的函数(为了后面执行收集依赖)
  4. 设置oldValue是空对象
  5. 定一个job函数,job函数做了几件事
    1. 设置newValue的值为执行的effect.run
    2. 若深度监听或对比新旧有变化则调用watch的cb完成触发回调函数
  1. 定一个scheduler,使用queuePreFlusCb把job参数传入进去(为了提高性能,让watch的触发异步执行)
  2. 定义一个effect = new ReactiveEffect(getter, schduler)
  3. 若有cb且immediate为true,则执行调用job
    1. 调用job则会触发出effect.run就会触发设置依赖:activeEffect = effect
    2. 然后执行了getter函数,因getter的返回值会触发traverse就会触发testReactive对象收集依赖effect
  1. 若immediate不是true,也会触发effect.run(一样触发收集依赖effect),赋值给oldValue
  2. 然后此时如果修改testReactive,就会触发执行依赖
    1. 执行依赖发现依赖有scheduler就会执行scheduler
    2. 然后最后完成watch的cb的调用

看完了都是老套路了,不过都是有些弯弯绕绕,捋清楚就行了,基本都是:

  1. 新建ReactiveEffect
  2. 然后执行run,收集依赖
  3. 然后触发依赖执行触发对应函数完成我们想要的目标