跳至主要內容

异步操作

Mr.LRH大约 4 分钟

异步操作

进程与线程

  • 进程是 CPU 资源分配的最小单位,是能拥有资源和独立运行的最小单位。

  • 线程是 CPU 调度的最小单位,是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程。

单线程模型

JavaScript 只在一个线程上运行。JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。

注意:JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。新标准并没有改变 JavaScript 单线程的本质。

同步任务和异步任务

Javascript 有一个主线程(main thread)和调用栈(call-stack,执行栈),所有的任务都会被放到调用栈等待主线程执行。引擎提供一个任务队列(task queue),队列中是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。)

  • 主线程会执行所有的同步任务。
  • 等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。
  • 等到执行完,下一个异步任务再进入主线程开始执行。
  • 一旦任务队列清空,程序就结束执行。

调用栈(call-stack,执行栈)是一种后进先出的数据结构。当函数被调用时,会被添加到栈的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。栈可存放的函数是有限制的,一旦存放过多的函数且没有得到释放的话,就会出现爆栈的问题。

JavaScript 单线程中的任务广义可分为

  • 同步任务:在主线程上排队执行的任务,在调用栈中按照顺序等待主线程依次执行.
  • 异步任务:不进入主线程,而进入任务队列的任务。在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

JavaScript 单线程中的任务可细分为

  • MacroTask(宏任务):script (整体代码)、setTimeoutsetIntervalI/O 操作、UI Rendering (UI 渲染)、requestAnimationFramesetImmediate
  • MicroTask(微任务):PromiseMutationObserverProcess.nextTick(Node独有)

异步操作的模式

  • 回调函数

    function asyncFn(callback) {
      callback()
    }
    function callbackFn() {}
    
    // asyncFn 异步执行完成之后,回到执行 callbackFn 函数
    asyncFn(callbackFn)
    
  • 事件监听

    // 监听 done 事件
    asyncTriggerFn.on('done', fn)
    
    function asyncTriggerFn(
      setTimeout(function () {
        // 表示执行完成后,立即触发 donw 事件,从而开始执行 fn 函数
        asyncTriggerFn.trigger('done')
      })
    )
    
  • 发布/订阅

    // 订阅 done
    $.subscribe('done', fn)
    
    function asyncPublishFn(
      setTimeout(function () {
        // asyncPublishFn 异步执行完成之后,发布 done,从而开始执行 fn 函数
        $.publish('done')
      })
    )
    
    // 取消订阅 done
    $.unsubscribe('done', fn)
    

异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。

function async(arg, callback) {
  console.log("参数为 " + arg + " , 1秒后返回结果")
  setTimeout(function () {
    callback(arg * 2)
  }, 1000)
}

function final(value) {
  console.log("完成: ", value)
}

async(1, function (value) {
  async(2, function (value) {
    async(3, function (value) {
      async(4, function (value) {
        async(5, function (value) {
          async(6, final)
        })
      })
    })
  })
})
// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成:  12
  • 串行执行:控制异步任务,一个任务完成以后,再执行另一个。

    var items = [1, 2, 3, 4, 5, 6]
    var results = []
    
    function async(arg, callback) {
      console.log("参数为 " + arg + " , 1秒后返回结果")
      setTimeout(function () {
        callback(arg * 2)
      }, 1000)
    }
    
    function final(value) {
      console.log("完成: ", value)
    }
    
    function series(item) {
      if (item) {
        async(item, function (result) {
          results.push(result)
          return series(items.shift())
        })
      } else {
        return final(results[results.length - 1])
      }
    }
    
    series(items.shift())
    
  • 并行执行:所有异步任务同时执行,等到全部完成以后,才执行 final 函数。

    var items = [1, 2, 3, 4, 5, 6]
    var results = []
    
    function async(arg, callback) {
      console.log("参数为 " + arg + " , 1秒后返回结果")
      setTimeout(function () {
        callback(arg * 2)
      }, 1000)
    }
    
    function final(value) {
      console.log("完成: ", value)
    }
    
    items.forEach(function (item) {
      async(item, function (result) {
        results.push(result)
        if (results.length === items.length) {
          final(results[results.length - 1])
        }
      })
    })
    
  • 并行和串行结合:每次最多只能并行执行 n 个异步任务,避免了过分占用系统资源。

    var items = [1, 2, 3, 4, 5, 6]
    var results = []
    var running = 0
    var limit = 2
    
    function async(arg, callback) {
      console.log("参数为 " + arg + " , 1秒后返回结果")
      setTimeout(function () {
        callback(arg * 2)
      }, 1000)
    }
    
    function final(value) {
      console.log("完成: ", value)
    }
    
    function launcher() {
      while (running < limit && items.length > 0) {
        var item = items.shift()
        async(item, function (result) {
          results.push(result)
          running--
          if (items.length > 0) {
            launcher()
          } else if (running == 0) {
            final(results)
          }
        })
        running++
      }
    }
    
    launcher()
    
上次编辑于:
贡献者: lingronghai