来聊聊前端的异步

2,928 阅读15分钟

异步

  • 出现的原因
    • JavaScript语言的执行环境是"单线程"
    • 所谓"单线程",就是指一次只能只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
    • 单线程这种模式好处是实现起来比较简单,执行环境单一。坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段JavaScript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
    • 为了解决这个问题,JavaScript语言将任务的执行模式分成两种,同步和异步。
  • 异步的使用场景
    • Ajax操作
  • 异步编程的实现
    • 回调函数

      • 优点:简单丶很容易理解和部署。
      • 缺点:不利于阅读和维护,各部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。
      • 代码实现
        // 假如有两个函数f1和f2 后者等待前者的执行结果
        // 如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数
        
        function f2(c) {
            console.log(c, 'hehe');
        }
        function f1(callback) {
            setTimeout(function() {
                // f1任务代码
                console.log('f1执行完');
                callback('f2开始执行', 'hehe');
            }, 1000);
        }
        f1(f2);
        
    • 事件监听

      • 代码实现
      • 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。
      • 缺点:整个程序都变成事件驱动,运行流程会变得很不流畅。
        f1.on('done', f2);
        
    • 发布/订阅

      • 定义:我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"整个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式",又称"观察者模式"。
      • 优点:
        • 这种方法的性质和"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号丶每个信号有多少订阅者,从而监控程序的运行。
        • 一是时间上的解耦,而是对象上的解耦
        • 即可用于异步编程中,也可以用帮助我们完成更松耦合的代码编写
      • 缺点:
        • 创建订阅者本身需要消耗一定的时间和内存
        • 当订阅一个消息时,也许次消息并没有发生,但这个订阅者会始终存在内存中
        • 观察者模式弱化了对象之间的联系,这本是好事,但如果过度使用,对象与对象之间的联系也会隐藏的很深,会导致项目的难以追踪维护和理解。
      • 使用场景
        • DOM事件
        • 自定义事件
      • 代码实现
        function Event() {
            // 存储不同的事件类型对应不同的处理函数,保证后续emmit可以执行。
            this.cache = {};
        }
        // 绑定事件
        Event.prototype.on = function(type, handle) {
            if(!this.cache[type]) {
                this.cache[type] = [handle];
            }else {
                this.cache[type].push(handle);
            }
        }
        // 事件触发
        Event.prototype.emmit = function() {
            var type = arguments[0],
                arg = [].slice.call(arguments, 1);
            for(var i = 0; i < this.cache[type].length; i++) {
                this.cache[type][i].apply(this, arg);
                if(this.cache[type][i].flag) {
                    this.cache[type].splice(i, 1);
                    if(this.cache[type][i].flag) {
                        this.cache[type].splice(i, 1);
                    }
                }
            }
        }
        // 解除某个事件类型
        Event.prototype.empty = function(type) {
            this.cache[type] = [];
        }
        // 解除某个事件
        Event.prototype.remove = function(type, handle) {
            this.cache[type] =  this.cache[type].filter((ele) => ele != handle);
        }
        // 绑定一次事件
        Event.prototype.once = function(type, handle) {
            if(!this.cache[type]) {
                this.cache[type] = [];
            } 
            // 做标记
            handle.flag = true;
            this.cache[type].push(handle);
        } 
          
          
        function detail1(time) {
            console.log('overtime1' + time);
        }
        function detail2(time) {
            console.log('overtime2' + time);
        }   
        var oE = new Event();
        oE.on('over', detail1);
        oE.on('over', detail2);
        oE.emmit('over', '2019-11-11');
        oE.remove('over', detail2);
        oE.emmit('over', 'second-11-11');
        
    • Promise对象

      • 定义
        • 是CommonJS工作组提出的一种规范,目的是为了异步编程提供统一接口。
        • 简单来说,它的思想就是,每一个异步任务都返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
      • 优点:
        • 回调函数变成链式写法,程序的流程可以看得很清楚,而且有一整套流程的配套方法。
        • 而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不要担心是否错过了某个事件或信号。
      • 缺点:就是编写和理解,相对比较难。

Promise详解

  • 定义:Promise是异步编程的一种解决方案,所谓的Promise简单来说就是一个容器,里面保存着未来才会结束的事件的结果。
  • 特点
    • 对象的状态不受外界影响,Promise对象代表一种异步操作,有三种状态:Pending(进行中)丶Resolve(已完成)丶Rejected(已失败),只有异步操作的结果可以改变状态,其它的任何操作都不能改变状态。
    • 一旦状态改变了,就不会再变了,任何时候都可以得到这个结果。Promise对象的状态只有两种可能:Pending->Resolve或Pending->Resolve,只要这两种情况发生了,并且会一直保持这个结果,这与事件监听不同,事件的特点是不同时间。
  • 缺点:
    • 首先无法取消Promise,一旦创建它就会立即执行,中途无法取消。
    • 其次,如果还不设置回调函数,Promise内部抛出的错误,不会反映到外部
    • 最后,当处于Pending状态时,无法得知目前进展到哪一个阶段了。
  • 源码实现
    • 功能
      • 同步
      • 异步
      • then链式操作
        • 处理返回普通值
        • 处理返回Promise值
      • then异步操作
      • then捕捉错误
      • 空then
      • Promise.all():全部成功才成功,一个失败全部失败
      • Promise.race():哪个状态改变了,P的状态就改变了
      function MyPromise(executor) {
          var self = this;
          self.status = 'pending';
      
          self.resolveValue = null;
          self.rejectReason = null;
      
          self.resolveCallBackList = [];
          self.RejectCallBackList = [];
      
          function resolve(value) {
              if(self.status === 'pending') {
                  self.status = 'Fulfilled';
                  self.resolveValue = value;
                  self.resolveCallBackList.forEach(function(ele) {
                      ele();
                  });
              }
          }
      
          function reject(reason) {
              if(self.status === 'pending') {
                  self.status = 'Rejected';
                  self.rejectReason = reason;
                  self.RejectCallBackList.forEach(function(ele) {
                      ele();
                  })
              }
          }
      
          try {
              executor(resolve, reject);
          }catch(e) {
              reject(e);
          }
      }
      
      
      function ResolutionReturnPromise(nextPromise, returnValue, res, rej) {
          if(returnValue instanceof MyPromise) {
              returnValue.then(function() {
                  res(val);
              }, function(reason) {
                  rej(reason);
              })
          }else {
              res(returnValue);
          }
      }
      
      MyPromise.prototype.then = function(onFulfilled, onRejected) {
          if(!onFulfilled) {
              onFulfilled = function(val) {
                  return val;
              }
          }
          if(!onRejected) {
              onRejected = function(reason) {
                  throw new Error(reason);
              }
          }
      
          var self = this;
      
          var nextPromise = new MyPromise(function(res, rej) {
              if(self.status === 'Fulfilled') {
                  setTimeout(function() {
                      try {
                          var nextResolveValue = onFulfilled(self.resolveValue);
                          ResolutionReturnPromise(nextPromise, nextResolveValue, res, rej);
                      }catch(e) {
                          rej(e);
                      }
                  }, 0);
              }
              if(self.status === 'Rejected') {
                  setTimeout(function() {
                      try{
                          var nextResolveValue = onRejected(self.rejectReason);
                          ResolutionReturnPromise(nextPromise, nextRejectValue, res, rej);
                      }catch(e) {
                          rej(e);
                      }
                  })
              }
      
              if(self.status === 'pending') {
                  self.resolveCallBackList.push(function() {
                      setTimeout(function() {
                          try {
                              var nextResolveValue = onFulfilled(self.resolveValue);
                              ResolutionReturnPromise(nextPromise, nextResolveValue, res, rej);
                          }catch(e) {
                              rej(e);
                          }
                      }, 0)
                  })
              }
          })
      
      
          return nextPromise;
      }
      
      MyPromise.race = function(promiseArr) {
          return new Promise(function(resolve, reject) {
              promiseArr.forEach(function(promise, index) {
                  promise.then(resolve, reject);
              })
          })
      }
      
  • 使用场景
    • Ajax请求
      • 定义:
        • Ajax是Asynchronous javascript and xml的缩写,用JavaScript以异步的形式操作XML(现在操作的是JSON)。随着谷歌地图的横空出世,这种不需要刷新页面就可以与服务器通讯的方式很快被人们所知。在传统的Web模型中,客户端向服务端发送一个请求,服务端会返回整个页面。
        • 我们前面学习的form表单来传输数据的方式就属于传统的Web模型,当我们点击submit按钮之后,整个页面就会被刷新一下。form表单有三个很重要的属性,分别是method丶action和enctype。method是数据传输的方式,一般是GET或者POST,action是我们要把数据传送到的地址,enctype的默认值是"application/x-www-form-urlencoded",即在发送前编码所有字符,这个属性值即使我们不写也是默认这个的。但是当我们在使用包含文件上传控件的表单的时候,这个值就必须更改成"multipart/form-data",即不对字符进行编码。而在Ajax模型中,数据在客户端与服务器之间独立传输,服务器不再返回整个页面。
      • 优点
        • 页面无刷新,在页面内与服务器进行通信,给用户的体验更好。
        • 使用异步的形式与服务器进行通信,不需要打断用户的操作,给用户的体验更好。
        • 减轻服务器的负担。
        • 不需要插件或者小程序
      • 缺点
        • 不支持浏览器的后退机制
        • 安全问题,跨站点脚本攻击丶sql注入攻击
        • 对搜索引擎支持较弱
        • 不支持移动端设备
        • 违背了url和资源定位的初衷
      • 对象属性
        • onreadystatechange:状态改变触发器
        • readyState:对象状态
          • 0:表示为初始化,此时已经创建了一个XMLHttpRequest对象
          • 1:表示读取中,此时代码已经调用XMLHttpRequest的open方法并且XMLHttpRequest已经将请求发送到服务器。
          • 2:表示已读取,此时已经通过open方法把一个请求发送到服务端,但是还没收到。
          • 3:代表交互中,此时已经收到http响应头部信息,但是消息主体信息还没有完全接收。
          • 4:代表完成,此时响应已经被完全接受
        • responseText:服务器进程返回数据的文本版本
        • responseXML:服务器进程返回数据的兼容DOM的XML文本对象
        • status:服务器返回的状态码
      • 代码实现
        function AJAX(json) {
            var url = json.url,
                method = json.method,
                flag = json.flag,
                data = json.data,
                callBack = json.callBack,
                xhr = null;
            // 1. 创建异步对象    
            if(window.XMLHttpRequest) {
                // 一般主流浏览器支持这个
                xhr = new window.XMLHttpRequest();
            }else {
                // IE6以下用这个
                xhr = new ActiveXObject('Microsoft.XMLHTTP');
            } 
            // 2. 让异步对象监听接收服务器的响应数据
            xhr.onreadystatechange = function() {
                if(xhr.readyState === 4 && xhr.status === 200) {
                    // 数据已经可用了
                    callBack(xhr.responseText);
                }
            } 
            // 建立对服务器的调用
            if(method === 'get') {
                url += '?' + data + new Data().getTime();
                // 3. 设置请求方式
                xhr.open('get', url, flag);
                xhr.send();
            }else {
                xhr.open('post', url, flag); 
                // 4. 设置请求头
                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                // 5. 设置请求体
                xhr.send(data);
            }
        }
        
    • 自定义弹窗处理
    • 图片加载

Generator

  • 定义:生成器,本身是函数,执行后返回迭代对象,函数内部要配合yeild使用Generator函数,会分段执行,遇到yeild即暂停。
  • 特点
    • function和函数名之间需要带*
    • 函数体内部yeild表达式,产出不同的内部状态(值)
    • 可以交出函数的执行权
  • 作用:配合Promise解决回调地狱问题
  • 配合库
    • Co
  • 使用场景
    • 可以在任意对象上部署Iterator接口,配合Promise和Co库解决回调地狱的问题。

async

  • 定义:是Generator语法糖,通过babel编译后可以看出它就是Generator+Promise+Co递归思想实现的,配合await使用。
  • 目的:优雅解决异步操作的问题
    • 异步编程会出现问题
      • 回调地狱
        // 这样能层层回调的事情,你会发现要写多个回调函数,一堆.then不够优雅,
        //     有没有写法写起来像同步很优雅,最后也是通过异步的方式来搞定层层回调的结果。
        function readFile(path) {
            return new Promise((res, rej) => {
                fs.readFile(path, 'utf-8', (err, data) => {
                    if(err) {
                        rej(err); 
                    }else {
                        res(data);
                    }
                })
            })
        };
        readFile('https://api.github.com/users/superman66')
          .then((val) => {
              return readFile(val);
          }, () => {}).then((val) => {
              return readFile(val);
          })
          .then((val) => {
            console.log(val);
          });
        
        // async写法
        async function read(url) {
            let val1 = await readFile(url);
            let val2 = await readFile(val1);
            let val3 = await readFile(val2);
        }
        read('https://api.github.com/users/superman66')
          .then((val) => console.log(val));
        
      • 解决了try catch可以异步的方式捕获异常
        async function read(url) {
            try {
                let val1 = await readFile(url);
                let val2 = await readFile(val1);
                let val3 = await readFile(val2);
            }catch(e) {
                console.log(e);
            }
        }
        readFile('./data/number.txt').then((val) => console.log(val));
        
      • 解决同步并发异步的结果
        • Promise.all()
          // 之前通过解决同步并发异步的结果是使用Promise.all()去解决的,首先Promise.all()使用的时候需要传递多个promise对象
          //     其次他是全部成功才成功,一个失败全部失败
          Promise.all([readFile('./data/number1.txt), readFile('./readFile/number2.txt'), readFile('./data/number3.txt')])
              .then((val) => console.log(val), (reason) => console.log(reason));
          
        • async
          async function read1() {
              let val1 = null;
              try{
                  val1 = await readFile('./data/number1.txt');
                  console.log(val1);
              }catch(e) {
                  console.log(e);
              }
          }
          async function read2() {
              let val2 = null;
              try{
                  val2 = await readFile('./data/number2.txt');
                  console.log(val2); 
              }catch(e) {
                  console.log(e);
              }
          }
          async function read3() {
              let val3 = null;
              try {
                  val3 = await readFile('./data/number3.txt');
                  console.log(val3);
              }catch(e) {
                  console.log(e);
              }
          }
          function readAll(...args) {
                  args.forEach((ele) => {
                      ele();
                  })
          }
          readAll(read1, read2, read3);
          
  • 异步编程的最高境界,就是根本不用关心它是不是异步。一句话,async函数就是Generator函数的语法糖。
  • 优点:
    • 内置执行器
      • Genarator函数的执行必须依靠执行器,所以才有Co函数库,而async函数自带执行器,也就是说,async函数的执行,与普通的函数一模一样,只要一行。
    • 更好的语义化
      • async和await比起星号与yield语义化更清楚了,async表示函数里有异步操作,async表示紧跟在后面的表达式需要等待结果。
    • 更广的适应性
      • co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值(数值丶字符串丶布尔值 但这时等同于同步操作)
    • 返回值是Promise
      • async函数返回值是Promise对象,比Generator函数返回的Iterator对象方便,可以直接使用then()方法进行调用。
  • async函数是非常新的语法功能,新到都不属于ES6,而是属于ES7。目前,它任处于提案阶段,但是转码器Babel和regenerator已经支持,转码后就能使用。
  • 注意点
    • await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
    • await命令只能用在async函数中,如果用在普通函数,就会报错,上面的代码会报错,因为await函数用在普通函数之中了,但是,如果将foreach方法的参数改成async函数,也会有问题。上面的代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行,正确的写法是采用for循环
    • 如果确实希望多个请求并发执行,可以使用Promise.all方法。

Rest参数

  • 目的:
    • Rest参数用于获取函数的多余参数,组成一个数组,放在形参的最后,这样就不需要arguments对象了。
    • 主要用于处理不定参数
    funtion fn(a, b, ...args) {
          // ...
    }
    
  • Rest参数和arguments对象区别:
    • rest参数只包括那些没有给出名称的参数,arguments包含所有参数。
    • arguments对象不是真正的数组,而rest参数是数组实例,可以直接应用sort,map,等方法
    • arguments对象拥有一些自己额外的功能(比如callee)
    • Rest参数简化了使用arguments获取多余参数的方法
  • 注意:
    • rest参数之后不能再有其他参数,否则会报错。
    • 函数的length属性,不包括rest参数
    • Rest参数可以被结构化(通俗一点,将rest参数的数据解析后一一对应),不要忘记参数用[]括起来,因为它是数组。
      function fn(...[a, b, c]) {
          console.log(a + b + c);
      }
      fn(1);
      fn(1, 2, 3);
      fn(1, 2, 3, 4);
      

Iterator

  • 目的:ES6引入这个Iterator是了这些数据有统一的遍历或者进行for of ...Array.from等操作,方便我们写代码的时候不用大范围的重构。
  • 定义
    • Iterator的思想取自于我们的迭代模式
    • forEach可以迭代数组,for in可以迭代对象,$.each只能迭代数组和对象,Iterator还能迭代Set和Map。
    • Set和Map原型上都有Symbol.Iterator这个迭代口,只要有这个接口都可以被for..of迭代
      // 把不可迭代数据变成可迭代数据
      let obj = {
          0: 'a',
          1: 'b',
          2: 'c',
          length: 3,
          [Symbol.iterator]: function() {
              let curIndex = 0;
              let next = () => {
                  return {
                      value: this[curIndex],
                      done: this.length == ++ curIndex
                  }
              }
              return {
                  next
              }
          }
      }
      console.log([...obj])
      for(let p of obj) {
          console.log(p);
      }
      
    • Generator要去生成一个迭代对象,为什么生成迭代对象呢,就是根据Symbol.iterator。
  • 分类
    • 内部迭代器
      • Array.prototype.forEach就是我们所说的迭代器,它就是用迭代模式的思想做出的函数,本质是内部迭代器。
    • 外部迭代器
      • 代码实现(ES5实现)
        function OuterInterator() {
            let curIndex = 0;
            let next = () => {
                return {
                    value: o[curIndex],
                    done: o.length == ++ curIndex;
                }
            }
            return {
                next
            }
        }
        let oIt = OuterIterator(arr);
        

Symbol

  • 特点
    • 唯一性
    • arr丶Set丶Map丶arguments丶nodelist都有这个属性Symbol.iterator 这个属性等于iterator迭代函数

异步加载JavaScript

  • JS加载的缺点:加载工具方法没必要阻塞文档,过多的JS加载会影响页面效率,一旦网速不好,那么整个网站都将等待JS加载而不进行后续等渲染工作
  • 目的:有些工具方法需要按需加载,用到再加载,不用不加载。
  • JavaScript异步加载的三种方案
    • defere异步加载
      • 但要等到dom文档全部解析完才会被执行,只有IE能用。(执行时不阻塞页面加载)(支持IE)
    • async异步加载
      • 加载完就执行,async只能加载外部脚本,不能把JS写在script标签里面(执行时不阻塞页面加载)
    • 创建script,插入到DOM中,加载完callBack
      • 代码实现
        function loadScript(url, callback) {
            var script = document.createElement('script'),
                script.type = 'text/javaScript';
            if(script.readyState) { // IE
                if(script.onreadystatechange === 'complete' || script.onreadystatechange === 'loaded') {
                    callback();
                }
            }else { // FireFox丶Safari丶Chrome and Opera 
                script.onload = function() {
                    callback();
                }
            }    
            script.url = url;
            document.head.appendChild(script);
        }
        

异步加载CSS

  • 通过JS动态插入link标签来异步载入CSS代码
    var myLink = document.createElement('link');
    myLink.rel = 'stylesheet';
    'myLink.href = './index.css';
    documemt.head.insertBefore(myLink, document.head.childNodes[document.head.childNodes.length - 1].nextSibling);
    
  • 利用link上的media属性
    • 将它设置为和用户当前浏览器环境不匹配的值,比如:media="print",甚至可以设置为一个完全无效的值media="jscourse"之类的。
    • 这样的话,浏览器就会认为CSS文件优先级非常低,就会在不阻塞的情况下进行加载。但是为了让CSS规则失效,最后还是要将media值改对才行。
      <link rel="style" href="css.style" media="jscourse" onload="this.media='all'">
      
  • rel="preload"
    • 通过preload属性值就是告诉浏览器这个资源随后会用到,请提前加载好。所以你看它加载完毕后,还是需要将rel值改回去,这才能让CSS生效。
    • 语义更好一些
    • as="style"这个属性,所以preload不仅仅可以用在CSS文件上,而是可以用在绝大多数的资源文件上。比如JS文件
      <link rel="preload" href="sccriptfile.js" as="script">
      
      // 要用的时候 就创建一个script标签加载它,这个时候直接从缓存中拿到这个文件了,因为提前加载好了
      var oScript = document.createElement('script');
      script.src = 'index.js';
      document.body.appendChild(script); 
      
    • Chrome完美支持,其他不支持。

参考链接:www.cnblogs.com/cjx-work/p/… juejin.cn/post/684490…