专栏分享: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. 文件系统:ext4 的 block 分布(1G分区为例)

    总的block数量:262144 $ dumpe2fs /dev/vg01/test | grep "Block count" dumpe2fs 1.42.9 (28-Dec-20 ...

  2. Python和PyTorch深入实现线性回归模型:一篇文章全面掌握基础机器学习技术

    1. 简介 1.1 线性回归模型概述 线性回归是一种统计学中的预测分析,该方法用于建立两种或两种以上变量间的关系模型.线性回归使用最佳的拟合直线(也称为回归线)在独立(输入)变量和因变量(输出)之间建 ...

  3. 【技术积累】Linux中的命令行【理论篇】【一】

    7z命令 命令介绍 7z命令是Linux系统中的一个压缩和解压缩工具,它可以用来创建.压缩和解压缩7z格式的文件.7z是一种高压缩率的文件格式,通常比其他常见的压缩格式(如zip和gzip)具有更高的 ...

  4. echarts-for-react:实时更新数据

    解决方案 echarts 注解 详细链接 https://echarts.apache.org/zh/api.html#echartsInstance.setOption 参考链接 https://b ...

  5. 深度系统安装mysql

    # 安装 Mysql 8.0.19下载 MySQL Community Server 8.0.19 [Compressed TAR Archive](mysql-8.0.19-linux-glibc2 ...

  6. LangChain:打造自己的LLM应用

    1.LangChain是什么 LangChain是一个框架,用于开发由LLM驱动的应用程序.可以简单认为是LLM领域的Spring,以及开源版的ChatGPT插件系统.核心的2个功能为: 1)可以将 ...

  7. Xshell使用技巧及常用配置

    Xshell使用 1.调整 Xshell 的终端显示和回滚缓冲区大小 磨刀不误砍柴工,为了更方便地学习 Linux,首先得对终端进行一些调整,步骤如下: 首先通过 xshell 顶部菜单中的文件--& ...

  8. 关于预处理器 sass 的超全用法

    随着用户需求的增加,应用于页面的 css 代码越来越复杂越发臃肿难以维护,但是又没有 css 的替代品,css 预处理器作为 css 的扩展,出现在前端技术中. sass 是 css 预处理器中常用的 ...

  9. 免费拥有自己的 Github 资源加速器

    TurboHub 是一个免费的 Github 资源加速下载站点,可以帮助你快速下载 Github 上的资源.其核心逻辑是通过 Azure Static Web Apps 服务和 Azure Funct ...

  10. JOIN 关联表中 ON、WHERE 后面跟条件的区别

    SQL中join连接查询时条件放在on后与where后的区别 数据库在通过连接两张或多张表来返回记录时,都会生成一张中间的临时表,然后再将这张临时表返回给用户. 在使用left jion时,on和wh ...