【Vue3源码分析】响应式原理

739 阅读12分钟

首先温馨提示一下:看源码一定要有耐心,心急吃不了热豆腐,需要一个个模块去深挖和理解,正因为它的不简单,除了会知道这么开发的原因,也更能提高自己的竞争力,当然也会更深入熟悉Typescript。

本文仅仅研究了 reactivity,后面模块逐渐会有输出。此模块也有学习顺序,直接输出我的经验,不让大家走弯路:

reactive.ts 顺腾摸瓜 ——》createReactiveObject ——》 new Proxy ——》 createGetter 和 createSetter ——》 track 和 trigger ——》 effect ——》 createReactiveEffect

响应式原理主要包含4部分:

/* 建立响应式数据 */
function reactice(obj){}
 
/* 声明响应函数cb(依赖响应式数据) */
function effect(cb){}
 
/* 依赖收集:建立 数据&cb 映射关系 */
function track(target,key){}

/* 触发更新:根据映射关系,执行cb */
function trigger(target,key){}

总结此文,也结合了别人的一些文章,让我最受启发的是如果看别人源码,不知道函数用法,可以从测试用例去了解,因为测试用例很清楚的知道这个函数要达到什么效果。

此文结构:

  1. 先整理出调用顺序流程图,并大概进行描述;
  2. 接着让大家先了解Proxy拦截方式
  3. 然后分别输出函数源码,并对其分析
  4. 总结学习心得和收获

对于Vue3源码reactivity部分,还有其他篇幅,因为内容太多,分几部分分析:

【Vue3源码分析】computed

【Vue源码分析】Refs

如果能读到这篇blog,非常感谢,如果总结不到位的地方希望指正,如果不介意,也麻烦给个赞给予鼓励。

响应式流程图

描述:

  • 通过 effect 声明依赖响应式数据的函数cb ( 例如视图渲染函数render函数),并执行cb函数,执行过程中,会触发响应式数据 getter

  • 在响应式数据 getter中进行 track依赖收集:建立 数据&cb 的映射关系存储于 targetMap

  • 当变更响应式数据时,触发 trigger,并根据 targetMap 找到关联的cb执行

  • 映射关系 targetMap 结构:

targetMap: WeakMap{ 
 target:Map{ 
  key: Set[cb1,cb2...] 
 }
}

一个target对应多个key

Proxy引入的原因

vue2响应式有多个痛点

  • 递归,消耗大

  • 新增/删除属性,需要额外实现单独的API

  • 数组,需要额外实现

  • Map Set Class等数据类型,无法响应式

  • 修改语法有限制

而使用ES6的 Proxy进行数据响应化,解决上述Vue2所有痛点

Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截

相比 Object.defineProperty ,Proxy支持的对象操作十分全面:get、set、has、deleteProperty、ownKeys、defineProperty等,但是也有一些缺点,对数组操作会执行多次set,举例如下:

const a = new Proxy([1,2], {
  get: function(obj, prop) {
    console.log('get', obj, prop);
    return Reflect.get(obj, prop);
  },
  set: function(obj, prop, value) {
    console.log('set', obj, prop, value);
    return Reflect.set(obj, prop, value);
  },
});
a.push(1);
 
get [1,2] push
get [1,2] length
set [1,2] 2 1
set [1,2, 1] length 3

直接调用push插入一新数据,能明显看到getter和setter被调用2次。

当然Vue3源码中肯定处理了这种情况,提前看下Vue3的trigger方法,看下针对数组做的优化。这个是vue在set完成之后触发的依赖更新。

else if (key === 'length' && isArray(target)) {
  depsMap.forEach((dep, key) => {
   if (key === 'length' || key >= (newValue as number)) {
    add(dep)
   }
  })
} 

由于在一次操作数组的时候会进行多次的set,那么如果每次set都要去更新依赖的话,会造成性能上的浪费,所以在vue3里面只有在set length的时候才会去调用add方法,然后统一执行所有的更新

几个枚举

