Vue3响应式系统源码解析-Ref篇

前言的前言

阅读本文须要有必定的TypeScript基础,要求不高,看过一遍TS的文档便可。html

咱们阅读源码的缘由是什么?无非是1:学习;2:更好的使用这个库。若是只是想大体的了解下原理,倒没必要花时间阅读源码,几句话,几张图就能搞清楚,网上搜搜应该就有不少。所以,阅读源码的过程必定是要对不明白的地方深刻了解,确定是很费时间的。vue

在这过程当中,有些知识点,跟库自己可能没什么关系,但若是不懂,又难继续理解。对于这些知识点,我会尽可能少的解释,但会贴上尽可能完善的文档,方便不了解的同窗先阅读学习。react

鉴于篇幅太长,信息量较大,我会将文章拆开,边写边发,有兴趣的同窗能够连载阅读,写完之后再汇总一篇,方便时间充沛的同窗一股脑看。git

前言

在上篇文章中说道,ref是最影响源码阅读的文件。但若是不先搞明白它,看其余的只会更晕。我先帮你们理清ref的逻辑跟概念。github

因为如今(2019/10/9)vue@3还未正式发版,你们还不熟悉其相关的用法。上篇文章虽然介绍了很多,但其实仍是有很多疑问。在阅读本篇文章以前,若是有时间,建议先阅读Vue官方对Composition API的介绍:typescript

  1. Vue Composition API
  2. Ref Vs Reactive

读完关于Composition API的介绍,会对了解本库有更多认识,便于更好的理解源码。api

refreactive是整个源码中的核心,经过这两个方法建立了响应式数据。要想彻底吃透reactivity,必须先吃透这两个。数组

Ref

ref最重要的做用,实际上是提供了一套Ref类型,咱们先来看,它究竟是个怎么样的数据类型。(为了更好的作解释,我会调整源码中的接口、类型、函数等声明顺序,并会增长一些注释方便阅读)数据结构

// 生成一个惟一key,开发环境下增长描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)

// 声明Ref接口
export interface Ref<T = any> {
  // 用此惟一key,来作Ref接口的一个描述符,让isRef函数作类型判断
  [refSymbol]: true
  // value值,存放真正的数据的地方。关于UnwrapNestedRefs这个类型,我后续单独解释
  value: UnwrapNestedRefs<T>
}

// 判断是不是Ref数据的方法
// 对于is关键词,若不熟悉,见:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
  return v ? v[refSymbol] === true : false
}

// 见下文解释
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

复制代码

要想了解UnwrapNestedRefsUnwrapRef,必须先要了解ts中的infer。若是以前不了解,请先阅读相关文档。看完文档,再建议去google一些案例看看加深下印象。app

如今咱们假设你了解了infer概念,也了解了它的平常用法。再来看源码:

// 不该该继续递归的引用数据类型
type BailTypes =
  | Function
  | Map<any, any>
  | Set<any>
  | WeakMap<any, any>
  | WeakSet<any>

// 递归地获取嵌套数据的类型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
  // 若是是ref类型,继续解套
  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
  ? 'ref'
  : T extends Array<any>
    ? 'array'
    : T extends BailTypes
      ? 'stop' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'stop']

// 声明类型别名:UnwrapNestedRefs
// 它是这样的类型:若是该类型已经继承于Ref,则不须要解套,不然多是嵌套的ref,走递归解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

复制代码

若是仍是懵,建议后续再去看看infer的相关介绍。在这咱们直接抛结果:

Ref是这样的一种数据结构:它有个key为Symbol的属性作类型标识,有个属性value用来存储数据。这个数据能够是任意的类型,惟独不能是被嵌套了Ref类型的类型。 具体来讲就是不能是这样 Array<Ref> 或者这样 { [key]: Ref }。但很奇怪的是,这样Ref<Ref> 又是能够的。具体为何也不知道,因此我勇敢地提了个PR...

(果真Ref<Ref>是不够完美的,2019.10.10晚,我这PR被合并了。你们遇到疑问时,也能够勇敢的提PR,说不定就被合了....)

