vue3响应式原理

2,096 阅读8分钟

Vue3源码终于发布了!迫不及待的撸一下源码,看看Vue3和Vue2到底有什么区别。

之前写过两篇Vue2响应式原理的文章:

从源码解析vue的响应式原理-依赖收集、依赖触发

从源码解析vue的响应式原理-响应式的整体流程

Vue2的原理是:

  • 对object类型使用defineProperty重写对象的getter和setter函数,在getter中收集依赖,在setter中触发依赖,以此实现响应式。但是要递归观测object中的所有key,会有性能问题。
  • 对array类型的数据,拦截修改数组的几个方法:push、pop、shift、unshift、splice、sort、reverse以此实现响应式。但是当数组中的元素为基本类型的数据时无法被观测。

Vue3的响应式原理是用了proxy的方式来实现,优化了Vue2响应式存在的几个问题,今天就从源码来分析下vue3的响应式原理:

(Vue3的源码是使用ts开发的,需要大家提前学习ts相关知识)

先从使用聊起

在Vue3中我们想要创建一个响应式数据,要怎么做呢?

查看下官方api我们看到一段最基础的示例代码:

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script>
import { reactive, computed } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }
}
</script>

示例中我们发现使用reactive生成的state是响应对象,当state.count变化时,依赖state.count的template片段和double都会相应的变化。

那么reactive到底做了什么使数据变成了响应对象呢?

####解析reactive

reactive和Vue2中的Vue.observable()类似,返回一个响应对象;reactive返回的响应对象主要用于页面显示,当响应对象改变时视图会自动重新渲染,实现数据和视图的双向绑定。

此处解析代码:

reactive

代码结构非常清晰,我们可以很明白的看到reactive函数的入参必须是一个Object类型的数据,而返回则是一个UnwrapNestedRefs类型的对象(被解嵌套以后的响应式对象,后面会分析UnwrapNestedRefs),此处可以直接简单的理解为返回了一个响应式对象。

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    // 弱引用的map结构,用于保存原始数据对应的响应式数据
    rawToReactive,
    // 弱引用的map结构,用于保存响应式数据对应的原始数据
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
createReactiveObject

reactive中关键点在于调用createReactiveObject方法,通过该方法返回传入对象的对应的响应式对象。在createReactiveObject方法中调用new proxy,生成一个代理对象,将该代理对象作为最终的响应式对象并返回。

