Event loop ,宏任务,微任务
宏任务
// 宏任务
// 1. script代码块
// 2. setTimeout / setInterval
// 3. setImmediate
微任务
// 微任务
// 1. promise.then() / promise.catch()
// 2. async / await
// 3. MutationObeserver ==> 监听dom的改变的(Vue源码中有)
// 4. process.nextTick (node)
事件循环机制
// 在当前事件循环中,微任务的优先级高,比下一个宏任务的优先级高
// 微任务是属于(包含于)宏任务里面的嘛? 是
// 1. 一个宏任务中,可以包含多个微任务
// 2. 每执行完一个宏任务,就会清空当前的微任务队列中的所有微任务。
// 执行宏任务,清空当前宏任务中产生的所有微任务,然后再执行下一个宏任务,再清空所有的微任务。
/* ==================== 事件循环 宏微任务 ===================== */
// 1. 首先,我们的script代码块,可以看做是一个宏任务,开始第一个Tick事件循环。
// 2. 先执行当前任务中的所有同步代码,
// 3. 如果遇到宏任务,就放到宏任务队列中等待执行,如果遇到微任务,就放到微任务队列中。
// 4. 当主线程执行完所有的同步代码,首先,去微任务队列中清空当前事件循环的所有微任务(表示这一轮Tick事件循环结束)
// 5. 再去执行下一个宏任务 (下一个宏任务的开始)
浏览器中的事件循环和Node中的有什么区别?
主要从Node11以后(包括),Node中和浏览器的事件循环行为统一了,都是每执行一个宏任务就执行完微任务队列
Node11之前(Node 10),Node是先执行完一个阶段的所有任务(微任务),再执行下一个阶段的所有任务(宏任务),再下一个阶段所有任务(微任务),按阶段执行。
function test () {
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() => {console.log('children2-1')})
}, 0)
setTimeout(() => {
console.log('children3')
Promise.resolve().then(() => {console.log('children3-1')})
}, 0)
Promise.resolve().then(() => {console.log('children1')})
console.log('end')
}
test()
// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1
// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1
Promise有哪三种状态,如何变化
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败) Promise有两个特点
- 对象的状态不受外界影响。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
- 代码里面 fulfilled => resolved
promise then 和 catch 的连接问题
const p1 = Promise.resolve().then(() => {
return 100;
});
p1.then((res) => {
console.log(res); // 100
console.log(123); // 123
});
const p2 = Promise.resolve().then(() => {
// then 内部有报错,返回一个rejected状态的promise
// p2的状态是rejected
throw new Error("then error");
});
p2.then(() => {
console.log(456);
}).catch((err) => {
// catch 可以捕获p2的rejected
console.log(err);
});
const p3 = Promise.reject("my error").catch((err) => {
console.log(err);
});
console.log("p3", p3); // p3是resolved
const p4 = Promise.reject("my error").catch((err) => {
throw new Error("p4 Error");
});
console.log("p4", p4); // p4是rejected
1 3
1 2 3 // .catch() 中 是未报错,整个返回一个resolved状态,会触发后面的.then回调
1 2
async / await 语法
async/await 和 Promise的关系
手写深拷贝
// 检测对象
const isObject = (obj) => {
return typeof obj === 'object' && obj !== null // !!obj
}
const deepClone = (obj, hash = new WeakMap()) => {
// 值类型 直接返回
if (!isObject(obj)) return obj
// Date, RegExp constructor容易被修改丢失,被认为不安全,不推荐作为判断
// instanceof好一些
// if (obj.constructor === Date) return new Date(obj)
// if (obj.constructor === RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 解决循环引用,查哈希表
if (hash.has(obj)) return hash.get(obj)
// let allDesc = Object.getOwnPropertyDescriptors(obj)
// let target = Object.create(Object.getPrototypeOf(obj),allDesc)
let target = Array.isArray(obj) ? [] : {} // 考虑数组
hash.set(obj, target)
Reflect.ownKeys(obj).forEach(key => {
if (isObject(obj[key])) {
target[key] = deepClone(obj[key], hash)
} else {
target[key] = obj[key]
} // 这里不需要else,基本类型直接返回
})
return target
}
- **Object.keys(obj) **: 只返回对象自身的可枚举属性键,不包括不可枚举属性和Symbol属性。
- Reflect.ownKeys(obj) :返回对象自身的所有属性键,包括可枚举属性、不可枚举属性和Symbol属性。
- **for in **: 会遍历对象自身及其原型链上的所有可枚举属性(不包括Symbol属性),类似于 Reflect.ownKeys() 返回的结果
- obj.hasOwnProperty(): 表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。
/**
* 深拷贝函数,可处理循环引用
* @param {Object|Array} obj 要拷贝的对象或数组
* @returns {Object|Array} 拷贝后的新对象或新数组
*/
function deepClone(obj, map = new WeakMap()) {
// 处理基本数据类型
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理日期对象
if (obj instanceof Date) {
return new Date(obj);
}
// 处理正则表达式
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 检查是否已经拷贝过该对象
if (map.has(obj)) {
return map.get(obj);
}
// 创建一个新的对象或数组
let clone = Array.isArray(obj) ? [] : {};
// 将新对象存储到 map 中,避免循环引用
map.set(obj, clone);
// 递归拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
记忆版本
// 检测是否是对象
const isObject = (obj) => {
return obj !== null && typeof obj === "object";
};
const deepClone = (obj, map = new WeakMap()) => {
// 1. 基本数据类型,直接返回
if (isObject(obj)) return obj;
// 2. 处理日期对象
if (obj instanceof Date) return new Date(obj);
// 3. 处理正则
if (obj instanceof RegExp) return new RegExp(obj);
// 4. 解决循环引用问题,如果已经拷贝过当前对象,直接返回
if (map.has(obj)) return map.get(obj);
// 5. 拷贝逻辑 创建一个新的对象或者数组
let clone = Array.isArray(obj) ? [] : {};
// 6. 将新对象clone存储到map中,避免循环引用,obj为key
map.set(obj, clone);
// 7. 递归拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
// 8.第7部分改为这个也ok
// Reflect.ownKeys(obj).forEach((key) => {
// clone[key] = deepClone(obj[key], map);
// });
return clone;
};
// 为什么使用WeakMap?
// 因为WeakMap的键是弱引用,如果没有强引用指向这个键,
// 它对应的键值对就会自动的被垃圾回收机制清楚,不会造成内存泄露
for in / for of 区别
for in ==> 遍历key
- 遍历对象(Object)的可枚举属性(包括原型链上的属性)
- 遍历顺序有可能不是按照实际定义的顺序, 不太推荐for in 遍历数组
- 可以直接获取到属性名(键名) ==> key
- 可以使用 hasOwnProperty() 方法过滤掉原型链上的属性
let obj = {a: 1, b: 2};
Object.prototype.c = 3;
for (let prop in obj) {
console.log(prop, obj[prop]); // 输出 a 1, b 2, c 3
}
// 为了只遍历对象自身的可枚举属性,可以在循环内部加上一个条件判断:
let obj = {a: 1, b: 2};
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log(prop, obj[prop]); // 只输出 a 1, b 2
}
}
for of ==> 遍历value
- 遍历可迭代对象(Array, String, Map, Set, arguments等)的值
- 遍历顺序就是它们的插入顺序
- 直接获取到对应的值,而不是索引
- 配合 entries()、keys()、values() 等方法可以获取键值对、键名或键值
=> 总的来说:
- 如果要遍历普通对象的属性,使用 for...in
- 如果要遍历数组、字符串等迭代对象的值,使用 for...of
for...of 更加现代、简洁,是 ES6 新增的语法 对于普通对象,建议使用 Object.keys()、Object.values()、Object.entries() 等方法配合 for of 使用。
for of 不可以遍历普通对象
可迭代对象必须实现 Symbol.iterator 方法,而普通对象并没有实现 Symbol.iterator。因此,如果在 for...of 语句中使用普通对象,会抛出一个TypeError异常:
let obj = {a: 1, b: 2};
for (let value of obj) { // TypeError: obj is not iterable
console.log(value);
}
不过,我们可以利用 Object.keys()、Object.values()、Object.entries() 这些方法将普通对象转换为可迭代对象,然后再使用 for...of:
let obj = {a: 1, b: 2};
// 遍历对象的键
for (let key of Object.keys(obj)) {
console.log(key); // 'a', 'b'
}
// 遍历对象的值
for (let value of Object.values(obj)) {
console.log(value); // 1, 2
}
// 遍历对象的键值对
for (let [key, value] of Object.entries(obj)) {
console.log(key, value); // 'a' 1, 'b' 2
}
发布订阅/观察者模式
两者区别
观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。 发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到事件调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
差异:=> 发布订阅模式中 有一个事件中心(或者中间媒介) 在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不会直接联系,实现了两者的解耦
总结:
- 观察者模式 Subject 和 Observer 直接绑定,没有中间媒介
- 如addEventListener绑定事件(将具体的事件处理函数作为观察者对象)
- 当主题的状态发生变化时,它会通知所有的观察者并调用它们的更新方法。(click => 通知所有绑定click事件的回调函数执行)
- 发布订阅: Publisher 和 Subscriber 互不认识,需要中间的 Event Channel
- 如 EventBus 自定义事件
// 观察者模式
// 一个主题,一个观察者,主题变化之后触发观察者执行
btn.addEventListener('click',() => {...})
// 发布订阅模式
// 绑定
event.on('click', () => {
console.log('事件1')
})
event.on('click',()=>{
console.log('事件2')
})
// 触发执行
event.emit('click')
// {'click':[fn1,fn2], 'change':[fn3,fn4]}
class EventEmitter {
constructor() {
// 使用 Map 来存储事件及其对应的回调函数
this.subs = new Map();
}
// 注册事件 订阅者
// 对于某个事件,每一个回调函数都是一个订阅者
$on(event, callback) {
// 如果当前事件不存在订阅者(没有注册过),创建一个新数组
if (!this.subs.has(event)) {
this.subs.set(event, []);
}
// 注册过, 将订阅者回调函数添加到事件订阅者数组中
this.subs.get(event).push(callback);
}
// 触发事件 发布者
$emit(event, ...args) {
if (this.subs.has(event)) {
// 获取订阅者回调函数
const callbacks = this.subs.get(event);
// 遍历执行每个订阅者回调函数, 并传入参数
callbacks.forEach((callback) => callback(...args));
}
}
// 取消订阅
$off(event, callback) {
// 如果当前事件存在订阅者(注册过)
if (this.subs.has(event)) {
// 获取订阅者回调函数数组
const callbacks = this.subs.get(event);
// 移除置顶的订阅者回调函数
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
// 如果订阅者数组为空,移除该事件
if (callbacks.length === 0) {
this.subs.delete(event);
}
}
}
}
// 使用示例
const eventEmitter = new EventEmitter();
// 订阅事件
const callback1 = (...args) => console.log("callback1:", ...args);
const callback2 = (...args) => console.log("callback2:", ...args);
eventEmitter.$on("myEvent", callback1);
eventEmitter.$on("myEvent", callback2);
// 触发事件
eventEmitter.$emit("myEvent", 1, 2, 3); // 输出: callback1: 1 2 3 和 callback2: 1 2 3
// 移除事件订阅
eventEmitter.$off("myEvent", callback1);
// 再次触发事件
eventEmitter.$emit("myEvent", 4, 5); // 输出: callback2: 4 5
发布订阅模式
class EventEmitter {
constructor() {
// 使用 Map 来存储事件及其对应的回调函数
this.subs = new Map();
}
// 注册事件 订阅者
on(event, callback) {
// 如果当前事件不存在订阅者(没有注册过),创建一个新数组
if (!this.subs.has(event)) {
this.subs.set(event, []);
}
// 注册过, 将订阅者回调函数添加到事件订阅者数组中
this.subs.get(event).push(callback);
}
// 触发事件 发布者
emit(event, ...args) {
if (this.subs.has(event)) {
// 获取订阅者回调函数
const callbacks = this.subs.get(event);
// 遍历执行每个订阅者回调函数, 并传入参数
callbacks.forEach((callback) => callback(...args));
}
}
// 取消订阅
off(event, callback) {
// 如果当前事件存在订阅者(注册过)
if (this.subs.has(event)) {
// 获取订阅者回调函数数组
const callbacks = this.subs.get(event);
// 移除置顶的订阅者回调函数
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
// 如果订阅者数组为空,移除该事件
if (callbacks.length === 0) {
this.subs.delete(event);
}
}
}
}
观察者模式
// 1. 定义主体对象
class Subject {
constructor() {
// 存储所有的观察者(订阅者)
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知所有观察者(发布通知)
notify(...args) {
this.observers.forEach((observer) => {
observer.update(...args);
});
}
}
// 2. 定义具体的观察者
class Observer {
constructor(name) {
this.name = name;
}
// 接收主体对象的通知
update(...args) {
console.log(`观察者 ${this.name} 收到通知,附加信息: ${args.join(", ")}`);
}
}
// 使用示例
const subject = new Subject();
const observer1 = new Observer("观察者1");
const observer2 = new Observer("观察者2");
const observer3 = new Observer("观察者3");
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);
subject.notify("状态发生改变", 123);
// 输出:
// 观察者 观察者1 收到通知,附加信息: 状态发生改变, 123
// 观察者 观察者2 收到通知,附加信息: 状态发生改变, 123
// 观察者 观察者3 收到通知,附加信息: 状态发生改变, 123
subject.removeObserver(observer2);
subject.notify("另一个状态改变", 456);
// 输出:
// 观察者 观察者1 收到通知,附加信息: 另一个状态改变, 456
// 观察者 观察者3 收到通知,附加信息: 另一个状态改变, 456
Vue源码类似的实现
// 发布者 - 目标
class Dep {
constructor() {
// 记录所有的订阅者(观察者)
this.subs = [];
}
// 添加订阅者
addSub(sub) {
// 注意,sub这个对象具有update方法,无需判断
this.subs.push(sub);
}
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
// 源码中 这么写的
// this.subs[this.subs.indexOf(sub)] = null;
}
// 发布通知 - 通知所有观察者
notify(...args) {
this.subs.forEach((sub) => {
sub.update(...args);
});
}
}
// 2. 定义具体的观察者(订阅者)
class Watcher {
constructor(name) {
this.name = name;
}
update(...args) {
// 数据变化更新视图
console.log(`观察者 ${this.name} 收到通知,附加信息: ${args.join(", ")}`);
}
}
// 发布者,将记录所有的观察者
const dep = new Dep();
const watcher1 = new Watcher("观察者1");
const watcher2 = new Watcher("观察者2");
const watcher3 = new Watcher("观察者3");
dep.addSub(watcher1);
dep.addSub(watcher2);
dep.addSub(watcher3);
dep.notify("状态发生改变", 123);
总结:
- 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
- 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
PS => Vue2响应式系统的缺陷
对象属性的新增或删除不是响应式
<template>
<div id="app">
<h2>我是通过vite搭建的vue2项目</h2>
<h3>{{ obj.a }}</h3>
<h3>{{ obj.b }}</h3>
<h3>{{ obj.c }}</h3>
<h3>{{ obj.d }}</h3>
<button @click="changeObj">click me</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
a: 1
}
}
},
methods: {
changeObj() {
// 3. 如果添加多个响应式属性 , Object.assign({}, this.obj, 新属性)
this.obj = Object.assign({}, this.obj, { c: 777, d: 888 })
}
},
// 最早,可以在这个生命周期中操作数据
created() {
// vue2 针对对象
// 1. Vue无法检测property的添加或者删除(对象上新增的属性不是响应式的,监听不了变化)
// this.obj.b = 2
// 2. 解决方案 this.$set(哪个对象,哪个属性,属性值)
this.$set(this.obj, 'b', 666)
}
}
</script>
无法监听数组索引和长度的变化
<template>
<div id="app">
<h2>我是通过vite搭建的vue2项目</h2>
<div v-for="item in arr" :key="item">{{ item }}</div>
<button @click="changeArr">click me</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: [1, 2, 3]
}
},
methods: {
changeArr() {
// 1. 利用数组下标 设置值的时候
// this.arr[1] = 666
this.$set(this.arr, 1, 777)
// 2. 修改arr.length 数组长度的时候
// this.arr.length = 2
this.arr.push(9999)
}
},
created() {
// vue2 针对数组
// 解决 =>
// 1. 组件内:vm.$set(vm.arr, 下标,newVal)
// 2. 数组的7个方法,是响应式, 通过这7个方法修改数组,是响应式
// (vue底层,针对数组,改写了数组的原型 Array.prototype => 重写了这7个方法)
// push / pop / shift / unshift / splice / sort / reverse
// Vue2的缺陷
// 1. 针对对象 , 属性的新增和删除,不是响应式
// 2. 针对数组 , ==> Vue底层重写了数组的原型 7个方法
// 2.1 通过数组下标修改值,不是响应式
// 2.3 修改数组的length,不是响应式
}
}
</script>
递归遍历导致性能问题
在初始化实例时,Vue 2 需要对象做深层次、递归的遍历,一旦属性树过于复杂,就会导致 CPU 承载过大的压力。 当数据量较大或数据层级较深时,会导致数据劫持和依赖追踪的性能下降,影响页面的渲染性能。
let / const / var 区别
// 1. let / const 会产生块级作用域
// 2. let / const 不存在变量提升,var有
// 3. let / const 不允许重复声明,var可以
// 4. let / const 存在暂时性死区(temporal dead zone) TDZ
// ==> let / const 声明的变量,不能在声明之前使用, var可以
// 5. 浏览器中,var声明的变量会挂载到window对象下, let / const 不会
// 针对const
// 1. const 一旦声明,必须赋值
// 2. const声明的变量不能改变值,基本数据类型不能改变值,引用数据类型不能改变地址。
讲一讲原型/原型链
原型
/* ==================== 什么是原型 ===================== */
// 原型就是一个对象
// 1. 每个函数都有prototype属性,它的值是一个指针,指向的就是原型对象
// 2. 通过构造函数创建的实例,都有一个__proto__属性,也指向原型对象
// 3. 原型上默认由一个constructor属性,指回构造函数
// 作用:我们可以把一些公共的属性和方法放到原型上,节约内存。
// ===> 所有通过构造函数创建的实例都共享原型上的属性和方法
/* ==================== end ===================== */
function A(){}
const a = new A()
a.__proto__ === A.prototype
// 原型的结构如下
// 通过构造函数访问
A.prototype = {
constructor: A,
// [[prototype]] / __proto__
// 其他原型上的属性和方法
}
// 实例通过__proto__访问
a.__proto__ = {
constructor: A,
// [[prototype]] / __proto__
// 其他原型上的属性和方法
}
原型链
/* ==================== 面试题 背下来 ===================== */
// 什么是原型链?
// 每个对象通过__proto__属性能访问到它的原型,原型也有它的原型。
// 当访问一个对象的属性和方法的时候,先在自身中寻找,
// 如果没有,就会沿着__proto__这条链,往上(在它的原型中)寻找,一直找到最顶层Object.prototype为止
// Object.prototype.__proto__ === null
/* ==================== end ===================== */
// 数组的原型链
const arr = [1, 2, 3] // new Array()
// 数组也是一个对象,它由Array这个构造函数创建
// arr.__proto__ === Array.prototype
arr ---> Array.prototype ---> Object.prototype ---> null
// __proto__ 是一个桥梁
// 函数的原型链
const fn = function(){}
// 函数也是一个对象 它由Function构造函数创建
// fn.__proto__ === Function.prototype
fn ---> Function.prototype ---> Object.prototype ---> null