回调
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单的说回调函数是参数传递函数,并指定它在响应某个事件(定时器、鼠标点 击、Ajax 响应等)时执行这个参数传递的函数。这个参数就是回调。回头调用。回调传参
假定有两个函数f1和f2,后者等待前者的执行结果。
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback(); // 这是回调 因为是setTimeout计时后触发的。
}, 1000);
}
function f1(callback){
callback(); // 这不是回调 因为是callback()自己直接调用的。
}
执行代码就变成下面这样:f1(f2);
回调会造成的问题:
- 层次的嵌套,俗称回调金字塔
还有什么问题吗?仅仅是代码看起来混乱吗?
顺序的大脑
//伪代码
doA(function(){
doB();
doC(function() {
doD();
});
doE();
});
doF();
无论多么熟悉JS异步的人,要完全搞懂这段代码实际的运行顺序,恐怕也得思考一番。
为什么会这样? → 因为人的大脑是顺序的,天生适合顺序的思考,难以理解(不是不能理解)非顺序的东西。
无论是在书写还是在阅读这段代码的时候,我们的大脑都会下意识地以为这段代码的执行逻辑是这样的doA→doB→doC→doD→doE→doF
,然而实际运行逻辑很可能(假设)是这样的doA→doF→doB→doC→doE→doD
。
思想实验
我们来进行两种游戏
- 第一种游戏,举办方提前跟你说:“这游戏总共有X关。第一关你应该做.....然后在....(地方)进入第二关。第二关你应该做....然后在....(地方)进入第三关。……"。我称之为呆板的游戏。
- 第二种游戏,举办方提前跟你说:”你只管从这个门口进去,等你快到下一关的时候,自然会有人出来给你提示。“我称之为灵活的游戏。
对应到代码当中,我们便能发现回调的另一个严重问题:硬编码。
前后的操作被回调强制硬编码绑定到一起了。在调用函数A的时候,你必须指定A结束之后该干什么,并且显式地传递进去。这样,其实你已经指定了所有的可能事件和路径,代码将变得僵硬且难以维护。同时,在阅读代码的时候,由于必须记住前后的关联操作,这也加重了大脑记忆的负担。
Promise
Promise 是 ES 2015 原生支持的,他把原来嵌套的回调改为了级联的方式。
var a = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('1')
}, 2000)
})
a.then(function(val) {
console.log(val)
})
如果要涉及到多个异步操作的顺序执行问题,我们可以这样写:
var a = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('1')
}, 2000)
})
a
.then(function(val){
console.log(val)
return new Promise(function(resolve, reject) { // 必须加return不然promise不止到这个then函数中又是个promise。只有通过返回值判断一下。
setTimeout(function() {
resolve('2')
}, 2000)
})
})
.then(function(val) {
console.log(val)
})
以Ajax为例。我们都知道,在Ajax执行成功之后,指定的回调函数会被放入”任务队列“中。JS执行引擎在主线程空闲的时候便会轮询任务队列,执行其中的任务。
我们仔细想想,是不是漏了一个关键点:”我知道最终是JS引擎执行了这个回调函数。但是,到底是谁调度这个回调函数的?到底是谁在特定的时间点把这个回调函数放入任务队列中去?“
答案是宿主环境,在本例中也就是浏览器。是浏览器检测到Ajax已经成功返回,是浏览器主动将指定的回调函数放到”任务队列”中,JS引擎只不过是执行而已。
由此,我们澄清了一件(可能令人震惊)的事情: 在回调时代,尽管你已经能够编写异步代码了。但是,其实JS本身,从来没有真正內建直接的异步概念,直到ES6的出现。
事实就是如此。JS引擎本身所做的只不过是在不断轮询任务队列,然后执行其中的任务。JS引擎根本不能做到自己主动把任务放到任务队列中,任务的调度从来都是宿主完成的。举个形象的例子就是:“JS引擎就像是流水线上的工人,宿主就像是派活的老板。工人只知道不断地干活,不断地完成流水线上出现的任务,这些任务都是老板给工人指定的。工人从来没有(也不能)自己给自己派活,自己给自己的流水线上放任务。”
ES6从本质上改变了在哪里管理事件循环,这意味着在技术上将其纳入了JavaScript引擎的势力范围,而不再是由宿主来管理。
为什么说ES6的Promise才真正有了异步呢?因为Promise是基于任务队列(job queue)。可以js方面的在事件循环中插队。并不是宿主环境控制的。
Promise 是一个 Job,所以必然异步的,因为 then 总是返回 Promise,xxx.then(a => a) 的效果实际上是 return new Promise(resolve => resolve(a)),所以then也是异步。
es6引入了Generator
为什么会出现Generator
本质上,generator是一个函数,它执行的结果是一个iterator迭代器,每一次调用迭代器的next方法,就会产生一个新的值。迭代器本身就是用来生成一系列值的,同时也广泛应用于扩展运算符...、解构赋值和for...of循环遍历等地方。
generator函数的函数是分段的。第一次执行next的时候,程序会执行到第一个yield,然后返回{ value:1, done:false },表示yield后面返回1,但是函数Hello还没执行完,函数既不会退出,也不会往下执行。
function p(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Date());
}, time)
});
}
function* delay(){
let time1 = yield p(1000);
console.log(1, time1);
let time2 = yield p(1000);
console.log(2, time2)
let time3 = yield p(2000);
console.log(3, time3);
}
function co(gen){
let it = gen();
next();
function next(arg){
let ret = it.next(arg);
if(ret.done) return;
ret.value.then((data) => {
next(data)
})
}
}
co(delay);// 这里写的co函数只适用于yield后跟promise。
await/async
ES7中提供async函数,利用它,不需要依赖co库,也一样可以解决这个问题。
使用方法和 co 非常类似,同时也支持同步写法的异常捕获。async的返回值为Promise 对象。
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
var b = async function() {
var val = await a()
console.log(val)
}
b()
使用await/async或者Generator进行http请求的时候经常是配合这Promise的。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
async既可以处理promise又可以处理普通的,不过此时相当于同步,而且更有语义化。
async function asyncPrint() {
await yibu1();
await yibu2();
}
使用async/await yibu1请求完了再去请求yibu2。如果使用Promise。是不等yibu1的结果就去请求yibu2。就等于async可以把本身的回调或者then里的内容拿出来当做同步代码写。但是一般http请求函数都没有返回值,只有请求有了结果才会有结果,所以await后一般跟着promise。不然await也不知道异步函数请求结束了呀。
yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
所以
async function hhh () {
var ll = await setTimeout(function() {
console.log('ss')
}, 3000)
console.log('ll')
}
hhh()
回调主要有两个问题,信任问题和顺序问题,Promise解决信任问题,Generator解决顺序问题。但我认为Generator也不是把顺序问题完完全全的解决了。因为如果
async function async1(value, ms) {
console.log(value)
await new Promise()
async function async2(value, ms) {
//
}
console.log(value1)
}
假设 console.log(value1)本身是一个很费时的同步任务,并且不需要等待async2执行完成。这时候,我们还是会先执行console.log(value1) ,后执行async2的回调。await会使得async函数卡住不动,所以只能解决一层一层的嵌套的顺序问题,不能解决这种同一级别的顺序问题。
async函数的返回值是一个Promise,上面这个问题可以改为
async function async1(value, ms) {
console.log(value)
await new Promise()
console.log(value1)
}
async1().then(function(){
async2(value, ms)
})
网友评论