function createReactiveObject(
  target: any,
  // 保存原始数据的weakMap
  toProxy: WeakMap<any, any>,
  // 保存响应式数据的weakMap
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // reactive的数据只能是Object类型
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  // 数据已经被转化为响应式数据了,直接返回其对应的响应式对象
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 数据本身就是一个响应式对象,则直接返回数据本身
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 一些不可被观测的对象,直接返回
  if (!canObserve(target)) {
    return target
  }
  // 定义new proxy中的处理函数handlers
  // 集合类型的对象使用 collectionHandlers.ts中的 mutableCollectionHandlers
  // 其他类型的对象使用 baseHandlers.ts中的 mutableHandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 调用new proxy,生成一个代理对象,将该代理对象作为最终的响应式对象并返回
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

其中调用new Proxy传入的handler是响应式的关键,集合类型的对象使用 collectionHandlers.ts中的 mutableCollectionHandlers作为new Proxy的handler;其他类型的对象使用 baseHandlers.ts中的 mutableHandlers作为new Proxy的handler。

mutableHandlers

baseHandlers.ts中的 mutableHandlers中使用createGetter代理对象的get方法、set代理对象的set方法。

其中createGetter方法中做了四件事:1、获取数据的值;2、判断数据是否已经进行过响应式处理;3、使用track方法进行依赖收集;4、对数据的每一个Object类型的属性进行reactive递归

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    // 获取数据的值
    const res = Reflect.get(target, key, receiver)
    // 已经经过响应式处理的ref数据则直接返回
    if (typeof key === 'symbol' && builtInSymbols.has(key)) {
      return res
    }
    if (isRef(res)) {
      return res.value
    }
    // 未经过响应式处理的数据收集其依赖
    track(target, OperationTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

set方法中做了三件事:1、将set行为更新到原属数据对象上;2、判断代理数据中有没有相应的key,没有则做新增处理,有则做修改处理;3、调用trigger方法,触发其依赖;

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  value = toRaw(value)
  // 判断代理对象中是否有这个key。若有的话就进行修改,若没有则新增
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  // 原值是ref类型,新值不是,则直接赋值,因为原值已经被监听了set触发trigger。此处避免重复触发。
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // 将set行为更新到原属数据对象上
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  // 如果target是该代理数据相对应的原始数据才做处理,如果targer只是代理数据相对应的原始数据原型链上的数据则不做操作
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (value !== oldValue) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        // 代理数据中没有响应的key,则做新增处理,并触发其依赖
        trigger(target, OperationTypes.ADD, key)
      } else if (value !== oldValue) {
        // 代理数据中有响应的key,则做修改处理,并触发其依赖
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}
mutableCollectionHandlers

不做过多解释,大家直接看这里吧: vue3响应式源码解析-Reactive篇-collectionHandlers

解析ref

从上述解析中我们一直看到一个概念就是ref,那么在vue3中ref到底是什么呢?vue3中主要的是ref()函数,ref()函数接受一个基本类型的数据,并返回一个响应式的ref对象。了解ref()函数之前需要了解下面两个基本概念:Ref接口、UnwrapNestedRefs。

Ref接口

对Ref接口的定义如 ref.ts:

// Ref接口
export interface Ref<T> {
	// 唯一标识位,标识对象是一个ref对象
  [refSymbol]: true
  // 存放数据,是基本类型数据真正存在的地方,UnwrapNestedRefs表示被接嵌套以后的ref类型的对象
  value: UnwrapNestedRefs<T>
}
UnwrapNestedRefs

之前分析reactive时又讲到过reactive返回的是一个UnwrapNestedRefs类型的数据,且上面说Ref接口中的value也是UnwrapNestedRefs类型的数据。

export type UnwrapNestedRefs<T> = T extends Ref<any> ? T : UnwrapRef<T>

由上面代码可见UnwrapNestedRefs是Ref类型的数据,或者经过UnwrapRef接嵌套以后的数据。 UnwrapRef的定义如下:

// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
  object: { [K in keyof T]: UnwrapRef<T[K]> }
  stop: T
}[T extends Ref<any>
  ? 'ref'
  : T extends Array<any>
    ? 'array'
    : T extends BailTypes
      ? 'stop' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'stop']

这个接嵌套写的非常巧妙,其中通过infer V推断出类型进行进一步的递归解构(其中infer语句表示在 extends 条件语句中待推断的类型变量:infer定义),也就是说UnwrapNestedRefs只能是ref类型或者其他任意类型的的对象,但是不能是嵌套了ref类型的对象,即不能是这样Ref<Ref> 这样Array<Ref> 或者这样 { [key]: Ref }嵌套型的ref类型对象。

ref() 函数
// 判断数据是不是对象,是对象的话调用reactive()函数将其变为响应式数据,不是对象的话直接返回
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
// ref函数,接受一个原始数据,返回其响应式的Ref类型的对象
export function ref<T>(raw: T): Ref<T> {
  raw = convert(raw)
  const v = {
    // 添加唯一标识位,标识对象是一个ref对象
    [refSymbol]: true,
    get value() {
      // 依赖收集
      track(v, OperationTypes.GET, '')
      // 返回get结果
      return raw
    },
    set value(newVal) {
      // 对新值进行响应式处理
      raw = convert(newVal)
      // 依赖触发
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}

此处ref()主要解决基本类型的数据无法变成响应式数据的问题。 ref和reactive的功能相似,ref用于将基本类型的数据转为响应式数据;reactive用于将对象类型的数据转为响应式数据