专栏分享:vue2源码专栏vue3源码专栏vue router源码专栏玩具项目专栏,硬核推荐

欢迎各位ITer关注点赞收藏

背景

以下是柏成根据Vue3官方课程整理的响应式书面文档 - 第二节,课程链接在此:Proxy and Reflect - Vue 3 Reactivity | Vue Mastery

本篇文章将解决 上一篇文章 结尾遗留的问题:如何让代码自动实现响应性? 换句话说就是,如何让我们的 effect 自动保存 & 自动重新运行?

上一篇文章 中,我们最终运行的代码长这样

聪明的你会立马发现,我们现在仍要手动调用 track() 来保存 effect;手动调用 trigger() 来运行 effects,这不是脱裤子放屁么

我们想让我们的响应性引擎自动调用 track()trigger()。那么问题就来了,何时才是调用它们的最好时机呢?

从逻辑上来说,如果访问了对象的属性,就是我们调用 track() 去保存 effect 的最佳时机;如果对象的属性改变了,就是我们调用 trigger() 来运行 effects 的最佳时机

所以问题变成了,我们该如何拦截对象属性的访问和赋值操作?

Proxy(代理)

MDN 上的 Proxy 对象是这样定义的

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

也可以理解为在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,生活中也有许许多多的“proxy”, 如代购,中介,因为他们所有的行为都不会直接触达到目标对象

语法

  • target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

  • handler: 一个通常以函数作为属性的对象,用来定制拦截行为;它包含有 Proxy 的各个捕获器(trap),例如 handler.get() / handler.set()

const p = new Proxy(target, handler)

常用方法

比较常用的两个方法就是 get()set() 方法

方法 描述
handler.get(target, key, ?receiver) 属性读取操作的捕捉器
handler.set(target, key, value, ? receiver) 属性设置操作的捕捉器

handler.get

用于代理目标对象的属性读取操作,其接受三个参数 handler.get(target, propKey, ?receiver)

  • target: 目标对象
  • key: 属性名
  • receiver: Proxy 本身或者继承它的对象,后面会重点介绍

举个栗子

const origin = {}
const obj = new Proxy(origin, {
get: function (target, key, receiver) {
return 10
}
}) obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

在这个栗子中,我们给一个空对象 origin 的 get 架设了一层代理,所有 get 操作都会直接返回我们定制的数字10

需要注意的是,代理只会对 proxy 对象生效,如访问上方的 origin 对象就没有任何效果

handler.set

用于代理目标对象的属性设置操作,其接受四个参数 handler.set(target, key, value, ?receiver)

  • target: 目标对象
  • key: 属性名
  • value: 新属性值
  • receiver: Proxy 本身或者继承它的对象,后面会重点介绍
const obj = new Proxy({}, {
set: function(target, key, value, receiver) {
target[key] = value
console.log('property set: ' + key + ' = ' + value)
return true
}
}) 'a' in obj // false
obj.a = 10 // "property set: a = 10"
'a' in obj // true
obj.a // 10

Reflect(反射)

MDN 上的 Reflect 对象是这样定义的

Reflect 是一个内建的对象,用来提供方法去拦截 JavaScript的操作。Reflect 不是一个函数对象,所以它是不可构造的,也就是说你不能通过 new操作符去新建一个 Reflect对象或者将 Reflect对象作为一个函数去调用。Reflect的所有属性和方法都是静态的(就像Math对象)

常用方法

Reflect对象挂载了很多静态方法,所谓静态方法,就是和 Math.round() 这样,不需要 new 就可以直接使用的方法。

比较常用的两个方法就是 get()set() 方法:

方法 描述
Reflect.get(target, key, ?receiver) 和 target[key] 类似,从对象中读取属性值
Reflect.set(target, key, value, ? receiver) 和 target[key] = value 类似,给对象的属性设置一个新值

Reflect.get()

Reflect.get方法允许你从一个对象中取属性值,返回值是这个属性值

Reflect.set()

