异步编程

1,279 阅读12分钟

js为什么是单线程的

单线程的特点是同一个时刻只能执行一个任务。因为一些和用户的互动和操作DOM等相关操作决定了js要使用单线程,否则多线程会带来同步问题。如果一个线程在修改DOM,另一个线程要删除DOM,就需要对这个共享资源进行加锁,使执行任务非常繁琐。

单线程的问题

单线程会带来阻塞问题,只有前一个任务执行完,后一个任务才可以执行,这样就会出现页面卡死,页面无响应,影响用户体验,所以出现了同步任务和异步任务的解决方法。

同步任务:在主线程上排队的执行任务,只有前一个完成才可以执行后一个。 异步任务:在任务队列,只有任务队列通知主线程,某个异步任务准备好了,该任务才会进入主线程进行调用。

具体运行机制

  • 所有同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外有一个任务队列,只要异步任务有了运行结果,就在任务队列里面放一个事件。
  • 当执行栈中的同步任务都执行完,就会开始读取任务队列里的事件,事件对应的异步任务就会结束等待状态,进行执行栈开始执行。

Event Loop

主线程不断地从任务队列读取事件,这个过程是循环不断地,所以整个运行机制又称为Event Loop(事件循环)。

宏任务

  • script内的代码
  • setTimeout
  • setInterval
  • setImmediate(Node)
  • I/O(ajax请求)

微任务

  • Promise(在创建 Promise 实例对象时,代码顺序执行,如果 到了执行· then 操作,该任务就会被分发到微任务队列中去。)
  • process.nextTick(会比promise先执行)
  • MutationObserver

循环机制的运行

    1. script内容就是一个宏任务,从script标签开始,遇到函数调用就放到执行栈中执行。
  • 2.在这过程中如果遇到宏任务就把其放到一个宏任务队列中。
  • 3.如果遇到微任务,就把微任务放到当前宏任务的微任务队列中(每个宏任务都有一个微任务队列)。
  • 4.当前宏任务执行完准备退出执行栈前,会执行微任务队列中所有的回调函数(如果遇到新的微任务会继续加入当前微任务队列队尾)。执行完毕后清空微任务队列,退栈。
  • 5.执行requestAnimationFrame回调、事件处理以及刷新页面。
  • 6.从宏任务队列取出下一个宏任务开始执行(回到1)。

简而言之:执行一个宏任务,执行一队微任务

测试1

	console.log('1');
	setTimeout(() => {
		console.log('2')
	}, 1000);
	new Promise((resolve, reject) => {
		console.log('3');
		resolve();
		console.log('4');
	}).then(() => {
		console.log('5');
	});
	console.log('6');//1,3,4,6,5,2

测试2

console.log('1');

setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
    setTimeout(function() {
	    console.log('5');
	    new Promise(function(resolve) {
	        console.log('6');
	        resolve();
	    }).then(function() {
	        console.log('7')
	    })
	})
	console.log('14');
})

new Promise(function(resolve) {
    console.log('8');
    resolve();
}).then(function() {
    console.log('9')
})

