设计模式和事件轮询

设计模式

  • 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案
  • 我们有很多的设计模式
    • 单例模式
    • 观察者模式
    • 策略模式

单例模式

  • 什么是单例模式呢?
  • 我们都知道,构造函数可以创造一个对象
  • 我们 new 很多次构造函数就能得到很多的对象
  • 单例模式: 就是使用构造函数实例化的时候,不管实例化多少回,都是同一个对象
    • 也就是一个构造函数一生只能 new 出一个对象
  • 也就是说,当我们使用构造函数,每一次 new 出来的对象 属性/功能/方法 完全一样 的时候,我们把他设计成单例模式

核心代码

  • 单例模式的核心代码很简单

  • 其实就是判断一下,他曾经有没有 new 出来过对象

  • 如果有,就还继续使用之前的那个对象,如果没有,那么就给你 new 一个

    // 准备一个构造函数
    // 将来要 new 的
    function Person() {}
    
    // 准备一个单例模式函数
    // 这个单例模式函数要把 Person 做成一个单例模式
    // 将来再想要 new Person 的时候只要执行这个 singleton 函数就可以了
    function singleton () {
      let instance
      
      if (!instance) { // 如果 instance 没有内容
        // 来到这里,证明 instance 没有内容
        // 给他赋值为 new Person
          instance = new Person()
      }
      
      // 返回的永远都是第一次 new Person 的实例
      // 也就是永远都是一个实例
      return instance
    }
    
    const p1 = singleton()
    const p2 = singleton()
    console.log(p1 === p2) // true
    

应用

  • 我们就用这个核心代码简单书写一个 demo

    // 这个构造函数的功能就是创建一个 div,添加到页面中
    function CreateDiv() {
        this.div = document.createElement('div')
        document.body.appendChild(this.div)
    }
    
    CreateDiv.prototype.init = function (text) {
        this.div.innerHTML = text
    }
    
    // 准备把这个 CreateDiv 做成单例模式
    // 让 singleton 成为一个闭包函数
    const singleton = (function () {
    
        let instance
    
        return function (text) {
            if (!instance) {
                instance = new CreateDiv()
            }
            instance.init(text)
            return instance
        }
    })()
    
    singleton('hello') // 第一次的时候,页面中会出现一个新的 div ,内容是 hello
    singleton('world') // 第二次的时候,不会出现新的 div,而是原先的 div 内容变成了 world
    

策略模式

  • 策略模式的核心思想是将不同的算法封装成独立的策略对象,并使它们可以互相替换,而不会影响到使用这些算法的客户端代码。这样可以在不修改客户端代码的情况下,根据需要选择合适的算法
// 策略对象:加法策略
const addStrategy = {
  calculate: function (num1, num2) {
    return num1 + num2;
  }
};

// 策略对象:减法策略
const subtractStrategy = {
  calculate: function (num1, num2) {
    return num1 - num2;
  }
};

// 策略对象:乘法策略
const multiplyStrategy = {
  calculate: function (num1, num2) {
    return num1 * num2;
  }
};

// 上下文对象
const calculator = {
  strategy: null, // 当前所使用的策略

  // 设置策略
  setStrategy: function (strategy) {
    this.strategy = strategy;
  },

  // 执行计算
  calculate: function (num1, num2) {
    if (this.strategy) {
      return this.strategy.calculate(num1, num2);
    } else {
      throw new Error('未设置策略');
    }
  }
};

// 使用示例
calculator.setStrategy(addStrategy);
console.log(calculator.calculate(5, 3)); // 输出: 8

calculator.setStrategy(subtractStrategy);
console.log(calculator.calculate(5, 3)); // 输出: 2