Reflect.set 方法允许你在对象上设置属性,返回值是 Boolean 值,代表是否设置成功

  • target: 目标对象
  • key: 属性名
  • value: 新属性值
  • receiver: 后面会重点介绍
Reflect.get(target, key[, receiver])
// 等同于
target[key] Reflect.set(target, key, value[, receiver])
// 等同于
target[key] = value

举个栗子

let product = {price: 5, quantity: 2}

// 以下三种方法是等效的
product.quantity
product['quantity']
Reflect.get(product, 'quantity') // 以下三种方法是等效的
product.quantity = 3
product['quantity'] = 3
Reflect.set(product, 'quantity', 3)

关于receiver参数

在 Proxy 和 Reflect 对象中 get/set() 方法的最后一个参数都是 receiver,它到底是个什么玩意?

receiver 是接受者的意思,译为接收器

  1. 在 Proxy trap 的场景下(例如 handler.get() / handler.set()), receiver 永远指向 Proxy 本身或者继承它的对象,比方说下面这个例子
let origin = { a: 1 }

let p = new Proxy(origin, {
get(target, key, receiver) {
return receiver
},
}) let child = Object.create(p) p.getReceiver // Proxy {a: 1}
p.getReceiver === p // true
child.getReceiver // {}
child.getReceiver === child // true
  1. 在 Reflect.get / Reflect.set() 的场景下,receiver 可以改变计算属性中 this 的指向
let target = {
firstName: 'li',
lastName: 'baicheng',
get a() {
return `${this.firstName}-${this.age}`
},
set b(val) {
console.log('>>>this', this)
this.firstName = val
},
} Reflect.get(target, 'a') // li-undefined
Reflect.get(target, 'a', { age: 24 }) // undefined-24 Reflect.set(target, 'b', 'huawei', { age: 24 })
// >>>this {age: 24}
// true

搭配Proxy

在 Proxy 里使用 Reflect,我们会有一个附加参数,称为 receiver (接收器),它将传递到我们的 Reflect调用中。它保证了当我们的对象有继承自其它对象的值或函数时, this 指针能正确的指向对象,这将避免一些我们在 vue2 中有的响应式警告

let origin = { a: 1 }

let p = new Proxy(origin, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
},
})

Reflect对象经常和Proxy代理一起使用,原因有三点:

  1. Reflect提供的所有静态方法和Proxy第2个handle对象中的方法参数是一模一样的,例如Reflect的 get/set() 方法需要的参数就是Proxy get/set() 方法的参数

  2. Proxy get/set() 方法需要的返回值正是Reflect的 get/set() 方法的返回值,可以天然配合使用,比直接对象赋值/获取值要更方便和准确

  3. receiver 参数具有不可替代性!!!

    在下面示例中,我们在页面中访问了 alias 对应的值,稍后 name 变化了,要重新渲染么?

    target[key] 方式访问 proxy.alias 时,获取到 this.name,此时 this 指向 target,无法监控到 name ,不能重新渲染

    Reflect 方式访问 proxy.alias 时,获取到 this.name,此时 this 指向 proxy,可监控到 name ,可以重新渲染

const target = {
name: '柏成',
get alias() {
console.log('this === target', this === target)
console.log('this === proxy', this === proxy)
return this.name
},
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('key:', key)
return target[key]
// return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
},
})
proxy.alias

使用 target[key] 打印结果:

使用 Reflect 打印结果:

如何用(How)

让我们创建一个称为 reactive 的函数,如果你使用过Composition API,你会感觉很熟悉。然后再封装一下我们的 handler 方法,让它长得更像 Vue3 的源代码,最后我们将创建一个新的 Proxy对象

代码如下

function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// 保存effect
track(target, key)
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
// 运行effect
trigger(target, key)
}
return result
},
} return new Proxy(target, handler)
} let product = reactive({ price: 5, quantity: 2 })

现在我们已经不再需要手动调用 track()trigger()

