Vue3核心源码解析 (二) : 响应式原理

响应式reactivity是Vue 3相对于Vue 2改动比较大的一个模块,也是性能提升最多的一个模块。其核心改变是,采用了ES 6的Proxy API来代替Vue 2中的Object.defineProperty方法来实现响应式。那么什么是Proxy API呢,Vue 3的响应式又是如何实现的?

1. Proxy API

Proxy API对应的Proxy对象是ES 6就已引入的一个原生对象,用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
从字面意思来理解,Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等)都必须通过该代理器。因此,我们可以对来自外界的所有操作都进行拦截、过滤、修改等操作。
基于Proxy的这些特性常用于:
· 创建一个“响应式”的对象,例如Vue 3.0中的reactive方法。
· 创建可隔离的JavaScript“沙箱”。沙箱参考
定义proxy 的基本语法

  1. new Proxy
const p = new Proxy(target, handler)

target参数表示要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数,甚至另一个代理);handler参数表示以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p的行为。

  1. Proxy.revocable
const { proxy,revoke } = Proxy.revocable(target, handler)

该方法的返回值是一个对象,其结构为:{"proxy": proxy, "revoke": revoke}。其中,proxy表示新生成的代理对象本身,和用一般方式new Proxy(target, handler)创建的代理对象没什么不同,只是它可以被撤销掉;revoke表示撤销方法,调用的时候不需要加任何参数就可以撤销掉和它一起生成的那个代理对象。一旦某个代理对象被撤销,它将变得几乎完全不可调用,在它身上执行任何的可代理操作都会抛出TypeError异常。事例如下

let student = { name: 'zhagnsan', sex: 'male' }
let handler = {
    get: (target, key, receiver) => {
        console.info('proxy get')
        return key in target ? target[key] : undefined
    }
}
let { proxy, revoke } = Proxy.revocable(student,handler)

proxy.name //zhagnsan
revoke() //撤销
//此时
proxy.name //proxy2.html:37 Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
revoke()后proxy

Proxy共有接近13个handler,也可以称为钩子,它们分别是:
· handler.getPrototypeOf():在读取代理对象的原型时触发该操作,比如在执行Object.getPrototypeOf(proxy)时。
· handler.setPrototypeOf():在设置代理对象的原型时触发该操作,比如在执行Object.setPrototypeOf(proxy, null)时。
· handler.isExtensible():在判断一个代理对象是否可扩展时触发该操作,比如在执行Object.isExtensible(proxy)时。
· handler.preventExtensions():在让一个代理对象不可扩展时触发该操作,比如在执行Object.preventExtensions(proxy)时。
· handler.getOwnPropertyDescriptor():在获取代理对象某个属性的描述时触发该操作,比如在执行Object.getOwnPropertyDescriptor(proxy, "foo")时。
· handler.defineProperty():在定义代理对象某个属性的描述时触发该操作,比如在执行Object.defineProperty(proxy, "foo", {})时。
· handler.has():在判断代理对象是否拥有某个属性时触发该操作,比如在执行"foo" in proxy时。
· handler.get():在读取代理对象的某个属性时触发该操作,比如在执行proxy.foo时。
· handler.set():在给代理对象的某个属性赋值时触发该操作,比如在执行行proxy.foo = 1时。
· handler.deleteProperty():在删除代理对象的某个属性时触发该操作,即使用delete运算符,比如在执行delete proxy.foo时。
· handler.ownKeys():当执行Object.getOwnPropertyNames(proxy)和Object.getOwnProperty Symbols(proxy)时触发。
· handler.apply():当代理对象是一个function函数,调用apply()方法时触发,比如proxy.apply()。
· handler.construct():当代理对象是一个function函数,通过new关键字实例化时触发,比如new proxy()。
结合这些handler,我们可以实现一些针对对象的限制操作; 禁止删除和修改对象的某个属性,代码如下:

