vue3源码之响应式原理

reactive的函数实现如下:

function reactive (target) {

   // 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy

  if (target && target.__v_isReadonly) {

     return target

  } 

  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)

}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {

  if (!isObject(target)) {

    // 目标必须是对象或数组类型

    if ((process.env.NODE_ENV !== 'production')) {

      console.warn(`value cannot be made reactive: ${String(target)}`)

    }

    return target

  }

  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {

    // target 已经是 Proxy 对象,直接返回

    // 有个例外,如果是 readonly 作用于一个响应式对象,则继续

    return target

  }

  if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {

    // target 已经有对应的 Proxy 了

    return isReadonly ? target.__v_readonly : target.__v_reactive

  }

  // 只有在白名单里的数据类型才能变成响应式

  if (!canObserve(target)) {

    return target

  }

  // 利用 Proxy 创建响应式

  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)

  // 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了

  def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)

  return observed

}

可以看到, reactive内部通过createReactiveObject函数吧target变成了一个响应式对象.

在这个过程中, createReactiveObject函数主要做了一下几件事:

  1. 函数首先判断target是不是数组或者对象类型, 如果不是则直接返回. 所以原始属于target必须是对象或者数组
  2. 如果对一个已经响应式的对象再次执行reactive, 还是返回这个响应式对象
  3. 如果对同一个原始数据多次执行reactive, 那么会返回相同的响应式对象.
  4. 使用canObserve(白名单)target对象做进一步限制
const canObserve = (value) => {
  return (!value.__v_skip &&
   isObservableType(toRawType(value)) &&
   !Object.isFrozen(value))
}
const isObservableType = /*#__PURE__*/ makeMap('Object,Array,Map,Set,WeakMap,WeakSet')

 比如, 带有__v_skip属性的对象, 被冻结的对象, 以及不再白名单内的对象如Date类型的对象实例是不能变成响应式的.

5.通过Proxy Api劫持target对象, 把它编程响应式, 我们把Proxy函数返回的结果称作响应式对象, 这里Proxy对应的处理器对象会根据数据类型的不同而不同. 我们稍后会重点分析基本数据类型的Proxy处理器对象, reactive函数传入的baseHandlers值是mutableHandlers.

6.给原始数据打个标识, 作为对同一个原始数据多次执行reactive的判断依据

 

 mutableHandlers :

const mutableHandlers = {

  get,

  set,

  deleteProperty,

  has,

  ownKeys

}

它其实就是劫持了我们对 observed 对象的一些操作,比如:

  • 访问对象属性会触发 get 函数;

  • 设置对象属性会触发 set 函数;

  • 删除对象属性会触发 deleteProperty 函数;

  • in 操作符会触发 has 函数;

  • 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。

因为无论命中哪个处理器函数,它都会做依赖收集和派发通知这两件事其中的一个,所以这里我只要分析常用的 get 和 set 函数就可以了。

 

依赖收集: get函数

依赖收集发生在数据访问的阶段, 使用Proxy API劫持数据对象, 当这个响应式对象属性被访问的时候就会执行get函数.

const get = /*#__PURE__*/ createGetter()

// ...

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* isReactive */) {
      // 代理 observed.__v_isReactive
      return !isReadonly
    }
    else if (key === "__v_isReadonly" /* isReadonly */) {
      // 代理 observed.__v_isReadonly
      return isReadonly;
    }
    else if (key === "__v_raw" /* raw */) {
      // 代理 observed.__v_raw
      return target
    }
    const targetIsArray = isArray(target)
    // arrayInstrumentations 包含对数组一些方法修改的函数
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 求值
    const res = Reflect.get(target, key, receiver)
    // 内置 Symbol key 不需要依赖收集
    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
      return res
    }
    // 依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    return isObject(res)
      ? isReadonly
        ?
        readonly(res)
        // 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
        : reactive(res)
      : res
  }
}

get函数主要做了四件事情:

  1. 对特殊的key进行代理
  2. 通过Reflect.get方法求值, 如果target是数组, 且key命中了arrayInstrumentations, 则执行对应的函数. 大致实现如下:
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function (...args) {
    // toRaw 可以把响应式对象转成原始数据
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      // 依赖收集
      track(arr, "get" /* GET */, i + '')
    }
    // 先尝试用参数本身,可能是响应式数据
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // 如果失败,再尝试把参数转成原始数据
      return arr[key](...args.map(toRaw))
    }
    else {
      return res
    }
  }
})

target是一个数组的时候, 我们在访问'includes', 'indexOf', 'lastIndexOf'方法的时候就会执行代理函数.

  1. 通过Reflect.get求值, 然后会执行track函数收集依赖.
  2. 通过对计算的值res进行判断, 如果它也是数组或对象, 则递归执行reactiveres变成响应式对象, 则递归执行reactiveres变成响应式对象.

#Track

// 是否应该收集依赖

let shouldTrack = true

// 当前激活的 effect

let activeEffect

// 原始数据对象 map

const targetMap = new WeakMap()

function track(target, type, key) {

  if (!shouldTrack || activeEffect === undefined) {

    return

  }

  let depsMap = targetMap.get(target)

  if (!depsMap) {

    // 每个 target 对应一个 depsMap

    targetMap.set(target, (depsMap = new Map()))

  }

  let dep = depsMap.get(key)

  if (!dep) {

    // 每个 key 对应一个 dep 集合

    depsMap.set(key, (dep = new Set()))

  }

  if (!dep.has(activeEffect)) {

    // 收集当前激活的 effect 作为依赖

    dep.add(activeEffect)

   // 当前激活的 effect 收集 dep 集合作为依赖

    activeEffect.deps.push(dep)

  }

}

