⭐️ 本文首发自 前端修罗场(点击加入),是
一个由 资深开发者 独立运行 的专业技术社区
,我专注Web 技术、Web3、区块链、答疑解惑、面试辅导以及职业发展
。博主创作的 《前端面试复习笔记》(点击订阅),广受好评,已帮助多人提升实力、拿到 offer。现在订阅,私聊我即可获取一次免费的模拟面试机会
,帮你评估知识点的掌握程度,获得更全面的学习指导意见!
Promise:一种解决回调问题的技术
首先我们要理解同步与异步的含义:
- 同步:函数在执行时会阻塞调用者,并在执行完毕后返回结果。
- 异步:函数在执行时不会阻塞调用者,但是一旦执行完毕就会返回结果。例如,处理Ajax请求时就是在处理异步调用。
简言之:
-
异步:操作之间没啥关系,同时进行多个操作
-
同步:同时只能做一件事
-
异步:代码更复杂
-
同步:代码简单
Promise —消除异步操作,本质上是一个状态机
- 当状态改变的时候,要调用之前挂起的then队列
- then的时候直接执行对应的函数,并且要给参数
- 用同步方式,书写异步代码
用法
let p = new Promise(function(resolve,reject){
//异步代码
//resolve --- 成功
//reject --- 失败
$.ajax({
url:'arr.txt',
dataType:'json',
success(arr){
resolve(arr);
},
error(err){
reject(err);
}
});
});
//当有结果的时候就调用then
p.then(function(arr){//相当于resolve
alert(arr);
},function(err){//相当于reject
alert(err);
});
用 Promise 创建一个 getJSON
function getJSON(url) {
return new Promise((resolve,reject)=>{
const request = new XMLHttpRequest();//创建一个XMLHttpRequest对象
request.open("GET",url);//初始化请求
request.onload = function() {//注册一个onload方法,当服务器响应后被钓鱼那个
try {
if(this.status===200){
resolve(JSON.parse(this.response));//尝试解析字符串
}else {
reject(this.status+" "+this.statusText);
}
}catch(e) {
reject(e.message);
}
};
request.onerror = function() {//和服务器通信发生错误
reject(this.status+" "+this.statusText);
};
request.send();
})
}
//调用
function handle() {
const result1 = getJSON('a.json');
result1.then(res=>{//回调成功返回
console.log(res);
}).catch(e=>{//回调发生错误
console.log("Error:",e);
});
}
// Promise.all
function handle() {
const result1 = Promise.all([getJSON('a.json'),
getJSON('b.json')]);
result1.then(res=>{
console.log(res); //返回一个数组
}).catch(e=>{
})
}
// Promise.race
function handle() {
const result1 = Promise.race([getJSON('a.json'),
getJSON('c.json')]);
result1.then(res=>{
console.log(res);//只会返回成功的第一个
}).catch(e=>{
})
}
//Async
async function handle() {
try{
const result1 = await getJSON('./statics/json/a.json');
console.log(result1);
}catch(e){
console.log(e);
}
}
handle()
用Promise封装fetch,FormData数据流方式
let data = {data1:data1,data2:data2};//使用json格式
let p = postData(url,data);
p.then(response=>{
//响应结果代码逻辑
});
function postData(url,data) {
let formData = new FormData();//将json转换成FormData数据流
if(data instanceof Object){
for(let i in data){formData.append(i, data[i]);}
}
return new Promise(function(resolve,reject){
fetch(url,{
method: 'POST',
mode: 'cors',
credentials: 'include',
body:formData
})
.then(response => {
return response.ok === true ? (response.json()) : reject({status:response.status});
})
.then(response => {
resolve(response);
})
.catch(err => {
console.log(err);
// reject({status:-1});
})
})
}
- 基本使用
let p = new Promise(function(resolve,reject){
setTimeout(function(){
resolve(12);
},500);
});
p.then(function(num){
alert(num)
},function(error){
alert(error);
});
多个Promise
- Promise.all();
let p1 = new Promise(function(resolve,reject){
//异步代码
//resolve --- 成功
//reject --- 失败
$.ajax({
url:'arr.txt',
dataType:'json',
success(arr){
resolve(arr);
},
error(err){
reject(err);
}
});
});
let p2 = new Promise(function(resolve,reject){
//异步代码
//resolve --- 成功
//reject --- 失败
$.ajax({
url:'json.txt',
dataType:'json',
success(arr){
resolve(arr);
},
error(err){
reject(err);
}
});
});
//all(),Promise全都执行完之后
Promise.all([
p1,p2
]).then(function(arr){
alert('全都成功');
let [res1,res2] = arr;
alert(res1);
},function(err){
alert('至少有一个失败')
});
简化:不写两个Promise
function createPromise(url){
return new Promise(function(resolve,reject){
//异步代码
//resolve --- 成功
//reject --- 失败
$.ajax({
url:url,
dataType:'json',
success(arr){
resolve(arr);
},
error(err){
reject(err);
}
});
});
}
Promise.all([
createPromise('arr.txt'),
createPromise('json.txt')
]).then(function(arr){
alert(’全都成功‘);
let [res1,res2] = arr;
alert(res1);
},function(err){
alert(’至少有一个失败‘)
});
结合jquery,高版本jquery自带Promise
Promise.all([
$.ajax({url:'arr.txt',dataType:'json'}),
$.ajax({url:'json.txt',dataType:'json'})
]).then(function(results){
let [arr,json] = result;
},function(err){
});
其他用法
- Promise.race 竞争,
可以把它想象成赛马
,每个函数都是一匹马,最快的那匹马会取得胜利。
同时如5个资源,哪个先来,先用哪个
Promise.race([
$.ajax({url:'http://1.com'}),
$.ajax({url:'http://2.com'}),
$.ajax({url:'http://3.com'}),
])
Promise.all([
$.ajax({url:'arr.txt',dataType:'json'}),
$.ajax({url:'json.txt',dataType:'json'})
$.ajax({url:'num.txt',dataType:'json'}),
]).then(results=>{
let [arr,json,num] = results;
},err=>{
console.log(err);
})
自个写一个Promise
class Promise {
constructor(fn){
//重点
const _this = this;
this.__queue = [];
this.__succ_res = null;
this.__erro_res = null;
this.status='';
fn(function(...arg){
_this.__succ_res = arg;
_this.status = 'succ';
_this.__queue.forEach(json=>{
json.fn1(...arg)
});
},function(...arg){
_this.__erro_res = arg;
_this.status='error';
});
}
then(fn1,fn2){
if(this.status == 'succ'){
fn1(...this.__succ_res)
}else if(this.status=='error'){
fn2(...this.__erro_res)
}else {
this.__queue.push({fn1,fn2})
}
}
all(arr) {
let aResult= [];
return new Promise(function(resolve,rject){
let i =0;
next();
function next(){
arr[i].then(function(res){
aResult.push(res);
i++;
if(i==arr.length){
resolve(aResult);
}else {
next();
}
},reject);
}
})
}
}
Generator
Generator不是一种函数式编程技术,但它是函数的一部分,因为函数式编程正是围绕着函数的技术。
我们前面提到,Promise是用于处理回调问题的技术,但是,随着ES6的发展与支持Generator,已经可以不需要Promise。
生成器,一种特殊类型的函数, 一种语法糖
- 普通函数:一路到底
- generator函数:中间能停
示例1
# 带星号:即在函数名称前使用一个星号来表示这是一个Generator函数。
function* show(){//创建一个generator对象
alert('a');
yield;//暂停
alert('b');
}
let genObj = show(); //调用 Generator函数,生成一个迭代器,从而能够控制生成器的执行
console.log(genObj);//返回一个Generator原始类型的实例
genObj.next(); // 调用实例的next()函数,从Generator实例genObj中获取一个值,即:执行alert('a');
//如果再一次.next()就执行alert('b');
但是,我们不能无限制地调用next从Generator实例中获取值。否则最后会返回undefined。原因:Generator犹如一种序列,一旦序列中的值被消费,你就不能再次消费它。即,序列为空后,再次调用就会返回undefined!。
那么,要怎么能够才能再次消费呢?——需要另外再创建一个Generator实例
。例如:
let genObj2 = show();
因此,迭代器用于控制生成器的执行,迭代器对象暴露的最基本接口是next方法。这个方法可以用来向生成器请求一个值,从而控制生成器。
next函数被调用后,生成器就开始执行代码,当代码直行道yield关键字时,就会生成一个中间结果(生成值序列中的一项),然后返回一个新对象
,其中封装了结果值(value)和一个指示完成的指示器(done)。
每当生成一个当前值后,生成器就会非阻塞地挂起执行,随后耐心等待下一次值请求鄂到达
,这是普通函数完全不具备的特性。
Generator是如何转换的
示例2
function* show(){
let a = 12;
let data1 = yield $.ajax('data/1.txt');
let b = 5;
let data2 = yield $.ajax('data/2.txt');
return a+b;
}
//实际上,转换为:
function show() {
let a =12;
$.ajax('data/1.txt').then(res=>{
let data1 = res;
})
let b = 5;
$.ajax('data/2.txt').then(res=>{
let data2 = res;
return a+b;
})
}
适合场景
- 请求数据
- 最大特点:让一个函数走走停停
重点:关键字 yield
yield 使Generator函数暂停了执行并将结果返回给调用者。第一次调用Generator实例时,yield将函数置于暂停模式并返回值。当下一次调用Generator实例时,Generator函数将从它中断的地方恢复执行。
用一段代码和对应的一张图演示Generator的操作序列:
function* genetatorSeq() {
yield 'first';
yield 'second';
yield 'third';
}
let genSeq = genetatorSeq();
console.log(genSeq.next().value); //first
console.log(genSeq.next().value); //second
console.log(genSeq.next().value); //third
需要注意的是:所有带有yield的Generator都会以惰性求值
的顺序执行。
何为惰性求值
:
它指的是:代码直到调用时才会执行。即,当我们需要时,相应的值才会被计算并返回。
- 可以传参和返回
function* show(num1,num2) {
alert('a');
let a =yield;
alert('b');
alert(a);//5
}
let gen = show(1,2);
gen.next(12);//没法给yeild传参
gen.next(5);//参数会被传递到
- 返回
function* show() {
alert('a');
yield 12;
alert('b');
return 1;
}
let gen = show();
let res1= gen.next();
console.log(res1);
//Obejct {value:12,done:false;}
let res2= gen.next();
console.log(res2);
//Object {value:1,done:true}
//value来自return,若没有return 则value为undefined
function* 炒菜(菜市场买回来的){
洗菜->洗好的菜
let 干净的菜= yeild 洗好的菜
干净的菜->切菜->丝
let 切好的菜 = yeild 丝;
切好的菜->炒->熟的菜
return 熟的菜;
}
done属性
每次对next函数调用,都将返回一个如下示例的对象:
{value:'value',done:false}
其中,
- value来自Generator的值;
- done是一个判断Generator序列是否已经被完全消费掉了的属性。当done的值为true时就应该停止调用Generator实例的next。
generator 实例
生成ID序列
function* countNum() {
let i=0;
while(true){
yield ++i;
}
}
const countGenerator = countNum();//生成一个迭代器
const obj1 = {
id: countGenerator.next().value
}
const obj2 = {
id: countGenerator.next().value
}
const obj3 = {
id: countGenerator.next().value
}
console.log(obj1)
console.log(obj2)
console.log(obj3)
- 处理异步操作
实例1
//httpGetAsync 通过node中的https模块触发一个Ajax调用以便获取响应
let https = require('https');
function httpGetAsync(url,callback) {
return https.get(url,
function(response) {
let body = '';
response.on('data',function(d) {
body+=d;
});
response.on('end',function(d) {
let parsed = JSON.parse(body);
callback(parsed);
});
});
}
//request: 将httpGetAsync封装到一个单独的方法request中
function request(url) {
httpGetAsync(url,function(response){
generator.next(response);//用generator的next调用替换回调
});
}
//main : 将业务需求封装到一个Generator函数内部
function* main() {
//调用yield将暂停函数执行,直到request通过接收Ajax的响应调用generator的next
let json1 = yield request("https://www.reddit.com/r/pics/.json");
let data1 = yield request(json1.data.children[0].data.url+".json")
console.log(data1);
}
//运行
let gen = main();
gen.next();
实例2
cnpm i yield-runner-blue //获取文件夹中的index.js
数据读取
好处:像写同步操作一样写一步操作
runner(function* (){
let data1 = yield $.ajax({url:'1.txt',dataType:'json'});//yield 出一个Promise对象给runner,然后执行返回给data1
let data2 = yield $.ajax({url:'2.txt',dataType:'json'});
let data3 = yield $.ajax({url:'3.txt',dataType:'json'});
console.log(data1,data2,data3);
});
几种异步操作:
- 回调
- Promise
- Generator
Generator对比Promise:
- Promise 带逻辑
Promise.all([
$.ajax({url:'getUserData',dataType:'json'})
]).then(results=>{
let userData = results[0];
if(userData.type=='vip'){
Promise.all([
$.ajax({url:'getVipItems',dataType:'json'})
]).then(results=>{
let items = results[0];
//生成列表
},err=>{
alert('fail');
})
}
},err=>{
alert('失败');
})
- Generator 带逻辑
runner(function* (){
let userData = yield $.ajax({url:'getUserData',dataType:'json'});
if(userData.type=='vip'){
let items = yield $.ajax({url:'getVipItems',dataType:'json'});
}else {
let items = yield $.ajax({url:'getItems',dataType:'json'});
}
//生成列表...
})
综上,generator适合处理逻辑性判断,Promise适合一次读一堆。另外,generator是对Promise一个封装。
用迭代器遍历DOM树
<body>
<div id="subTree">
<form>
<input type="text" name="" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script type="text/javascript">
function* DomTraversal(element){
yield element;
element = element.firstElementChild;
while(element) {
yield* DomTraversal(element); //用yield* 将迭代器控制器转移到另一个DomTraversal生成器实例上
element = element.nextElementSibling;
}
}
const subTree = document.getElementById("subTree");
for(let element of DomTraversal(subTree)){//使用for-of 对节点进行循环迭代
console.log(element)
}
</script>
</body>
Generator在KOA中的应用
https://koa.bootcss.com/
下载koa 与 koa-mysql
cnpm i koa
cnpm i koa-mysql
const koa = require('koa');//使用require引入koa库
const mysql = require('koa-mysql');
let db=mysql.createPool({host:'localhost',user:'root',password:'123456',dataBase
:'test'});//创建mysql
let server = new koa(); //创建一个服务
//generator在KOA中的应用
server.use(function *(){
//this.body = 'abc';//在页面上输出abc
let data = yield db.query('SELECT * FROM user_table');//查询user_table
this.body = data;
});
server.listen(8080); //服务器监听
Generator 内部构造
调用一个生成器不会实际执行它。相反,它创建了一个新的迭代器,通过该迭代器我们才能从生成器中请求值。在生成器生成了一个之后,生成器会进入挂起执行并等待下一个请求到来的状态。从某种方面上说,生成器的工作更像一个状态机。
它分别有4种状态:
- 挂起开始:创建一个生成器后,它最先以这种状态开始。
其中的任何代码并没有执行。
- 执行:生成器中的代码已开始执行。
可能是刚开始执行,也可能是从上次挂起的时候继续执行
。当生成器对应的迭代器调用了next()
方法时,并且当前存在可执行的代码,生成器就会转移到这个状态。 - 挂起让渡:当生成器在执行过程中遇到一个
yield
表达式,它会创建一个包含返回值的新对象,随后再挂起执行。生成器在这个状态暂停并等待继续执行。 - 完成:在生成器执行期间,如果代码执行到
return语句,或者全部代码执行完
,生成器就会进入完成状态。
不同于标准函数,每次退出后就会销毁,生成器中,只要我们从生成器中取得控制权,生成器的执行环境上下文一直是保存的。
Generator和Promise结合
将生成器和Promise结合,能实现更加优雅的代码。例如:我们可以把异步任务放在生成器中,然后执行生成器函数。因为没法知道Promise什么时候会被resolved,所以生成器执行的时候,我们需要将执行权让渡给生成器,从而不会造成UI阻塞。当Promise被resolved,我们会继续通过迭代器的next函数执行生成器。
async(function* (){
try {
const a = yield getJSON('a.json');
const b = yield getJSON('b.json');
const c = yield getJSON('c.json');
}catch(e){
console.log("Error:",e);
}
});
//辅助函数,对生成器执行操作
function async(generator) {
let iterator = generator(); //创建一个迭代器,进而控制生成器
function handle(iteratorResult) {//handle对生成器产生的每个值进行处理
if(iteratorResult.done) {return;}//生成器没有结果时停止执行
const iteratorValue = iteratorResult.value;
if(iteratorValue instanceof Promise) {//如果生成器的值是一个Promise,则对其注册成功和失败回调
//如果promise成功返回,则恢复生成器的执行并传入Promise的返回结果
//遇到错误,向生成器抛出异常
iteratorValue.then(res=>handle(iterator.next(res)))
.catch(err=>iterator.throw(err));
}
}
try {
handle(iterator.next());//重启生成器的执行
}catch(e) {
iterator.throw(e);
}
}
由上述代码我们知道:
- 函数是一等对象:向async函数传入函数参数
- 生成器函数:它的特性可以用于挂起和恢复执行
- Promise:帮助处理异步代码
- 回调函数:在Promise对象上注册成功和失败的回调函数
- 箭头函数:适合用在回调函数上
- 闭包:迭代器在async函数内被创建,在promise的回调函数内通过闭包获取该迭代器
generator+promise 异步请求
function* exportGenerator(data){
let queryContent = "requestUrl";
let content = yield getJSON(queryContent)//返回一个Promise对象
return content;
}
let btn = docment.querySelector('.btn');
btn.addEventListener('click',function(){
const exportInterator = exportGenerator();
exportInterator.next().value.then(res=>{
//todo List
}).catch(e=>console.log("Error:",e));
})
//promise
function getJSON(url) {
return new Promise((resolve,reject)=>{
const request = new XMLHttpRequest();//创建一个XMLHttpRequest对象
request.open("GET",url);//初始化请求
request.onload = function() {//注册一个onload方法,当服务器响应后被钓鱼那个
try {
if(this.status===200){
resolve(JSON.parse(this.response));//尝试解析字符串
}else {
reject(this.status+" "+this.statusText);
}
}catch(e) {
reject(e.message);
}
};
request.onerror = function() {//和服务器通信发生错误
reject(this.status+" "+this.statusText);
};
request.send();
})
}
Async/Await
从上面看到,我们仍然要写一些样板代码。因为每次需要的时候都要复用,但如果不关心这个过程就好了!
通过在关键字function
之前使用关键字async
,可以表明当前的函数依赖一个异步返回的值,在每个调用异步任务的位置上,都要放置一个await关键字,用于告诉javascript引擎,请在不阻塞应用执行的情况下在这个位置上等待执行结果
。
(async function(){
try {
const a = await getJSON('a.json');
const b = await getJSON('b.json');
}catch(e){
console.log("Error:",e);
}
})