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)
})
模拟请求网络差的情况

image-20210624225559141

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