likes
comments
collection
share

Vue3 源码解析 - reactive 响应式工作原理

作者站长头像
站长
· 阅读数 56

前言

reactive 响应式函数入手,探究 Vue3 响应式原理实现。首先从官方文档的基本使用开始,然后解读 reactive 函数源码,理解它的功能是怎么实现,知其然到知其所以然。最后是源码调试,同时理解 Vue3 渲染和更新流程,理论和实践结合,理解响应式工作原理

为了更好理解掌握,可以打开 链接 根据下面的调试部分动手试试

reactive 基本使用

reactive 是用于创建响应式数据的函数,接收一个引用对象,并返回一个响应式的 Proxy 对象。当我们修改这个 Proxy 对象时,Vue3会自动追踪这些变化,并触发视图相应的更新

1、reactive 函数的基本使用方法

reactive 只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。不支持 stringnumberboolean 这样的原始类型

import { reactive } from 'vue';

let state = reactive({
  count: 0
});

// 在组件中使用 state.count 并且当修改值时,界面会自动更新

2、不能替换整个对象 state,不能直接解构

如果直接赋值 state 会改变它的引用指向,不再是 proxy 对象,丢失了响应式

state = {
  count: 1
}

state.count = 2 // 视图不会更新,丢失了响应式

直接解构,同样断开了响应式连接

const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接
let { count } = state

3、reactive 创建集合对象

reactive 函数可以支持 SetMap 数据结构

import { reactive } from 'vue'

const state = reactive(new Map())

state.set('count', 1)
console.log(state.get('count')) // 1

4、Reactive Proxy 对象 vs 原对象

reactive 创建的 Proxy 对象和原对象是不相等的,如果重复创建响应式对象,和原来的对象是相等的

import { reactive } from 'vue'

const data = { count: 0 }
const state = reactive(data)

// 代理对象和原始对象不是全等的
console.log(data === state) // false

const state1 = reactive(data) // 在同一个对象上调用 reactive() 会返回相同的代理
console.log(state1 === state) // true

5、reactive 传入 ref 对象 value 会自动解包

ref 创建对象传入 reactive 函数,不需要访问 .value 会自动解包,并且修改 state.count 的值,ref 值也会发生变化

import { reactive, ref } from 'vue'

const data = ref(0)
const state = reactive({ count: data }) // 自动解构 data.value

state.count = 1
console.log(state.count, data.value) // 1 1

data.value += 1
console.log(state.count) // 2

注意,数组和集合不会自动解包

const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)

reactive 源码解析

访问链接,F12 打开控制台,刷新页面

<script setup>
debugger
import { reactive } from 'vue'

const state = reactive({
  count: 0
})
</script>

<template>
  <h1>{{ state.count }}</h1>
  <button @click="state.count++"> add </button>
</template>

单步进入 reactive 函数调试

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

reactive 函数

reactive API 接受一个对象参数,通过 createReactiveObject 函数处理后,直接返回一个 proxy 对象

export function reactive(target: object) {
  // 如果试图去观察一个只读的代理对象,会直接返回只读版本
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 创建一个代理对象并返回
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

其中调用 isReadonly 判断 target 中是否有只读对象 key ReactiveFlags.IS_READONLY ,如果是直接返回

Vue3 源码解析 - reactive 响应式工作原理

ReactiveFlags 枚举对象会在源码中多个地方引用,isRef, isReadonly 等工具方法都是根据对象是否有这些 key 判断的

export const enum ReactiveFlags {
  SKIP = '__v_skip', // 是否跳过响应式 返回原始对象
  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
  IS_READONLY = '__v_isReadonly', // 标记一个只读对象
  RAW = '__v_raw' // 标记获取原始值
}

createReactiveObject

createReactiveObject 函数执行返回一个 proxy 实例对象,函数接收 5 个参数

  • target:传入 reactive 的目标对象
  • isReadonly:生成的代理对象是否只读。shallowReadonlyreadonly 传入 true
  • baseHandlers:生成代理对象的 handler 参数。当 target 类型是 Array 或 Object 时使用该 handler
  • collectionHandlers:当 target 类型是 Map、Set、WeakMap、WeakSet 时使用该 handler
  • proxyMap:存储生成代理对象后的 Map 对象,防止重复创建
function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  // 目标必须是对象类型(包括数组、set、map结构)
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // target 已经是 Proxy类型对象,直接返回
  // 有个例外,如果 readonly 是响应式对象,则继续
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 目标已经存在对应的代理对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 只有白名单里的类型才能被创建响应式对象,1是对象或数组类型,2是 Set/Map/WeakSet/WeakMap类型
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 利用 Proxy 创建响应式对象,对象或数组类型使用 baseHandlers,Set/Map/WeakSet/WeakMap类型collectionHandlers
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 缓存已经代理的对象
  proxyMap.set(target, proxy)
  return proxy
}

