问题
- 什么是单线程和异步有什么关系?
单线程:只有一个线程,同一时间只能做一件事情,多段JS不能同时执行
// 循环运行期间,JS执行和DOM渲染暂时卡顿
var i, sum = 0;
for (i = 0;i < 100000000; i++){
sum += i
}
console.log(sum)
// alert不处理,JS执行和DOM渲染暂时卡顿
console.log(1)
alert('hello')
console.log(2)
原因:JS可以修改DOM结构,浏览器要渲染DOM,单线程就是为了避免DOM渲染的冲突
浏览器需要渲染DOM
JS可以修改DOM
JS执行的时候,浏览器DOM渲染会暂停
两段JS不能同时执行(都修改DOM,就冲突了)
webworker支持多线程,但不能访问DOM
解决方案:异步
异步是一个很无奈的解决方案,虽然有很多问题
console.log(100)
setTimeout(function () {
console.log(200)
},1000)
console.log(300)
console.log(400)
// 先打印100,然后打印300,400,1s后才打印200
console.log(100)
$.ajax({
url: 'xxxxx',
success: function (result) {
console.log(result)
}
})
console.log(300)
console.log(400)
// 先打印100,然后打印300,400,ajax请求成功后才打印result
异步存在的问题
问题一:没有按照书写的顺序执行,可读性差
问题二:callback中,不容易模块化
- 什么是event-loop(事件循环或事件轮询)?
JS实现异步的具体解决方案:event-loop
同步代码,直接执行
异步函数先放在异步队列中
待同步函数执行完毕,轮询执行异步队列的函数
//实例解析
// setTimeout明显异步,这个时候就把function () {console.log(100)}先放入异步队列,先不管它
// console.log(200)是同步函数,会放入主进程中,立刻执行。
// 主进程中的代码执行完后,回头去看异步队列有没有,发现里边是有要执行的函数的,就把它取出来放入主进程,立即执行,执行完后再去异步队列里找。
// 如此反复循环,直到主进程和异步队列都为空
setTimeout(function () {
console.log(100)
},0)
console.log(200)
// 执行第一个setTimeout会告诉计算机,100毫秒后,再把function () {console.log(1)}放入异步队列
// 然后执行第二个setTimeout,会立刻把function () {console.log(2)}放入异步队列中
// console.log(3) 会直接放入主进程,立即执行,执行完后,会进入异步队列,发现有要执行的函数
// 然后把function () {console.log(2)} 放入主进程中立即执行,这个时候function () {console.log(1)}还没有放入异步队列
// 此时异步队列为空,JS引擎会继续轮询主进程和异步队列
// 100毫秒对于计算机来说,算是一个比较长的时间,等100毫秒后function () {console.log(1)}放入了异步队列
// JS引擎,就会把它放入主进程中立即执行。
setTimeout(function () {
console.log(1)
}, 100)
setTimeout(function () {
console.log(2)
})
console.log(3)
// 执行结果 3,2,1
// ajax 请求成功后,才会把function(result){console.log(result)} 放入异步队列中
$.ajax({
url: 'xxxxx',
success: function (result) {
console.log(result)
}
})
// 100毫秒后,function () {console.log(1)} 会被放入异步队列中
setTimeout(function () {
console.log(1)
}, 100)
// 立即放入异步队列中
setTimeout(function () {
console.log(2)
})
// 放入主进程,立即执行
console.log(3)
// 执行结果可能有2种
ajax请求超过100毫秒,那么执行结果就是 3,2,1,result
ajax请求不到100毫秒,那么执行结果就是 3,2,result,1
- 是否用过jQuery的Deferred?
jQuery1.5前后对ajax的改变
// jQuery1.5之前
var ajax = $.ajax({
url:'./data.json',
success:function(){
console.log('success 1')
console.log('success 2')
console.log('success 3')
},
error:function(){
console.log('error')
}
})
console.log(ajax) // 返回一个XHR对象
// jQuery1.5之后
var ajax = $.ajax('./data.json')
ajax.done(function(){
console.log('success 1')
}).fail(function(){
console.log('fail1')
}).done(function(){
console.log('success 2')
}).fail(function(){
console.log('fail2')
}).done(function(){
console.log('success 3')
}).fail(function(){
console.log('fail3')
})
console.log(ajax) // 返回一个deferred对象
// 这种写法的改变,无法改变JS异步和单线程的本质
// 只能从写方法上杜绝callback的形式
// 它是一种语法糖形式,但是解耦了代码,之前是callback中一下子全部写完。现在是分配到好多个函数中,而且这些函数是按顺序执行的
// 很好的体现了开发封闭原装 (对扩展开发,对修改封闭)
// 1.5之前的写法,如果我们想成功时再打印 success 4,那我们只能修改success方法。因为没有地方可扩展。
// 而1.5之后的写法,如果我们想成功时再打印 success 4,我们再加一个.done(console.log(success 4))就可以了
// 这种请求就是对扩展开发,对修改封闭,我们不需要去修改原先的函数
// 这种方法,在实际开发中,多人协作很有帮助,各人负责一个函数,互不干扰;需要新功能了,就扩展
// 而1.5之前那种方式,多人维护一坨代码。很容易搞混
// 而且测试的时候,1.5之前的那种,改了代码之后,之前的所有的功能都要测试一一遍
// 而1.5之后的这种写法,不需要,因为之前的代码是没有变的 ,只是扩展了一个方法而已,只需要测试扩展的功能就可以了
// 很像promise的写法
// 这个时候then接收两个参数,一个是成功时执行的函数,一个是失败时执行的函数
// promise是2015年ES6出来的时候才有的,而jQuery这种.then的写法很早就有了。
// 所以这种异步解决方案并不是promise出来才有的,只不过是ES6的时候把它变成了一种标准
var ajax = $.ajax('./data.json')
ajax.then(function(){
console.log('success 1')
},function(){
console.log('error 1')
})
ajax.then(function(){
console.log('success 2')
},function(){
console.log('error 2')
})
使用jQuery deferred
// 给出一段非常简单的异步操作代码,使用setTimeout函数
var wait = function (){
var task = function(){
console.log('执行完成')
}
setTimeout(task,2000)
}
wait()
// 新增需求:要求执行完成后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤
function waithandle(){
var dtd = $.Deferred() // 创建一个deferred对象
var wait = function(dtd){
var task = function(){
console.log('执行完成')
dtd.resolve() //表示异步任务已经完成
// dtd.reject() //表示异步任务失败或出错
}
setTimeout(task,2000)
return dtd //要求返回deferred 对象
}
//注意这里一定要有返回值
return wait(dtd) //这里表示 waithandle 执行结果也是返回dtd,只不过式放入wait函数加工后的dtd
}
var w = waithandle()
var w = waithandle()
w.then(function(){
console.log('ok 1')
},function(){
console.log('err 1')
})
w.then(function(){
console.log('ok 2')
},function(){
console.log('err 2')
})
// 还有 w.done() w.fail()
总结
dtd的api,可分成两类,用意不同
第一类 dtd.resolve(要成功) dtd.reject(要失败) 他们俩是一个需要主动去执行的角色
第二类 dtd.then dtd.done dtd.fail ; dtd.then是一个监听的角色,如果成功了就执行then的第一个参数,如果失败了就执行then的第二个参数
这两类应该分开,否则后果很严重,比如我们在w.then前,先执行w.reject()就会导致输出顺序的错乱
初步引入promise概念
// deferred.promise
function waithandle(){
var dtd = $.Deferred() // 创建一个deferred对象
var wait = function(dtd){
var task = function(){
console.log('执行完成')
dtd.resolve() //表示异步任务已经完成
// dtd.reject() //表示异步任务失败或出错
}
setTimeout(task,2000)
return dtd.promise() //注意这里不再返回deferred 对象,而是返回promise对象
}
//注意这里一定要有返回值
return wait(dtd)
}
var w = waithandle() // 这里是promise对象
w.reject() // 这个时候,如果再调用reject就会报错
$.when(w).then(function(){
console.log('ok 1')
},function(){
console.log('err 1')
})
// promise对象,可以调用.then这种监听式的方法,但是不能调用resolve,reject这种主动触发式的方法
// 是成功还是失败,是内部封装说了算,但是监听是使用者唯一的权力,不能去干预promise是成功还是失败
// 这个是最初提的promise概念,也就是在这个概念的基础上,慢慢发展出了ES6标准的promise
promise和deferred的区别
deferred对象有resolve,reject这类可以主动触发的函数,还有then,done,fail这种被动监听式的函数。这些函数都混在一起式不行的,容易被篡改
这个时候我们可以通过生成一个promise对象来进行隔离,promise对象只能被动监听,不能主动修改。这个promise算是ES6标准的promise的前世吧
- Promise的基本使用和原理
基础语法
现在高级浏览器基本上都支持promise
image.png
如果是低版本IE浏览器,不支持promise,那么我们可以通过在页面引入bluebird
的cdn,来解决
function loadImg(src){
const promise = new Promise(function(resolve,reject){
var img = new Image()
img.onload = function(){
resolve(img)
}
img.onerror = function(){
reject()
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result = loadImg(src)
result.then(function(img){
console.log(img.width)
return img
},function(){
console.log('failed')
}).then(function(img){
console.log(img.height)
})
异常捕获
// 正常情况下,then接收两个参数,第一个是成功执行的回调函数,第二个参数是失败后执行的回调函数。
// 但是如果我们要异常捕获的话,就不能传两个参数了,每个then只接收一个成功的回调函数
// 失败的回调函数,统一用catch接收,也就是统一用catch捕获异常
result.then(function(img){
console.log(img.width)
return img
}).then(function(img){
console.log(img.height)
}).catch(function(ex){
// 统一捕获异常,ex为异常对象
console.log(ex)
})
我们加一个自定义错误,看下是否能捕获
function loadImg(src){
const promise = new Promise(function(resolve,reject){
var img = new Image()
throw new Error('自定义错误')
img.onload = function(){
resolve(img)
}
img.onerror = function(){
reject()
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result = loadImg(src)
result.then(function(img){
console.log(img.width)
return img
}).then(function(img){
console.log(img.height)
}).catch(function(ex){
console.log(ex)
})
捕获到了 自定义错误
image.png
这个捕获异常的方式,跟我们之前用的 try catch的捕获异常的方式是一样的原理, 只不过try被promie里封装起来了
try {
xxx
}catch(ex){
xxx
}
刚才我们通过throw new Errow
的方式,模拟了逻辑之外的,语法的错误。然后通过catch捕获到了。但是我们希望的是catch不仅仅捕获逻辑之外的语法的错误。还可以捕获逻辑之内的错误。比如我们的图片如果加载失败了,就会走onerror的回调,然后reject会传递错误信息。如果想要catch捕获,我们给reject传递错误信息
// 我们可以给src赋值一个不存在的图片地址,然后就会触发onerror
img.onerror = function(){
reject('图片加载失败')
}
image.png
多个串联
工作中,我们经常会遇到这样一种需求场景,比如我们要先通过接口获取到用户的ID,手机号等信息,然后再根据用户ID获取到用户的列表等数据。那么有先后顺序的这种需求,就需要用到promise的串联来解决。
下边我们用加载两张图片来模拟这个过程。
var src1 = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result1 = loadImg(src1)
var src2 = 'https://www.imooc.com/static/img/course/logo-course.png'
var result2 = loadImg(src2)
// 链式操作
result1.then(function(img){
console.log('第一个图片加载完成')
return result2 // 注意这里,比如要return result2,下边才是result2的.then
}).then(function(img){
console.log('第二个图片加载完成')
}).catch(function(ex){
// 统一捕获异常
console.log(ex)
})
promise.all() promise.race()
var src1 = 'https://www.imooc.com/static/img/index/logo-recommended.png'
var result1 = loadImg(src1)
var src2 = 'https://www.imooc.com/static/img/course/logo-course.png'
var result2 = loadImg(src2)
// 注意 此时的Promise开头大写,是window对象的属性
// Promise.all 接受一个promise对象的数组
// 待全部完成之后,统一执行 success
Promise.all([result1,result2]).then(datas => {
// 接受到的datas是一个数组,包含多个promise返回的内容
console.log(datas[0]) // 这里打印的都是img标签,因为图片加载成功的时候,我们resolve的是img标签
console.log(datas[1])
})
// Promise.race 接受一个包含多个promise对象的数组
// 只要有一个完成就执行success
Promise.race([result1, result2]).then(data => {
// 接受到的data是最先执行完成的promise的返回值
console.log(data)
})
// 打印结果,肯定是先打印Promise.race 里的data,因为它是有一个promise完成就会执行success.
// 而Promise.all因为要等所有的promise都执行完,所以花费的时间,要比Promise.race长
promise标准
任何技术推广使用,都需要一套标准来支撑
如html,js,css,http,无规矩不成方圆
任何不符合标准的东西,终将会被用户抛弃
不要挑战标准,不要自造标准
promise标准 状态变化
三种状态 pending,fulfilled(成功),rejected(失败)
初始状态是 pending(比如我们之前加载图片的函数,一开始传入src还没有开始加载,就是pendging)
pending变为 fulfilled(图片加载成功 执行了resolve后,状态改为fulfilled),或fulfilled变为 rejected(图片加载失败,执行了reject后,状态改为rejected)
状态是不可逆的,只能是初始变为成功,或初始变为失败
promise标准 then
Promise实例,必须实现then这个方法
then()必须接受两个函数作为参数,第一个函数是成功之后的回调,第二个参数是失败后的回调
then()返回的必须是一个Promise实例
如果then里边的函数,没有明文返回一个Promise实例,那么这个then返回的就是本身的Promise实例,所以可以继续链式调用.then
- 介绍一下async/await(和 promise的区别和联系)
我们无法改变JS单线程异步的本质,但是单线程异步导致了我们编写代码的顺序和执行的顺序不一致,所以之前我们一直在解决callback带来的编写代码和执行代码顺序不一致的问题。而async/await 就是目前的终极解决方案。
then只是将callback拆分了,但因为then接收的是一个函数,本质上还是callback
var w = waithandle()
w.then(function(){
console.log('ok 1')
},function(){
console.log('err 1')
})
w.then(function(){
console.log('ok 2')
},function(){
console.log('err 2')
})
async/await是最直接的同步写法
// 语法,
// 一个函数如果想要在函数体里用 await, 必须要再函数前加async标识
// await 后必须跟一个 Promise实例
// 需要用 babel-polyfill
const load = async function(){
const result1 = await loadImg(src1)
console.log(result1)
const result2 = await loadImg(src2)
console.log(result2)
}
load()
async/await总结
// 首先是函数要加async标识,才可以在函数内部使用await,函数可以当作普通函数调用
// 使用了promise,并没有和promise冲突
// promise算是对异步调用的封装,如果单纯使用promise 只能通过.then这种方式来使用,而async/await算是对promise的扩展,
// async/await以同步的方式使用promise,再也没有回调函数
// 但是改变不了JS单线程,异步的本质
- 当前JS解决异步的方案
jQuery deferred
ES6 Promise
Async/Await
网友评论