让我们分析一下上图内容

  1. 现在我们的响应式函数返回一个 product 对象的代理,我们还有变量 total ,方法 effect()

  2. 当我们运行 effect() ,试图获取 product.price 时,它将运行track(product, 'price')

  3. targetMap 里,它将为 product 对象创建一个新的映射,它的值是一个新的 depsMap ,这将映射 price 属性得到一个新的 dep ,这个 dep就是一个 effects集(Set),把我们 total 的 effect加到这个集(Set)中

  4. 我们还会访问 product.quantity ,这是另一个get请求。我们将会调用track(product, 'quantity')。这将访问我们 product 对象的 depsMap,并添加一个 quantity 属性到一个新的 dep 对象的映射

  5. 然后我们把 total 打印到控制台是 10

  6. 然后我们运行product.quantity = 3,它会调用 trigger(product, 'quantity'),然后运行被存储的所有 effect

  7. 调用 effect() , 就会访问到 product.price ,触发track(product, 'price');访问到 product.quantity ,则触发track(product, 'quantity')

ActiveEffect

我们每访问一次Proxy实例属性,都将会调用一次 track 函数。然后它会去历遍 targetMap、depsMap,以确保当前 effect 会被记录下来,这不合理,不需要多次添加 effect

这不是我们想要的,我们只应该在 effect() 里调用 track 函数

console.log('Update quantity to = '+ product.quantity)
console.log('Update price to = '+ product.price)

为此,我们引入了 activeEffect 变量,它代表现在正在运行中的 effect, Vue3 也是这样做的,代码如下

let activeEffect = null
...
// 负责收集依赖
function effect(eff){
activeEffect = eff
activeEffect() // 运行
activeEffect = null //复位
} // 我们用这个函数来计算total
effect(() => {
total = product.price * product.quantity
})

现在我们需要新的 track() 函数,让它去使用这个新的 activeEffect 变量

function track(target, key){
// 关键!!!
// 我们只想在我们有activeEffect时运行这段代码
if(!activeEffect) return let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
//当我们添加依赖(dep)时我们要添加activeEffect
dep.add(activeEffect)
}

这样就保证了,如果不是通过 effect() 函数去访问Proxy实例属性,则这时的 activeEffect 为 null ,进入 track() 函数立即就被 return 掉了

完整代码

这样一来,我们就实现了 Vue3 基本的响应性了。完整代码如下

// The active effect running
let activeEffect = null // For storing the dependencies for each reactive object
const targetMap = new WeakMap() // 负责收集依赖
function effect(eff) {
activeEffect = eff
activeEffect() // 运行
activeEffect = null //复位
} // Save this code
function track(target, key) {
// 关键!!!
// 我们只想在我们有activeEffect时运行这段代码
if (!activeEffect) return let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
console.log('>>>track', target, key)
//当我们添加依赖(dep)时我们要添加activeEffect
dep.add(activeEffect)
} // Run all the code I've saved
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
let dep = depsMap.get(key)
if (dep) {
console.log('>>>trigger', target, key)
dep.forEach(eff => {
eff()
})
}
} // 响应式代理
function reactive(target) {
// 如果不是对象或数组
// 抛出警告,并返回目标对象
if (!target || typeof target !== 'object') {
console.warn(`value cannot be made reactive: ${String(target)}`)
return target
}
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
track(target, key) // 递归创建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return reactive(target[key])
}
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
} let product = reactive({ price: 5, quantity: 2, rate: { value: 0.9 } })
let total = 0 effect(() => {
total = product.price * product.quantity * product.rate.value
})

控制台打印结果如下

参考资料