可以看到,在该函数中基础数据类型会直接返回原始值。

将已生成的代理对象缓存进 proxyMap,当这个代理对象已存在时不会重复生成,会直接返回已有对象。

然后通过 TargetType 来判断 target 目标对象的类型,仅支持 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他对象会被标记为 INVALID,并返回原始值。

当目标对象通过类型校验后,会通过 new Proxy() 生成一个代理对象 proxy,Proxy handler 参数根据 target 的类型进行判断,对象和数组使用 baseHandlers,集合类型使用 collectionHandlers

假如 reactive 函数传入的是对象,baseHandlers 取的是 mutableHandlers,它是从 baseHandlers 文件引入

import {
  mutableHandlers, // reactive 函数
  readonlyHandlers, // readonly 函数
  shallowReactiveHandlers, // shallowReactive 函数
  shallowReadonlyHandlers // shallowReadonly 函数
} from './baseHandlers'

mutableHandlers 对象定义了 5 个拦截方法,分别是 getsetdeletePropertyhasownKeys 方法

export const mutableHandlers: ProxyHandler<object> = {
  get, // 用于拦截对象的读取属性操作
  set, // 用于拦截对象的设置属性操作
  deleteProperty, // 用于拦截对象的删除属性操作
  has, // 检查一个对象是否拥有某个属性
  ownKeys // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法
}

getset 都是通过 createGetter 工厂函数生成的,以便适配除 reactive 外的其他 api,例如 shallowReactivereadonlyshallowReadonly 等。

const get = createGetter()
const shallowGet = createGetter(false, true)
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)

createGetter 根据不同参数生成不同的功能函数,运用到了函数柯里化思想

get 函数

对特殊 key 处理

get 函数对 key 进行特殊处理

// 如果 get 访问的 key 是 '__v_isReactive',返回 createGetter 的 isReadonly 参数取反结果
if (key === ReactiveFlags.IS_REACTIVE) {
  return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
  // 如果 get 访问的 key 是 '__v_isReadonly',返回 createGetter 的 isReadonly 参数
  return isReadonly
} else if (
  // 如果 get 访问的 key 是 '__v_raw',并且 receiver 与原始标识相等,则返回原始值
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
    ).get(target)
) {
  return target
}

数组特殊方法处理

判断 target 不是只读对象,并且目标对象是个数组,访问的 key 又在数组需要劫持的方法里,直接调用修改后的数组方法执行

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

arrayInstrumentations 对象包括的数组方法

  • 索引数组方法:includesindexOflastIndexOf
  • 改变数组长度 length 方法:pushpopshiftunshiftsplice
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations = {}

  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // toRaw 可以把响应式对象转成原始数据
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        // 依赖收集
        track(arr, TrackOpTypes.GET, i + '')
      }

      // 先尝试用参数本身,可能是响应式数据
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // 如果失败,再尝试把参数转成原始数据
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })

  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}

之所以 get 函数跳过数组这些内置处理方法,原因是它们都会访问和修改数组的 length 属性,如果重复访问,又同时被修改了,会造成循环引用