let human = {
    name: 'jack',
    age: 20
}
let handler = {
    get: (obj, key,receiver) => {
        console.info('proxy get')
        return key in obj ? obj[key] : undefined
    },
    set: (obj, key, value, receiver) => {
        console.info('proxy set',`receiver==proxy1:${receiver==proxy1}`,`receiver==male:${receiver==male}`)
        if (key == 'name') {
            throw new Error(`can not change property ${key}`)
        }
        obj[key] = value
        return true
    },
    deleteProperty: (obj, key,receiver) => {
        console.info('proxy delete');
        if (key == 'name') {
            throw new Error(`can not delete property ${key}`)
            delete obj[key]
            return true
        }
    }
}
let proxy1 = new Proxy(human, handler)
//赋值name属性报错
proxy1.name = 100;  //Uncaught Error: can not change property name at Object.set
// 删除name属性报错
delete proxy1.name;  //Uncaught Error: can not delete property name at Object.deleteProperty

上面的代码中,set方法多了一个receiver参数,这个参数通常是Proxy本身(即p),但一种场景除外:当有一段代码执行male.moustache=true时,male不是一个proxy,且自身不含moustache属性,但是它的原型链上有一个proxy,那么那个proxy的handler中的set方法会被调用,而此时male会作为receiver参数传进来,如下事例:

let male = {}
Object.setPrototypeOf(male,proxy1)

//触发handler的set方法
proxy1.age = 30; //set方法输出 proxy set receiver==proxy1:true receiver==male:false
male.moustache = true; //set方法输出 proxy set receiver==proxy1:false receiver==male:true

Proxy也能监听到数组变化,代码如下:

let arr = [1]
let handler = {
    set: (target, key, value, receiver) => {
       console.info('Proxy set')
       return Reflect.set(target,key,value);
    }
}
let p = new Proxy(arr,handler);
p.push(2);
console.info(p);

Reflect.set()用于修改数组的值,返回布尔类型,也可以用在修改数组原型链上的方法的场景,相当于target[key] = value。

2. Proxy和响应式对象reactive

Vue 3中使用响应式对象的方法如下

import {ref,reactive} from 'vue'
setup(){
   const name = ref('test')
   const state = reactive({
     list: []
   })
   return {name,state}
}

在Vue 3中,组合式API中经常会使用创建响应式对象的方法ref/reactive,其内部就是利用Proxy API来实现的,特别是借助handler的set方法可以实现双向数据绑定相关的逻辑,这对于Vue 2中的Object.defineProperty()是很大的改变,主要提升如下:
· Object.defineProperty()只能单一地监听已有属性的修改或者变化,无法检测到对象属性的新增或删除(Vue 2中采用$set()方法来解决),而Proxy则可以轻松实现。
· Object.defineProperty()无法监听响应式数据类型是数组的变化(主要是数组长度的变化,Vue 2中采用重写数组相关方法并添加钩子来解决),而Proxy则可以轻松实现。
正是由于Proxy的特性,在原本使用Object.defineProperty()需要很复杂的方式才能实现的上面两种能力,在Proxy无须任何配置,利用其原生的特性就可以轻松实现。

3. ref()方法运行原理

在Vue 3的源码中,所有关于响应式的代码都在core/package/reactivity下,其中reactivity/src/index.ts中暴露了所有可以使用的方法。我们以常用的ref()方法为例,来看看Vue 3是如何利用Proxy的。
ref()方法的主要逻辑在reactivity/src/ref.ts中,其代码如下:

//入口文件
export function ref(value?: unknown) {
  return createRef(value, false)
}
// rawValue表示原始对象,shallow表示是否递归
// 如果本身已经是ref对象,则直接返回
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  // 创建一个新的RefImpl对象
  return new RefImpl(rawValue, shallow)
}

createRef这个方法接收的第二个参数是shallow,表示是否是递归监听响应式(shallow = true表示浅层非递归,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式),这个和另一个响应式方法shallowRef()是对应的。在RefImpl构造函数中,有一个_value 属性,这个属性是由toReactive()方法返回的,toReactive()方法则在reactivity/src/reactive.ts文件中,代码如下:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
      // 如果是递归,则调用toReactive    
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