【Vue3响应式原理#02】Proxy and Reflect的更多相关文章

  1. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  2. vue3响应式原理以及ref和reactive区别还有vue2/3生命周期的对比,第二天

    前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...

  3. vue2响应式原理与vue3响应式原理对比

    VUE2.0 核心 对象:通过Object.defineProtytype()对对象的已有属性值的读取和修改进行劫持 数组:通过重写数组更新数组一系列更新元素的方法来实现元素的修改的劫持 Object ...

  4. 第三十六篇:vue3响应式(关于Proxy代理对象,Reflect反射对象)

    好家伙,这个有点难. 1.代理对象Proxy Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找.赋值.枚举.函数调用等). 拦截对象中任意属性的变化,包括:查get, ...

  5. vue3响应式模式设计原理

    vue3响应式模式设计原理 为什么要关系vue3的设计原理?了解vue3构建原理,将有助于开发者更快速上手Vue3:同时可以提高Vue调试技能,可以快速定位错误 1.vue3对比vue2 vue2的原 ...

  6. vue3 第二天vue响应式原理以及ref和reactive区别

    前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...

  7. vue3剖析:响应式原理——effect

    响应式原理 源码目录:https://github.com/vuejs/vue-next/tree/master/packages/reactivity 模块 ref: reactive: compu ...

  8. 详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  9. Vue响应式原理的实现-面试必问

    Vue2的数据响应式原理 1.什么是defineProperty? defineProperty是设置对象属性,利用属性里的set和get实现了响应式双向绑定: 语法:Object.definePro ...

  10. vue2.0与3.0响应式原理机制

    vue2.0响应式原理 - defineProperty 这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截 ...

随机推荐

  1. 2021-3-9 保存csv格式文件

    public void SaveCSV(DataTable dt, string fullPath) { FileInfo fi = new FileInfo(fullPath); if (!fi.D ...

  2. React: React-Router嵌套路由 exact问题

    说明 当使用嵌套路由时,不能在父路由中添加exact,因为要先匹配父路由才能匹配子路由 父路由 子路由 效果如下所示 参考链接 https://www.jianshu.com/p/8bc3251079 ...

  3. chrome浏览器插件react devtools、redux devtools,无需安装、解压即可用

    react devtools用于调试react代码,可以查看到props.state的值,以及定义的hooks,而redux devtools可以追踪到action的派发.store的变化,两个都是r ...

  4. 基于Pair-wise和CrossEncoder训练单塔模型

    本文分享自华为云社区<语义检索系统排序模块:基于ERNIE-Gram的Pair-wise和基于RocketQA的CrossEncoder训练单塔模型>,作者: 汀丶. 文本匹配任务数据每一 ...

  5. Pixi.js的使用整理

    最近在做的一个前端项目中,有一些图像的处理操作(3D图,2D图都有),其中3D图也是通过获取后端服务的图像2D数据进行绘制展示,通过鼠标各种操作调用后端服务来进行重新获取新图数据.这里前端设计到图像的 ...

  6. Azure Data Factory(六)数据集类型为Dataverse的Link测试

    一,引言 之前有讲过 Azure Data Factory 的 Copy Data 的操作,演示了将 Blob Storage1 的数据通过 Azure Data Factory 复制到 Blob S ...

  7. trino on yarn

    一.前言 最近在研究trino on yarn 功能,网上大部分都是关于presto on yarn文章,关于trino on yarn 资料很少,但是本质上差不多,需要修改一些内容比,主要在调试方面 ...

  8. yum&二进制安装PostgreSQL 12

    一.yum安装&配置PostgreSQL 12 目录 一.yum安装&配置PostgreSQL 12 一.前言 1.本文主要内容 2.本文环境信息与适用范围 二.PostgreSQL安 ...

  9. 使用TorchLens可视化一个简单的神经网络

      TorchLens:可用于可视化任何PyTorch模型,一个包用于在一行代码中提取和映射PyTorch模型中每个张量运算的结果.TorchLens功能非常强大,如果能够熟练掌握,算是可视化PyTo ...

  10. 其它——MyCat实现分库分表

    文章目录 MyCat实现分库分表 一 开源数据库中间件-MyCat 二 MyCat简介 三 MyCat下载及安装 3.1 MySQL安装与启动 3.2使用docker启动多个数据库 3.3 MyCat ...