setTimeout(function() {
    console.log('10');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
console.log('13')
// 1,8,13,9,2,3,14,4,10,11,12,5,6,7

https://juejin.cn/post/6913368493035356167

nodejs端的eventloop

juejin.cn/post/691336…

hejialianghe.github.io/jsadvanced/…

如何实现异步编程

回调函数

异步任务必须指定回调函数,回调函数不直接调用,而是在特定的事件或者条件发生时执行的,用于对该事件进行响应。 例如AJAX的onload事件之后就会执行一个回调函数,但是回调函数不断嵌套就会形成回调地狱。

ajax("./test1.json",function(data){
	console.log(data);
    ajax("./test2.json",function(data){
    	console.log(data);
        ...
    })
})

回调地狱缺点

  • 嵌套函数存在耦合性,一旦有所改动就会牵一发动全身。
  • 回调函数不能使用try catch捕获错误。(因为异步任务执行的时候,执行栈中的同步任务都已经退出了,捕获异常的函数也推出了)
  • 回调函数不能直接return。(只能终止回调函数的执行,不能终止外部代码的执行。)

事件发布/订阅

const pbb = new PubSub();

ajax("./test1.json",function(data){
	//第一次ajax请求成功 就发布test1Success,和请求来的数据
	pbb.publish("test1Success",data);
})

//订阅test1Success的事件就会被触发
pbb.subscribe("test1Success",function(data){
	console.log(data);
    //去申请第二次ajax请求
    ajax("./test2.json",function(data){
    //第二次ajax请求成功发布消息
    	pbb.publish("test2Success",data);
    })
})
//订阅test2Success
pbb.subscribe("test2Success",function(data){
	console.log(data);
    ...
})

PubSub的实现

class PubSub{
    constructor(){
        //事件队列
        this.events = {}; 
    }
    //发布事件
    publish(eventName, data){
        if(this.events[eventName]){
            this.events[eventName].forEach(cb => {
                cb.apply(this, data);//事件一旦成功发布就去执行对应队列中的回调函数
            });
        }
    }
    //订阅事件
    subscribe(eventName, callback){
        if(this.events[eventName]){
            this.events[eventName].push(callback);
        }else{
            this.events[eventName] = [callback];
        }
    }
    //取消订阅
    unSubcribe(eventName, callback){
        if(this.events[eventName]){
            this.events[eventName] = this.events[eventName].filter(
                cb => cb !== callback
            );
        }
    }
}

nodejs中的封装(event)

let events = require("events");
const pbb  = new events.EventEmitter();//创建发布订阅对象

ajax("./test1.json",function(data){
	//第一次ajax请求成功 就发布test1Success,和请求来的数据
	pbb.emit("test1Success",data);
})

//订阅test1Success的事件就会被触发
pbb.on("test1Success",function(data){
	console.log(data);
    //去申请第二次ajax请求
    ajax("./test2.json",function(data){
    //第二次ajax请求成功发布消息
    	pbb.emit("test2Success",data);
    })
})
//订阅test2Success
pbb.on("test2Success",function(data){
	console.log(data);
});

Generator

juejin.cn/post/684490…

使用时的状态变化:

  • 首先调用这个生成器函数,内部代码不会立刻执行,先被挂起。
  • 调用next(),会把生成器唤醒,执行当前所在yield,并且返回一个对象,然后继续挂起。
  • 直到没有可执行的代码,就返回一个结果对象{value:undefined,done:true}。
function request(url) {
  makeAjaxCall(url, function(response) {
    it.next(response);//第二次调用返回结果
  })
}

function *foo() {
  var data = yield request('http://api.example.com');
  console.log(JSON.parse(data));
}

var it = foo();
it.next();//第一次调用 执行request

Promise

promise可以解决回调地狱(promise链式调用)和处理并行任务(promise.all)。

promiseA+规范

promise的使用

Promise是一个构造函数,可以传入一个函数(执行器函数 立即执行)作为参数,而这个参数函数有两个形参resolve,reject(也是函数)。

promise的属性

调用new之后 p是一个Promise实例对象,每一个实例对象都有两个属性:状态和结果

有三种可选状态

  • pending(准备)
  • fulfilled(已完成)
  • rejected(已拒绝)

执行resolve(str)可以使状态pending->fulfilled,当前promise的值为str

执行reject(str)可以使状态pending-> rejected,当前promise的值是str

而且状态改变是一次性的(只能改变一次)

let p = new Promise((resolve,reject)=>{
	resolve("成功的结果");
    //reject("失败的结果");
})
console.log(p);

promise.prototype.then方法

then方法有两个参数,这两个参数都是函数,第一个函数是当状态是fulfilled时执行的(同时形参是promise的值),第二个方法是状态时rejected时执行的(同上)。

let p = new Promise((resolve,reject)=>{
	resolve("成功的结果");
    //reject("失败的结果");
})

let ret = p.then((value)=> {
	console.log("成功时调用"+ value);
},(err)=> {
	console.log("失败时调用"+ err);
})

then方法返回一个新的promise,且当前状态是pending(因为then是异步的属于微任务,会在当前宏任务执行完之后才执行,所以暂时是Pending状态)。then最后的返回值是由then中回调函数执行的结果决定。

  • 如果抛出异常,则新的promise的状态就是rejected,reason就是这个异常。
  • 如果返回一个非promise的任意值,新promise的状态就是fulfilled,value就是这个返回值
  • 如果返回一个promise,那就是这个promise

因为then返回一个promise,所以他也可以调用then,这样就可以实现链式调用(解决回调地狱/串联执行任务)。

	new Promise((resolve,reject)=>{
    
    }).then((value)=>{
    	console.log("第一个promise成功")
        //return 123;//返回之后这个promise的状态改成fulfilled。
        console.log(a);//但是这里的代码出错就会把当前的promise的状态改为rejected
    },(reason)=>{
    	console.log("第一个promise失败")
    }).then((value)=>{
    	console.log("第二个promise成功")
    },(reason)=>{
    	console.log("第二个promises失败"+reason);//可以捕获到失败的原因
    })

当状态不改变的时候(pending)不会执行then。

当一个promise有多个then的时候都会调用。

promise.prototype.catch方法

catch事捕获错误的。以下三种情况(reject执行,代码出错,出动抛出错误)都会让catch捕获到错误。

	const p = new Promise((resolve,reject) => {
    //reject();
    //console.log(a);
    throw new Error('出错了')
    })
    
    let t = p.catch((reason)=>{
    	console.log('失败'+reason);
    })

promise最常见的写法

new Promise((resolve,reject)=>{

}).then((value)=>{
	console.log('成功'+value);
}).catch((reason)=>{
	console.log('失败'+reason);
})

使用方法

 function fn(){
     return new Promise((resolve, reject)=>{
         成功时调用 resolve(数据)
         失败时调用 reject(错误)
     })
 }
 fn().then(success1, fail1).then(success2, fail2)

Promise.all/Promise.race

//把多个promise包装成一个promise实例对象,只有都成功的时候才调用success1
Promise.all([promise1, promise2]).then(success1, fail1)

const p1 = Promise.resolve(1);//返回一个成功值是1的promise对象
const p2 = Promise.reject(2);
const p3 = Promise.resolve(3);

const pAll = Pomise.all([p1,p2,p3]);
pAll.then(
	(value) => {
    	console.log('成功'+value)//[1,3]
    },
    (reason) => {
     	console.log('失败'+reason)//2
    }
)
//数组中第一个确定状态的promise来调用then
 Promise.race([promise1, promise2]).then(success1, fail1)

手写Promise

Promise构造函数、then、catch

//1.定义结构 不写具体实现
//ES5的自定义模块
(function(window){
    //构造函数 传入一个执行器函数
    function Promise(executor){
        let _this = this;
        _this.status = 'pending';//存储当前的状态
        _this.data = undefined;//存储当前的值
        _this.callbacks = [];//每一个对象都有两个回调函数 onResolved  onRejected

        function resolve(value){
            if(_this.status !== 'pending') return;//状态只能从pending开始改
            _this.status = 'fulfilled';//改状态
            _this.data = value;//改值
            if(_this.callbacks.length){//如果当前已经有回调函数就异步执行每一个 onResolved  并且要传vaule值
                setTimeout(()=>{
                    _this.callbacks.forEach((callbackObj)=>{
                        callbackObj.onResolved(value);
                    })
                },0);
            }

        }
        function reject(reason){
            if(_this.status !== 'pending') return;
            _this.status = 'rejected';
            _this.data = reason;
            if(_this.callbacks.length){
                setTimeout(()=>{
                    _this.callbacks.forEach((callbackObj)=>{
                        callbackObj.onRejected(reason);
                    })
                },0);
            }

        }
        try{
            executor(resolve,reject);//立即执行executor这个函数。
        }catch(err){
            reject(err);//如果出错就会调用rejecte
        }
    }

    Promise.prototype.then = function(onResolved, onRejected){
        let _this = this;//保存调用Then的Promise对象

        onResolved = typeof onRejected === 'function'? onRejected : value => value;//如果传入不是回调函数 自行传一个函数
        onRejected = typeof onRejected === 'function'? onRejected : reason => {throw reason};//传透异常

        return new Promise((resolve, reject)=>{

            //执行指定的回调函数 并且改变返回promise的状态
            function handle(callback){
                try{
                    const result =  callback(self.data);
                    if(result instanceof Promise){//3.返回的是一个promise, 返回promise就是这个promise
                        result.then(resolve,reject);//result成功了会调用返回promise的resolve 
                    }else{
                        resolve(result);//2.返回的不是一个Promise ,返回Promise就是fulfilled
                    }

                }catch(err){
                    reject(err);//1.抛出异常,返回的promise就是rejected
                }
            }

            //判断调用者的状态
            if(_this.status === 'rejected'){//如果调用者失败则异步执行onRejected 并传参
                setTimeout(()=>{
                    handle(onRejected);
                })
            }else if(_this.status === 'fulfilled'){//调用者成功则执行 onResolved 应且传参
                setTimeout(()=>{
                    handle(onResolved);
                })
            }else{//调用者状态是pending 保存onResolved 和 onRejected
                self.callbacks.push({
                    onResolved(value){
                        handle(onResolved);
                    },
                    onRejected(reason){
                        handle(onRejected);
                    }
                })
            }
        })
    }
    Promise.prototype.catch = function(onRejected){
        return this.then(undefined,onRejected);
    }

    //返回一个成功的Promise
    Promise.resolve = function(value){
        return new Promise((resolve,reject)=>{
            if(value instanceof Promise){//传入的是一个promise
                value.then(resolve,reject);
            }else{//传入的是一个普通值
                resolve(value);
            }
        })
    }

    //返回一个失败的Promise
    Promise.reject = function(reason){
        return new Promise((resolve,reject)=>{
            reject(reason);
        })
    }

    Promise.all = function(promises){
        const values = [];//定义数组保存成功的返回值
        let count = 0;
        return new Promise((resolve, reject)=>{//返回一个新的promise
            //遍历获取每个promise的结果
            promises.forEach((p, index)=>{
                if(!p instanceof Promise){//如果数组中有不是promise对象的把他视为成功
                    values[index] = value;
                    count++;
                }else{
                    p.then(
                        value=>{
                            values[index] = value;// p成功了就存入数组中,存的顺序要和promises保持一致
                            count++;
                            if(count === promises.length){
                                resolve(values);//如果都成功了就把返回promise设为成功,并且传入数组
                            }
                        },
                        reason =>{//只要有一个失败了 返回promise就是失败
                            reject(reason);
                        })
                }

            })

        })
    }
    //返回第一个完成/失败的结果
    Promise.race = function(promises){
        return new Promise((resolve, reject)=>{
            promises.forEach((p)=>{
                p.then((value)=>{
                    resolve(value);
                },(reason)=>{
                    reject(reason);
                })
            })
        })
    }

    window.Promise = Promise;//挂载到window下
})()//匿名立即执行函数
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./newPromise.js"></script>
    <script>
        new Promise((resolve, reject) => {

            setTimeout(()=>{
                console.log("延时1s完成")
                resolve(1);
            },1000);

            console.log("状态改变成功")
        }).then((value)=>{
            console.log('onResolved1()'+value);
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    resolve(2);
                    console.log('延时两秒完成')
                },2000)
            })
        },
        (reason)=>{
            console.log("onRejected()" + reason);
        }).then((value)=>{
            console.log('onResolved2()'+ value);
            throw 3;
        },(reason)=>{
            console.log("onRejected2()" + reason);
        }).catch((reason)=>{
            console.log("onRejected3()" + reason);
        })
        /*
        状态改变成功
        test.html:15 延时1s完成
        test.html:21 onResolved1()1
        test.html:25 延时两秒完成
        test.html:32 onResolved2()2
        test.html:37 onRejected3()3
        */
    </script>
