Javascript异步编程

同步模式与异步模式


事件循环与消息队列


  JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程

  当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。

  定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。

JS引擎线程什么时候去处理呢?消息队列又是什么?

JavaScript 通过 事件循环 event loop 的机制来解决这个问题。

其实 事件循环 机制和 任务队列 的维护是由事件触发线程控制的。

事件触发线程 同样是浏览器渲染引擎提供的,它会维护一个 任务队列。

  JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。

同时,JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。

如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。

消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。
    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。

异步编程的几种方式


Promise异步方案 宏任务/微任务队列


Promise

  • promise是一个类,在执行这个类的时候需要传递一个执行器进去 执行器,执行器会立即执行
  • promise中有三种状态 分别为成功fulfilled 失败rejected 等待pending
    一旦状态确定就不可更改
  • resolve和reject函数是用来更改状态的
  • then方法内部做的事情就是判断状态,成功就调用成功函数,反之调用失败函数,then方法是被定义到原型对象中的
  • then成功回调有一个参数表示成功之后的值,同样失败也有参数
  • then方法是可以被链式调用的,后面的then拿到的值是,是上一个then的返回值
const PENDING='pending'
const FULFILLED='fulfilled'
const REJECTED='rejected'

class MyPromise{
  construtor(executor){
    try{
      executor(this.resolve,this.reject)
    }catch(e){
      this.reject(e)
    }
  }
  
  status=PENDING
//成功之后的值
  value=undefined
//失败后的原因
  reason=undefined

//成功回调
successCallback=[]
//失败回调
failCallback=[]

  resolve=(value)=>{  
  //如果状态不是等待阻止程序向下执行
  if(this.status!==PENDING) return;
    this.status=FULFILLED
    //保存成功之后的值
    this.value=value
    while(this.successCallback.length){
      this.successCallback.shift()()
    }
  }
  reject=()=>{
    if(this.status!==PENDING) return;
    this.status=REJECTED
    //保存失败后的原因
    this.reason=reason
    
    while(this.failCallback.length){
      this.failCallback.shift()()
    }
  }
  then(successCallback,failCallback){
    let promise2=new MyPromise((resolve,reject)=>{
      if(this.status===FULFILLED){
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            //判断x的值是普通值还是promise对象
            //如果是普通值直接调用resolve
            //如果是promise对象查看promise对象的返回结果
            //再根据promise对象返回的结果决定调用resolve还是reject
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
      }
      if(this.status===REJECTED){
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        
      }else{
      //等待,存储成功和失败回调
        this.successCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
        this.failCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
      }
    })
    
    return promise2
  }
}

function resolvePromise(promise2,x,resolve,reject){
  if(x===promise2){
  reject(new TypeError('chaining cycle detected for promise'))
  }
  if(x instanceof MyPromise){
    //x.then(value=>resolve(value),reason=>reject(reason))
    x.then(resolve,reject)
  }else{
    resolve(x)
  }
}

macro-task(宏任务) 和 micro-task(微任务)。


所有任务分为 macro-task 和 micro-task:

  • macro-task:主代码块、setTimeout、setInterval等(可以看到,事件队列中的每一个事件都是一个 macro-task,现在称之为宏任务队列)
  • micro-task:Promise、process.nextTick等

JS引擎线程首先执行主代码块。

每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。

在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾。

micro-task必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。

也就是说,在某一个macro-task执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有micro-task都执行完毕(在渲染前)。

这样就可以解释 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。

在node环境下,process.nextTick的优先级高于Promise,也就是说:在宏任务结束后会先执行微任务队列中的nextTickQueue,然后才会执行微任务中的Promise。
执行机制:

  1. 执行一个宏任务(栈中没有就从事件队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  5. 渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)

宏任务 macro-task(Task)

一个event loop有一个或者多个task队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来说task任务源:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任务 micro-task(Job)

microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差异

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

宏任务和微任务的区别

  • 宏队列可以有多个,微任务队列只有一个,所以每创建一个新的settimeout都是一个新的宏任务队列,执行完一个宏任务队列后,都会去checkpoint 微任务。
  • 一个事件循环后,微任务队列执行完了,再执行宏任务队列
  • 一个事件循环中,在执行完一个宏队列之后,就会去check 微任务队列

Generator异步方案 async/await语法糖

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353