vue源码学习响应系统

105 阅读6分钟

对于vue3而言,核心分为响应性,运行时,编译器三大块。运行时可以利用render将虚拟dom转化为真实dom。编译器是把html节点编译成render函数

响应性

vue2响应性

vue2使用的是Object.defineProperty来听对象指定属性的getter和setter行为,来实现响应性,但有一种致命缺陷:由于javascript限制,vue不能检测到数组和对象的变化。

1.当对象新增一个没有在data中声明的属性时,新增的属性没有响应性。(可以通过Vue.set设置新属性的响应性)

2.当数组通过下标的形式新增元素时,新增的元素不是响应性(可以通过vue.set,也可以使用vue2内部重写的7个数组api:push(),pop(),shift(),unshift(),splice(),sort(),reverse())

vue3响应性

vue3使用Proxy实现响应性,Proxy可以代理整个对象,监听所有属性变化,包括数组和动态添加的属性,proxy与reflect一般是一起配合使用

<script>
  const p1 = {
    lastName: '张',
    firstName: '三',
    // 通过 get 标识符标记,可以让方法的调用像属性的调用一样
    get fullName() {
      return this.lastName + this.firstName
    }
  }

  const proxy = new Proxy(p1, {
    // target:被代理对象
    // receiver:代理对象
    get(target, key, receiver) {
      console.log('触发了 getter');
      return target[key]
    }
  })

  console.log(proxy.fullName);
</script>

此时proxy.fullName只打印一次,因为通过target[key]触发的fullName函数中this是p1中的this,需要将其修改为proxy代理对象的this,故需要改为

const proxy = new Proxy(p1, {
    // target:被代理对象
    // receiver:代理对象
    get(target, key, receiver) {
      console.log('触发了 getter');
+      // return target[key]
+      return Reflect.get(target, key, receiver)
    }
  })

reactivity模块

使用weakMap缓存代理对象:weakMap与map的不同在于,key必须是对象且key是弱引用的(WeakMap中的key不存在任何引用时,会被直接回收)。

reactive主要是通过proxy实现响应性,核心是监听复杂数据类型的getter和setter行为,监听到getter时收集当前依赖(effect),触发setter时触发收集到的依赖,一次完成响应性。

收集依赖的WeakMap实例:

1.key:响应性对象 2.value:map对象 2.1 key:响应性对象的指定属性 2.2 map:指定属性的fn函数的set数组

ref模块

ref接收复杂数据类型时,通过toReactive方法,把复杂数据类型交给reactive处理。监听简单数据类型是监听value属性,触发get value()时收集依赖存储到RefImpl中的dep中,触发set value()时触发依赖

computed模块

核心代码

