网站首页 > 技术文章 正文
前言
上一篇,我们解析了尤雨溪的初级版响应式代码,一定觉得初级版的响应式和我们平常用的vue3相差太远了,定义一个响应式对象,竟然还要手写get和set,而我们日常使用vue3来定义一个响应式对象只需要用ref或者reactive包起来。
那么,这一篇我们看看尤雨溪对初级版做了哪些升级。
封装reactive
上一版中,定义响应式变量需手写get和set:
const state = {
get count() {
dep.depend()
return actualCount
},
set count(newCount) {
actualCount = newCount
dep.notify()
}
}
那么这一版首先就将这一步封装起来。
function reactive(raw) {
// use Object.defineProperty
// 1. iterate over the existing keys
Object.keys(raw).forEach(key => {
// 2. for each key: create a corresponding dep
const dep = new Dep()
// 3. rewrite the property into getter/setter
let realValue = raw[key]
Object.defineProperty(raw, key, {
get() {
// 4. call dep methods inside getter/setter
dep.depend()
return realValue
},
set(newValue) {
realValue = newValue
dep.notify()
}
})
})
return raw
}
封装后,定义响应式变量只需要:
const state = reactive({
count: 0
})
现在看来,是不是和vue3的写法一样了呢?
接下来我们解读下这个优化。
首先翻译几个名词,为什么想翻译一下呢?因为我觉得尤雨溪写的代码命名非常好,一段代码读下来,就像看小说,即使不懂编程,也会大概知道是做什么。
reactive: 反应的
raw: 未加工的、不成熟的
这两个变量名取的好呀,一眼看过去,函数reactive接受一个未加工的变量raw,然后返回了raw。咱们不看内容,就可以猜到reactive函数做了什么。
下面我们来看看reactive函数的内容(代码中的英文注释也是尤雨溪写的哦)。
传进来的raw对象,我们对key进行遍历,取得key对应的value值后,立即重写raw。
这里用到Object.defineProperty(obj, prop, descriptor),这个方法会直接在一个对象obj上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中obj是要定义属性的对象,prop是要定义或修改的属性的key,descriptor是更新或定义的属性的描述。
所以这里重写时,给key值对应的value绑定了get和set函数,实现了上一版手动绑定的过程。
然后优化watchEffect,执行effect后立刻将activeEffect置空,防止不必要的订阅行为。
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
再次进阶
上面的优化比较小,只是简化了响应式变量的定义,接下来我们看看尤雨溪还能做什么优化。
首先,重写了Dep类。
之前的Dep类比较简单,只有一个发布订阅模式。
let activeEffect
class Dep {
subscribers = new Set()
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => effect())
}
}
优化后:
let activeEffect
class Dep {
// imeplement this
subscribers = new Set()
constructor(value) {
this._value = value
}
get value() {
this.depend()
return this._value
}
set value(value) {
this._value = value
this.notify()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach((effect) => {
effect()
})
}
}
多了一个叫_value的私有变量,并且这个_value是响应式的。然后写了一个reactiveHandlers函数:
// proxy version
const reactiveHandlers = {
get(target, key) {
// how do we get the dep for this key?
const value = getDep(target, key).value
if (value && typeof value === 'object') {
return reactive(value)
} else {
return value
}
},
set(target, key, value) {
getDep(target, key).value = value
}
}
接着是getDep函数:
const targetToHashMap = new WeakMap()
function getDep(target, key) {
let depMap = targetToHashMap.get(target)
if (!depMap) {
depMap = new Map()
targetToHashMap.set(target, depMap)
}
let dep = depMap.get(key)
if (!dep) {
dep = new Dep(target[key])
depMap.set(key, dep)
}
return dep
}
最后就是用Proxy替代defineProperty重写reactive函数:
function reactive(obj) {
return new Proxy(obj, reactiveHandlers)
}
这里补充下Proxy知识点:
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法是const p = new Proxy(target, handler),
target是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
示例:
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
};
const p = new Proxy({ a: 1 }, handler);
console.log(p.a, p.b); // 1, 37
补充完Proxy知识点,我们再去看reactive。
我们用reactiveHandlers代理obj,读写obj时会进入reactiveHandlers,读的时候,通过getDep获取value值,如果value是object类型,就将value变成响应式,然后返回value;写的时候就把新值写到getDep获取的value上。
在getDep函数中,用到了Map和WeakMap。
Map对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
getDep中,首先去targetToHashMap中获取target,并赋值给depMap,没有获取到的话,就将target作为键,new Map()生成的depMap作为值,存到targetToHashMap中。
然后在depMap中取key的值,取不到的话,就new Dep(target[key])作为value和key一起,组成键值对,加到depMap中。最后返回dep。
使用还是一样:
const state = reactive({
count: 0
})
watchEffect(() => {
console.log(state.count)
})
state.count++
在上面的代码中加上一些打印,使代码流程看得更加清楚:
可见getDep最后返回了一个Dep的实例。
还有什么不清楚的地方,自行断点调试。
结语
所以,看到这里,你明白Proxy比起defineProperty,在实现vue的响应式时,有什么好处吗?
后期后空,我们继续学习尤雨溪会怎么实现一个mini vue。
- 上一篇: JS中内存泄漏的几种情况
- 下一篇: 如何写出一个能让面试官直呼“666”的深拷贝?
猜你喜欢
- 2024-11-18 浏览器垃圾回收
- 2024-11-18 JavaScript中各种源码实现(前端面试笔试必备)
- 2024-11-18 2021年要了解的34种JavaScript优化技术
- 2024-11-18 你可能不知道的JS开发技巧
- 2024-11-18 Javascript面试题总结1
- 2024-11-18 深入JavaScript教你内存泄漏如何防范
- 2024-11-18 关于前端174道 JavaScript知识点汇总(一)
- 2024-11-18 前端面试计划(二)ES6
- 2024-11-18 2022前端大厂VUE 面试题
- 2024-11-18 javascript中的内置对象和数据结构
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-