为了更好看懂源码,先从几个枚类型举开始

github.com/vuejs/vue-n…

// 使用字面值字符串而不是数字,以便更容易检查debugger events
export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}
export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

reactive

  • createReactiveObject

Proxy是对对象的操作,只要访问对象,就会走到Proxy的逻辑中。 Reflect是一个内置的对象,提供拦截js操作的方法。将Object对象一些明显属于语言内容方法(比如Object.defineProperty())放在Reflect对象中。修改某些Object方法的返回结果,让其变得更合理。让Object操作都变成函数行为。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 场景1:非对象直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  // 场景2:target已经被代理过,直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
   // 场景3:target本身是一个Proxy对象,直接返回
  // target already has corresponding Proxy
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
   // 场景4:target不在被观察的白名单中,直接返回
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // baseHandlers用于普通对象;collectionHandlers用于Set/Map/WeakMap/WeakSet

  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 存储原生=>响应式数据映射表,联系场景2,可知目的:防止reactive已经被reactive的值,导致多次Proxy
  proxyMap.set(target, proxy)
  return proxy
}

基本类型代理执行baseHandlers,仅仅留下shallowReactiveHandlers,其他同理,它就是Proxy中要执行的get和set拦截,get中执行track收集依赖,set中执行trigger函数

  • baseHandler.js
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

const arrayInstrumentations: Record<string, Function> = {}
//   仪器识别敏感阵列方法,以解释可能的reactive
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    const arr = toRaw(this)
    // 对数组每个元素执行track,也就是手机依赖
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    //我们首先使用原始args运行method(可能是响应性的)
    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      // 如果不起作用,也就是在数组中找不到args,使用raw值再次运行它。
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})
// instrument length-altering mutation methods是为了避免长度被track,长度被track在某些情况下导致无限循环(#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    //碰到这些函数暂停track,直接返回结果
    pauseTracking()
    const res = method.apply(this, args)
    resetTracking()
    return res
  }
})

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
    //也将返回值转换为proxy。我们在这里做isObject检查以避免无效值警告。这里还需要惰性访问只读和响应,以避免循环依赖。
    // 通过对Reflect.get()的返回结果进行reactive递归调用,达到深度监测
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

//触发get和set,此函数传给Proxy
export const shallowReactiveHandlers: ProxyHandler<object> = extend(
  {},
  mutableHandlers,
  {
    get: shallowGet,
    set: shallowSet
  }
)

track收集依赖

在createGetter中执行track

targetMap 储存了 target --> depsMap ,depsMap存储了 key --> dep,dep是Set集合, dep 中存储 effect

  • 依赖收集器
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
  • 收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 不能重复添加activeEffect
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

trigger响应触发

在createSetter和deleteProperty中执行trigger

trigger 触发器,拿到target key下的对应的所有 effect,然后遍历执行 effect()

// target是响应对象,depsMap(依赖收集器)存储effect,depsMap有键值对关系,一个key对应一个effect,computed中key为'value'

trigger是为了触发收集的effect,其中包含

  1. 先执行add函数先获取Set类型的effects ,
const effects = new Set<ReactiveEffect>()
  1. 再执行run函数调用effect.options.scheduler(effect)或者effect(),最终得到activeEffect
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  // add函数是得到effects,是将当前的依赖项添加进一个等待更新的数组中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)  //activeEffect和effect是通过什么比较的?需要去了解ts恒等
        }
      })
    }
  }
//  需要循环清除,触发所有的effect
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 由于在一次操作数组的时候会进行多次的set,那么如果么此set都要去更新的话,会造成性能上的浪费,所以此处只有在set length的时侯才会去调用add方法,然后统一执行所有的更新

    // target是数组,key更大或者为'length',则执行add
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {

    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // 遍历 ADD | DELETE,区分是否是数组, 如果是Map.SET,仅仅判断isMap
    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

// **这里是定义的run函数,抽离出来,以防止太长,方便查看**

// 循环执行effects,其实就是获得ReactiveEffect,最终会得到数组,看effect函数中effectStack逻辑  effectStack.push(effect)
  effects.forEach(run)
}
  • trigger中的run函数
