Vue3源码分析(一)- 响应式原理

随着尤大发文停止对Vue2项目的维护,Vue3继承了Vue方便易用的传统,并且在Vue2的基础上做了一系列的优化。本文主要针对Vue3的响应式系统进行源码层面分析,包含以下内容:

  • Vue2响应式系统
  • Vue3响应式系统
  • Reactive源码实现

Vue2响应式系统

源码管理

按照不同的功能模块划分目录结构,响应式原理位于 core 目录下的 observer 模块

  • compiler:模版编译
  • core:通用运行代码
  • platforms:平台专有代码
  • shared:共享工具
  • server:服务端渲染
  • sfc:单文件解析

功能实现

使用 ES5 中的 object.definedProperty 对数据对象的一个属性进行劫持,当属性被访问时,会触发 getter 的调用,随之 observer 目录下的 wather.js 模块就会开始收集依赖,收集到的依赖是一个个 Dep 类的实例化对象。而当属性发生变更时,会触发 setter 的调用,随之触发 dep 的 notify 函数开始派发更新事件,由此实现数据的响应监听

缺陷

  • 必须预先知道劫持的key (对象的属性),并不能检测对象属性的添加和删除
  • 对于嵌套层级比较深的对象,需要一次性递归到底,影响性能
  • 无法对数组的变化进行响应式监听,如push、shift等方法
  • 无法通过数组下标改变数组的值

Vue3响应式系统

源码管理

将Vue2中这些模块拆分到不同的 package 中,每个 package 有各自的API、类型定义和测试,并且可以单独按需引入模块,更方便用户理解和使用。响应式原理位于 package 目录下的 reactivity 模块

功能实现

使用 ES6 中的 Proxy 代理,对数据对象进行劫持,当数据被访问时进行依赖追踪,建立响应式数据与 effect 副作用函数之间的关联,通过 track 的处理器函数来收集依赖。当数据发生变更时,通过 trigger 的处理器函数来触发派发更新,通知相关联的effect副作用函数进行响应式更新,这样依赖的值就被更新了

Reactive源码实现

第一层 reactive入口

reactive() 会创建并返回一个 Proxy 代理对象,其中包含对该对象的劫持处理Handlers

ts 复制代码
// packages/reactivity/src/reactive.ts

export function reactive(target: object) {
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}
ts 复制代码
// packages/reactivity/src/reactive.ts

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>, // 基本类型的Handlers
  collectionHandlers: ProxyHandler<any>, // Set、Map类型的Handlers
  proxyMap: WeakMap<Target, any>,
) {
    // 只展示核心语法,省略了相关的类型判断和性能优化代码
    // - 判断target是否为对象
    // - 判断target是否为Proxy或者是否有相关联Proxy
    // - 判断target是否可被观察
    
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
  
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
  }

辅助信息

  • shallowReactive 浅响应式,只会代理第一层的对象,使第一层的对象产生响应式的效果
  • readonly 表示数据会被代理,但不进行依赖收集,可以节约性能。而且数据只读,无法被修改
  • shallowReadonly 表示只会代理第一层的对象,是第一层的对象产生只读的效果,而第二层没有只读特性(属性可以修改),从而使得第一层属性不进行依赖收集,由于没有依赖收集第二层的属性,不具备响应式的效果,也就是视图不会发生变化。

第二层 Handlers

BaseReactiveHandler

ts 复制代码
// packages/reactivity/src/baseHandlers.ts
class BaseReactiveHandler implements ProxyHandler<Target> {
    get(target: Target, key: string | symbol, receiver: object) {
            const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
            
            // 对数组作特殊处理,解决Vue2针对数组的缺陷
            function createArrayInstrumentations() { 
                  const instrumentations: Record<string, Function> = {}
                  // 劫持不改变数组长度的方法
                  (['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
                    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
                      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) {
                       // 如果返回-1或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() // 暂停依赖收集,数组改变不会触发依赖更新
                      pauseScheduling()
                      const res = (toRaw(this) as any)[key].apply(this, args)
                      resetScheduling()
                      resetTracking() //重新启动依赖收集,保证数组的响应式特性
                      return res
                    }
                  })
                  return instrumentations
                }
            
            const targetIsArray = isArray(target) //判断target是否为数组

            if (!isReadonly) {
              if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
                return Reflect.get(arrayInstrumentations, key, receiver)
              }
              if (key === 'hasOwnProperty') {
                return hasOwnProperty
              }
            }
        
         const res = Reflect.get(target, key, receiver)
         
         if (!isReadonly) {
             track(target, TrackOpTypes.GET, key) //收集依赖
         }
         return res
    }
}

无限循环问题

  1. 为了确保数据变化及时响应从而更新视图,响应式系统会在改变数组长度时触发依赖更新
  2. 为了获取最新的值,这些数组方法可能会在响应式更新过程中被重新调用
  3. 调用这些方法又会触发新的依赖更新,如此循环

MutableReactiveHandler

ts 复制代码
// packages/reactivity/src/baseHandlers.ts

