Vue源码实现 computed&watch~

53 阅读7分钟

响应系统 — computed&&watch

computed源码实现:

创建如下测试实例,逐步执行代码进行实现:

<body>
    <div id="app"></div>
    <script>
      const { reactive, effect, computed } = Vue
​
      const obj = reactive({
        name: '张三'
      })
      const computedObj = computed(() => {
        return '姓名:' + obj.name
      })
​
      effect(() => {
        document.querySelector('#app').innerText = computedObj.value
      })
​
      setTimeout(() => {
        obj.name = '李四'
      }, 2000)
    </script>
  </body>
  1. 首先执行reactive函数生成响应式数据,逻辑与之前相同;

  2. 接着执行computed,进入computed函数,通过iSFunction函数判断传入的参数是否为函数,如果是函数则重新赋值给getter变量,执行new ComputedRefImpl得到实例并返回;

  3. 该类中通过之前的reactiveEffect类传入调用computed的匿名函数以及一个调度器函数,并赋值给ComputedRefImpl实例上的effect变量。

代码实现:

export class ComputedRefImpl {
  public dep?: Dep //存放依赖set对象
  public readonly effect  
  public _value
  public __v_isRef = true //标记是否为ref数据
  public _dirty = true //通过判断脏状态来确定是否需要触发依赖
  constructor(getter) {
    //通过reactiveEffect创建实例  该实例的run方法会执行fn函数,也即getter函数,也即调用computed传入的匿名函数,第二个为调度器函数  后续执行到相应的代码再做解释
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  //为effect增加computed属性为当前实例,以便后期触发区分 
    this.effect.computed = this
  }
}
​
​
export function computed(getterOrOptions) {
  let getter
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
  }
  const cRef = new ComputedRefImpl(getter)
  return cRef
​
}
//isFunction方法
export const isFunction = (val: unknown) => {
  return typeof val === "function"
}

代码向下执行,调用effect函数,为activeEffect赋值,调用传入的匿名函数。

export class ReactiveEffect<T = any> {
  constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {
  }
  run() {
    activeEffect = this
    return this.fn()
  }
  stop() { }
}
// effect
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
  const _effect = new ReactiveEffect(fn)
  if (!options) {
    _effect.run()
  }
}
  1. 执行document.querySelector('#app').innerText = computedObj.value

  2. 会触发ComputedRefImpl实例的get value,通过trackRefValue方法收集依赖,与之前逻辑相同;

  3. 然后通过判断脏状态来执行run方法,也即执行() => { return '姓名:' + obj.name},执行这个会调用reactive函数的getter函数收集依赖;

  4. dep中建立起指定属性名与fn函数,即() => {document.querySelector('#app').innerText = computedObj.value }的关系

代码实现:

export class ComputedRefImpl {
  public dep?: Dep
  public readonly effect
  public _value
  public __v_isRef = true
  public _dirty = true
  constructor(getter) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
  }
  get value() {
   //收集依赖
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
        //执行当前传入的computed的函数
      this._value = this.effect.run()
    }
    return this._value
  }
}
​
​
export function computed(getterOrOptions) {
  let getter
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
  }
  const cRef = new ComputedRefImpl(getter)
  return cRef
​
}
  • 接着两秒之后执行obj.name = '李四',触发reactive函数的setter函数;

  • 此时要触发依赖,与之前不同的是,此时要触发的是computed的依赖,之前的this.effect.computed = this这行代码就起到作用了,触发依赖时,首先会判断当前依赖中是否有conputed属性,需要先触发有computed属性的依赖;

  • 触发依赖又会判断当前是否存在调度器,如果有调度器则直接执行调度器;

  • 也即再次执行document.querySelector('#app').innerText = computedObj.value

  • 调用ComputedImpl实例的get value方法返回最新的值,视图发生改变,结束执行;

  • 调度器方法,即之前创建ComputedImpl实例传入的第二个方法:

() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
}
//其余代码与之前相同  触发依赖时需要进行判断
export const triggerEffects = (dep: Dep) => {
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
 triggerEffect(effect)
}
}
for (const effect of effects) {
if (!effect.computed) {
 triggerEffect(effect)
}
}
}
export const triggerEffect = (effect: ReactiveEffect) => {
if (effect.scheduler) {
//执行computed传入的函数
effect.scheduler()
} else {
effect.run()
}
}
✓处理computed的缓存性:

computed函数依赖的响应性数据没有发生变化时,他不会重新调用;

创建如下实例:

<script>
  const { reactive, computed, effect } = Vue
​
  const obj = reactive({
    name: '张三'
  })
​
  const computedObj = computed(() => {
    console.log('计算属性执行计算');
    return '姓名:' + obj.name
  })
​
  effect(() => {
    document.querySelector('#app').innerHTML = computedObj.value
    document.querySelector('#app').innerHTML = computedObj.value
  })
​
  setTimeout(() => {
    obj.name = '李四'
  }, 2000);
</script>
  • 当调用effect函数,第一次执行document.querySelector('#app').innerHTML = computedObj.value

  • 触发ComputedImpl实例的get value收集依赖,当前脏状态为真,将脏状态改为false;

  • 执行run方法,也即执行computed传入的匿名函数;

  • 第二次执行document.querySelector('#app').innerHTML = computedObj.value,因为脏状态为false,所以不会执行匿名函数直接返回之前的值。

() => {
// 判断当前脏的状态,如果为 false,表示需要《触发依赖》
   if (!this._dirty) {
    // 将脏置为 true,表示
    this._dirty = true
    triggerRefValue(this)
  }
})
watch源码实现:

创建如下测试实例:

 <script>
      const { ref, watch, reactive } = Vue
      const obj = reactive({
        name: '张三'
      })
      watch(
        obj,
        (value, oldValue) => {
          console.log('watch 监听被触发')
          console.log('value', value)
        },
        {
          immediate: true
        }
      )
      setTimeout(() => {
        obj.name = '李四'
      }, 2000)
    </script>

这里我们跳过上篇文章的reactive的执行,直接开始执行watch

  1. 执行watch方法,进入Watch方法,这个方法返回doWatch方法的调用,主要逻辑都在这个方法当中。

export function watch(source, cb: Function, options?: WatchOptions) {
return doWatch(source, cb, options)
}
function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) {
}
  1. 首先声明getter变量,判断传入的source是否为reactive响应式对象,source即调用watch传入的监听的对象,如果是reactive响应式对象,就为getter赋值为一个函数返回值是这个source,并且默认开始深度监听