</body>
</html>

// ES6 的书写方式
(function(Window){
    class Promise{
        constructor(exector){
            }
            function reject(reason){
        }
        //定义在原型上
        then(onResolved, onRejected){
        }
        catch(onRejected){
           
        }
    
        //定义在类对象上
        //返回一个成功的Promise
        static resolve = function(value){
        }
    
        //返回一个失败的Promise
        static reject = function(reason){
        }
    
        static all = function(promises){
            
        }
        //返回第一个完成/失败的结果
        static race = function(promises){
    }


    Window.Promise = Promise;//挂载到window下
})()//匿名立即执行函数

测试题

setTimeout(()=>{
    console.log("0");
})
// 1 7 2 3 8 4 6 5 0    
new Promise((resolve, reject)=>{
    console.log("1");
    resolve();
}).then(()=>{
    console.log("2");
    new Promise((resolve, reject)=>{
        console.log("3");
        resolve();
    }).then(()=>{
        console.log("4");
    }).then(()=>{
        console.log("5");
    })
}).then(()=>{
    console.log("6");
})

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

async await

async和await关键字可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。

async的返回值是一个Promise对象,await右边的表达式一般是promise,但也可以不是。

await会返回fulfilled的值 ,所以需要用try{}catch(){}来获取rejected的值。

        function fn1(){
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    resolve(1);
                },1000)
            })
        }
        async function fn2(){
            let result = await fn1();
            console.log(result);
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    reject(2);
                },1000)
            })

        }
        
        async function fn3(){
            try{
                let result = await fn2();
                console.log(result);
            }catch(err){
                console.log(err);
            }
        }

        fn3();