const run = (effect: ReactiveEffect) => {
  // 在onTrigger传入(effect,target,key,type,newValue, oldValue,oldTarget)参数
  if (__DEV__ && effect.options.onTrigger) {
    effect.options.onTrigger({
      effect,
      target,
      key,
      type,
      newValue,
      oldValue,
      oldTarget
    })
  }
  //  scheduler是调度,其实就是执行effect配置的scheduler函数,看scheduler中配置的函数;否则执行effect()获得effect
  // scheduler是调度再computed中有
  if (effect.options.scheduler) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

effect

调用effect()是为了得到activeEffect,effect中是一对象,包含{id,allowRecurse ,_isEffect,_isEffect, raw, deps, options }

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果fn是ReactiveEffect,fn为传入的函数,比如computed属性中传入的函数
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
 // 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次, 而要实现computed显然是不需要的 
  if (!options.lazy) {
    effect()
  }
  // 返回通过createReactiveEffect()函数返回的一activeEffect对象
  return effect
}

createReactiveEffect在effect中调用

  • createReactiveEffect
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      // 清除该effect下的deps,为了防止fn函数中访问的响应数据属性改动的情况,此时需要重新收集相关属性依赖
      cleanup(effect)
      try {
        enableTracking()
        // 运行前先把effect压入栈
        effectStack.push(effect)
        activeEffect = effect
        // 执行原始函数并返回,因为fn中引入了依赖数据,执行fn触发track依赖收集
        return fn()
      } finally {
        // 运行完再把effect推出栈
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn //把回调fn赋值给.raw属性
  effect.deps = []
  effect.options = options
  return effect
}

手写简版Vue3响应式

reactive

/* 建立响应式数据 */
function reactive(obj){
  // Proxy:http://es6.ruanyifeng.com/#docs/proxy
  // Proxy相当于在对象外层加拦截
  // Proxy递归是惰性的,需要添加递归的逻辑
  
  // Reflect:http://es6.ruanyifeng.com/#docs/reflect
  // Reflect:用于执行对象默认操作,更规范、更友好,可以理解成操作对象的合集
  // Proxy和Object的方法Reflect都有对应
  if(!isObject(obj)) return obj
  const observed = new Proxy(obj,{
    get(target, key, receiver){
      const ret = Reflect.get(target, key, receiver)
      console.log('getter '+ret)
      // 跟踪 收集依赖
      track(target, key)
      return reactive(ret)
    },
    set(target, key, val, receiver){
      const ret = Reflect.set(target, key, val, receiver)
      console.log('setter '+key+':'+val + '=>' + ret)
      // 触发更新
      trigger(target, key)
      return ret
    },
    deleteProperty(target, key){
      const ret = Reflect.deleteProperty(target, key)
      console.log('delete '+key+':'+ret)
      // 触发更新
      trigger(target, key)
      return ret
    },
  })
  return observed
}

effect

/* 声明响应函数cb */
const effectStack = []
function effect(cb){ 
  // 对函数进行高阶封装
  const rxEffect = function(){
    // 1.捕获异常
    // 2.fn出栈入栈
    // 3.执行fn
    try{
      effectStack.push(rxEffect)
      return cb()
    }finally{
      effectStack.pop()
    }
  }
 
  // 最初要执行一次,进行最初的依赖收集
  rxEffect()
  return rxEffect
}

track