function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) {
let getter: () => any
if (isReactive(source)) {
 getter = () => source
 deep = true
} else {    
 getter = () => { }
}
}
  1. 接下来会判断deep和调用watch是否传入了回调函数,会重新声明baseGetter变量保存getter的变量,getter会被重新赋值为一个函数返回值是traverse方法,这个方法会处理baseGetter函数调用的返回值;

    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) {
      let getter: () => any
      if (isReactive(source)) {
        //source即传入的响应式对象
        getter = () => source
        deep = true
      } else {
        getter = () => { }
      }
      if (cb && deep) {
          //保存getter值
        let baseGetter = getter
        getter = () => traverse(baseGetter())
      }
    }
    
    //traverse方法  为了响应式对象的原始值  也即”张三“  为了给watch回调函数中传入新值和旧值得参数
    export function traverse(value) {
      if (!isObject(value)) {
        return value
      }
      for (const key in value) {
        traverse(value[key])
      }
      return value
    }
    
  2. 接下来会定义一个job方法,这个方法主要是获取新值,调用传入watch的回调函数,并处理新旧值,也即watch的监听触发也是job的触发;

    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) {
    //..............省略上述代码
      let oldValue = {}
      const job = () => {
        if (cb) {
          const newValue = effect.run()
          //haschanged   判断新值和旧值是否相同
          if (deep || hasChanged(newValue, oldValue)) {
              //调用回调函数
            cb(newValue, oldValue)
              //新值给旧值处理
            oldValue = newValue
          }
        }
      }
    }
    
  3. 接下来会声明一个调度器let scheduler = () => queuePreFlushCb(job)queuePreFlushCb这个方法的主要目的是将job方法推入到微任务当中,待同步代码执行完毕之后,然后才回去调用job方法;

    let scheduler = () => queuePreFlushCb(job)
    
    //queuePreFlushCb 逻辑
    //标记状态 逐步执行
    let isFlushPending = false
    //promise.resolve
    let resolvePromise = Promise.resolve() as Promise<any>
    ​
    let currentFlushPromise: Promise<void> | null = null
    //任务数组
    let pendingPreFlushCbs: Function[] = []
    //cb 即job方法
    export function queuePreFlushCb(cb: Function) {
      queueCb(cb, pendingPreFlushCbs)
    }
    ​
    // 将所有需要处理的任务推到任务数组
    function queueCb(cb: Function, pendingQueue: Function[]) {
      pendingQueue.push(cb)
      queueFlush()
    }
    ​
    // 依次处理队列中的函数 放入微任务
    function queueFlush() {
      if (!isFlushPending) {
        isFlushPending = true
      //将flushjobs方法得执行放入微任务,当所有同步代码执行完毕之后才会执行这个方法
        currentFlushPromise = resolvePromise.then(flushjobs)
      }
    }
    ​
    // 处理队列
    function flushjobs() {
      isFlushPending = false
      flushPreFlushCbs()
    }
    // 依次执行微任务  循环遍历任务数组 调用函数  也即调用job函数 也即触发watch监听
    function flushPreFlushCbs() {
      if (pendingPreFlushCbs.length > 0) {
        //相当于 let activeFlushCbs = pendingPreFlushCbs
        let activeFlushCbs = [...new Set(pendingPreFlushCbs)]
        pendingPreFlushCbs.length = 0
        for (let i = 0; i < activeFlushCbs.length; i++) {
          activeFlushCbs[i]()
        }
      }
    }
    

    6.接下来会new一个reactiveEffect实例,处理依赖关系。

    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) {
    //............  
    /**
     * @param getter getter = () => traverse(baseGetter()) 调用这个也即返回原始值张三
     * @param scheduler scheduler = () => queuePreFlushCb(job)  调用这个也即将job放入任务队列
     */
     const effect = new ReactiveEffect(getter, scheduler)
      if (cb) {
        if (immediate) {
         //有immediate配置 立即执行job方法
          job()
        } else {
         //调用run方法即调用getter函数,上篇文章中有reactiveEffect逻辑 可以查看
         //拿到旧值
          oldValue = effect.run()
        }
      } else {
        effect.run()
      }
      return () => {
        effect.stop()
      }
    }
    

watch方法执行完毕,两秒之后执行obj.name = '李四',也就是会触发reactive中的setter方法,他会进行依赖的触发,也即触发triggerEffect,这个方法会判断当前的effect中是否存在scheduler,存在则直接调用,这个方法的调用即将job方法放入到微任务当中,等待后续执行;

两秒完毕,reactive响应式对象obj中的name被改为李四,接下来会执行微任务数组,也就是会执行job方法;

//也即如下代码
// 处理队列
function flushjobs() {
  isFlushPending = false
  flushPreFlushCbs()
}
// 依次执行微任务
function flushPreFlushCbs() {
  if (pendingPreFlushCbs.length > 0) {
    let activeFlushCbs = [...new Set(pendingPreFlushCbs)]
    pendingPreFlushCbs.length = 0
    for (let i = 0; i < activeFlushCbs.length; i++) {
      activeFlushCbs[i]()
    }
  }
}

job方法执行,意外着watch执行监听,触发回调函数,获取到新值,并进行新旧值处理,watch处理完毕。

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