在reactive.ts中,开始真正创建一个响应式对象,代码如下:

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 (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target, //原始对象
    false, //是否readonly 
    mutableHandlers, //proxy的handler对象的baseHandlers(get,set,has,deleteProperty,ownKeys)
    mutableCollectionHandlers, // proxy的handler对象collectionHandlers (get: /*#__PURE__*/ createInstrumentationGetter(false, false))
    reactiveMap // proxy对象映射
  )
}

再看下function createReactiveObject 的源码:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {  //如果不是object(是null,undefined),则不能创建响应式
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
 // 如果已经是proxy对象或者只读,则直接返回
  if ( 
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy ,已经创建过也直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  //只有复合类型的target才能被创建响应式
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
 // 调用Proxy Api
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  //标记该对象已经创建响应式
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject()方法传递了两种handler,分别是baseHandlers和collectionHandlers。如果target的类型是Map、Set、WeakMap、WeakSet,这些特殊对象则会使用collectionHandlers;如果target的类型是Object、Array,则会使用baseHandlers;如果是一个原始对象,则不会创建Proxy对象,reactiveMap会存储所有响应式对象的映射关系,用来避免同一个对象重复创建响应式。
createReactiveObject 主要逻辑为:

  • 防止只读和重复创建响应式。
  • 根据不同的target类型选择不同的handler。
  • 创建Proxy对象。
    我们在看下mutableHandlers的源码,具体要实现哪几个handler (packages\reactivity\src\baseHandlers.ts)
const get = /*#__PURE__*/ createGetter()

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

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

以handler.get为例,我们看下具体实现逻辑,当读取对象的某个属性时候就为调用get()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) { // 如果访问对象的key是__v_isReactive, 则直接返回常量
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) { // 如果访问对象的key是  __v_isReadonly,则直接返回常量
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) { // 如果访问对象的key是  __v_isShallow,则直接返回常量
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {// 如果访问对象的key是__v_raw,或者原始对象,只读对象等直接返回target
      return target
    }
 // 如果target是数组类型
    const targetIsArray = isArray(target)

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {  // 访问的key值是数组的原生方法,那么直接返回调用结果
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }
    //反射求值
    const res = Reflect.get(target, key, receiver)
   // 判断访问的key是否是Symbol或者不需要响应式的key,例如__proto__,__v_isRef,__isVue
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
// 收集响应式,为了后面的effect方法可以检测到
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
// 如果是非递归绑定,则直接返回结果
    if (shallow) {
      return res
    }
// 如果结果已经是响应式的,则先判断类型,再返回
    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
 // 如果当前key的结果也是一个对象,那么就要递归调用reactive方法对该对象再次执行响应式绑定逻辑
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
    // 返回结果
    return res
  }
}

createGetter 实现比较复杂,主要逻辑如下:

  • 对于handler.get方法来说,最终都会返回当前对象对应key的结果,即obj[key],所以这段代码最终会return结果。
  • 对于非响应式key、只读key等,直接返回对应的结果。
  • 对于数组类型的target,key值如果是原型上的方法,例如includes、push、pop等,则采用Reflect.get直接返回。
  • 在effect添加收集监听track,为响应式监听服务。
  • 当前key对应的结果是一个对象时,为了保证set方法能够被触发,需要循环递归地对这个对象进行响应式绑定,即递归调用reactive()方法。

我们再看下set的源码:

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)
      }
      // 如果旧值已经是一个RefImpl对象且新值不是RefImpl对象
      // 例如var v = Vue.reactive({a:1,b:Vue.ref({c:3})})场景的set
      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
    }
  // 用来判断是新增key还是更新key的值
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
 // 设置set结果,并添加监听effect逻辑
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
// 判断target有没有动过,包括在原型上添加或者删除某些项
    if (target === toRaw(receiver)) {
      if (!hadKey) {// 新增key的触发监听
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {// 更新key的触发监听
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
      // 返回set的结果 true/false
    return result
  }
}

handler.set方法的核心功能是设置key对应的值,即obj[key] = value,同时对新旧值进行逻辑判断和处理,最后添加trigger触发监听track逻辑,以便于触发effect。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容