async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}
async function async2() {
    console.log("async2");
}
console.log("script start");

setTimeout(function () {
    console.log("settimeout");
}, 0);
async1();
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
    console.log("script end");
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
*/

AJAX

juejin.cn/post/684490…

AJAX 异步的javascript和XML,在不重新加载页面情况下,可以和服务器交换数据并且更新部分网页内容。

四个步骤

  • 使用new XMLHttpRequest创建一个ajax实例对象。
  • 使用open方法设定要请求的地址和请求方式。
  • 使用send()发送数据
  • responseText获取服务器返回的数据

get请求

let btn = document.getElementById('btn');
let username = document.getElementById('username');
let age = document.getElementById('age');

btn.onlick = function(){
    let xhr = new XMLHttpRequest();
    let name = username.value;
    let age = username.value;
    let params = 'username='+name+'&age='+age;
    xhr.open('get','https://localhost:3000/get?'+params);
    xhr.send();
    xhr.onload = function(){
	 console.log(xhr.responseText);
    }
}

post请求(请求格式:属性名=属性值)

let btn = document.getElementById('btn');
let username = document.getElementById('username');
let age = document.getElementById('age');

btn.onlick = function(){
    let xhr = new XMLHttpRequest();
    let name = username.value;
    let age = username.value;
    let params = 'username='+name+'&age='+age;
    xhr.open('post','https://localhost:3000/post');
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xhr.send(params);
    xhr.onload = function(){
	 console.log(xhr.responseText);
    }
}

