JavaScript常见的异步操作及回调地狱问题的解决历程

1. 定时器函数(setTimeout / setInterval)

// setTimeout定时器:待到duration毫秒后执行回调函数callback,
// 只执行一次。
setTimeout(callback,duration);

// setInterval定时器:待到duration毫秒后执行回调函数callback,
// 不清除定时器则一直执行回调函数。
setInterval(callback,duration)// 清除定时器:
clearTimeout(定时器名称)

2. 事件函数(click点击事件等)

$("#btn").click(function(){......})
$("#btn").dblclick(function(){......})
$("#btn").mouseover(function(){......})
$("#btn").mouseout(function(){......})
document.getElementById("myBtn").addEventListener("click", function(){
    document.getElementById("demo").innerHTML = "Hello World";
});
...

3. 网络请求(Ajax、Axios、wx、Request)

jQuery中发起Ajax请求:

// GET请求传参也可以直接在地址后传参 xxx.com?num=10
$.get(url,dataParams,function(result,status){})

// POST请求传参有请求头和请求体的限制,不可以直接在地址后传参
$.post(url,dataParams,function(result,status){})
// $.post请求默认传参类型为JSON对象

$.ajax({
	url:"",
	type:"POST",
	async:true, // 是否异步
	data:{...},
	dataType:"text", // 预计返回的数据格式
	success:function(res,status,xhr){
		...
	},
	error:function(xhr,status,error){
		...
	},
	complete:function(xhr,status){ //请求完成后的回调函数,不论请求成功还是失败。
		...
	}
	// 更多属性请善用搜索引擎
})

4. Promise对象

ECMAscript 6 原生提供了 Promise 对象。
Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
Promise 对象有以下两个特点:
1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
pending: 初始状态,不是成功或失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected 。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

创建Promise对象:

<script>
  let promise = new Promise(function (resolve, reject) {
    // 异步处理
    $.ajax({......})
    // 处理结束后、调用resolve 或 reject
  })
</script>

5. Generator函数

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供了解决方案。
Generator 有两个区分于普通函数的部分:
一是在 function 后面函数名之前有个 '*'
函数内部有 yield 表达式。
其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

function* func(){
 console.log("one");
 yield '1';
 console.log("two");
 yield '2'; 
 console.log("three");
 return '3';
}

Generator函数的执行机制:
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行
上面代码的运行结果:

f.next();
// one
// {value: "1", done: false}
 
f.next();
// two
// {value: "2", done: false}
 
f.next();
// three
// {value: "3", done: true}
 
f.next();
// {value: undefined, done: true}

6. async / await关键字

async 是 ES7 才有的与异步操作有关的关键字,是和 Promise , Generator 有很大关联的。

async function fn1(params){
	const response = await fetch("...")
	...
	return "abc"
}

async函数返回值为Promise对象,可以使用 then 方法添加回调函数。

async function helloAsync(){
    return "hello";
  }
  
console.log(helloAsync())  // Promise {<resolved>: "hello"}
 
helloAsync().then(v => {
   console.log(v);         // hello
})

async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。

但是await关键字并没有表面这么简单,深入解析见另一篇文章:async/await面试题

await 关键字仅在 async函数中有效。 如果在 async 函数体外使用 await 则会报错。

await的返回值为 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回其对应的值。

如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。

function testAwait (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}
 
async function helloAsync() {
  var x = await testAwait ("hello world");
  console.log(x); 
}
helloAsync ();
// hello world

正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。

function testAwait(){
   console.log("testAwait");
}
async function helloAsync(){
   await testAwait();
   console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync

7. 回调函数

即被传递给另一个函数作为参数的函数,如定时器函数中的callback,各种事件函数中的 function 等等等等。

setTimeout(callback,duration);
setInterval(callback,duration)$("#btn").click(function(){......})
$("#btn").dblclick(function(){......})
$("#btn").mouseover(function(){......})
$("#btn").mouseout(function(){......})
...

只要是被传递给另一个函数作为参数函数 都可以被叫做回调函数,回调函数的执行通常伴随着某种条件,即当…时执行此函数;


二、异步操作时的问题(回调地狱)及解决方案

在通过Ajax进行网络请求时有这么一种特殊的情况:当前请求的返回值是下一次请求执行时所需要的参数;

这种情况在我们书写代码时会造成回调函数的层层嵌套,使代码看起来像这个效果↓ :

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

代码的左侧形成了一个空的三角形地带,代码的可读性变得非常差,极其不利于代码的维护,我们为这种情况起了一个霸气的名字:回调地狱

为了解决这个问题,ES6中推出了Promise对象。

见上述,Promise对象有三个状态:
Pending(初始) 、Fulfilled(操作成功)、Rejected(操作失败)

Promise的状态只能由Pending → Fulfilled,或者Pending → Rejected。

在创建Promise对象时又有两个参数:resolve函数和reject函数;

resolve函数为操作成功后的回调函数,reject函数为操作失败后的回调函数。

function getCourseList(type = "free", pageNum = '1', pageSize = '10') {
  return new Promise(function (resolve, reject) {
    $.post(baseUrl + "/weChat/applet/course/list/type", { type, pageNum, pageSize }, function (res, err) {
      if (res.code == 0) {
        resolve(res)
      } else {
        console.log(err);
        reject(err)
      }
    })
  })
}

此时 resolve(res) 为操作成功后把返回的数据res作为返回值返回,然后我们可以通过 then() 方法来获取数据 res,而 then() 方法的返回值又是一个Promise对象,所以我们可以进行链式调用:

getCourseList().then(res => { renderCourses("#freeClassContent", res.rows, "free") }).then(jumpToClassContent)...

但是如果需要连续调用N次,链式调用的写法也不是很好看,可读性也不高,属于把原来的纵向的地狱问题变成了横向的

于是ES7又推出了 async 关键字 与 await 关键字,其中 await 关键字必须在async函数中使用,否则会报错。

利用它们可以彻底解决回调地狱的问题,把代码从视觉上变回了正常代码,还在视觉效果上将异步操作变为了同步操作,但是从底层逻辑来说,异步操作还是异步操作、同步还是同步,这个是不变的。(即底层层面上异步操作并不能变成同步操作)

async function fn1(){
	let a = await getCourseList()
	let b = await renderCourses(a)
	b.jumpToClassContent()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值