回调地狱
callback hell最大的问题不是因为缩进。它引起的问题比缩进要大得多。
根本
它真正的问题是剥夺了程序员使用return和throw等捕捉错误和返回值的能力。
程序的执行流程是基于一个函数在执行过程中调用另一个函数时候会产生函数调用栈,而回调函数不是运行在栈上的,因此不能使用return和throw。
举例
function A(){
setTimeout(req,5000);
}
function B(){
ajax(url,response);
}
假设是这两个函数有顺序依赖的关系,我们要让A发生后B才执行,我们要把它们连接到一起的话只能手工硬编码:
function cb(){
setTimout(function(){
ajax(url,response);
},500);
}
这种方法会使得代码脆弱,你要在回调中捕获错误,一旦你指定了所有可能的事件或者各种错误的处理函数,代码会变得非常复杂。
这就是信任问题
信任问题
信任问题就是你的函数的执行是交给第三方的,而不是在JS的控制范围内。
不是你编写的代码,不是你的直接控制下,而是第三方提供的工具。
控制反转
这就是控制反转: 把自己程序的一部分交给某个第三方。
因为对这个第三方的API (像ajax()),它可能会出现很多问题,比如:
调用回调过早,
调用回调过晚,
不正确的调用回调(调用次数太多或太少),
没有把所需的参数成功返回给回调函数等。
这可能会导致你回调中的代码被错误执行了若干次而没有提示!!!!!!!!
eg:
本来你的ajax只是希望在<p>输出一段话,
结果多次错误回调导致输出了N段话!
困境
你必须在这个回调函数中创建大量逻辑来判断处理这些可能的情况。。。
回到两个挽救方法
分离回调
function success(data){
//
}
function failure(data){
//
}
ajax(url,success,failure);
一个用于成功的处理函数,一个用于错误的处理函数!
ES6就是这种分离回调设计。
error-first风格,nodejs
回调第一个参数作为错误对象(if exists)。
如果成功,error为 清空/置假
如果失败,if(err)为真。
function cb(err,data){
if(err){}
...
}
但这并没有解决 重复调用回调的问题。 你可能同时得到成功或失败的结果! 或者都没有!
事实上:你需要额外的写更多的逻辑来处理回调过快或者失败或者太慢的问题,
所以我们需要一个通用的方法解决这些信任问题,这就是Promise
Promise
MDN中
定义
Promise是一些不必要现在知道的值的代理。
它允许你将handler和一个异步的方法(动作)的最终成功值和失败原因关联起来。
这使得异步方法返回的值就像一个同步方法一样,而不是立即返回最终值,异步方法返回的是一个promise,它会在将来的某个时间提供值。
用法
Promise 对象用于异步调用返回值的集中处理。 Promise 对象表示一个现在、将来或永不可用的值。
var myFirstPromise=new Promise(function(resolve,reject){
//当异步代码执行成功时才会调用resolved,异步失败调用reject
//会用setTImeout模拟异步,实际编码可能是XHR请求或H5的一些API
setTimeout(function(){
resolve('success');
},250);
});
myFirstPromise.then(function(success){
//success的值是上面调用的resolve方法出入的值
console.log(success);
});
关键:惰性求值,延时计算的特性
必须理解下面这张图!
Promise处理现在值和将来值
现在值
就是我们当前可以用的值,比如
var x,y=2;
console.log(x+y);
我们计算x+y的时候对它做了一个假设,就是它们是现在值,是已经resolved的(准备好的)。
将来值
但是考虑到异步在JS中的广泛使用。
有的值是没有被准备好的,我们就不能马上用。
Promise的做法就是不管是现在还是未来的值,
我们都统一当做将来的值来处理。即便是add()
这样的简单的运算我们也看作是异步的了。
例子
function add(xPromise,yPromise){
return Promise.all([xPromise,yPromise]) //创建并返回第一个promise对象
.then(function(values){
//调用then,等待上面这个promise,再创建一个promise
//values来源于之前resolved的promise数组
return values[0]+values[1]; //这个promise
});
}
add(fetchX(),fetchY())
.then(function(sum){
console.log(sum)
});
深刻理解
调用Promise结果可能是reject或resolve。
而通过Promise其实我们封装了依赖于时间的状态。
Promise本身与时间无关,它按照可预测的方式组合,不关心时序或底层的结果。
一旦Promise resolved了,它就永远保持这个状态。
此时他成了不变值。
这是Promise最需要理解也是最强大的一个概念。
你可以多次访问这个值,可以多方查看同一个Promise的决议情况!!
Promise 的then方法
Promise实例具有then方法,then方法的作用是给Promise实例添加状态改变的的回调函数。
then方法的第一个参数是Resolved,第二个尝试是Rejected状态的回调函数。
then方法返回一个新的Promise实例
getJSON(url).then(function(json){
return json.post;
}).then(function A(){
//...
},function B(){
//...
});
A方法会在Resolved时调用,
B方法会在Rejected时调用
重点
then上注册的回调都会在下一个异步时机点上
依次被立即调用。
Promise对比事件机制
如果调用函数foo()会执行一个任务,我们不关心它的底层。
它可能立即完成任务也可能过一段时间才完成。
我们只需要知道他什么时候结束,这样就可以进行下一个任务了。
我们在典型的JS风格中,可能会利用事件来做。
function foo(x){
...
return listener;
}
var evt=foo(42);
evt.on('completion',function(){});
evt.on('failure',function(){});
调用foo()显式创建并返回事件订阅对象,
并在上面注册事件。
我们用这个evt对象实现了分离关注点,
其实和观察者模式很像,它是一个中立的第三方协商机制。
Promise”事件”
function foo(x){
//这是一个revealing constructor ,传入的函数会立即执行。两个参数resolve和reject这就是promise的决议函数(resolution FUNCTION)
return new Promise(function(resolve,reject){....});
}
var p=foo(42); //p存了这个promise对象
bar(p); baz(p);
thenable鸭子类型
如何在promise中确定某个值是不是一个Promise值??
我们无法通过p instanceof Promise来检查。
库或框架会实现自己的Promise
或者有些浏览器没有Promise的实现
duck typing
类型检查,如果它看起来像只鸭子,叫起来像只鸭子,它就是一只鸭子。
thenable鸭子类型检测:
//它是一个对象或一个函数
//它有一个then方法
if(p!==null&&(typeof p==='object'||
typeof p==='function')&&
typeof p.then==='function'){
//它是一个thenbale
}else {//它不是一个thenable}
问题
A. 有可能你希望这个对象或函数完成一个promise,但你不希望它被当做promise
B 原型链的问题,如果有代码无意或恶意给Object.prototype 或者其他原生原型添加then()
你无法控制也无法预测。
new Promise的理解
一般我们使用这种方式:
new Promise(function(resolve,reject){
/*Executor*/
})
executor function
这样的方式,传入了一个executor function, 这个函数一般是马上被执行的(这个函数是同步的或立即调用的)。 传递resolve和reject函数作为参数给它。
这个executor function用于初始化一些异步的工作,触发一些异步的任务,一旦这些任务完成了就会调用resolved 回调函数,一旦失败了调用reject回调函数!
Promise.resolve的作用
Promise.resolve 相当有用,它可以将现有的对象转为Promise对象。
我们不需要判断了直接转成Promise对象!
例如:
var jsPromise=Promise.resolve($.ajax('/whatever.json'));
因为jQuery生成的是Defered的对象,所以需要转为Promise对象。
1.传入Promise实例就会立即返回这个实例
2. 传入thenable的对象
具有then方法的对象,会将这个对象转为Promise对象,然后立即执行thenbale对象的then方法
3. 我们可以传入非thenable或原始值到这个函数:
var p1=Promise.resolve('Hello');
返回一个新的Promise对象,状态为Resolved
现在我们可以解决Promise的信任问题
调用回调过早
Promise不必担心,因为即使是立即完成的promise。
对一个promise调用then的时候,即使这个promise已经resolution。
提供给then的回调也总是异步调用的!
不需要setTimeout(,,0) hack, Promise不会导致竞态。
调用过晚
Promise创建对象调用resolve() 或reject() 的时候,
这个promise的then(…)注册的观察回调会被自动调度。
见下面的Promise调度的解释
Promise调度
看这个例子:
p.then(function(){
p.then(function(){
console.log('C');
});
console.log('A');
});
p.then(function(){
console.log('B');
})
这里的”C”函数无法打断或抢占”B”,这是Promise的运作方式。
也就是说,两个独立的Promise链接的回调的相对顺序是无法可靠预测的
它们是独立的。
我们要避免两个独立的Promise链接有依赖关系,好的实践不会让
多个回调的顺序有丝毫的关系~!
因为它们存在一个异步任务队列,这是回调执行的顺序!
回调未调用
如果你对一个Promise注册了一个完成回调和拒绝回调,promise 在resolution的时候总会选择调用其中一个。
当你的js出现错误,可能回调执行了,但你不知道! 这就是需要有个错误提示(传递)
“race” 这是一个解决如果Promise永远不能被resolved的解决方法
Promise.race([
foo(),
timeoutPromise(3000);
])
调用次数过少或过多
回调被调用的正确次数应该是1,。
Promise的定义方式使得它只能被resolved 一次,如果处于某种原因,
Promise创建的代码试图call resolve(…) or reject(..)多次,
那这个promise只会接收第一次resolved,并忽略后面的调用。
但对于回调:你注册了多次,回调就会被调用多次!!