对象类型处理

createGetter 函数传入的两个参数 isReadonlyshallow

  • readonly 函数 isReadonly 为 true 是只读对象,不进行 track 依赖收集

  • shallowReactive函数 shallow 为 true 是浅层响应式,直接返回 get 结果,不做递归执行

  • 如果传入的是 ref 值,不是数组类型,会自动解包

  • 最后判断如果是嵌套引用类型,会进行递归处理,将最终结果返回

/**
 * @description: 用于拦截对象的读取属性操作
 * @param {isReadonly} 是否只读 
 * @param {shallow} 是否浅观察  
 */
function createGetter(isReadonly = false, shallow = false) {

  return function get(target, key, receiver) {
    // 省略这段判断

    const res = Reflect.get(target, key, receiver)
    // 如果是 key 是 Symbol,并且 key 是 Symbol 对象中的 Symbol 类型的 key
    // 或者 key 是不需要追踪的 key: __proto__,__v_isRef,__isVue
    // 直接返回 get 结果
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    // 只读不进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 如果是 shallow 浅层响应式,直接返回 get 结果
    if (shallow) {
      return res
    }
    // 如果是 ref ,则返回解包后的值 - 当 target 是数组,key 是 int 类型时,不需要解包
    if (isRef(res)) {
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
    // 值是引用类型,递归reactive处理响应式
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

set 函数

set函数相对简单点,它也有一个 createSetter 的工厂函数,通过柯里化的方式返回一个 set 函数。

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

set 函数判断 target 是否相同,然后判断是否是存在的数组内点下标和对象的 key,调用 trigger 进行派发更新

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    // 如果模式不是浅观察
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 目标对象不是数组,旧值是ref,新值不是ref,则直接赋值
      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)

    // 如果目标是原型链的某个属性,通过 Refect.set 修改它会再次触发 setter,这种情况没必要触发两次 trigger
    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
  }
}

这时可能有一个疑问, reactive 只负责创建数据对象,那么它是怎么和视图渲染和更新进行关联的呢,接下来从组件的挂载和渲染分析揭晓

组件挂载渲染

源码调试

在 vue 文件搜索 createAppAPI,在 mount 挂载方法上打上断点

Vue3 源码解析 - reactive 响应式工作原理

它首先执行 createVNode 创建虚拟 DOM 对象

Vue3 源码解析 - reactive 响应式工作原理

然后执行 render 函数,调用 patch 方法,目标是将虚拟 DOM 转化为真实的 DOM

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

如果是渲染组件而非元素,调用 processComponent 方法

Vue3 源码解析 - reactive 响应式工作原理

单步进入,调用 mountComponent 方法,继续往下执行

Vue3 源码解析 - reactive 响应式工作原理

mountComponent 方法调用 createComponentInstance 创建组建实例 instance,将其作为参数传入 setupComponent,主要是调用我们定义的 setup 函数,所以 setup 函数执行时机是在 mount 方法

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

执行 setup 函数,会返回一个函数,在 handleSetupResult 函数,赋值给组件实例的 render 作为渲染函数

Vue3 源码解析 - reactive 响应式工作原理

setup 函数执行了,reactive 对象也创建了,接下来就是将 reactive 数据和render 函数做关联

当执行 render 函数访问 reactive 数据,触发 get 拦截方法进行依赖追踪,当数据发现变化,触发 set 进行视图更新,那么起中间桥梁的是 ReactiveEffect

ReactiveEffect

ReactiveEffect 类实例化主要传入两个参数,第一个是 effect 副作用函数,第二个参数是调度配置对象

重点理解 run 方法,它执行后,将副作用函数收集到 reactive 对应数据依赖集合上,这时数据和模版 render 关联上了,当修改 reactive 数据,自然可以从依赖中取出,然后执行副作用函数更新视图

