最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 学习笔记—JavaScript异步编程

    正文概述 掘金(是贝贝贝贝贝波a)   2021-08-16   798

    前言

    正文

    一、同步和异步

    同步

    在学习异步之前我们先来看一下同步,比如在调用函数取得返回值的时候,能够直接得到预期结果(得到了预期的返回值),是按照你的代码顺序执行的,是连续的,那么就说这个函数是同步执行的。

    下边看一个例子:

    //在函数返回的时候,获得了预期的效果,即在控制台上打印了‘123’
    var A = function(){};
    A.prototype.n = 123;
    var b = new A();
    console.log(b,n);  // 123
    

    如果函数是同步的,即是调用函数执行的任务比较耗时,也会一直等待直到得到预期结果。 因为它是按代码执行顺序执行的。

    异步

    如果在调用函数返回值的时候,不直接得到预期结果(预期的返回值),而是需要通过一定的方式获得,是不连续的不按代码顺序执行的,那么就可以说这个函数是异步的。

    如下所示:

    //读取文件
    fc.readFile('hello','utf8',function(err,data){
        console.log(data)
    });
    //网络请求
    var pzh = new XMLHttpRequest();
    pzh.onreadystatechange = yyy;  // 这里添加回调函数
    pzh.open('GET',url);
    pzh.send();//发起函数
    

    上述示例中读取文件函数 readFile和网络请求的发起函数 ,send都将执行耗时操作,虽然函数会立即返回,但是不能立刻获取预期的结果,因为耗时操作交给其他线程执行,暂时获取不到预期结果。而在JavaScript中通过回调函数 function(err, data) { console.log(data); }和 onreadystatechange ,在耗时操作执行完成后把相应的结果信息传递给回调函数,通知执行JavaScript代码的线程执行回调。

    简单来说:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

    二、首先要知道的异步机制

    浏览器内核的多线程

    学习笔记—JavaScript异步编程

    我们都知道JavaScript是单线程的,但是浏览器的内核是多线程的;他们在内核的控制下互相配合以保持同步,一个浏览器至少实现三个常驻:Javascrpt引擎线程、GUI渲染线程、浏览器事件触发线程。

    • JS引擎:基于事件驱动单线程执行的,JS引擎一直等待这任务队列中任务的到来,然后加以处理;浏览器无论什么时候都只有一个JS线程在运行JS程序。

    • GUI渲染线程:当界面需要重绘或由于某种操作引发回流时,线程就会执行。这里需要注意的是,渲染线程和JS引擎线程是不能同时进行的。

    • 事件触发线程:当一个事件被触发时,该线程会把世间添加到等待队列的队尾,等待JS引擎的处理,这些时间可来自JavaScript引擎执行当前的代码块,如:setTimeOut,也可以来自浏览器内核和其他线程如鼠标点击;AJAX异步请求等,但是由于JS 的单线程关系,所有这些事情都得排队等待JS引擎处理。

    事件循环机制

    学习笔记—JavaScript异步编程

    如上图所示,左边的栈存储的是同步任务,就是那些能立即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不需要回调函数的操作都可归为这一类。

    右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每个异步任务都和回调函数相关联。

    JS引擎线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。

    JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环(Eventloop)。

    什么是宏任务与微任务?

    我们都知道 Js 是单线程的,但是一些高耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。

    在异步模式下,创建异步任务主要分为宏任务与微任务两种。ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。

    • 1)宏任务 (macrotask):优先级低,先定义的先执行。包括:ajax,setTimeout,setInterval,事件绑定,postMessage,MessageChannel(用于消息通讯)。
    • 2)微任务 (microtask):优先级高,并且可以插队,不是先定义先执行。包括:promise.then,async/await [generator],requestAnimationFrame,observer,MutationObserver,setImmediate。

    学习笔记—JavaScript异步编程 由上图可以看到:

    从JS主线程(整体代码)开始第一次循环,发起异步任务后,由(橙色)线程执行异步操作,而JS引擎主线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局)。然后执行所有的micro-task,当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的macro-task,这样一直循环下去。

    根据事件循环机制,我们重新梳理一下流程:

    1)首先执行栈里的任务

    2)先找微任务队列,如果微任务队列中有,先从微任务队列中,一般按照存放顺序获取并且去执行。

    3)如果微任务队列中没有,则再去宏任务队列中查找,在宏任务队列中,一般是按照谁先到达执行的条件,就先把谁拿出来执行。

    4)以此循环

    明白事件循环之后我们要知道Javascript异步编程先后经历了四个阶段,分别是Callback阶段,Promise阶段,Generator阶段和Async/Await阶段。

    三、回调函数(Callback)阶段

    回调函数是异步操作最基本的方法。

    demo1:假定有一个异步操作(asyncFn),和一个同步操作(normalFn)。

    function asyncFn(){
        setTimeout(() => {
            console.log('asyncFn');
         ),0)
    }
    
    function normalFn(){
        console.log('normalFn');
    }
    asyncFn();   //asyncFn
    normalFn();   //normalFn
        
    

    如果按照正常的JS处理机制来说,同步操作一定发生在异步之前。如果我想要将顺序改变,最简单的方式就是使用回调(callback)的方式处理。

    function asyncFn(callback){
        setTimeout(() => {
            console.log('asyncFn');
            callback();
        },0);
    }
    function normalFn(){
        console.log('normalFn');
    }
    
    asyncFn(normalFn);
    //asyncFn
    //normalFn
    

    回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况,容易出现回调地狱,可读性差),而且每个任务只能指定一个回调函数。此外不能使用 try catch 捕获错误,不能直接return。

    回调函数易混淆点——传参:

    一,将回调函数的参数作为与回调函数同等级的参数进行传递。

    学习笔记—JavaScript异步编程

    二,回调函数的参数在调用回调函数内部创建。

    学习笔记—JavaScript异步编程

    事件监听、发布订阅

    事件监听

    事件监听也是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。

    下边看例子:还是以函数f1和f2为例

    f1.on('done', f2);  //f2必须等到f1执行完成,才可执行
    
    function f1() { 
        setTimeout(function () { // ... 
            f1.trigger('done'); 
        }, 1000); 
        }
    

    以上,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。

    这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”,有利于实现模块化。

    缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

    发布订阅模式

    发布订阅式的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。

    假定,一家三口,妈妈作为“发布者”(publisher)实施和发布信号,爸爸作为中介“订阅”(subscribe)和处理这个信号,最后小明"订阅者"(subscriber)知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern)。

    下边来看代码:

    //订阅者接收到消息
    function eat() {
        console.log('妈妈做好饭啦,去吃饭啦');
    }
    
    function cooking() {
        console.log('妈妈认真做饭中');
        //发布者向订阅中介发布消息
        setTimeout(() => {
            console.log('孩儿他爸饭做好了,叫小明来吃饭')
            Dad.publish("done");//中介接收消息
        },3000)
    }
    
    function read(){
        console.log('小明假装学习') //订阅者等消息
        Dad.subscribe('done',eat);
    }
    
    //执行代码
    cooking();
    read()
    
    /*执行顺序
    妈妈认真做饭中
    小明假装学习
    孩儿他爸饭做好了,叫小明来吃饭
    妈妈做好饭啦,去吃饭啦
    
    */
    

    这种模式下实现的异步编程,本质上还是通过回调函数实现的 ,但是依然存在回调嵌套和无法捕捉异常问题的情况,接下来进入Promise阶段,看看是否能解决这两个问题。

    四、Promise阶段

    Promise 并不是指某种特定的某个实现,它是一种规范(PromiseA+规范),是一套处理JavaScript异步的机制。

    1.Promise的三种状态

    • Promise有三种状态pending,fulfilled和rejected
    • 状态转换只能是 pending到 resolved
    • 或者pending到 rejected

    状态一旦转换完成,不能再次转换

    可以由下图表示:

    学习笔记—JavaScript异步编程 附上代码栗子:

    let p = new Promise((resolve,reject) => {
        reject('reject');
        resolve('success')  //无效代码不会执行
    })
    p.then(
        value => {
            console.log(value)
        },
        reason => {
            console.log(reason)  //reject
        }
       )
    

    当我们构造Promise 的时候,构造函数内部的代码是立即执行的

    2.链式Promise

    先看两个例子:

    demo1;

    //例1:
    Promise.resolve(1)
        .then(res => {
            console.log(res);        //打印 1
            return 2   //包装成Promsie.resolve(2)
        })
        .catch(err => 3);   //这里catch会捕获没有捕获的异常
        .then(res => console.log(res))   //打印 2
    

    当Promise创建对象调用resolve(...)或reject(...)时,这个Promise通过then(...)注册的回调函数就会在新的异步时间点上被触发。(then的链式调用); 在then中使用return,那么return的值会被Promise.resolve()包装。

    demo2:以家务分配为例:

    function read() {
      console.log('小明认真读书');
    }
    
    function eat() {
      return new Promise((resolve, reject) => {
        console.log('好嘞,吃饭咯');
        setTimeout(() => {
          resolve('饭吃饱啦');
        }, 1000)
      })
    }
    
    function wash() {
      return new Promise((resolve, reject) => {
        console.log('唉,又要洗碗');
        setTimeout(() => {
          resolve('碗洗完啦');
        }, 1000)
      })
    }
    const cooking = new Promise((resolve, reject)=>{ 
        console.log('妈妈认真做饭'); 
        setTimeout(() => { 
            resolve('小明快过来,开饭啦'); 
        }, 2000); 
    })
    
    cooking.then(msg => { 
        console.log(msg); 
        return eat(); 
    }).then(msg => { 
        console.log(msg); 
        return wash();
    }).then(msg => {
        console.log(msg);
        console.log('做完家务了,可以玩了')
    })
    
    read();
    
    /* 执行顺序: 
    妈妈认真做饭 
    小明认真读书 
    小明快过来,开饭啦 
    好嘞,吃饭咯 
    饭吃饱啦 
    唉,又要洗碗
    碗洗完啦 
    做完家务了,可以玩了 
    */
    

    其实可以看出Promise.then()可以解决的回调地狱(callback hell),但是无法捕获异常,还需要调用回调函数来解决。

    五、生成器Generators/yield

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。

    • function *会定义一个生成器函数,并返回一个Generator(生成器)对象,其内部可以通过 yield 暂停代码,通过调用 next 恢复执行。

    简单看一下例子:

     function * gen() {
         yield console.log('hello');
         yield console.log('world');
         return console.log('ending');
    }
    
    var hw = gen();
    

    在控制台输入hw.next():

    hw.next(); 
    index.html:156 hello 
    hw.next(); 
    index.html:157 world 
    hw.next(); 
    index.html:158 ending
    

    上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

    必须调用遍历器对象的next()方法,使得指针移向下一个状态。每次调用next方法,内部指针就从上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

    generator很方便处理异步(一般要配合tj/co库来用),这里举例说一下coco是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码

    安装co库只需:npm install co

    也可以自己去github找一下源码,了解一下

    index.js

    var co = require('co')
    var fs = require('fs')
    // wrap the function to thunk
    function readFile(filename) {
        return new Promise(function(resolve, reject) {
            fs.readFile(filename, function(err, date) {
                if (err) reject(err)
                resolve(data)
            })
        })
    }
    // generator 函数
    function *gen() {
        var file1 = yield readFile('./file/1.txt') // 1.txt内容为:content in 1.txt
        var file2 = yield readFile('./file/2.txt') // 2.txt内容为:content in 2.txt
        console.log(file1)
        console.log(file2)
        return 'done'
    }
    // co
    co(gen).then(function(err, result) {
        console.log(result)
    })
    // content in 1.txt
    // content in 2.txt
    // done
    

    co 函数库可以让你不用编写 generator 函数的执行器,generator 函数只要放在 co 函数里,就会自动执行。 再来看一个例子

    co(function *(){ 
        try { 
          var res = yield get('http://baidu.com');
          console.log(res); 
        } catch(e) { 
          console.log(e.code) 
       } 
    })
    

    co 最大的好处在于通过它可以把异步的流程以同步的方式书写出来,并且可以使用 try/catch。

    六、async/await

    使用async/await,可以轻松地达成之前使用生成器和co函数所做到的工作; 一句话,async 函数就是 Generator 函数的语法糖。

    然后用async/await实现上边(两个文件)的例子就可以这么写:

    var asyncReadFile = async function (){
      var f1 = await readFile('./file/1.txt');
      var f2 = await readFile('./file/2.txt');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    

    一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。

    1.async函数的特点:

    1.执行 async 函数,返回的都是Promise 对象

    async function test1(){
         return 123;
    }
    async function test2(){
         return Promise.resolve(2);
    }
    const result1 = test1();
    const result2 = test2();
    console.log('result1',result1);   //promise
    console.log('result2',result2)    //promise
    

    自己可以 打印验证一下。

    2.Promise.then 成功的情况对应 await

    async function test3(){
        const p3 = Promise.resolve(3);
        p3.then(data => {
            console.log('data',data);    
        })
        //await 后边跟一个promise对象
        const data =await p3;  
        console.log('data',data);   //data3
    }
    test3()
    
    async function test4(){       //await跟一个普通的数
        const data4 = await 4;   //await Promise.resolve(4)
        console.log('data4',data4);     //data4.4
    }
    test4();
    
    async function test5(){
         const data5 = await test1();   //  await跟一个异步的函数
         console.log('data5',data5);     //data5.123
    }
    test5()
    
    1. Promise.catch 异常的情况 对应 try...catch
    async function test6(){
        const p6 = Promise.reject(6);
        // const data6 = await p6;
        // console.log('data6',data6)    //报错:Uncaught (in promise)     6
        try{
            const data6 = await p6;
            console.log('data6',data6);
        }catch(k){
            console.error('k',k);     //捕获异常  k 6
        }
    }
    test6()
    

    总结了这么多,如果还是不太理解,我推荐看一下这些 实战题(ES6Promise实战练习题)加速帮助消化。

    此文章为个人学习笔记分享,技术有限,欢迎大家一起讨论学习。

    参考文章:

    1.JavaScript异步机制详解

    2.JS 异步编程六种方案

    3.Javascript异步编程的4种方法

    4.JS 基础之异步(五):Generator


    下载网 » 学习笔记—JavaScript异步编程

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元