Vue3 响应式原理(二)

简介: Vue3 响应式原理(二)

baseHandlers.ts 文件

baseHandlers.ts 文件中针对 4 种 proxy 实例定义了不对的处理器。

由于它们之间差别不大,所以在这只讲解完全响应式的处理器对象:

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

处理器对五种操作进行了拦截,分别是:

  1. get 属性读取
  2. set 属性设置
  3. deleteProperty 删除属性
  4. has 是否拥有某个属性
  5. ownKeys

其中 ownKeys 可拦截以下操作:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. Reflect.ownKeys()

其中 get、has、ownKeys 操作会收集依赖,set、deleteProperty 操作会触发依赖。

get

get 属性的处理器是用 createGetter() 函数创建的:

// /*#__PURE__*/ 标识此为纯函数 不会有副作用 方便做 tree-shaking
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // target 是否是响应式对象
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
      // target 是否是只读对象
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (
      // 如果访问的 key 是 __v_raw,并且 receiver == target.__v_readonly || receiver == target.__v_reactive
      // 则直接返回 target
      key === ReactiveFlags.raw &&
      receiver ===
        (isReadonly
          ? (target as any).__v_readonly
          : (target as any).__v_reactive)
    ) {
      return target
    }
    const targetIsArray = isArray(target)
    // 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
    // 如果不用 Reflect 来获取,在监听数组时可以会有某些地方会出错
    // 具体请看文章《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
    const res = Reflect.get(target, key, receiver)
    // 如果 key 是 symbol 并且属于 symbol 的内置方法之一,或者访问的是原型对象,直接返回结果,不收集依赖。
    if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
      return res
    }
    // 只读对象不收集依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 浅层响应立即返回,不递归调用 reactive()
    if (shallow) {
      return res
    }
    // 如果是 ref 对象,则返回真正的值,即 ref.value,数组除外。
    if (isRef(res)) {
      // ref unwrapping, only for Objects, not for Arrays.
      return targetIsArray ? res : res.value
    }
    if (isObject(res)) {
      // 由于 proxy 只能代理一层,所以 target[key] 的值如果是对象,就继续对其进行代理
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

这个函数的处理逻辑看代码注释应该就能明白,其中有几个点需要单独说一下:

  1. Reflect.get()
  2. 数组的处理
  3. builtInSymbols.has(key) 为 true 或原型对象不收集依赖
Reflect.get()

Reflect.get() 方法与从对象 (target[key]) 中读取属性类似,但它是通过一个函数执行来操作的。

为什么直接用 target[key] 就能得到值,却还要用 Reflect.get(target, key, receiver) 来多倒一手呢?

先来看个简单的示例:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
    }
})
p.push(100)

运行这段代码会报错:

Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'

但做一些小改动就能够正常运行:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
        return true // 新增一行 return true
    }
})
p.push(100)

这段代码可以正常运行。为什么呢?

区别在于新的这段代码在 set() 方法上多了一个 return true。我在 MDN 上查找到的解释是这样的:

set() 方法应当返回一个布尔值。

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。

这时我又试了一下直接执行 p[3] = 100,发现能正常运行,只有执行 push 方法才报错。到这一步,我心中已经有答案了。为了验证我的猜想,我在代码上加了 console.log(),把代码执行过程的一些属性打印出来。

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        console.log('get: ', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        target[key] = value
        return true
    }
})
p.push(100)
// get:  push
// get:  length
// set:  3 100
// set:  length 4

从上面的代码可以发现执行 push 操作时,还会访问 length 属性。推测执行过程如下:根据 length 的值,得出最后的索引,再设置新的置,最后再改变 length

结合 MDN 的解释,我的推测是数组的原生方法应该是运行在严格模式下的(如果有网友知道真相,请在评论区留言)。因为在 JS 中很多代码在非严格模式和严格模式下都能正常运行,只是严格模式会给你报个错。就跟这次情况一样,最后设置 length 属性的时候报错,但结果还是正常的。如果不想报错,就得每次都返回 true

然后再看一下 Reflect.set() 的返回值说明:

返回一个 Boolean 值表明是否成功设置属性。

所以上面代码可以改成这样:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})
p.push(100)

另外,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

通过上面的示例,不难理解为什么要通过 Reflect.set() 来代替 Proxy 完成默认操作了。同理,Reflect.get() 也一样。

数组的处理
// 如果 target 是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

在执行数组的 includes, indexOf, lastIndexOf 方法时,会把目标对象转为 arrayInstrumentations 再执行。

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    // 如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。
    // 所以这里的 this 指向 receiver,即 proxy 实例,toRaw 为了取得原始数据
    const arr = toRaw(this) as any
    // 对数组的每个值进行 track 操作,收集依赖
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    // 参数有可能是响应式的,函数执行后返回值为 -1 或 false,那就用参数的原始值再试一遍
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

从上述代码可以看出,Vue3.0 对 includes, indexOf, lastIndexOf 进行了封装,除了返回原有方法的结果外,还会对数组的每个值进行依赖收集。

builtInSymbols.has(key) 为 true 或原型对象不收集依赖
const p = new Proxy({}, {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})
p.toString() // get:  toString
             // get:  Symbol(Symbol.toStringTag)
p.__proto__  // get:  __proto__

p.toString() 的执行结果来看,它会触发两次 get,一次是我们想要的,一次是我们不想要的(我还没搞明白为什么会有 Symbol(Symbol.toStringTag),如果有网友知道,请在评论区留言)。所以就有了这个判断: builtInSymbols.has(key)true 就直接返回,防止重复收集依赖。

再看 p.__proto__ 的执行结果,也触发了一次 get 操作。一般来说,没有场景需要单独访问原型,访问原型都是为了访问原型上的方法,例如 p.__proto__.toString() 这样使用,所以 key 为 __proto__ 的时候也要跳过,不收集依赖。

set

const set = /*#__PURE__*/ createSetter()
// 参考文档《Vue3 中的数据侦测》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 如果原来的值是 ref,但新的值不是,将新的值赋给 ref.value 即可。
      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 = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 如果 target 没有 key,就代表是新增操作,需要触发依赖
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果新旧值不相等,才触发依赖
        // 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter
        // 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变
        // 但由于 length 也是自身属性,所以 value === oldValue
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set() 的函数处理逻辑反而没那么难,看注释即可。track()trigger() 将放在下面和 effect.ts 文件一起讲解。

deleteProperty、has、ownKeys

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  // 如果删除结果为 true 并且 target 拥有这个 key 就触发依赖
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}
function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

这三个函数比较简单,看代码即可。

目录
相关文章
|
24天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
21天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
50 7
|
26天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
40 9
|
23天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
41 3
|
21天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
39 1
|
21天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
44 1
|
24天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
25天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
3天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
20 0
|
28天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
25 0