calculator.setStrategy(multiplyStrategy);
console.log(calculator.calculate(5, 3)); // 输出: 15
  • 在上面的示例中,我们定义了三个策略对象:加法策略、减法策略和乘法策略。然后创建了一个上下文对象 calculator,它具有一个 strategy 属性用于保存当前所使用的策略对象。
  • 通过调用 setStrategy() 方法可以设置当前的策略。然后,我们可以通过调用 calculate() 方法来执行计算,它会委托给当前的策略对象来执行具体的算法。
    • 通过策略模式,我们可以在运行时动态地切换不同的算法,而不需要修改 calculator 对象的代码,从而实现了算法行为的灵活性和可扩展性。

订阅发布模式

  • 观察者模式:定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

一个例子

  • 当你想去书店买书,但是恰巧今天你要买的书没有了
  • 我们又不能总在书店等着,就把我们的手机留给店员
  • 当你需要的书到了的时候,他会打电话通知你,你去买了就好了
  • 你买到数了以后,就告诉他,我买到了,那么以后再来了书就不会通知你了

书写代码

  • 首先我们分析功能

    • 我们要有一个观察者(这里抽象为一个对象 {}

    • 需要有一个属性,存放消息的盒子(把你绑定的所有事件放在里面)

    • 需要一个 on 方法,用于添加事件

    • 需要一个 emit 方法,用于发布事件(触发)

    • 需要一个 off 方法,把已经添加的方法取消

      const observer = {
          message: {},
          on: function () {},
          emit: function () {},
          off: function () {}
      }
      
    • 我们把它写成一个构造函数的形式

      class Observer {
          constructor () {
              this.message = {}
          }
          
          on () {}
          
          emit () {}
          
          off () {}
      }
      
    • 现在,一个观察者的雏形就出来了

    • 接下来完善方法就可以了

ON
  • 先来写 ON 方法

  • 添加一个事件

  • 我们的 on 方法需要接受 两个参数

    • 事件类型
    • 事件处理函数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit () {}
        
        off () {}
    }
    
EMIT
  • 接下来就是发布事件

  • 也就是让我们已经订阅好的事件执行一下

  • 同样需要接受两个参数

    • 要触发的事件类型
    • 给事件处理函数传递的参数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit (type, ...arg) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有,那么我们就处理一下参数
            const event = {
                type: type,
                arg: arg || {}
            }
    
            // 循环执行为当前事件类型订阅的所有事件处理函数
            this.message[type].forEach(item => {
                item.call(this, event)
            })
        }
        
        off () {}
    }
    
OFF
  • 最后就是移除事件

  • 就是把已经订阅的事件处理函数移除掉

  • 同样需要接受两个参数

    • 要移除的事件类型
    • 要移除的事件处理函数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit (type, ...arg) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有,那么我们就处理一下参数
            const event = {
                type: type,
                arg: arg || {}
            }
    
            // 循环执行为当前事件类型订阅的所有事件处理函数
            this.message[type].forEach(item => {
                item.call(this, event)
            })
        }
        
        off (type, fn) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有我们再进行移除
            for (let i = 0; i < this.message[type].length; i++) {
                const item =  this.message[type][i]
                if (item === fn) {
                    this.message[type].splice(i, 1)
                    
                }
            }
        }
    }
    
  • 以上就是最基本的 观察者模式

  • 接下来我们就使用一下试试看

使用一下
const o = new Observer()

// 准备两个事件处理函数
function a(e) {
    console.log('hello')
}

function b(e) {
    console.log('world')
}

// 订阅事件
o.on('abc', a)
o.on('abc', b)

// 发布事件(触发)
o.emit('abc', '100', '200', '300') // 两个函数都回执行

// 移除事件
o.off('abc', 'b')

// 再次发布事件(触发)
o.emit('abc', '100', '200', '300') // 只执行一个 a 函数了

事件轮询(event loop)

含义

event loop即事件轮询,这个是js里面为了解决单线程阻塞问题提出的解决方案,也是js异步执行机制的原理

  • 异步任务执行的顺序又是怎样的呢?这时候我们就需要了解事件轮询机制
    console.log(1)
    setTimeout(()=>{
        console.log(2)
    },0)
    setInterval(() => {
        console.log(3)
    }, 30);
    console.log(4)
    console.log(5)
    new Promise((resolve)=>{
        console.log(6)
        resolve(7)
    }).then(res=>{
        console.log(res)
    })

