JavaScript 异步编程
javaScript采用单线程的原因:javaScript因为用来实现页面的动态交互,交互的核心就需要操作dom,这种情况决定了它必须使用单线程模型,否则就会产生复杂线程同步问题;例如:多线程的情况下,如果一个线程修改dom,一个线程同时对这个dom删除了,浏览器就无法以哪个工作线程为准。
同步模式
代码任务依次执行,后面代码任务必须等待前面一个任务执行完才能执行,前面的任务没有执行完,后面的任务就不会执行。【同步模型不是同时执行是排队执行】,程序执行顺序跟编写的顺序是一致的。
优点是容易理解、简单、阅读和思考逻辑。
缺点是碰到耗时或执行时间长的任务,会出现等待的情况,也就是阻塞。
使用异步处理ajax,node文件读取这些笔记耗时的任务。
console.log('start')
function bar(){
console.log('bar')
}
function fun() {
console.log('fun')
bar();
}
fun()
console.log('end')
代码运行
1、 将代码加载,在调用栈(Call Stack)压入一个匿名函数调用(anonymous),相当于把全部的代码放到放入到匿名函数中去调用执行,然后就开始一行一行执行每行的代码。
2、将console.log(‘start’) 取出,放到执行栈中执行,执行完移出。
3、bar和foo函数和变量在声明的时候不产生调用,不会放到调用栈中。
4、 fun()放到调用栈上执行, 执行的函数fun,将console.log(‘fun’)放到调用栈执行,执行完移出console,将bar()放入调用栈中,执行新的函数bar【此刻的调用栈顺序是 [bar> fun > anonymous]】,将console.log(‘bar’)拿出执行 ,执行完移除,bar函数执行结束,从调用栈移出。此刻调用栈只剩[fun, anonymous]
5、fun函数执行结束,从调用栈上移除,调用栈只剩[anonymous]
6、将console.log(‘end’) 取出执行,执行完移出,整体代码执行结束。
7、调用栈就会被清空掉。
调用栈
通俗的讲,JavaScript在执行引擎上维护了正在执行的工作任务表,里面记录一些正在做的事情,当里面的所有的任务被清空后,这一轮的工作就算结束。
异步模式
不会等待当前任务执行结束才执行,对于耗时任务,开启后就立即执行下一个任务,耗时任务的逻辑通过回调函数的方式去定义,耗时任务完成后自动调用传入的回调函数。
没有异步模式,单线程的javaScript无法同时处理大量耗时任务。
异步代码执行顺序相对混乱,没有同步代码通俗易懂
代码执行分析异步模式
//异步代码示例及执行分析
console.log(1)
setTimeout(function() {
console.log(2)
},1800)
setTimeout(function() {
console.log(3)
setTimeout(function() {
console.log(4)
},799)
},1000)
console.log(5)
分析上面代码执行的结果:
Web API 内部api的环境,因为在web平台执行
Event Loop 事件循环
Queue 事件队列/回调队列
Call Stack 调用栈,console控制台
如果所示,相比同步代码多了Event Loop 事件循环、消息队列
1、加载js代码,再Call stack调用栈里压入(anonyous匿名全局调用),依次执行每行代码。
2、console同步api,压入Call stack ,执行、输出、移出调用栈。
3、setTimeout 压入调用栈,setTimeout 函数的内部是异步调用,需要关注内部api做了什么事情,内部WEB api 为time1开启了倒计时器,单独等待执行,1.8s。setTimeout移出调用栈【倒计时是单独工作,不会收js线程影响】
4、第二个定时器为time2开启了一个定时器,放到web api里等待调用执行,1s。
5、console 是同步api,执行、输出、移出调用栈。
6、匿名调用执行完毕,清空调用栈。
7、调用栈没有任务的时候,调用栈相当于暂停了,Event loop 主要是监听调用栈和消息队列,当调用栈的任务结束了,就会从事件队列中取出第一个任务放到调用栈上去执行。
8、time1的倒计时比time2的倒计时长,所以time2的倒计时会先结束,放到事件队列中,time2的倒计时结束放到事件队列中【放入前此刻消息队列是空的,可以是说放在第一个】,等待事件循环调用,time1倒计时结束后放到事件队列中【也就是第二位】,事件循环取出取出最先放进事件队列的任务time2放到调用栈执行。
9、当事件队列发生了变化,事件循环就会监听的到,就会把事件队列的第一个任务放到调用栈上去执行,也就是time2的函数放到调用栈执行,执行结束将time2移出调用栈。
10、取出事件队列第二个任务,执行,如果里面又遇到一个定时器time3,会重新开器一个定时器单独进行等待。
11、timer 1.8s的定时比time2 1s+iner 1s 时间短,time1会比inner先放到调用栈去执行。
12、timer1执行完清空调用栈,inner定时器结束,放到事件队列中,事件循环监听事件队列的变化后,将inner 放到调用栈执行,执行结束后,清空调用栈,执行结束。
事件队列
一个工作待办的工作表
js引擎先作为调用栈所有的任务,再通过事件循环取出调用栈中取出任务放到调用栈去执行,以此类推,执行过程中随时可以向事件队列中放入任务,更像往消息队列中push一个任务,然后再消息队列中排队等待事件循环取出放到调用栈执行。
js线程执行
javaScript是单线程的,但是浏览器不是单线程的。
javaScript调用的内部api并不是单线程的,比如定时器,内部有个单独的线程,负责时间的倒数,时间到了会将回调放到放到事件队列中,这个事情有单独的线程去执行的。
js所谓的单线程指的是执行代码的线程是一个线程。
内部的api会用单独的线程去执行等待的操作。
生活中有耗时的东西要等,总得要有人来等,只不过不会让js线程去等。
同步和异步的区分
同步和异步不是些代码的方式的不同,而是运行环境提供的API是以同步还是异步的方式去执行。
同步会等待执行,异步不会等待执行,异步下达了任务开启的指令就会执行下一个任务。
异步差异值
时间差异,设置毫米执行效果,应该是定时器也没法精确到几毫秒的吧,如果说的不对请大佬指点指点。
console.log(1)
setTimeout(function() {
console.log(2)
},1800)
setTimeout(function() {
console.log(3)
setTimeout(function() {
console.log(4)
setTimeout(function() {
console.log(5)
},1)
},787)
},1000)
console.log(6)
//更新谷歌前是 163452,更新后是 163425
console.log(1)
setTimeout(function() {
console.log(2)
},1800)
setTimeout(function() {
console.log(3)
setTimeout(function() {
console.log(4)
setTimeout(function() {
console.log(5)
},1)
},786)
},1000)
console.log(6)
//更新谷歌前是 163425,更新后是 163425
//node 结果
console.log(1)
setTimeout(function() {
console.log(2)
},1800)
setTimeout(function() {
console.log(3)
setTimeout(function() {
console.log(4)
},800)
},1000)
console.log(5)
//1,5,3,2,4
console.log(1)
setTimeout(function() {
console.log(2)
},1800)
setTimeout(function() {
console.log(3)
setTimeout(function() {
console.log(4)
},790)
},1000)
console.log(5)
//15342
更新前后的区别
回调函数
所有的异步编程都是基于回调函数实现的,回调函数可以理解为想要去做的事情,等待结果出来了要去做的事情(比如10s的定时器结束后,自动回调函数去执行某些操作),调用者定义回调函数,执行者(异步任务执行者)去等待结束后执行调用者定义的回调函数。【setTimeout(fun,100) 调用者定义setTimeout里的fun回调函数,执行等待计时器结束后调用定义者的fun回调函数】
Promise 的本质就是通过回调函数定义 异步任务结束 时的任务
setTimeout(function(){ //调用者定义函数,执行者等待执行函数
console.log(1111)
},1000)
Promise
概念
优点:解决了函数嵌套的回调地狱问题。
promise对象的状态不受外界影响,表示一个异步任务最后的结果是成功还是失败;Promise 有三个状态分别是:Pending(进行中,等待结果)、Resolved(已完成,又称Fulfilled,成功)、Rejected(已失败)
对外Promise对外发起一个承诺,承诺的事情正在执行就是Pending(进行中的状态),一旦状态发生了改变(根据结果改变承诺的状态),就不会再变,改变的状态只有两种:从Pending变为成功Resolved和Pending 变为Rejected,这两种改变的情况一旦发生,状态就固定不会再变了,一直保持这个结果。
比如承若叫你起床(Pending),最后没叫你起床(结果由Pendng改成 Rejected),不管怎么解释,这个承若叫醒服务的承若是失败的,不可能再说这个承若达成了,即便说下次一定叫醒,下次一定叫醒那另外一个承若。
基本用法
先输出 打印的内容,再抛出then的异常
Promise 中的then方法是会放到事件队列中,必须等待同步代码执行完了才会执行
// 基本使用
const prom = new Promise((resolve, reject) => { //发起一个承若
//在回调里兑现承诺
// 成功兑现承诺
// resolve({name:'start'})
//承诺兑现失败
reject(new Error('呜呜呜 失败了'))
})
prom.then((data) => {
console.log(data) //{name:'start'}
}, (err) => {
console.log(err) //抛出异常
})
for(var i = 0; i < 1000000000; i++) {
if(i === 999999999) console.log(i)
}
console.log('end')
ajax 请求五个步骤
1、创建XMLHttpRequest 异步对象
const xhr = new XMLHttpRequest();
2、设置请求方式和请求地址,open分别对象三个参数:请求方式(get|post),请求地址、是否异步。
xhr.open('GET', 'pageage.json', true);
3、用send发送请求
xhr.open()
4、监听状态变化,其实监听 readyState 变化的函数 onreadystatechange
//onreadystatechange 事件在readyState改变时就会调用该函数
xhr.onreadystatechange = function() {
/**
* readyState 的五个状态
* 0:请求未初始化
* 1: 服务器连接已建立
* 2:请求已接受
* 3:请求处理中
* 4:请求已完成,且响应已就绪(数据下来了,可以使用status 拿到http状态码了)
*/
if(xhr.readyState === 4) {
if(xhr.status === 200){ //状态码200请求成功
//5、接收返回的数据
console.log(xhr.responseText)
}
}
}
ajax实现get请求
/***
* ajax请求五步骤
* 1、创建XMLHttpRequest 对象
* 2、设置请求方式和请求地址
* 3、然后 send 发送请求
* 4、监听状态变化
* 5、接受返回的数据
*/
function ajax(){
//1、创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
//2、设置请求方式和请求状态
xhr.open("GET", 'package.json', true);
xhr.responseType = 'json'; //加上这type可以返回json的格式数据
//3、用send发送请求
xhr.send()
//4、监听状态变化 ,xhr.onload = function(){} 也可以
xhr.onreadystatechange = function (){
//onreadystatechange 事件在readyState改变时就会调用该函数
/**
* readyState 的五个状态
* 0:请求未初始化
* 1: 服务器连接已建立
* 2:请求已接受
* 3:请求处理中
* 4:请求已完成,且响应已就绪(数据下来了,可以使用status 拿到http状态码了)
*/
if(xhr.readyState === 4) {
if(xhr.status === 200){ //状态码200请求成功
//5、接收返回的数据
console.log(xhr.responseText)
console.log(JSON.parse(xhr.responseText))
}
}
}
}
ajax()
Promise 常见误区
Promise尽量将异步扁平化,不要嵌套使用
Promise 链式调用
1、Promise 对象的then方法会返回一个新的Promise 对象,所有可以使用then进行链式调用。
2、后面的then方法其实就是作为上一个then的返回的Promise回调。
3、前面then方法中回调函数返回值作为后面then 方法回调的参数。
4、如果回调中返回的是Promise,那后面then方法的回调等待它的结构
//链式调用
ajax().then((res) => {
console.log('then~',res)
return res;
}).then((res) => {
console.log(res)
return ajax()
}).then((res) => {
console.log(res)
})
Promise 异常处理
使用catch
reject 为Promise 的异常做处理:代码异常,出现错误,请求失败了都会执行
ajax('package.jso').then((res) => {
console.log('then~',res)
return ajax('package'); //如果返回 是给异常,then的第二个参数是无法捕获到
},(err) => {
console.log(err)
}).then((res) => {
console.log(res)
})
//使用catch为整个链条做异常处理,因为任何错误都会被传递下来,直至被捕获【建议链式掉用使用】
ajax('package.json').then((res) => {
console.log('then~',res)
return ajax('package.jso')
}).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
unhandledrejection 事件
当Promise 被 reject 且没有被手动 reject 捕获的异常,会触发 unhandledrejection 事件;
应该明确捕获每一个可能出现的异常,而不是丢给全局处理。
//全局捕获,不建议写
window.addEventListener('unhandledrejection',(event) => {
console.log(event)
const {reason, promise} = event;
console.log(reason,promise);
event.preventDefault()
}, false)
Promise 静态方法
1、Promise.resolve() 快速把一个值转换为promise对象。
2、如果Promise.resolve() 传入的是一个promise对象,则原路返回。
3、如果传入的值是个对象,且对象有then 方法,是可以被传递的。
resolve
//传入promise 对象 返回原来的promise对象
var promise = ajax('package.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2) //true
//传入的对象包含then方法,是可以拿到的;
//可以传入第三方的promise库,基本都then方法,可以将第三方库的then转成原生的promise
Promise.resolve({
then(resolve, reject) {
resolve('111')
}
}).then((res) => {
console.log(res)// '111'
})
reject
快速创建一个失败的promise对象,无论接受的是什么值,都是失败的原因
Promise.reject(new Error('123123')).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
Promise.reject("12313").then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
Promise 并行处理
Promise.all()
同时处理没有相互依赖的请求,同时请求多个请求,所有的请求成功才成功,一个异常则结果为失败。
Promise_ajax('./api/all.json').then((res) => {
const urlArr = Object.values(res)
console.log(urlArr)
const tasks = urlArr.map((url) => Promise_ajax(url))
return Promise.all(tasks)
}).then((res) => {
console.log(res)
})
Promie.race()
根据第一个结果任务的结果而定,第一个成功的结果为真则为真,一个结果结果失败最后的结果为失败
const prom = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('请求失败'))
}, 500)
})
Promise.race([
Promise_ajax('./api/all.json'),
prom
]).then((res) => {
console.log(res)
})
模拟请求网络差的情况
Promise执行时序
宏任务
setTimeout 会作为一个宏任务会到回调队列中重新排队等待,目前大部分异步调用都是作为宏任务去执行。
微任务
Promise 会作为微任务,当前任务结束,立即执行微任务,不用到列队末尾排队,所以Promise的回调会作为微任务,本轮调用的末尾任务结束后就立即执行的。
Promise总结
Promise相比传统的异步调用,提供的链式调用,解决了回调地狱嵌套的问题
缺点
Promise 的then虽然可以解决回调地狱的问题,但是在串联请求上一个then处理一个异步调用,最终形成一个整体的异步任务的链条,实现串联执行;但是这样写会有大量的回调函数,虽然没有出现嵌套,但是没有传统代码那样的可读性。
Generator 函数生成器
描述
1、其实就是在函数声明加个 *。
2、Generator 函数生成器不会立即执行,而是会返回一个Generator函数生成器对象,通过.next()一步步执行。
3、Generator第一次执行next的时候函数才会执行。
4、yield 不是像return 直接结束函数,只是暂停函数执行。
5、yield 关键词向外返回一个包含值的对象,然后暂停,就会出现yield后面的console不执行的情况;返回对象中的value属性就是yield执行的结果值,done属性是当前函数生成器对象的执行状态是否执行完。
特点
让异步调用回归扁平化。
有了Generator后,异步代码有了同步代码的体验了,但是还需要写个执行器函数,会比较麻烦。
基本语法
function * fun() {
console.log('Generator')
yield 'foo'; //使用yield关键词可以向外返回一个值,第一个next(执行到这里结束)
console.log('start 11')
const res = yield 'start'; //第二个next()执行到这里结束
console.log("end")// 第三个next(执行到这里结束)
console.log(res)
}
var generator = fun() //不会立即执行,和返回返回一个Generator 的函数生成器对象
console.log(generator) // 打印Generator 函数生成器对象
const reval = generator.next() //调用next()这个函数体才会执行
//{value: "foo", done: false}
//value是 yield 返回的值 ;done 函数生成器的执行状态,当前函数生成器是否全部执行完。
console.log(reval)
const reval2 = generator.next();
console.log(reval2) //{value: "start", done: false}
var he =generator.next("haha"); //在next传了值会在下一个
console.log(he) //{value: undefined, done: true}
对Generator函数内部抛出异常
function * fun (){
console.log('Generator');
try{
let res = yield "foo";
console.log(res)
} catch (e){
console.log(e)
}
}
const gener = fun();
console.log(gener) //返回Generator对象
console.log('---------')
console.log(gener.next()) //打印 Generator,并返回yield的值 {value: "foo", done: false}
console.log('---------')
console.log(gener.throw(new Error('Generator'))) //拿到异常值,并返回最后的Generator的值对象和执行状态
连续异步
但是这样写有些问题:手写这样的代码虽然在请求列表可读很好,但是但在回调里却嵌套很深,有点像回调地狱;和洋葱代码有些相似。
function * fun() {
try{
const res1 = yield ajax('package.json');
console.log(res1)
const res2 = yield ajax('api/info.json');
console.log(res2)
const res3 = yield ajax('api/user.json')
console.log(res3)
} catch(err) {
console.log(err)
}
}
const gener = fun();
//使用嵌套的方式
const gener1 = gener.next();
gener1.value.then((res) => {
const gener2 = gener.next(res);
gener2.value.then((res) => {
const gener3 = gener.next(res);
gener3.value.then((res) => {
gener.next(res)
})
})
})
//使用递归的方式
function handleResult(result) {
if(result.done) return;
result.value.then((res) => {
handleResult(gener.next(res))
})
}
handleResult(gener.next())
//再递归的基础做异常处理,上面再加try catch
function handleResult(result) {
if(result.done) return;
result.value.then((res) => {
handleResult(gener.next(res))
},(err) => {
gener.throw(new Error(err))
})
}
handleResult(gener.next())
//使用高阶函数对上面进行封装,在async和await出来之前很流行这种些法,也出现对应的开源库:https://github.com/tj/co
function generatorFun(fun) {
const generator = fun();
function handleResult(result) {
if(result.done) return;
result.value.then((res) => {
handleResult(generator.next(res))
},(err) => {
generator.throw(new Error(err))
})
}
handleResult(generator.next())
}
generatorFun(fun)
async和await函数
描述
使用Async和await 实现扁平化,async 就是Generator函数的一种更方便的语法糖,语法也有些相似,其实说白了async的底层就是使用了Generator函数
await 必须放到 async函数里面执行,不能放到全局或者其他不是await的函数里
和Generator的区别
async 不需要像Generator 那样配个co 生成器
基本语法
就是将Generator的*去掉再普通函数的前面加上 async,将yield 换成 await
async function fun(){
try{
const res1 = await ajax('package.json');
console.log(res1)
const res2 = await ajax('api/info.json');
console.log(res2)
const res3 = await ajax('api/user.json')
console.log(res3)
} catch(err) {
console.log(err)
}
}
fun()
注意
then() 里面的实参一定要是函数,如果不是函数,就无视它
看看案例:
1、2是数值,无视他。
2、Promise.resolve(3) 是Promise 对象无视它。
3、它们都会被替换为 val => val,等价于:Promise.resolve(1).then(val => val).then(val => val).then(console.log)
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
html-webpack-plugin 的使用
如果不想node 方式,想在浏览器的话就使用这个,不用改一次启动一次,这个热加载,保存即更新,非常方便。
html-webpack-plugin 的使用 https://blog.csdn.net/qq_25286361/article/details/118121882
html-webpack-plugin 踩坑 https://blog.csdn.net/qq_25286361/article/details/118122912