八、ES7~ES12常用函数
ES7
Array Includes
- 在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。
- 在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。
const names = ["abc", "cba", "nba", "mba", NaN];
if (names.indexOf("cba") !== -1) {
console.log("包含abc元素");
}
// ES7 ES2016
if (names.includes("cba", 2)) {
console.log("包含abc元素");
}
if (names.indexOf(NaN) !== -1) {
console.log("包含NaN");
}
if (names.includes(NaN)) {
console.log("包含NaN___");
}
指数(乘方) exponentiation运算符
- 在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
- 在ES7中,增加了 ** 运算符,可以对数字来计算乘方。
const result1 = Math.pow(3, 3);
// ES7: **
const result2 = 3 ** 3;
console.log(result1, result2);
ES8
Object values
- 之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值:
const obj = {
name: "why",
age: 18
}
console.log(Object.keys(obj)) // [ 'name', 'age' ]
console.log(Object.values(obj)) // [ 'why', 18 ]
// 用的非常少
console.log(Object.values(["abc", "cba", "nba"]))
console.log(Object.values("abc"))
Object entries
- 通过Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
const obj = {
name: "why",
age: 18
}
console.log(Object.entries(obj)) // [ [ 'name', 'why' ], [ 'age', 18 ] ]
const objEntries = Object.entries(obj)
objEntries.forEach(item => {
console.log(item[0], item[1])
})
console.log(Object.entries(["abc", "cba", "nba"]))
console.log(Object.entries("abc"))
String Padding
- 某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的
const message = "Hello World";
const newMessage = message.padStart(15, "*").padEnd(20, "-");
console.log(newMessage);
// 案例 比如需要对身份证、银行卡的前面位数进行隐藏:
const cardNumber = "321324234242342342341312";
const lastFourCard = cardNumber.slice(-4);
const finalCard = lastFourCard.padStart(cardNumber.length, "*");
console.log(finalCard);
Trailing Commas
在ES8中,我们允许在函数定义和调用时多加一个逗号:
function foo(m, n) {}
foo(20, 30);
Object Descriptors
Object.create(prototype, descriptors) 以指定的原型创建对象,并且可以(可选) 的 设置对象的属性 Object.defineProperty(object, propertyname, descriptor) 对指定的对象的一个属性设置丰富的值控制
ES9
Async iterators:后续迭代器讲解
Object spread operators:
你可以通过展开操作符(Spread operator)...扩展一个数组对象和字符串。展开运算符(spread)是三个点(…),可以将可迭代对象转为用逗号分隔的参数序列。如同rest参数的逆运算。
Promise finally:后续讲Promise讲解
ES10
flat flatMap
-
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
-
flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
- 注意一:flatMap是先进行map操作,再做flat的操作;
- 注意二:flatMap中的flat相当于深度为1;
// 1.flat的使用
const nums = [
10,
20,
[2, 9],
[
[30, 40],
[10, 45],
],
78,
[55, 88],
];
const newNums = nums.flat();
console.log(newNums); // [ 10, 20, 2, 9, [ 30, 40 ], [ 10, 45 ], 78, 55, 88 ]
const newNums2 = nums.flat(2);
console.log(newNums2);
/*
[
10, 20, 2, 9, 30,
40, 10, 45, 78, 55,
88
] */
// 2.flatMap的使用
const nums2 = [10, 20, 30];
const newNums3 = nums2.flatMap((item) => {
return item * 2;
});
const newNums4 = nums2.map((item) => {
return item * 2;
});
console.log(newNums3); // [ 20, 40, 60 ]
console.log(newNums4);
// 3.flatMap的应用场景
const messages = ["Hello World", "hello lyh", "my name is coderwhy"];
const words = messages.flatMap((item) => {
return item.split(" ");
});
console.log(words);
/*
[
'Hello', 'World',
'hello', 'lyh',
'my', 'name',
'is', 'coderwhy'
] */
Object fromEntries
- 在前面,我们可以通过 Object.entries 将一个对象转换成 entries ,那么如果我们有一个entries了,如何将其转换成对象呢?
- ES10提供了 Object.formEntries 来完成转换:
const obj = {
name: "why",
age: 18,
height: 1.88,
};
const entries = Object.entries(obj);
console.log(entries);
const newObj = {};
for (const entry of entries) {
newObj[entry[0]] = entry[1];
}
// 1.ES10中新增了Object.fromEntries方法
const newObj2 = Object.fromEntries(entries);
console.log(newObj2);
// 2.Object.fromEntries的应用场景
const queryString = "name=why&age=18&height=1.88";
const queryParams = new URLSearchParams(queryString);
for (const param of queryParams) {
console.log(param);
}
const paramObj = Object.fromEntries(queryParams);
console.log(paramObj); // { name: 'why', age: '18', height: '1.88' }
trimStart trimEnd
- 去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
- ES10中给我们提供了trimStart和trimEnd;
const message = " Hello World "
console.log(message.trim())
console.log(message.trimStart())
console.log(message.trimEnd())
/*
Hello World
Hello World
Hello World
*/
Symbol description:已经讲过了
symbol.description是JavaScript中的内置属性,用于返回指定符号对象的可选描述。\
console.log(Symbol('desc').description);
// expected output: "desc"
Optional catch binding:后面讲解try cach讲解
ES11
BigInt
-
在早期的JavaScript中,我们不能正确的表示过大的数字:
大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。
-
那么ES11中,引入了新的数据类型BigInt,用于表示大的整数:
BitInt的表示方法是在数值的后面加上n
// ES11之前 max_safe_integer
const maxInt = Number.MAX_SAFE_INTEGER
console.log(maxInt) // 9007199254740991
console.log(maxInt + 1)
console.log(maxInt + 2)
// ES11之后: BigInt
const bigInt = 900719925474099100n
console.log(bigInt + 10n)
const num = 100
console.log(bigInt + BigInt(num))
const smallNum = Number(bigInt)
console.log(smallNum)
Nullish Coalescing Operator
- Nullish Coalescing Operator增加了空值合并操作符:
// ES11: 空值合并运算 ??
const foo = undefined;
// const bar = foo || "default value"
const bar = foo ?? "defualt value";
console.log(bar); // defualt value
Optional Chaining
- 可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:
const info = {
name: "why",
// friend: {
// girlFriend: {
// name: "hmm"
// }
// }
};
// console.log(info.friend.girlFriend.name);
if (info && info.friend && info.friend.girlFriend) {
console.log(info.friend.girlFriend.name);
}
// ES11提供了可选链(Optional Chainling)
console.log(info.friend?.girlFriend?.name);
console.log("其他的代码逻辑");
Global This
- 在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
- 比如在浏览器中可以通过this、window来获取;
- 比如在Node中我们需要通过global来获取;
- 那么在ES11中对获取全局对象进行了统一的规范:globalThis
// 获取某一个环境下的全局对象(Global Object)
// 在浏览器下
// console.log(window)
// console.log(this)
// 在node下
// console.log(global)
// ES11
console.log(globalThis);
for..in标准化
- 在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化。
- 在ES11中,对其进行了标准化,for...in是用于遍历对象的key的:
// for...in 标准化: ECMA
const obj = {
name: "why",
age: 18,
};
for (const item in obj) {
console.log(item);
}
Dynamic Import:
后续ES Module模块化中讲解。
Promise.allSettled:
后续讲Promise的时候讲解。
import meta:
后续ES Module模块化中讲解
ES12
FinalizationRegistry
-
FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。
- FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );
- 你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;
// ES12: FinalizationRegistry类
const finalRegistry = new FinalizationRegistry((value) => {
console.log("注册在finalRegistry的对象, 某一个被销毁", value)
})
let obj = { name: "why" }
let info = { age: 18 }
finalRegistry.register(obj, "obj")
finalRegistry.register(info, "value")
obj = null
info = null
WeakRefs
- 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:
- 如果我们希望是一个弱引用的话,可以使用WeakRef;
// ES12: WeakRef类
// WeakRef.prototype.deref:
// > 如果原对象没有销毁, 那么可以获取到原对象
// > 如果原对象已经销毁, 那么获取到的是undefined
const finalRegistry = new FinalizationRegistry((value) => {
console.log("注册在finalRegistry的对象, 某一个被销毁", value);
});
let obj = { name: "why" };
// 弱引用
let info = new WeakRef(obj);
finalRegistry.register(obj, "obj");
obj = null;
setTimeout(() => {
console.log(info.deref()?.name);
console.log(info.deref() && info.deref().name);
}, 10000);
logical assignment operators
// 1.||= 逻辑或赋值运算
let message = "hello world";
message = message || "default value";
message ||= "default value";
console.log(message);
// 2.&&= 逻辑与赋值运算
// &&
const obj = {
name: "why",
foo: function () {
console.log("foo函数被调用");
},
};
obj.foo && obj.foo();
// &&=
let info = {
name: "why",
};
// 1.判断info
// 2.有值的情况下, 取出info.name
// info = info && info.name
info &&= info.name;
console.log(info);
// 3.??= 逻辑空赋值运算
let message2 = 0;
message2 ??= "default value";
console.log(message2);
Numeric Separator:
讲过了;
String.replaceAll:
字符串替换;
九、监听对象与响应式原理
监听对象方法
第一种:Object.defineProperty
Object.defineProperty的存储属性描述符来对属性的操作进行监听
但是这样做有什么缺点呢?
-
首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
- 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
-
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。
-
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。
// 遍历object所有的key
Object.keys(obj).forEach((key) => {
let value = obj[key];
Object.defineProperty(obj, key, {
get: function () {
console.log(`监听到obj对象的${key}属性被访问了`);
return value;
},
set: function (newValue) {
console.log(`监听到obj对象的${key}属性被设置值`);
value = newValue;
},
});
});
第二种:Proxy类
这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
- 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
- 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
我们可以将上面的案例用Proxy来实现一次:
-
首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;
- const p = new Proxy(target, handler)
-
其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;
const objProxy = new Proxy(obj, {
// 获取值时的捕获器
get: function (target, key) {
console.log(`监听到对象的${key}属性被访问了`, target);
return target[key];
},
// 设置值时的捕获器
set: function (target, key, newValue) {
console.log(`监听到对象的${key}属性被设置值`, target);
target[key] = newValue;
},
});
Proxy的set和get捕获器
-
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
-
set和get分别对应的是函数类型;
-
set函数有四个参数:
- target:目标对象(侦听的对象);
- property:将被设置的属性key;
- value:新属性值;
- receiver:调用的代理对象;
-
get函数有三个参数:
- target:目标对象(侦听的对象);
- property:被获取的属性key;
- receiver:调用的代理对象;
-
const objProxy = new Proxy(obj, {
// 获取值时的捕获器
get: function (target, key) {
console.log(`监听到对象的${key}属性被访问了`, target);
return target[key];
},
// 设置值时的捕获器
set: function (target, key, newValue) {
console.log(`监听到对象的${key}属性被设置值`, target);
target[key] = newValue;
},
// 监听in的捕获器
has: function (target, key) {
console.log(`监听到对象的${key}属性in操作`, target);
return key in target;
},
// 监听delete的捕获器
deleteProperty: function (target, key) {
console.log(`监听到对象的${key}属性in操作`, target);
delete target[key];
},
});
其他的捕获器
Proxy的construct和apply
到捕捉器中还有construct和apply,它们是应用于函数对象的:
function foo() {}
const fooProxy = new Proxy(foo, {
apply: function (target, thisArg, argArray) {
console.log("对foo函数进行了apply调用");
return target.apply(thisArg, argArray);
},
construct: function (target, argArray, newTarget) {
console.log("对foo函数进行了new调用");
return new target(...argArray);
},
});
fooProxy.apply({}, ["abc", "cba"]);
new fooProxy("abc", "cba");
Reflect对象
-
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
-
那么这个Reflect有什么用呢?
- 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
- 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
- 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
-
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
- 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
- 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
- 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
- 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
-
那么Object和Reflect对象之间的API关系,可以参考MDN文档
Reflect的常见方法
Reflect的使用
那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
const obj = {
name: "why",
age: 18,
};
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
console.log("get---------");
return Reflect.get(target, key);
},
set: function (target, key, newValue, receiver) {
console.log("set---------");
target[key] = newValue;
const result = Reflect.set(target, key, newValue);
if (result) {
} else {
}
},
});
objProxy.name = "kobe";
console.log(objProxy.name);
Receiver的作用
-
我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
- 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
-
我们来看这样的一个对象
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
// receiver是创建出来的代理对象 改变this
console.log("get方法被访问--------", key, receiver);
console.log(receiver === objProxy); // ture
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
console.log("set方法被访问--------", key);
Reflect.set(target, key, newValue, receiver);
},
});
Reflect的construct
function Student(name, age) {
this.name = name;
this.age = age;
}
function Teacher() {}
// const stu = new Student("why", 18)
// console.log(stu)
// console.log(stu.__proto__ === Student.prototype)
// 执行Student函数中的内容, 但是创建出来对象是Teacher对象
const teacher = Reflect.construct(Student, ["why", 18], Teacher);
console.log(teacher); // Teacher { name: 'why', age: 18 }
console.log(teacher.__proto__ === Teacher.prototype); // ture
响应式原理
-
我们先来看一下响应式意味着什么?我们来看一段代码:
- m有一个初始化的值,有一段代码使用了这个值;
- 那么在m有一个新的值时,这段代码可以自动重新执行;
-
上面的这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的。
- 那么我们再来看一下对象的响应式:
// 对象的响应式
const obj = {
name: "why",
age: 18
}
const newName = obj.name
console.log("你好啊, 李银河")
console.log("Hello World")
console.log(obj.name) // 100行
obj.name = "kobe"
-
首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:
- 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数;
-
但是有一个问题:在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?
- 很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应;
- bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作;
响应式函数的实现watchFn
// 封装一个响应式的函数
let reactiveFns = [];
function watchFn(fn) {
reactiveFns.push(fn);
}
// 对象的响应式
const obj = {
name: "why",
age: 18,
};
watchFn(function () {
const newName = obj.name;
console.log("你好啊, 李银河");
console.log("Hello World");
console.log(obj.name); // 100行
});
//当值改变时进行遍历
obj.name = "kobe";
reactiveFns.forEach((fn) => {
fn();
-
但是我们怎么区分呢?
- 这个时候我们封装一个新的函数watchFn;
- 凡是传入到watchFn的函数,就是需要响应式的;
- 其他默认定义的函数都是不需要响应式的;
响应式依赖的收集
-
目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
-
所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
- 相当于替代了原来的简单 reactiveFns 的数组;
class Depend {
constructor() {
this.reactiveFns = [];
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn);
}
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
// 封装一个响应式的函数
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn);
}
// 对象的响应式
const obj = {
name: "why", // depend对象
age: 18, // depend对象
};
watchFn(function () {
const newName = obj.name;
console.log("你好啊, 李银河");
console.log("Hello World");
console.log(obj.name); // 100行
});
watchFn(function () {
console.log(obj.name, "demo function -------");
});
obj.name = "kobe";
depend.notify();
-
我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:
- 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
- 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
-
在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应
式的数据依赖:
监听对象的变化
-
那么我们接下来就可以通过之前学习的方式来监听对象的变量:
- 方式一:通过 Object.defineProperty的方式(vue2采用的方式);
- 方式二:通过new Proxy的方式(vue3采用的方式);
Proxy的方式
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
depend.notify();
},
});
Object.defineProperty的方式
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
return value
},
set: function(newValue) {
value = newValue
depend.notify()
}
})
})
return obj
}
对象依赖管理的实现
我们可以写一个getDepend函数专门来管理这种依赖关系:
// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn)
}
// 封装一个获取depend函数 获取每个响应式函数的depend对象
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 对象的响应式
const obj = {
name: "why", // depend对象
age: 18 // depend对象
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
正确的依赖收集
-
我们之前收集依赖的地方是在 watchFn 中:
- 但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖;
- 你只能针对一个单独的depend对象来添加你的依赖对象;
-
那么正确的应该是在哪里收集呢?应该在我们调用了Proxy的get捕获器时
- 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)
}
notify() {
console.log(this.reactiveFns)
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数和全局activeReactiveFn
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 对象的响应式
const obj = {
name: "why", // depend对象
age: 18 // depend对象
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数activeReactiveFn
depend.addDepend(activeReactiveFn)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
对Depend重构
-
但是这里有两个问题:
- 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
- 问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Depend的行为;
-
所以我们需要对Depend类进行重构:
- 解决问题一的方法:不使用数组,而是使用Set;
- 解决问题二的方法:添加一个新的方法,用于收集依赖;
class Depend {
constructor() {
this.reactiveFns = new Set();
}
// 不需要手动往depend里增加了响应式函数 直接增加activeReactiveFn
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
封装响应式对象
我们目前的响应式是针对于obj一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象:
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key);
// 给depend对象中添加响应函数
// depend.addDepend(activeReactiveFn)
depend.depend();
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
// depend.notify()
const depend = getDepend(target, key);
depend.notify();
},
});
}
响应式原理总结
-
我们前面所实现的响应式的代码,其实就是Vue3中的响应式原理:
- Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的;
- Vue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;
-
我们可以将reactive函数进行如下的重构:
- 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
- 在setter和getter方法中的逻辑和前面的Proxy是一致的;
Vue3响应式原理
// 保存当前需要收集的响应式函数
let activeReactiveFn = null;
class Depend {
constructor() {
this.reactiveFns = new Set();
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn;
fn();
activeReactiveFn = null;
}
// 封装一个获取depend函数
const targetMap = new WeakMap();
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target);
if (!map) {
map = new Map();
targetMap.set(target, map);
}
// 根据key获取depend对象
let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key);
// 给depend对象中添加响应函数
// depend.addDepend(activeReactiveFn)
depend.depend();
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
// depend.notify()
const depend = getDepend(target, key);
depend.notify();
},
});
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "why", // depend对象
age: 18, // depend对象
});
const infoProxy = reactive({
address: "广州市",
height: 1.88,
});
watchFn(() => {
console.log(infoProxy.address);
});
infoProxy.address = "北京市";
const foo = reactive({
name: "foo",
});
watchFn(() => {
console.log(foo.name);
});
foo.name = "bar";
Vue2响应式原理
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
/**
* Depend优化:
* 1> depend方法
* 2> 使用Set来保存依赖函数, 而不是数组[]
*/
class Depend {
constructor() {
this.reactiveFns = new Set()
}
// addDepend(reactiveFn) {
// this.reactiveFns.add(reactiveFn)
// }
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
function reactive(obj) {
// {name: "why", age: 18}
// ES6之前, 使用Object.defineProperty
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "why", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "广州市",
height: 1.88
})
watchFn(() => {
console.log(infoProxy.address)
})
infoProxy.address = "北京市"
const foo = reactive({
name: "foo"
})
watchFn(() => {
console.log(foo.name)
})
foo.name = "bar"
foo.name = "hhh"