class MutableReactiveHandler extends BaseReactiveHandler {
    set(
    target: object,
    key: string | symbol, // 被劫持的属性名
    value: unknown, //要修改的值
    receiver: object, //Proxy
  ): boolean {
      let oldValue = (target as any)[key]
      if (!this._shallow) {
      const isOldValueReadonly = isReadonly(oldValue)
      if (!isShallow(value) && !isReadonly(value)) {
      // 将新值和旧值都通过toRaw转换成原始类型的值
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
        // 如果旧值为只读属性
          return false
        } else {
        // 旧值为ref对象,新值不是ref对象,直接将新值赋值给旧值
          oldValue.value = value
          return true
        }
      }
    } 
    
    // 判断target是否包含要设置的属性
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
        
    const result = Reflect.set(target, key, value, receiver)
    
    if (target === toRaw(receiver)) {
      if (!hadKey) {
      // 如果target不包含key属性,说明这是一个添加属性的操作,触发trigger通知副作用函数有个属性被添加
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
      // 如果包含key属性且值发生变化,说明这是一个更新属性的操作,触发trigger通知副作用函数有个属性被修改
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

第三层 effect

watch、computed都是基于 effect 来实现的

run

ts 复制代码
// packages/reactivity/src/effect.ts

export class ReactiveEffect<T = any> {
    active = true // 表示是否为当前活跃状态
    deps: Dep[] = [] // 响应式数据收集依赖时会将对应的依赖项添加到deps中
  
    constructor(
        public fn: () => T,
        public trigger: () => void,
        public scheduler?: EffectScheduler,
        scope?: EffectScope, // 副作用函数的作用域
  ) {
  // 记录副作用函数的作用域
    recordEffectScope(this, scope)
  }
  
  // 执行目标函数
   run() {
    this._dirtyLevel = DirtyLevels.NotDirty
    if (!this.active) {
    // 如果当前effect不是激活状态,就不需要收集依赖仅仅执行一下即可
      return this.fn()
    }
    let lastShouldTrack = shouldTrack // 保存当前依赖追踪的状态
    let lastEffect = activeEffect
    try {
      shouldTrack = true // 表示当前的effect实例可以进行依赖收集
      activeEffect = this // 将当前活跃的effect指向自己
      this._runnings++
      preCleanupEffect(this)
      return this.fn() // 执行副作用函数主题逻辑并返回结果
    } finally {
      postCleanupEffect(this) // 清理操作
      this._runnings--
      // 恢复状态
      activeEffect = lastEffect
      shouldTrack = lastShouldTrack
    }
  }
  }

stop

对effect的停止和清理操作,确保在正确的时机停止对effect的执行,避免不必要的的计算和依赖追踪

ts 复制代码
// packages/reactivity/src/effect.ts

stop() {
    if (this.active) {
      preCleanupEffect(this)
      postCleanupEffect(this)
      this.onStop?.() // 判断是否存在onStop函数,存在的话就执行onStop函数
      this.active = false // 将effect的激活状态调整成false
    }
  }

第四层 track

访问响应式数据时进行依赖追踪,建立响应式数据与副作用函数之间的联系

ts 复制代码
 // packages/reactivity/src/reactiveEffect.ts
 
 export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
  // targetMap 是一个WeakMap对象,用来存储响应式数据和它们对应的依赖集合,进行依赖追踪
  // 键:响应式对象 值:depsMap对象
  
  // depsMap  是一个Map对象,用来存储响应式数据的属性和它们对应的依赖集合,当属性发生变化时,通过Dep对象触发该属性关联的副作用函数
  // 键:属性名 值:Dep对象
    let depsMap = targetMap.get(target)
    if (!depsMap) {
    // 在targetMap中没有对应的依赖,表示第一次访问这个响应式数据,需要创建一个空的依赖并放入targetMap中
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
    // 在DepsMap中没有与key对应的Dep,表示第一次访问响应式数据的key属性,需要创建一个新的空Dep对象并放入depsMap中
      depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
    }
    trackEffect(
      activeEffect,
      dep,
      __DEV__
        ? {
            target,
            type,
            key,
          }
        : void 0,
    )
  }
}

第四层 trigger

在响应式数据发生变化时,触发相关联的副作用函数进行依赖更新

ts 复制代码
// packages/reactivity/src/reactiveEffect.ts

export function trigger(
  target: object,
  type: TriggerOpTypes, // set、add、delete、clear
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
    const depsMap = targetMap.get(target)
    if (!depsMap) {
    // 响应式数据没有依赖追踪
    return
  }
  
   let deps: (Dep | undefined)[] = [] // 申明一个deps数组用来存储target属性相关联的副作用函数
   if (type === TriggerOpTypes.CLEAR) {
   // 表示清空集合
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
  // target为数组且修改的是数组的长度
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {
    // 触发指定属性key相关联的副作用函数
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
相关推荐
计算机学姐10 分钟前
基于微信小程序的调查问卷管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
黄尚圈圈4 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
一路向前的月光9 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   9 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Jiaberrr10 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
程序员大金13 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
道爷我悟了14 小时前
Vue入门-指令学习-v-html
vue.js·学习·html
无咎.lsy14 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
工业互联网专业15 小时前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
计算机学姐15 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis