JS 异步相关

定时器相关

setInterval/clearInterval

设置/取消定时器,举例:

let i = 0;
let timer = setInterval(function() {
    if (i > 3) clearInterval(timer);
    console.log(i++);
}, 1000)
// 每秒输出一次i,当i大于3时取消定时器
setTimeout/clearTimeout

设置/取消异步延时代码,举例:

setTimeout(function() {
    console.log(1);
}, 1000)
// 1秒后输出1

ajax

Asynchronous JavaScript And XML,实质就是从服务器上读取文件,因此需要有服务器的环境下进行,这里在tomcat中测试。

编写ajax
1.创建一个ajax对象
var oAjax = new XMLHttpRequest();

上面那个有唯一的问题就是不兼容IE6及以下,如果是IE6就用:

var oAjax = new ActiveXObject("Microsoft.XMLHTTP");

所以兼容性判断时:

if (window.XMLHttpRequest) {    //如果不加window在IE6下还是会报错,看下面注解
    var oAjax = new XMLHttpRequest();
} else {
    var oAjax = new ActiveXObject("Microsoft.XMLHTTP");
}

注:
在JS中所有全局变量和一些关键字等都是window下的属性,比如定义一个:var a = 1,那么a已经定义,在console里结果是1,此时如果用window.a会发现结果和a一样都是1。
而变量作为一个属性去调用的话,就算没定义,结果也不会报错,而是显示undefined,比如前面很多标签的style属性没定义,但是我们直接在浏览器console里可以看到没设置值的属性结果不会报错,而是显示undefined。再比如没定义a之前在console里输入a会报错说没定义过,但是输入window.a会发现结果是undefined
上面兼容性判断要加window,就是因为在IE6下他会把这个当成一个变量值,所以如果不加window就会显示没定义,结果就报错了

2.连接服务器
Ajax对象.open('方法', '文件路径', 是否异步传输)

其三个参数中,方法有GETPOST,文件路径可以是绝对相对,第三个参数异步则true,同步则false,举例:

oAjax.open('get', 'aaa.txt', true);
3.发送请求
Ajax对象.send();
4.接收返回信息
Ajax对象.onreadystatechange=function(){
    if(oAjax.readyState == 4){  //内容解析完成
        if(oAjax.status == 200){    //文件读取成功
            …
        }
    }
}
Ajax对象属性

(1)Ajax对象下有readyState值,用来监控请求情况:

0   (未初始化)还未调用open()方法
1   (载入)已调用send()方法,正在发送请求
2   (载入完成)send()完成,已收到全部响应内容
3   (解析)正在解析响应内容,比如解析HTTPS发过来的加密内容
4   (完成)响应内容解析完成,可以在客户端调用了

(2)Ajax对象下还有status值,代表请求状态码,比如200是成功,参考http请求状态码即可。
(3)responseText,表示返回结果的内容

简单Ajax实例

(读取本地aaa.txt内容并显示)
html中:


js中:

function ajax_fun() {
    if (XMLHttpRequest) {
        var oAjax = new XMLHttpRequest();   //创建Ajax对象,除IE6及以下
    } else {
        var oAjax = new ActiveXObject("Microsoft.XMLHTTP"); //IE6及以下
    }
    oAjax.open('get', 'aaa.txt?t=' + new Date().getTime(), true);   //建立连接,且阻止缓存
    oAjax.send();   //发送请求
    oAjax.onreadystatechange = function() { //接收后执行内容
        if (oAjax.readyState == 4) { //响应解析完成
            if (oAjax.status == 200) { //文件读取成功,上面解析完成并不代表成功读取,可能会有文件不存在等情况
                alert(oAjax.responseText);  //显示读取的文本内容
            }
        }
    }
}

注:
可以看出原生的ajax语法过于麻烦,而JQuery框架为我们提供了一套语法更加简洁的ajax封装接口,方便我们编写ajax请求

编码

注意本地文件的编码和HTML页面的编码要相符,否则将会出现乱码

缓存/阻止缓存

一般ajax默认读取文件会缓存,所以在服务器文件更新后,用ajax读取的文件可能并不会即使更新,这在像股票之类的要求实时性高的页面里是很致命的,所以就需要阻止缓存,方法有很多,主要是为了让服务器感觉到接收请求的不同,比如常见的就是在读取的url后面加上会变化的参数,举例:

'url?t='+new Date().getTime()

因为getTime()获取的数据时1970年1月1号到现在所距离的毫秒数,所以每次访问时请求的参数值基本不可能一样,从而也就达到了阻止缓存的效果

Promise

语法优势

Primoise对象,可以实现链式调用,使用起来更加灵活,并且可以解决回调地狱的问题,例如下面代码:

asyncfun1(function(success1) {
  asyncfun2(success1, function(success2) {
    asyncfun3(success2, function(success3) {
      ...
    }, failCallback3())
  }, failCallback2())
}, failCallback1())

通过Promise对象改写就可以变成:

asyncfun1().then(function(success1) {
  return asyncfun2(success1)
})
.then(function(success2) {
  return asyncfun3(success2)
})
.then(function(success3) {
  ...
})
.catch(failCallback() {
  ...
})

可以看出使用Promise对象编写后的代码有以下优点:

  • 由于返回的是Promise对象,因此可以在异步任务完成后,后面自己指定何时进行回调操作
  • 代码无需嵌套,避免回调地狱
  • 统一失败异常处理
    当然使用async/await将更好的优化代码,但其本质也是基于Promise对象实现的(async声明的函数返回的是一个Promise对象)
定义Promise对象
new Promise((resolve, reject) => {
  ...
  resolve()
  ...
  reject()
})
.then(value => {
  ...
})
.catch(reason => {
  ...
})

可以看出Promise对象需要传入一个同步回调函数(不等待,立即执行),而该函数里会传入两个异步回调函数(会先放入回调队列,不会立即执行):resolvereject,分别代表在成功和失败时执行的回调(本质是调用onResolvedonRejected方法)。实例化的Promise对象通过then执行成功回调,catch捕捉失败回调

Promise状态

状态变化:pending(等待)->resolve(成功)/reject(失败)
只有通过resolve/reject发送通知时才改变状态,否则一直处于等待状态,并且状态改变是不可逆的,比如:

new Promise((resolve, reject) => {
  resolve("success");
  reject("failed");
})

因为已经先进入了成功状态,所以也就不会进入后面的失败状态了

then链式调用机制
  • 返回的是异常:会被catch捕捉
  • 返回的是Promise对象:对该对象的处理——then处理resolvecatch处理reject
  • 返回的是非异常且非Promise对象:返回值被当做下一个then的传入值(默认是成功状态)

举例:

new Promise((resolve, reject) => {
    resolve(1)
}).then(value => {  // value接收到promise对象
    console.log(value);
    return 2
}).then(value => {  // value接收到2
    console.log(value);
}).then(value => {  // value没接收到东西
    console.log(value);
    return new Promise((resolve, reject) => {
        resolve(3)
    })
}).then(value => {  // value接收到promise对象
    console.log(value);
    throw 5
}).catch(reason => {  // reason接收到异常5
    console.log(reason);
})

// 结果:
// 1
// 2
// undefined
// 3
// 5

注:
Promise还提供了resolvereject的api方便调用,例如:

new Promise((resolve, reject) => {
  resolve(xxx)
})

可以等价于:

Promise.resolve(xxx)

注2:
由于then会不停的链式调用下去,如果想要中断的话,可以返回return new Promise(() => {}),因为此时返回的是Promise对象,并且没有调用成功和失败的回调,所以后面的then也就不会执行了

finally

异步结果不论成功失败都一定会执行,举例:

new Promise((resolve, reject) => {
  resolve("success");
}).then(data => {
  ...
}).finally(() => {
  ...
})
Promise提供API
Promise.resolve()/Promise.reject()

成功和失败通知

Promise.all()

全部操作都成功则返回所有结果状态,传入一个数组里面存放所有异步操作

Promise.allSettled()

全部不管成不成功都返回所有结果状态,传入一个数组里面存放所有异步操作

Promise.race()

哪个先出结果状态,就返回哪个,其他的就不管了,传入一个数组里面存放所有异步操作

自定义Promise实现

通过上面的介绍,我们可以自己简单实现一个Promise对象,代码如下:

class MyPromise {
    // 三种状态:等待/成功/失败
    static _PENDING = 0;
    static _RESOLVE = 1;
    static _REJECT = -1;
  
    constructor(excutor) {
      // 初始化等待状态
      this._status = this._myClass._PENDING;
      // 存储所有任务队列,格式:[{onResolved(){}, onRejected(){}}, ...]
      this._callbacks = [];
      // 存放数据
      this._data = null;
      this._checkFun(excutor);
      try {
        // 回调绑定当前对象
        excutor(this.resolve.bind(this), this.reject.bind(this));
      } catch (e) {
        return this._myClass.reject(e);
      }
    }
  
    // 当前类
    get _myClass() {
      return this.constructor;
    }
  
    // 判断三种状态
    get _wait() {
      return this._status === this._myClass._PENDING;
    }
  
    get _success() {
      return this._status === this._myClass._RESOLVE;
    }
  
    get _failed() {
      return this._status === this._myClass._REJECT;
    }
  
    // 函数判断
    _checkFun(func) {
      if (!(func instanceof Function)) throw Error("请传入一个函数");
    }
  
    // 当前类判断
    _checkThis(_this) {
      return _this instanceof this._myClass;
    }
  
    // 由于状态改变不可逆,只有等待状态下才能转到成功状态,失败状态同理
    resolve(res) {
      if (!this._wait) return;
      this._status = this._myClass._RESOLVE;
      this._data = res;
      // 执行队列中的成功任务
      if (this._callbacks.length > 0) 
        setTimeout(() => this._callbacks.forEach(oCallback => oCallback.onResolved(res)));
    }
  
    reject(reason) {
      if (!this._wait) return;
      this._status = this._myClass._REJECT;
      this._data = reason;
      // 执行队列中的失败任务
      if (this._callbacks.length > 0)
        setTimeout( () => this._callbacks.forEach(oCallback => oCallback.onRejected(reason)));
    }
  
    // then方法,只有成功和失败状态才能执行对应的回调,否则将任务添加到队列当中
    then(onResolved, onRejected) {
      onResolved = onResolved instanceof Function ? onResolved : res => res;
      onRejected = onRejected instanceof Function ? onRejected : reason => { throw reason; };
      const self = this;
      // 返回一个新的Promise对象
      return new (this._myClass)((resolve, reject) => {
        function handle(callback) {
          try {
            const result = callback(self._data);
            if (result instanceof self._myClass) {
              result.then(resolve, reject);
            } else {
              resolve(result);
            }
          } catch (error) {
            reject(error);
          }
        }
        if (self._wait)
          self._callbacks.push({
            onResolved() {
              handle(onResolved);
            },
            onRejected() {
              handle(onRejected);
            }
          });
        else if (self._success) setTimeout(() => handle(onResolved));
        else if (self._failed) setTimeout(() => handle(onRejected));
      });
    }
  
    // catch方法,处理成功则返回成功回调
    catch(onRejected) {
      return this.then(null, onRejected);
    }
  
    // finally方法:除了等待状态都会执行,并返回当前对象
    finally(callback) {
      if (this._wait) return this;
      this._checkFun(callback);
      try {
        callback();
        return this;
      } catch (e) {
        return this._myClass.reject(e);
      }
    }
  
    // 成功回调静态方法,如果本身是当前类,则执行then,否则生成一个Promise对象并自动转换到成功状态
    static resolve(res) {
      return new MyPromise((resolve, reject) => {
        if (res instanceof MyPromise) res.then(resolve, reject);
        else resolve(res);
      });
    }
  
    static reject(reason) {
      return new MyPromise((resolve, reject) => {
        reject(reason);
      });
    }
  }
  
  // 测试代码
  let p = new MyPromise((resolve, reject) => {
    resolve("success");
  })
    .then(res => {
      console.log(res, 1);
      return MyPromise.resolve("2");
    })
    .then(res => {
      console.log(res, 2);
      return "3";
    })
    .then(res => {
      console.log(res, 3);
      throw Error("error3");
    })
    .catch(reason => {
      console.log(reason, 4);
      throw Error("error4");
    })
    .then(res => {
      console.log(res, 5);
    })
    .then(res => {
      console.log(res, 6);
    })
    .finally(() => {
      console.log("end");
    })
    .then(res => {
      console.log(res, 7);
    })
    .catch(reason => {
      console.log(reason, 8);
    });
  console.log("start...");
  // start...
  // success 1
  // 2 2
  // 3 3
  // Error: error3 ... 4
  // end
  // Error: error4 ... 8

上面的测试代码,把MyPromise改成Promise结果也是一样的。这里只实现了主要的resolve/reject/then/catch/finally功能,其他API其实也都是基于这些基本功能实现,就不一一实现了。

更多实现参考:
https://www.cnblogs.com/yjhua/articles/8549834.html
https://www.cnblogs.com/jerryleeplus/p/12122231.html

async/await

实际上就是对Promise操作的封装,使得我们编写异步代码像写同步代码一样。其中使用async关键字声明的函数返回是一个Promise对象,await关键字相当于then的包装,必须在声明了async关键字的函数里才能够使用,举例:

async function test() {
  let val = await ...;
  // 后面的语句必须等await执行完才能执行
  console.log(val);
}

上面例子等价如下:

new Promise((resolve, reject) => {
  ...
  resolve();
}).then(val => {
  console.log(val);
})

多个await并行实现
  • 使用Promise.all保证内容都执行完毕,举例:
async function test() {
    let res = await Promise.all([p1(), p2(), ...]);
    // ...
}
  • 分别使用await,举例:
async function test() {
    let res1 = await p1();
    let res2 = await p2();
    // ...
}
浏览器任务调度机制
  • 定时器会被放进定时器模块直接开始计时,时间到了以后则放入宏任务队列(一般有最小时间限制,4ms左右,因此即使设置为0,实际上也会稍微等待一小会才会到时间放入宏任务队列),如果多个定时器,那么谁时间先到就先放入宏任务队列,然后先执行
  • Promise内容会直接执行,但是状态改变的通知(resolve/reject)会放入微任务队列
  • 主线程会先将同步队列的内容全部执行完,然后开始轮询微任务队列并执行,最后再轮询宏任务队列并执行,期间执行完每个任务后都会重新依次遍历同步队列->微任务队列->宏任务队列

可以结合下面的例子来理解:

setTimeout(() => {
    console.log("settimeout");
}, 0)

new Promise((resolve, reject) => {
    console.log("promise");
    resolve("resolve");
}).then(data => {
    console.log(data);
})

console.log("main");

// promise
// main
// resolve
// settimeout

由于Promise里的内容和外面的内容一样是同步的,而其发布的成功/失败处理请求(resolve/reject)是异步的,所以then/catch的处理处于微任务队列,而setTimeout处于宏任务队列,因此得到了上面的最终结果。再来看下面示例:

new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("settimeout");
        resolve("resolve");
    }, 0)
    console.log("promise");
}).then(data => {
    console.log(data);
})

console.log("main");

// promise
// main
// settimeout
// resolve

上面发布的微任务是在将执行宏任务时创建的,此时宏任务已经相当于同步执行,所以setTimeout在微任务之前输出。如果上面两个都懂了,下面这个也差不多:

setTimeout(function(){
    console.log("timeout1");
}, 0);

console.log("main1");

for (let i=0; i< 10000000; i++);

new Promise(resolve => {
    console.log("promise");
    setTimeout(function() {
        resolve();
        console.log("timeout2");
    })
}).then(data => {
    console.log("then");
})

console.log("main2");

// main1
// promise
// main2
// timeout1
// timeout2
// then
其他
异步加载图片
function loadImg(src, resolve, reject) {
    let img = new Image();
    img.src = src;
    img.onload = resolve;
    img.onerror = reject;
}
loadImg("xxx.jpg", 
() => {
    console.log("success");
}, 
() => {
    console.log("failed");
})
Promise异步加载script
function loadScript(src) {
    return new Promise((resolve, reject) => {
        let script = document.createElement("script");
        script.src = src;
        script.onload = () => resolve(script);
        script.onerror = reject;
        document.body.appendChild(script);
    });
}

loadScript("http://xxx.js").then(script => {
    console.log(script);
    console.log("加载成功");
}).catch(() => {
    console.log("加载失败");
})
Promise封装setTimeout
let timeout = time => new Promise(resolve => setTimeout(resolve, time))

timeout(1000)
.then(() => {
    console.log(111);
})
Promise封装setInterval
let interval = (time, callback) => new Promise(resolve => {
    let inter = setInterval(() => callback(inter, resolve), time);
})

interval(1000, (inter, resolve) => {
    console.log(111);
    clearInterval(inter);
    resolve();
})
.then(() => {
    console.log("success");
})
Promise封装sleep函数
function sleep(time) {
    return new Promise(resolve => {
        setTimeout(() => resolve(), time);
    })    
}

async function test() {
    await sleep(2000);
    console.log(111);
}

test();

注:
Promise封装的sleep函数必须通过异步方式调用,并且同步代码也需要卸载异步函数里,如果想要实现同步的sleep函数,可以通过指定时间结束来实现,举例:

function sleep(n) {
    let date = Date.now() + n;
    while (date > Date.now());
}

sleep(1000)
Promise封装异步队列
function queue(num) {
  let p = Promise.resolve();
  num.map(v => {
    p = p.then(_ => {
      console.log(v);
    });
  });
}

function p1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("p1");
      resolve();
    }, 1000);
  });
}

function p2() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("p2");
      resolve();
    }, 1000);
  });
}

console.log(111);
queue([1,2,3,4,5, p1(), p2()])
console.log(222);
异步类

类里如果封装了then方法,也可以当成异步Promise对象来使用,举例:

class Test {
    constructor(name) {
        this.name = name;
        console.log("init");
    }
    then(resolve, reject) {
        console.log(this.name);
        resolve("success");
    }
}

async function test() {
    let res = await new Test("aaa");
    console.log(res);
}

test();

// init
// aaa
// success

WebSocket

是一种与后台新的交互方式,相较于之前的ajax,其无需遵循必须前台请求后台,后台再返回内容给前台的模式;通过WebSocket,双方可以建立通信接口(类似Socket通信的方式),从而前后台都能够互相主动向对方通信。代码示例:




websocket






使用场景
  • 解决一些曾经需要用ajax轮询监听后台的场景
  • 一些实时场景,如:聊天室、弹幕等
websocket封装

二进制读取+心跳检测+断线重连

class BaseWs {
  constructor(url) {
    this.url = url;
    this.init();
  }
  init() {
    try {
      this._stopHeart();
      this.ws = new WebSocket(this.url);
      this.ws.onopen = this.openCallback.bind(this);
      this.ws.onmessage = this.recvCallback.bind(this);
      this.ws.onclose = this.closeCallback.bind(this);
      this.ws.onerror = this.errorCallback.bind(this);
    } catch (e) {
      console.log(e);
      // 断线重连
      setTimeout(() => this.restart(), 3000);
    }
  }
  // 重启
  restart() {
    this.init();
  }
  send(data) {
    // 当连接状态时才允许发送信息,否则重连
    if (this._isOpen()) this.ws.send(data);
    else this.restart();
  }
  close(e) {
    this._stopHeart();
    this.ws.close();
  }
  // 连接成功状态
  _isOpen() {
    return this.ws.readyState === 1;
  }
  // 启动心跳检测
  _startHeart() {
    if (this.heart) this._stopHeart();
    this.heart = setInterval(() => {
      this.send(1);
    }, 1000);
  }
  // 停止心跳检测
  _stopHeart() {
    if (this.heart === null) return;
    clearInterval(this.heart);
    this.heart = null;
  }
  // 连接成功后启动计时器进行心跳检测
  openCallback(e) {
    this._startHeart();
  }
  // 接收消息读取,并调用recv回调
  recvCallback(msg) {
    let reader = new FileReader();
    let _this = this;
    reader.onload = function (event) {
      _this.recv(reader.result);
    };
    reader.readAsText(event.data);
  }
  // 读取信息回调
  recv(data) {}
  closeCallback(e) {
    this._stopHeart();
  }
  errorCallback(e) {
    console.log(e);
    this.close();
  }
}
nginx配置websocket支持及连接测试
连接测试

安装wscat:

npm install -g wscat 

输入命令测试是否连通:

wscat -c ws://xxx
nginx配置

参考:https://www.jianshu.com/p/a8e884228a16

你可能感兴趣的:(JS 异步相关)