/* 依赖收集:建立 数据&cb 映射关系 */
const targetMap = new WeakMap()
function track(target,key){
  // 存入映射关系
  const effectFn = effectStack[effectStack.length - 1]  // 拿出栈顶函数
  if(effectFn){
    let depsMap = targetMap.get(target)
    if(!depsMap){
      depsMap = new Map()
      targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if(!deps){
      deps = new Set()
      depsMap.set(key, deps)
    }
    deps.add(effectFn)
  }
}

trigger

/* 触发更新:根据映射关系,执行cb */
function trigger(target, key){
  const depsMap = targetMap.get(target)
  if(depsMap){
    const deps = depsMap.get(key)
    if(deps){
      deps.forEach(effect=>effect())
    }
  }
}

应用

<div id="app">
 {{msg}}
</div>
 
<script src="./mini-vue3.js"></script>
 
<script>
  // 定义一个响应式数据
  const state = reactive({
    msg:'message'
  })
 
  // 定义一个使用到响应式数据的 dom更新函数
 function updateDom(){
  document.getElementById('app').innerText = state.msg
 }
 // 用effect声明更新函数
  effect(updateDom)
 
  // 定时变更响应式数据
  setInterval(()=>{
    state.msg = 'message' + Math.random()
  },1000)
</script>

总结

  1. 学习顺序

看别人阅读顺序建议:

  • 先读 reactivity,能最快了解 Vue3 的新特性,何况基于了解到vue3和vue2的最大区别就是拦截方式改变,毋庸置疑从此处开始学习
  • 再读 rumtime,理解组件和生命周期的实现;
  • 再读 compiler,理解编译优化过程

其实总结过程中,让我最受启发的是如果看源码过程中,不知道函数用法,可以从测试用例去了解,因为测试用例很清楚的知道这个函数要达到什么效果。

vue3可以直接看所有 tests 目录里的测试用例来了解其所有功能,目前有不到 700 个测试用例。

  1. 响应式原理几个要点
  • track 追踪器,在 get 时调用该函数,将所有 get 的 target 跟 key 以及 effect 建立起对应关系

  • trigger 触发器:拿到target key下的对应的所有 effect,然后遍历执行 effect()

  • effect是vue3响应式的核心,effect是副作用的意思,也就是说它是响应式的副产品

看源码知道effect在mountComponent、computed、reactive、doWatch中调用作用,(此问仅仅写了reactive,computed看另一篇)由响应式到最终调用,最终发现effect才是vue3响应式的核心

每次触发了get时收集effect,每次set时在触发这些effecs。这样就可以做有些响应式数据之外的一些事情了,比如计算属性computed

effect实现逻辑?

  1. 首先 如果 effect 回调内有已响应的对象被触发了 get 时,effect就应该被储存起来

  2. 然后,我们需要一个储存effect的地方,在effect函数调用的时候就应该把effect放进这个储存空间,在vue中使用的是一个数组activeReactiveEffectStack = []

  3. 再后,每个target被触发的时候,都可能有多个effect,所以每个target需要有一个对应的依赖收集器 deps,等到 set 时遍历 deps 执行 effect()

  4. 然而,这个依赖收集器 deps 不能放在 target 本身上,这样会使数据看起来不是很简洁,还会存在多余无用的数据,所以我们需要一个 map 集合来储存 target 跟 deps 的关系, 在vue中这个储存集合叫 targetMap 。

  5. 主要函数调用顺序:

  1. 开发者可以调用的函数

reactive ——》createReactiveObject

shallowReactive ——》createReactiveObject

readonly ——》createReactiveObject

shallowReadonly ——》createReactiveObject

  1. 原理

createReactiveObject ——》new Proxy() ——》get或者set

普通类型和引用类型分别怎么监听的?

  1. get或者set调用的函数

get——》track——》收集effect

set——》trigger——》触发收集的effect

effect——》createReactiveEffect ——》 cleanup(effect) ——》 effectStack.push(effect) ——》fn() ——》 effectStack.pop() ——》 activeEffect = effectStack[effectStack.length - 1]

  1. 收集依赖的映射关系(看track函数能迅速了解):

targetMap 储存了 target --> depsMap ,depsMap存储了 key --> dep,dep是Set集合, dep 中存储 effect

target是响应对象,depsMap(依赖收集器)存储effect,depsMap有键值对关系,一个key对应effect集合,特别说明,computed中key为'value'

参考链接: blog.csdn.net/Crazymryan/…