收集依赖就是数据变化后执行的副作用函数.

派发通知: set函数

派发通知发生在数据更新的阶段, 响应式对象在数据更新的时候就会执行set函数:

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

function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    value = toRaw(value)
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add" /* ADD */, key, value)
      }
      else if (hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue)
      }
    }
    return result
  }
}

set函数的实现逻辑很简单:

  1. 通过Reflect.set求值
  2. 通过trigger函数派发通知

并依据key是否存在target上来确定通知类型: 新增/修改

核心部分就是通过trigger函数派发通知. trigger函数的实现如下:

// 原始数据对象 map
const targetMap = new WeakMap()
function trigger(target, type, key, newValue) {
  // 通过 targetMap 拿到 target 对应的依赖集合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 没有依赖,直接返回
    return
  }
  // 创建运行的 effects 集合
  const effects = new Set()
  // 添加 effects 的函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // SET | ADD | DELETE 操作之一,添加对应的 effects
  if (key !== void 0) {
    add(depsMap.get(key))
  }
  const run = (effect) => {
    // 调度执行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    else {
      // 直接运行
      effect()
    } 
  }
  // 遍历执行 effects
  effects.forEach(run)
}

trigger函数就是根据targetkeytargetMap中找到相关的所有副作用函数执行一遍.

主要是这么几个事情:

  1. 通过 targetMap 拿到 target 对应的依赖集合 depsMap;
  2. 创建运行的 effects 集合;
  3. 根据 key 从 depsMap 中找到对应的 effects 添加到 effects 集合;
  4. 遍历 effects 执行相关的副作用函数。

 

副作用函数

vue内部存在一个effect副作用函数栈:

// 全局 effect 栈
const effectStack = []
// 当前激活的 effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // 如果 fn 已经是一个 effect 函数了,则指向原始函数
    fn = fn.raw
  }
  // 创建一个 wrapper,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(...args) {
    if (!effect.active) {
      // 非激活状态,则判断如果非调度执行,则直接执行原始函数。
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      // 清空 effect 引用的依赖
      cleanup(effect)
      try {
        // 开启全局 shouldTrack,允许依赖收集
        enableTracking()
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        // 执行原始函数
        return fn(...args)
      }
      finally {
        // 出栈
        effectStack.pop()
        // 恢复 shouldTrack 开启之前的状态
        resetTracking()
        // 指向栈最后一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // 标识是一个 effect 函数
  effect._isEffect = true
  // effect 自身的状态
  effect.active = true
  // 包装的原始函数
  effect.raw = fn
  // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
  effect.deps = []
  // effect 的相关配置
  effect.options = options
  return effect
}

因此针对嵌套 effect 的场景,我们不能简单地赋值 activeEffect,应该考虑到函数的执行本身就是一种入栈出栈操作,因此我们也可以设计一个 effectStack,这样每次进入 reactiveEffect 函数就先把它入栈,然后 activeEffect 指向这个 reactiveEffect 函数,接着在 fn 执行完毕后出栈,再把 activeEffect 指向 effectStack 最后一个元素,也就是外层 effect 函数对应的 reactiveEffect。

 

effect内部通过执行creteReactiveEffect函数去创建一个新的efect函数. 后续称为reactiveEffect函数. effect函数还支持传入一个配置参数来支持更多的feature.

reactiveEffect函数就是响应式的副作用函数, 当执行trigger过程派发通知的时候, 执行的effect就是它

reactiveEffect函数做这么几件事:

  1. 判断effect是否是active, 这里控制的是允许非active状态且非调度执行状态直接执行原始函数fn并返回
  2. 判断effectStack中是否包含effect, 如果没有就把它推入栈中
  3. 把全局的activeEffect指向它
  4. 执行被包装的原始函数fn

#只读对象

readonly对象可以劫持一整个对象阻止属性的修改

export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
  if (!isObject(target)) {
    // 目标必须是对象或数组类型
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    // target 已经是 Proxy 对象,直接返回
    // 有个例外,如果是 readonly 作用于一个响应式对象,则继续
    return target
  }
  if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
    // target 已经有对应的 Proxy 了
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }
  // 只有在白名单里的数据类型才能变成响应式
  if (!canObserve(target)) {
    return target
  }
  // 利用 Proxy 创建响应式
  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
  // 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
  def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
  return observed
}

 

readonlyreactive对象的主要区别在于传入参数的不同.

readonly传入的isReadonly为true, 并且baseHandlers传入的是readonlyHandlers, 该handle实现如下:

const readonlyHandlers = {
  get: readonlyGet,
  has,
  ownKeys,
  set(target, key) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target)
    }
    return true
  },
  deleteProperty(target, key) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target)
    }
    return true
  }
}

 

readonlyHandlers不允许属性的修改和删除. 其中readonlyGet是函数createGetter(true)的返回:

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // ...
    // isReadonly 为 true 则不需要依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    return isObject(res)
      ? isReadonly
        ?
        // 如果 res 是个对象或者数组类型,则递归执行 readonly 函数把 res readonly
        readonly(res)
        : reactive(res)
      : res
  }
}

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值