ArthurWangCN / notepad

reading notepad
0 stars 2 forks source link

异步和单线程 #7

Open ArthurWangCN opened 1 year ago

ArthurWangCN commented 1 year ago

由于JavaScript是单线程语言,因此,在一个进程上,只能运行一个线程,而不能多个线程同时运行。也就是说JavaScript不允许多个线程共享内存空间。因此,如果有多个线程想同时运行,则需采取排队的方式,即只有当前一个线程执行完毕,后一个线程才开始执行。JavaScript中的线程包括函数调用、I/O设备(如向服务器发送请求获取响应等)、定时器、用户操作的事件(click、keyup、scroll等)。

ArthurWangCN commented 1 year ago

单线程就意味着,所有任务(线程)需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务不得不一直等待。

因此,所有任务可以分为两种,一种是同步任务,一种是异步任务:

任务队列是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

“任务队列”中的事件,除了IO设备(ajax获取服务器数据)的事件以外,还包括一些用户产生的事件(mousehover、click、scroll、keyup等)和定时器等。只要在事件中指定了回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。而主线程读取任务队列中的异步任务,主要就是读取回调函数。

当主线程的所有同步任务执行(排队执行)完毕之后,就会读取任务队列中的异步任务,将异步任务推入执行栈中执行。任务队列是一个先进先出的数据结构,即排在前面的事件,优先被主线程读取。如果存在定时器,时间越短的越先进入执行栈。

ArthurWangCN commented 1 year ago

因此,可以做一个简单的总结:

  1. JS将任务分为两种,同步任务和异步任务。
  2. 当主线程开始执行同步任务时,会创建一个“执行栈”,每一个同步任务排队执行,只有前一个任务执行完毕,才会执行下一个任务。同时,执行栈与函数的调用位置相关。
  3. 当主线程上的所有同步任务执行完毕之后,也就是当“执行栈”为空时,主线程会去读取任务队列上的异步任务(回调函数),并将异步任务推入执行栈中开始执行。
  4. 主线程不断重复第二、第三个步骤。
ArthurWangCN commented 1 year ago

异步解决方案

目前常见的有以下几种异步解决方案:

ArthurWangCN commented 1 year ago

回调函数(callback)

回调函数简单理解就是一个函数被作为参数传递给另一个函数。 回调并不一定就是异步,并没有直接关系,只不过回调函数是异步的一种解决方案。

fn1(callback){
  setTimeout(() => {
    callback && callback()
  }, 1000)
}
fn1(()=>{
  console.log("1")
})

如上所示,我们使用setTimeout在函数fn1中模拟了一个耗时1s的任务,耗时任务结束会抛出一个回调,那么我们在调用时就可以做到在函数fn1的耗时任务结束后执行回调函数了。

采用这种方式,我们把同步操作变成了异步操作,fn1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调函数的优劣:

ArthurWangCN commented 1 year ago

事件监听(发布订阅模式)

每个可用的事件都会有一个事件处理器,也就是事件触发时会运行的代码块。当我们定义了一个用来回应事件被激发的代码块的时候,我们说我们注册了一个事件处理器,事件处理器有时候被叫做事件监听器。

实现一个事件监听:

实现的过程中用到了一个设计模式,也就是发布订阅模式。

发布订阅模式(publish-subscribe pattern),又叫观察者模式(observer pattern),定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

其实我们都用过发布订阅模式,比如我们在DOM节点上绑定一个事件函数,就已经使用了

document.body.addEventListener('click', function () {
  console.log(1)
})

发布订阅模式有很多种实现方式,下面我们用class来简单实现下:

class Emitter {
  constructor() {
    // _listener数组,key为自定义事件名,value为执行回调数组-因为可能有多个
    this._listener = []
  }

  // 订阅 监听事件
  on(type, fn) {
    // 判断_listener数组中是否存在该事件命
    // 存在将回调push到事件名对应的value数组中,不存在直接新增
    this._listener[type] 
      ? this._listener[type].push(fn) 
        : (this._listener[type] = [fn])
  }

  // 发布 触发事件
  trigger(type, ...rest) {
    // 判断该触发事件是否存在
    if (!this._listener[type]) return
    // 遍历执行该事件回调数组并传递参数
    this._listener[type].forEach(callback => callback(...rest))
  }
}

如上所示,我们创建了一个Emitter类,并且添加了两个原型方法on和trigger。

使用:

// 创建一个emitter实例
const emitter = new Emitter()

emitter.on("done", function(arg1, arg2) {
  console.log(arg1, arg2)
})

emitter.on("done", function(arg1, arg2) {
  console.log(arg2, arg1)
})

function fn1() {
  console.log('我是主程序')
  setTimeout(() => {
    emitter.trigger("done", "异步参数一", "异步参数二")
  }, 1000)
}

fn1()

发布/订阅的优劣:

ArthurWangCN commented 1 year ago

promise

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知值的代理。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

一个 Promise 必然处于以下几种状态之一:

待定状态的 Promise 对象要么会通过一个值被兑现,要么会通过一个原因(错误)被拒绝。

Promise是一个构造函数,我们可以通过new关键字来创建一个Promise实例,也可以直接使用Promise的一些静态方法。

语法:

new Promise( function(resolve, reject) {...});

示例:

function fn1(){
  return new Promise((resolve,reject) => {
    setTimeout(()=>{
      let num = Math.ceil(Math.random()*10)
      if(num < 5){
        resolve(num)
      }else{
        reject('数字太大')
      }
    },2000)
  })
}
ArthurWangCN commented 1 year ago

Generator

ArthurWangCN commented 1 year ago

async / await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async 是异步的意思,而 await 是 async wait 的简写,即异步等待。所以从语义上就很好理解 async 用于声明一个 function 是异步的,await 用于等待一个异步方法执行完成。

另外 await 只能出现在 async 函数中。

示例:

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};