export class ReactiveEffect<T = any> {
  constructor(fn, scheduler, scope) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    try {
      this.parent = activeEffect
      // 当前激活的副作用对象
      activeEffect = this
      shouldTrack = true

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      // 执行副作用函数
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        // 完成依赖标记
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined

      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // 
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

继续往下执行 setupRenderEffect,实例化 ReactiveEffect ,传入第一个参数 componentUpdateFn 作为副作用函数 fn

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

在函数最后执行 update 方法,执行的是 ReactiveEffect 实例的 run 方法 Vue3 源码解析 - reactive 响应式工作原理

搜索 ReactiveEffectrun 打上断点

Vue3 源码解析 - reactive 响应式工作原理

执行 run 方法,将当前 this 实例对象赋值给了全局变量 activeEffect,然后执行副作用函数 fn,也就是组件的更新函数 `

Vue3 源码解析 - reactive 响应式工作原理

单步进入 fn 执行,也就是执行 componentUpdateFn 组件函数,调用 renderComponentRoot,执行 render 函数

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

这时就会到了 setup 返回的 渲染函数,内部就会访问到 state.count ,触发它的 get 函数,在 get 函数上打上断点(可以搜索 createGetter)

Vue3 源码解析 - reactive 响应式工作原理

track 函数进行依赖收集了

Vue3 源码解析 - reactive 响应式工作原理

track 依赖收集

track 函数会将当前激活的 activeEffect 添加到 targetMap 映射上

Vue3 源码解析 - reactive 响应式工作原理

上面的 onTrack 函数可以用于辅助调试收集依赖

逻辑上,创建一个全局的 WeakMap targetMap 映射,target 作为 keyMap 作为 value,然后为 target 的每一个 key 又创建映射,收集的依赖存储到 Set 集合中

const targetMap = new WeakMap();  // 为每个响应式对象存储依赖关系
// 然后track()函数就需要首先拿到targetMap的depsMap
function track(target, key) {
    let depsMap = targetMap.get(target);  // target是响应式对象的名称,key是对象中属性的名称
    if (!depsMap){  // 不存在则为这个对象创建一个新的deps图
        targetMap.set(target, (depsMap = new Map())
    };
    let dep = depsMap.get(key);  // 获取属性的依赖对象,和之前的一致了
    if(!dep){  //同样不存在就创建一个新的
        depsMap.set(key, (dep = new Set()))
    };
    dep.add(activeEffect);
}

依赖收集好了,最后执行 patch 挂载,组件的渲染就执行完了

trigger 派发更新

当点击按钮,修改 state.count 值,触发了 set 函数,执行 trigger 派发更新

Vue3 源码解析 - reactive 响应式工作原理

继续往下执行,根据 target 之前的映射,取出之前收集的依赖集合,遍历执行,然后调用依赖对象副作用函数 componentUpdateFn 进行更新视图

Vue3 源码解析 - reactive 响应式工作原理

Vue 的更新并不是同步的,而是放到 Promise 微任务队列中,做批量更新

Vue3 源码解析 - reactive 响应式工作原理

任务队列 flushJobs 执行 ReactiveEffect run 方法,走 patch 的更新 vnode

Vue3 源码解析 - reactive 响应式工作原理

Vue3 源码解析 - reactive 响应式工作原理

拿到最新的数据,最后又回到开始渲染的流程,再走一遍,此时 patch 是更新 DOM

总结

reactive 函数通过 Proxy 创建一个代理对象,定义了拦截方法

在组件渲染时,会触发 reactive get 方法,通过 track 的处理器函数来收集依赖,将组件更新函数添加到数据的依赖集合中

当数据发现变化,会触发 set 方法,通过 trigger 的处理器函数来派发更新,每个依赖的使用都会被包裹到一个副作用(effect)函数中,而派发更新后就会执行副作用函数,这样视图依赖的值就被更新了

Vue3 源码解析 - reactive 响应式工作原理

转载自:https://juejin.cn/post/7264073604496228411
评论
请登录