另外,Map、Set、WeakMap、WeakSet也是不支持解套的。说明Ref数据的value也有多是Map<Ref>这样的数据类型。

说回Ref,从上篇文章中,咱们已经了解到,Ref类型的数据,是一种响应式的数据。而后咱们看其具体实现:

// 从@vue/shared中引入,判断一个数据是否为对象
// Record<any, any>表明了任意类型key,任意类型value的类型
// 为何不是 val is object 呢?能够看下这个回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 若是传递的值是个对象(包含数组/Map/Set/WeakMap/WeakSet),则使用reactive执行,不然返回原数据
// 从上篇文章知道,这个reactive就是将咱们的数据转成响应式数据
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
  // 转化数据
  raw = convert(raw)
  const v = {
    [refSymbol]: true,
    get value() {
      // track的代码在effect中,暂时不看,能猜到此处就是监听函数收集依赖的方法。
      track(v, OperationTypes.GET, '')
      // 返回刚刚被转化后的数据
      return raw
    },
    set value(newVal) {
      // 将设置的值,转化为响应式数据,赋值给raw
      raw = convert(newVal)
      // trigger也暂时不看,能猜到此处就是触发监听函数执行的方法
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}
复制代码

其实最难理解的就在于这个ref函数。咱们看到,这里也定义了get/set,却没有任何Proxy相关的操做。在以前的信息中咱们知道reactive能构建出响应式数据,但要求传参必须是对象。但ref的入参是对象时,一样也须要reactive作转化。那ref这个函数的目的究竟是什么呢?为何须要有它?

在文章开头,我贴了这份官方介绍Ref vs Reactive,这其中其实已经说的很明白。

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,咱们无法让基本数据类型,或者解构后的变量(若是它的值也是基本数据类型的话),成为响应式的数据。

// 咱们是永远没办法让`a`或`x`这样的基本数据成为响应式的数据的,Proxy也没法劫持基本数据。
const a = 1;
const { x: 1 } = { x: 1 }
复制代码

可是有时候,咱们确实就是想一个数字、一个字符串是响应式的,或者就是想利用解构的写法。那怎么办呢?只能经过建立一个对象,也便是源码中的Ref数据,而后将原始数据保存在Ref的属性value当中,再将它的引用返回给使用者。既然是咱们本身创造出来的对象,也就不必使用Proxy再作代理了,直接劫持这个value的get/set便可,这就是ref函数与Ref类型的由来。

不过单靠ref还无法解决对象解构的问题,它只是将基本数据保持在一个对象的value中,以实现数据响应式。对于对象的解构还须要另一个函数:toRefs

export function toRefs<T extends object>( object: T ): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  // 遍历对象的全部key,将其值转化为Ref数据
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}
function toProxyRef<T extends object, K extends keyof T>( object: T, key: K ): Ref<T[K]> {
  const v = {
    [refSymbol]: true,
    get value() {
      // 注意,这里没用到track
      return object[key]
    },
    set value(newVal) {
      // 注意,这里没用到trigger
      object[key] = newVal
    }
  }
  return v as Ref<T[K]>
}
复制代码

经过遍历对象,将每一个属性值都转成Ref数据,这样解构出来的仍是Ref数据,天然就保持了响应式数据的引用。可是源码中有一点要注意,toRefs函数中引用的是toProxyRef而不是ref,它并不会在get/set中注入tracktrigger,也就是说,**向toRefs传入一个正常的对象,是不会返回一个响应式的数据的。**必需要传递一个已经被reactive执行返回的对象才能有响应式的效果。感受这点能够优化,暂时也不知道小右这样作的缘由是什么。因为这里会牵扯到tracktrigger,而这两个在我写本文时还没研究,就没胆子提PR了。

到这,咱们就把ref的源码给看完了。

下一章节咱们开始看reactive,它是核心,从它开始,内部的各个api开始真正的串连。


本文做者:蚂蚁保险-体验技术组-阿相

掘金地址:相学长

相关文章
相关标签/搜索