最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JS训练营(二) | JS异步的几种方式

    正文概述 掘金(程序员思语)   2021-06-21   376

    这是我参与更文挑战的第18天,活动详情查看: 更文挑战

    前言

    JS的任务执行模式只有两种:同步、异步。在同步操作中,如果某个请求或者资源出现死锁或假死,都会造成GUI渲染阻塞,页面基本挂掉的情况,于是,有很多场景下须用到异步处理。同步操作,任务遵循队列顺序,异步操作,就相当于并线了,因此异步任务不具有阻塞效应。 在浏览器端,耗时长的操作都应该异步执行,避免阻塞,比如Ajax操作;做服务器端,异步 将是唯一的模式,因为如果运行同步执行,那么服务器性能将快速被吃掉,服务失去响应,下面将具体介绍几种异步处理方式。

    1.回调函数

    回调函数是异步操作最原始的方法,简单来说就是执行一个操作A,操作A执行完毕之后会执行操作B,依次回调,并且后者依赖前者,条理清晰,缺点也很明显,回调嵌套很深会造成 回调地狱(Callback hell)。

    ajax(url, () => {
      // 操作A
      ajax(url1, () => {
        // 操作B
        ajax(url2, () => {
          // 操作C
        });
      });
    });
    

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

    2.延时器 setTimeout

    延时器本身就是异步操作,它不取决于代码的执行顺序,取决于某个事件是否发生。

    fun1.on('done', fun2);
    // 待fun1发生done事件,就执行fun2。
    
    function fun1() {
      setTimeout(function () {
        // ...
        fun1.trigger('done');
      }, 1000);
    }
    // fun1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行fun2。
    

    优点:每个事件都可以指定若干个回调函数,可以"去耦合",有利于实现模块化 缺点:整个程序都要变成事件驱动型,无法保障顺序,运行流程不清晰,不方便看出主流程

    发布/订阅模式

    发布订阅模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。

    简单来说:就和用户订阅微信公众号道理一样,公众号可以被若干用户同时订阅,当公众号有新增内容时候,只要发布就好了,用户就能接收到最新的内容。(不等同于 观察者模式)

    举个栗子:

    jQuery.subscribe('done', f2);
    // 操作一:f2向信号中心jQuery订阅done信号
    function f1() {
      setTimeout(function () {
        // ...
        jQuery.publish('done');
      }, 1000);
    }
    // 操作二:f1执行完成后,向中心jQuery发布done信号,从而引发f2的执行。
    jQuery.unsubscribe('done', f2);
    // 操作三:f2完成执行后,可以取消订阅(unsubscribe)
    

    优点:观察者和订阅者之间没有依赖,松散耦合,改进代码管理和潜在的复用。

    附上观察者模式实现代码(JavaScript 版)

    //观察者列表
    function ObserverList() {
      this.observerList = [];
    }
    ObserverList.prototype.add = function (obj) {
      return this.observerList.push(obj);
    };
    ObserverList.prototype.count = function () {
      return this.observerList.length;
    };
    ObserverList.prototype.get = function (index) {
      if (index > -1 && index < this.observerList.length) {
        return this.observerList[index];
      }
    };
    ObserverList.prototype.indexOf = function (obj, startIndex) {
      var i = startIndex;
      while (i < this.observerList.length) {
        if (this.observerList[i] === obj) {
          return i;
        }
        i++;
      }
      return -1;
    };
    ObserverList.prototype.removeAt = function (index) {
      this.observerList.splice(index, 1);
    };
    
    //目标
    function Subject() {
      this.observers = new ObserverList();
    }
    Subject.prototype.addObserver = function (observer) {
      this.observers.add(observer);
    };
    Subject.prototype.removeObserver = function (observer) {
      this.observers.removeAt(this.observers.indexOf(observer, 0));
    };
    Subject.prototype.notify = function (context) {
      var observerCount = this.observers.count();
      for (var i = 0; i < observerCount; i++) {
        this.observers.get(i).update(context);
      }
    };
    
    //观察者
    function Observer() {
      this.update = function () {
        // ...
      };
    }
    

    3.Promise

    ES6 的新特性之一 ES6 的新特性之一,Promise 本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。这明显是异步操作。Promise 有三种状态:Pending(初始)、Fulfilled(成功)、Rejected(失败),需要注意的是这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved ,就不能再次改变为 Fulfilled,具有唯一性。

    // 上面栗子的改写
    ajax(url)
      .then((res) => {
        console.log(res);
        return ajax(url1);
      })
      .then((res) => {
        console.log(res);
        return ajax(url2);
      })
      .then((res) => console.log(res));
    

    promise 的链式调用

    • 1.每次调用返回的都是一个新的 Promise 实例(这就是 then 可用链式调用的原因)
    • 2.如果 then 中返回的是一个结果的话会把这个结果传递下一次 then 中的成功回调
    • 3.如果 then 中出现异常,会走下一个 then 的失败回调
    • 4.在 then 中使用了 return,那么 return 的值会被 Promise.resolve() 包装(见例 1,2)
    • 5.then 中可以不传递参数,如果不传递会透到下一个 then 中(见例 3)
    • 6.catch 会捕获到没有捕获的异常
    let fs = require('fs');
    function read(url) {
      return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
          if (err) reject(err);
          resolve(data);
        });
      });
    }
    read('./name.txt')
      .then(function (data) {
        throw new Error(); //then中出现异常,会走下一个then的失败回调
      }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到
      .then(function (data) {
        console.log('data');
      })
      .then()
      .then(null, function (err) {
        console.log('then', err); // then error
      })
      .catch(function (err) {
        console.log('error');
      });
    

    4.Generators

    Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程

    // 使用 * 表示这是一个 Generator 函数
    // 内部可以通过 yield 暂停代码
    // 通过调用 next 恢复执行
    function* test() {
      let a = 1 + 2;
      yield 2;
      yield 3;
    }
    let b = test();
    console.log(b.next()); // > { value: 2, done: false }
    console.log(b.next()); // > { value: 3, done: false }
    console.log(b.next()); // > { value: undefined, done: true }
    

    从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是Generator 函数的简单实现。

    完整的栗子:

    function generator(cb) {
      return (function () {
        var object = {
          next: 0,
          stop: function () {},
        };
        return {
          next: function () {
            var ret = cb(object);
            if (ret === undefined)
              return {
                value: undefined,
                done: true,
              };
            return {
              value: ret,
              done: false,
            };
          },
        };
      })();
    }
    // 如果你使用 babel 编译后可以发现 test 函数变成了这样
    function test() {
      var a;
      return generator(function (_context) {
        while (1) {
          switch ((_context.prev = _context.next)) {
            // 可以发现通过 yield 将代码分割成几块
            // 每次执行 next 函数就执行一块代码
            // 并且表明下次需要执行哪块代码
            case 0:
              a = 1 + 2;
              _context.next = 4;
              return 2;
            case 4:
              _context.next = 6;
              return 3;
            // 执行完毕
            case 6:
            case 'end':
              return _context.stop();
          }
        }
      });
    }
    

    5.async / await

    一个函数如果加上 async ,那么该函数就会返回一个 Promise,并且await 只能在 async 函数中使用。

    function sleep() {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('finish');
          resolve('sleep');
        }, 2000);
      });
    }
    async function test() {
      let value = await sleep();
      console.log('object');
    }
    test();
    

    上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

    在举个栗子:

    var a = 0;
    var b = async () => {
      a = a + (await 10);
      console.log('2', a); // -> '2' 10
      //首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来因为 await 是异步操作,所以会先执行 console.log('1', a)这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10,然后后面就是常规执行代码了
      a = (await 10) + a;
      console.log('3', a); // -> '3' 20
    };
    b();
    a++;
    console.log('1', a); // -> '1' 1
    

    并发请求

    let fs = require('fs');
    function read(file) {
      return new Promise(function (resolve, reject) {
        fs.readFile(file, 'utf8', function (err, data) {
          if (err) reject(err);
          resolve(data);
        });
      });
    }
    function readAll() {
      read1();
      read2(); //这个函数同步执行
    }
    async function read1() {
      let r = await read('1.txt', 'utf8');
      console.log(r);
    }
    async function read2() {
      let r = await read('2.txt', 'utf8');
      console.log(r);
    }
    readAll(); // 2.txt 3.txt
    // async/await目前算是比较好的方案
    

    参考书籍

    JavaScript 高级程序设计

    JavaScript 设计模式

    InfoQ 前端面试指南


    起源地下载网 » JS训练营(二) | JS异步的几种方式

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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