import { isFunction } from '@vue/shared'
import { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { trackRefValue, triggerRefValue } from './ref'

/**
 * 计算属性类
 */
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true

  public _dirty = true
  constructor(getter: () => T) {
    this.effect = new ReactiveEffect(getter, () => {
      /**
       * 脏:为false时,表示需要触发依赖,为true时表示需要重新执行run方法,获取数据。也就是数据脏了
       */
      if (!this._dirty) {
        this._dirty = true
        //触发依赖,触发的是effect函数的依赖
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
  }
  get value() {
    //收集依赖,收集的是effect的依赖
    trackRefValue(this)
    //判断当前脏状态,如果为true,则需要重新执行run,获取最新数据
    if (this._dirty) {
      this._dirty = false
      //执行run函数,此时执行的是run函数
      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 as any
}

测试代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
const { reactive, computed, effect } = Vue

const obj = reactive({
  name: '张三'
})

const computedObj = computed(() => {

  return '姓名:' + obj.name
})

effect(() => {
  document.querySelector('#app').innerHTML = computedObj.value
})

setTimeout(() => {
  //此时触发依赖
  obj.name = '李四'
}, 2000);
  </script>
</html>

运行第一个effect时会触发计算属性对get value()的监听,收集测试实例中effect的,并判断是否为脏状态,如果是脏状态则要修改脏状态并运行this.effect.run()获取最新数据(通过脏状态设置缓存,当连续设置值时,因为没有通过依赖项的值改变触发依赖,就不会触发调度器函数,进而不会重新触发计算函数),在运行run方法时修改activeEffect的值,将其指向ComputedRefImpl中的effect。然后触发computed内部的函数,此时obj.name时会触发依赖收集,收集的activeEffect是指向ComputedRefImpl中的effect。当依赖项被触发,此时触发的是reactive的set value,由于调度器的存在会先触发调度器函数(也就是ComputedRefImpl中的effect函数中第二个参数),此时如果不为脏状态就更改为脏状态并触发测试实例中effect依赖。

watch模块

//apiwatch.ts
import { effect } from '@vue/reactivity'
import { EMPTY_OBJ, hasChanged, isObject } from '@vue/shared'
import { queuePreFlushCb } from './scheduler'
import { ReactiveEffect } from 'packages/reactivity/src/effect'
import { isReactive } from 'packages/reactivity/src/reactive'

/**
 * watch配置项属性
 */
export interface WatchOption<Immediate = boolean> {
  immediate?: Immediate
  deep?: boolean
}
/**
 * 指定的watch函数
 * @param source监听的响应性数据
 * @param cb 回调函数
 * @param options 配置对象
 * @returns
 */
export function watch(source, cb: Function, options?: WatchOption) {
  return doWatch(source as any, cb, options)
}
function doWatch(
  source,
  cb: Function,
  { immediate, deep }: WatchOption = EMPTY_OBJ
) {
  //触发getter的指定函数
  let getter: () => any
  //判断source的数据类型
  if (isReactive(source)) {
    getter = () => source
    //深度
    deep = true
  } else {
    getter = () => {}
  }
  //存在回调函数和deep属性
  if(cb&&deep){
    const baseGetter = getter
    getter=()=>traverse(baseGetter())
  }
  //旧值
  let oldValue={}
  //job执行方法
  const job=()=>{
    if(cb){
      const newValue=effect.run()
      if(deep||hasChanged(newValue,oldValue)){
        cb(newValue,oldValue)
        oldValue=newValue

    }
  }
}
//调度器
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()
  }
}
/**
 * 
 * 一次执行getter,从而触发收集依赖
 */
export function traverse(value:unknown){
  if(!isObject(value)){
    return value
  }
  for(const key in value as object){
    traverse((value as any)[key])
  }
  return value
}





//scheduler.ts
//对应promise的pending状态
let isFlushPending = false
/**
 * promise.resolve
 */
const resolvedPromise = Promise.resolve() as Promise<any>
/**
 * 当前的执行任务
 */
let currentFlushPromise: Promise<void> | null = null
/**
 * 待执行的任务队列
 */
const pendingPreFlushCbs: Function[] = []
/**
 * 队列预处理函数
 */
export function queuePreFlushCb(cb: Function) {
  queueCb(cb, pendingPreFlushCbs)
}
/**
 * 队列预处理函数
 */
function queueCb(cb: Function, pendingQueue: Function[]) {
  pendingQueue.push(cb)
  queueFlush()
}
/**
 * 依次执行队列中的执行函数
 */
function queueFlush() {
  if (!isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
/**
 * 处理队列
 */
function flushJobs() {
  isFlushPending = false
  flushPreFlushCbs()
}
/**
 * 一次处理队列中的任务
 */
export function flushPreFlushCbs() {
  if (pendingPreFlushCbs.length) {
    console.log('pendingPreFlushCbs: ', pendingPreFlushCbs);
    let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    console.log('activePreFlushCbs: ', activePreFlushCbs);
    pendingPreFlushCbs.length = 0
    for (let i = 0; i < activePreFlushCbs.length; i++) {
      activePreFlushCbs[i]()
    }
  }
}

//watch.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    const { reactive, watch } = Vue

    const obj = reactive({
      name: '张三'
    })

    watch(obj, (value, oldValue) => {
      console.log('watch 监听被触发')
      console.log('value', value)
    })
    //依赖触发时是同一个job,后面new set时会合并相同的,所以只打印王五

    setTimeout(() => {
      obj.name = '李四'
      obj.name = '王五'
    }, 2000)
  </script>
</html>

watch的实现本质还是收集依赖和触发依赖,在doWatch方法里面会生成一个带有调度器的ReactiveEffect函数,而 getter 行为的触发是依赖于内部的 traverse 方法进行的。traverse 方法就是依次遍历数据,分别触发 getter 行为。此时activityEffect指向的是带有调度器的ReactiveEffect,z settimeout中触发依赖,此时因为有调度器,会执行调度器内部的job函数,通过effect.run()获取新值,当新值发生改变时触发watch里面的回调以此实现监听效果。

调度器的实现: scheduler=()=>queuePreFlushCb(job),每次触发schedule并不会直接执行job方法,而是被push到set集合中,promise.then之后再一起执行。从而避免了每次已修改依赖项,就触发一次watch函数的情况

immediate:immediate的实现是当这个参数存在时,直接执行job函数,通过effect.run()获取新值,当新值发生改变时触发watch里面的回调以此实现监听效果。