杭州专业建设网站哪里好,themegallery模板网,wordpress用户登录显示请求失败,网站幻灯片js代码通过上一篇文章已经初始化项目#xff0c;集成了ts和jest。本篇实现Vue3中响应式模块里的reactive方法。
前置知识要求
如果你熟练掌握Map, Set, Proxy, Reflect#xff0c;可直接跳过这部分。
Map
Map是一种用于存储键值对的集合#xff0c;并且能够记住键的原始插入顺…通过上一篇文章已经初始化项目集成了ts和jest。本篇实现Vue3中响应式模块里的reactive方法。
前置知识要求
如果你熟练掌握Map, Set, Proxy, Reflect可直接跳过这部分。
Map
Map是一种用于存储键值对的集合并且能够记住键的原始插入顺序。 其中键和值可以是任意类型的数据。
初始化添加获取
let myMap new Map()myMap.set(name, wendZzoo)
myMap.set(age, 18)myMap.get(name)
myMap.get(age)Map 中的一个键只能出现一次它在 Map 的集合中是独一无二的重复设置的会被覆盖
myMap.set(name, jack)Map 的键和值可以是任意类型的数据
myMap.set({name: wendZzoo}, [{age: 18}])删除
let myMap new Map()
myMap.set(name, Tom)
myMap.delete(name)key数据类型是对象时需要使用对应的引用来删除键值对
let myMap new Map()
let key [{name: Tom}]
myMap.set(key, Hello)
myMap.delete(key)// 如果使用不同的引用来尝试删除键值对
// 它将无法正常工作
// 因为Map无法识别这两个引用是相同的键
myMap.set([{name: Tom}], Hello)
myMap.delete([{name: Tom}])Set
Set是一种集合数据结构它允许存储唯一的值无重复项。Set对象可以存储任何类型的值包括基本类型和对象引用。
let mySet new Set()mySet.add(wendZzoo)
mySet.add(18)
mySet.add({province: jiangsu, city: suzhou})可迭代
for (let key of mySet) {console.log(key)
}Proxy
Proxy 对象用于创建一个对象的代理从而实现基本操作的拦截和自定义如属性查找、赋值、枚举、函数调用等。
Vue 响应式的前提就是需要数据劫持在 JS 中有两种劫持 property 访问的方式getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制而在 Vue 3 中则使用了 Proxy 来创建响应式对象。
创建 Proxy 对象时需要提供两个参数目标对象 target被代理的对象和一个处理程序对象 handler用于定义拦截行为的方法。
其中 handler 常用的有 getset 方法。
handler.get() 方法用于拦截对象的读取属性操作完整使用可以参考MDN
它接收三个参数
target目标对象property被获取的属性名receiverProxy 或者继承 Proxy 的对象
const obj {name: wendZzoo, age: 18}
let myProxy new Proxy(obj, {get: (target, property, receiver) {console.log(收集依赖)return target[property]}
})// 执行 myProxy.name
// 执行 myProxy.agehandler.set() 方法是设置属性值操作的捕获器完整使用参考MDN
它接收四个参数
target目标对象property将被设置的属性名或 Symbol、value新属性值receiver最初被调用的对象。通常是 proxy 本身但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用因此不一定是 proxy 本身
const obj {name: wendZzoo, age: 18}
let myProxy new Proxy(obj, {get: (target, property, receiver) {console.log(收集依赖)return target[property]},set: (target, property, value, receiver) {console.log(触发依赖)target[property] valuereturn true}
})
myProxy.name Jack
myProxy.age 20Proxy 提供了一种机制通过拦截和修改目标对象的操作来实现自定义行为在 get 和 set 方法打印日志的地方也就是 Vue3 实现依赖收集和触发依赖的地方。
Reflect
Reflect 是一个内置的对象它提供拦截 JS 操作的方法。这让它可以完美的和 Proxy 配合Proxy 提供了对对象拦截的时机位置Reflect 提供拦截方法。
Reflect 不是一个构造函数因此不能 new 进行调用更像 Math 对象作为一个函数来调用它所有的属性和方法都是静态的。
常用的方法有 getset。
Reflect.get方法允许你从一个对象中取属性值完整使用参考MDN
它接收三个参数
target需要取值的目标对象propertyKey需要获取的值的键值receiver如果 target 对象中指定了getterreceiver 则为 getter 调用时的this值
let obj {name: wendZzoo, age: 18}
Reflect.get(obj, name)
Reflect.get(obj, age)Reflect.set 方法允许在对象上设置属性完整使用参考MDN
它接收三个参数
target设置属性的目标对象propertyKey设置的属性的名称value设置的值receiver如果遇到 setterreceiver则为setter调用时的this值
let obj {}
Reflect.set(obj, name, wendZzoo)let arr [name, address]
Reflect.set(arr, 1, age)
Reflect.set(arr, length, 1)更改目录
src下新建文件夹reactivity新建effect.ts和reactive.ts。
tests文件夹下删除上一篇文章中用于验证jest安装的index.spec.ts新建effect.spec.ts和reactive.spec.ts。
reactive
先写单测明确需要的成果再根据这个需求来实现函数。Vue3的reactive方法返回一个对象的响应式代理那代理的对象和源对象是不同的但是又能和源对象一样的嵌套结构。
那单测可以这样写reactive.spec.ts
import { reactive } from ../reactivity/reactive;describe(reactive, () {it(happy path, () {let original { foo: 1 };let data reactive(original);expect(data).not.toBe(original);expect(data.foo).toBe(1);});
});根据这两个断言来实现现阶段的reactive方法。Vue3中是使用Proxy实现。
reactive.ts
export function reactive(raw) {return new Proxy(raw, {get: (target, key) {let res Reflect.get(target, key);// TODO 依赖收集return res;},set: (target, key, value) {let res Reflect.set(target, key, value);// TODO 触发依赖return res;},});
}运行reactive单测来验证该方法实现是否正确执行yarn test reactive effect
在官网上是没有单独提到这个 API 的可以在进阶主题的深入响应式系统一篇中找到它的身影。
effect直接翻译为作用意思是使其发生作用这个使其的其就是我们传入的函数所以effect的作用就是让我们传入的函数发生作用也就是执行这个函数。
使用示例
import { reactive, effect } from vue;let user reactive({age: 10,
});let nextAge;function setAge() {effect(() {nextAge user.age 1;});console.log(nextAge);
}function updateAge() {user.age;console.log(nextAge);
}在没有使用effect作用于nextAge时直接触发updateAge方法输出的nextAge就是undefined
调用setAgeeffect中函数执行给nextAge赋值响应式数据user中age变化nextAge也在继续执行effect中函数。 单测
那effect的单测可以写成这样
import { effect } from ../reactivity/effect;
import { reactive } from ../reactivity/reactive;describe(effect, () {it(happy path, () {let user reactive({age: 10,});let nextAge;effect(() {nextAge user.age 1;});expect(nextAge).toBe(11);});
});effect方法就是接收一个方法并执行它。
effect.ts
class ReactiveEffect {private _fn: any;constructor(fn) {this._fn fn;}run() {this._fn();}
}export function effect(fn) {let _effect new ReactiveEffect(fn);_effect.run();
}通过抽离成一个Class类去执行传入的 fn 参数。
再来执行所有的单测验证是否成功执行yarn test 依赖收集
修改effect单测增加一个断言来判断当age变化时nextAge是否也更新了
import { effect } from ../reactivity/effect;
import { reactive } from ../reactivity/reactive;describe(effect, () {it(happy path, () {let user reactive({age: 10,});let nextAge;effect(() {nextAge user.age 1;});expect(nextAge).toBe(11);// updateruser.age;expect(nextAge).toBe(12);});
});执行单测发现无法通过是因为Proxy代理时候并没有实现依赖收集和触发依赖也就是reactive.ts中还有两个 TODO。
但是首先得清楚什么叫依赖
引用官方的例子
let A0 1
let A1 2
let A2 A0 A1console.log(A2) // 3A0 2
console.log(A2) // 仍然是 3当我们更改 A0 后A2 不会自动更新。
那么我们如何在 JavaScript 中做到这一点呢首先为了能重新运行计算的代码来更新 A2我们需要将其包装为一个函数
let A2function update() {A2 A0 A1
}然后我们需要定义几个术语
这个 update() 函数会产生一个副作用或者就简称为作用 (effect)因为它会更改程序里的状态。A0 和 A1 被视为这个作用的依赖 (dependency)因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。
因此我们可以大胆通俗的讲依赖就是指的是观察者通常是视图或副作用函数对数据的依赖关系。当观察者需要访问特定数据时它就成为该数据的依赖。
那依赖收集呢
依赖收集是用于追踪和管理数据依赖关系。常用于实现响应式系统其中数据的变化会自动触发相关的更新操作。
当数据发生改变时相关的视图或操作也能够自动更新以保持数据和界面的同步。依赖收集可以帮助我们建立起数据和视图之间的关联确保数据的变化能够自动反映在视图上。
从代码层面讲读取对象的时候也就是get操作时进行依赖收集将目标对象target对象中keyDep实例做关联映射。 在effect.ts中定义依赖收集的方法track。
class ReactiveEffect {private _fn: any;constructor(fn) {this._fn fn;}run() {reactiveEffect this;this._fn();}
}let targetMap new Map();
export function track(target, key) {// target - key - deplet depMap targetMap.get(target);if (!depMap) { // initdepMap new Map();targetMap.set(target, depMap);}let dep depMap.get(key);if (!dep) { // initdep new Set();depMap.set(key, dep);}dep.add(reactiveEffect);
}let reactiveEffect;
export function effect(fn) {let _effect new ReactiveEffect(fn);_effect.run();
}触发依赖
在设置对象属性时也就是进行set操作时触发依赖。将每个属性上挂载的dep的Set结构中的所有作用函数执行。
export function trigger(target, key) {let depMap targetMap.get(target);let dep depMap.get(key);for (const effect of dep) {effect.run();}
}至此再次执行所有单测yarn test 总结
先通过单测入手明确需要实现的函数方法的功能分布实现功能点即拆分功能点先初步实现了reactive方法简单版只要求原数据和代理之后的数据不同但是数据结构又要一样像深拷贝一样。通过Class类实现effect方法可以自执行其传入的函数参数依赖收集通过两个Map结构和一个Set结构来映射数据关系将所有的fn存放到dep中。通过一个全局变量reactiveEffect来获取到effct实例为后续触发依赖时直接拿dep中每一项去执行。触发依赖通过映射关系获取到dep因为dep是Set结构可迭代循环每项执行。