Skip to content

Event loop ,宏任务,微任务

宏任务

javascript
// 宏任务
// 1. script代码块
// 2. setTimeout / setInterval
// 3. setImmediate

微任务

javascript
// 微任务
// 1. promise.then() / promise.catch()
// 2. async / await 
// 3. MutationObeserver ==> 监听dom的改变的(Vue源码中有)
// 4. process.nextTick (node)

事件循环机制

javascript

// 在当前事件循环中,微任务的优先级高,比下一个宏任务的优先级高
// 微任务是属于(包含于)宏任务里面的嘛? 是
// 1. 一个宏任务中,可以包含多个微任务
// 2. 每执行完一个宏任务,就会清空当前的微任务队列中的所有微任务。

// 执行宏任务,清空当前宏任务中产生的所有微任务,然后再执行下一个宏任务,再清空所有的微任务。

/* ==================== 事件循环 宏微任务 ===================== */
// 1. 首先,我们的script代码块,可以看做是一个宏任务,开始第一个Tick事件循环。
// 2. 先执行当前任务中的所有同步代码,
// 3. 如果遇到宏任务,就放到宏任务队列中等待执行,如果遇到微任务,就放到微任务队列中。
// 4. 当主线程执行完所有的同步代码,首先,去微任务队列中清空当前事件循环的所有微任务(表示这一轮Tick事件循环结束)
// 5. 再去执行下一个宏任务  (下一个宏任务的开始)

浏览器中的事件循环和Node中的有什么区别?

主要从Node11以后(包括),Node中和浏览器的事件循环行为统一了,都是每执行一个宏任务就执行完微任务队列

Node11之前(Node 10),Node是先执行完一个阶段的所有任务(微任务),再执行下一个阶段的所有任务(宏任务),再下一个阶段所有任务(微任务),按阶段执行。

typescript
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有两个特点

  1. 对象的状态不受外界影响。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    1. Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
  3. 代码里面 fulfilled => resolved

promise then 和 catch 的连接问题

image.png

javascript
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);
});
javascript
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

image.png

javascript
1 3 
1 2 3   // .catch() 中 是未报错,整个返回一个resolved状态,会触发后面的.then回调
1 2

async / await 语法

image.png async/await 和 Promise的关系 image.pngimage.png

手写深拷贝

javascript
// 检测对象 
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(): 表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。
javascript
/**
 * 深拷贝函数,可处理循环引用
 * @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;
}

记忆版本

javascript
// 检测是否是对象
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() 方法过滤掉原型链上的属性
javascript
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() 等方法可以获取键值对、键名或键值

=> 总的来说:

  1. 如果要遍历普通对象的属性,使用 for...in
  2. 如果要遍历数组、字符串等迭代对象的值,使用 for...of

for...of 更加现代、简洁,是 ES6 新增的语法 对于普通对象,建议使用 Object.keys()、Object.values()、Object.entries() 等方法配合 for of 使用。

for of 不可以遍历普通对象

可迭代对象必须实现 Symbol.iterator 方法,而普通对象并没有实现 Symbol.iterator。因此,如果在 for...of 语句中使用普通对象,会抛出一个TypeError异常:

javascript
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:

javascript
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 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不会直接联系,实现了两者的解耦

总结:

  1. 观察者模式 Subject 和 Observer 直接绑定,没有中间媒介
    1. 如addEventListener绑定事件(将具体的事件处理函数作为观察者对象)
    2. 当主题的状态发生变化时,它会通知所有的观察者并调用它们的更新方法。(click => 通知所有绑定click事件的回调函数执行)
  2. 发布订阅: Publisher 和 Subscriber 互不认识,需要中间的 Event Channel
    1. 如 EventBus 自定义事件

image.png

javascript
// 观察者模式
// 一个主题,一个观察者,主题变化之后触发观察者执行
btn.addEventListener('click',() => {...})

// 发布订阅模式
// 绑定
event.on('click', () => {
  console.log('事件1')
})
event.on('click',()=>{
  console.log('事件2')
})
// 触发执行
event.emit('click')
javascript
// {'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

发布订阅模式

javascript
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);
      }
    }
  }
}

观察者模式

javascript
// 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源码类似的实现

javascript
// 发布者 - 目标
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响应式系统的缺陷

对象属性的新增或删除不是响应式

javascript
<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>

无法监听数组索引和长度的变化

javascript
<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 区别

javascript
// 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声明的变量不能改变值,基本数据类型不能改变值,引用数据类型不能改变地址。

讲一讲原型/原型链

原型

javascript
/* ==================== 什么是原型 ===================== */
// 原型就是一个对象
// 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__
    // 其他原型上的属性和方法
}

原型链

javascript
/* ==================== 面试题 背下来 ===================== */
// 什么是原型链?
// 每个对象通过__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