post请求 (请求格式是 JSON)

let xhr = new XMLHttpRequest();
xhr.open('post','http://localhost:3000/json');
xhr.setRequestHeader('Content-Type','application/json');
xhr.send(JSON.stringify({name:'tom',age:19}));
xhr.onload = function(){
	console.log(xhr.responseText);
}

ajax状态码

  • 0 为初始化,没有调用Open
  • 1 启动 ,已经调用open,没有调用send
  • 2 发送,已经send,但还没有接收响应
  • 3 接收,已经接收部分数据
  • 4 完成,以及完全收到数据,可以在客户端使用。
let xhr = new XMLHttpRequest();
xhr.open('get','http://localhost:3000/readystate');
console.log(xhr.readyState);//1
xhr.onreadystatechange = function(){
	console.log(xhr.readyState);
    if(xhr.readyState === 4){
    	console.log(xhr.responseText);
}
}
xhr.send();

封装AJAX

<script type="text/javascript">
	function ajax (options) {
		// 存储的是默认值
		var defaults = {
			type: 'get',
			url: '',
			data: {},
			header: {
				'Content-Type': 'application/x-www-form-urlencoded'
				},
			success: function () {},
			error: function () {}
		};
		// 使用options对象中的属性覆盖defaults对象中的属性
		Object.assign(defaults, options);
		// 创建ajax对象
		var xhr = new XMLHttpRequest();
		// 拼接请求参数的变量
		var params = '';
		// 循环用户传递进来的对象格式参数
		for (var attr in defaults.data) {
				// 将参数转换为字符串格式
				params += attr + '=' + defaults.data[attr] + '&';
			}
		// 将参数最后面的&截取掉 
		// 将截取的结果重新赋值给params变量
		params = params.substr(0, params.length - 1);
		// 判断请求方式
		if (defaults.type == 'get') {
				defaults.url = defaults.url + '?' + params;
			}
		// 配置ajax对象
		xhr.open(defaults.type, defaults.url);
		// 如果请求方式为post
		if (defaults.type == 'post') {
				// 用户希望的向服务器端传递的请求参数的类型
				var contentType = defaults.header['Content-Type']
				// 设置请求参数格式的类型
				xhr.setRequestHeader('Content-Type', contentType);
				// 判断用户希望的请求参数格式的类型
				// 如果类型为json
				if (contentType == 'application/json') {
					// 向服务器端传递json数据格式的参数
					xhr.send(JSON.stringify(defaults.data))
				}else {
					// 向服务器端传递普通类型的请求参数
					xhr.send(params);
				}

			}else {
			// 发送请求
			xhr.send();
		}
		// 监听xhr对象下面的onload事件
		// 当xhr对象接收完响应数据后触发
		xhr.onload = function () {
			// xhr.getResponseHeader()
			// 获取响应头中的数据
			var contentType = xhr.getResponseHeader('Content-Type');
			// 服务器端返回的数据
			var responseText = xhr.responseText;
			// 如果响应类型中包含applicaition/json
			if (contentType.includes('application/json')) {
				// 将json字符串转换为json对象
				responseText = JSON.parse(responseText)
			}
			// 当http状态码等于200的时候
			if (xhr.status == 200) {
				// 请求成功 调用处理成功情况的函数
				defaults.success(responseText, xhr);
			}else {
				// 请求失败 调用处理失败情况的函数
				defaults.error(responseText, xhr);
			}
		}
	}
	ajax({
		type: 'post',
		// 请求地址
		url: 'http://localhost:3000/responseData',
		success: function (data) {
			console.log('这里是success函数');
			console.log(data)
		}
	})
</script>

fetch(现代浏览器自带,IE不自带)

        fetch("http://localhost:91/",{
        	method:"POST",
            headers:{"content-type":"application/x-www-form-urlencoded"},
            body:"a=12&b=77"
        }).then(
            res => res.json()
        ).then(
            data => {console.log(data)}
        )

axios(默认是JSON格式)

axios({
	url:'http://localhost:90?b=12',
    method:'POST',
    data:{a:1,b:12}
}).then(res=>{console.log(res.data)})