事件轮询

  • 图示
    • img
  • 图解
    1. 首先加载js的时候,整个的script代码会一行一行的执行
    2. 每一行代码执行都会进入调用栈,调用栈是一个执行代码的地方
    3. 调用栈在运行代码的时候会判断这行代码是同步还是异步,如果是同步直接执行返回执行之后得到的结果,如果是异步,会将异步的代码放到webAPI里面,直接执行后面的同步代码
    4. 异步的代码在webAPI里面会等待时机,比如说一个定时器3s执行,在3s到达的时候会将对应的回调函数添加到执行的队列(task queue)里面
    5. 所有的同步代码执行结束之后,此时会开启事件轮询,查看task queue里面是否有任务需要执行,如果有就拿到调用栈执行,一直到所有的task queue任务结束
  • 案例
console.log(1)
setTimeout(function cb(){
    console.log(2)
}, 1000)
console.log(3)
  • 执行过程
    1. console.log(1)进入调用栈 是同步 控制台打印输出 1 结束
    2. setTimeout(cb, 1000) 进入调用栈 是异步, 将cb放入webAPI等待,继续执行下一步代码
    3. console.log(2)进入调用栈 是同步 控制台打印输出 2 结束
    4. 此时调用栈任务执行结束,开启事件轮询
    5. cb函数在一秒钟后进入执行队列(task queue)
    6. cb函数进入调用栈执行,输出 3 结束
  • 点击查看完整代码执行
  • 总结
    • 可以看出所有的同步任务都会在异步任务之前执行
    • 异步任务如果有很多 他们的执行顺序又是如何呢

宏任务和微任务

  • 宏任务:macrotask,setTimeout,setInterval, ajax, dom事件,宏任务是由浏览器提供的
  • 微任务:microtask,promise,async/await,微任务是由es6提供的
  • 为什么要区分宏任务和微任务?
    • 因为宏任务和微任务的执行时机不一样,这样就会导致我们对于异步的代码认识产生理解的误区
  • 执行顺序
    1. js首先会执行调用栈里面同步的代码
    2. 遇到宏任务,会将宏任务的回调函数压入到webapi中等待,合适进入task queue队列等待
    3. 遇到微任务,会将微任务的回调函数压入micro task queue队列中
    4. 调用栈中所有同步代码执行完成,开启event loop,会优先执行micro task queue队列中的回调函数
    5. 微任务执行完成之后,dom进行渲染
    6. dom渲染完成,会执行task queue中的的回调函数
    7. 重复以上步骤
  • 案例演示
console.log(1)
setTimeout(function cb(){
    console.log(2)
}, 0)
new Promise(function handler(resovle){
    resovle(3)
}).then(function(res){
    console.log(res)
})
console.log(4)

微任务执行在DOM渲染之前执行,宏任务执行在DOM渲染之后

<body>
    <h1>页面中的元素</h1>
    <script>
        console.log(1)
        setTimeout(function cb(){
            alert(222)
            console.log(2)
        }, 1000)
        new Promise(function handler(resovle){
            resovle(3)
        }).then(function(res){
            alert(111)
            console.log(res)
        })
        console.log(4)
    </script>
</body>
</html>

!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D)

微任务执行在DOM渲染之前执行,宏任务执行在DOM渲染之后

<body>
    <h1>页面中的元素</h1>
    <script>
        console.log(1)
        setTimeout(function cb(){
            alert(222)
            console.log(2)
        }, 1000)
        new Promise(function handler(resovle){
            resovle(3)
        }).then(function(res){
            alert(111)
            console.log(res)
        })
        console.log(4)
    </script>
</body>
</html>
  • 弹出alert(111)的弹窗此时页面上h1并没有生成,弹出alert(222)的时候页面h1已经生成,由此证明微任务是在dom渲染之前执行,宏任务是在dom渲染之后执行
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值