赞
踩
本文首先介绍了异步操作的前置知识及存在的问题。第二、三节分别介绍了ES6引入的两种异步编程解决方案Promise和Generator。
详细解释参看:JS运行机制详解 动图详解Event Loop
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步执行的运行机制:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
下图是主线程和任务队列的示意图:
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
const a = 2 const b = 3 console.log(a + b) // 同步任务 // 异步任务 setTimeout(() => { // 延迟1s执行 console.log(a + b) }, 1000) // 前后端数据分离 前端 <-> 后端 ajax console.log(1) setTimeout(() => { console.log(2) }, 1000) console.log(3) // 1 3 2 console.log(1) //不管延迟时间是多少,它都是异步任务,必须等主线程任务执行完成之后再执行 setTimeout(() => { console.log(2) }, 0) console.log(3) //1 3 2 // 伪代码 setTimeout(()=>{ task() // 表示一个任务 }, 2000) // 异步任务,首先进入Event table,等2s之后再进入Event Queue // 虽然异步任务2s就准备好了,但是还必须等到同步任务执行完成 sleep(5000) // 表示一个很复杂的同步任务,会先于异步任务执行
Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。
Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。
XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
在实现的时候,要考虑兼容性问题
// 封装成一个函数 // ajax的第二个参数是一个回调函数 function ajax(url, callback) { // 1、创建XMLHttpRequest对象 var xmlhttp if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest() } else { // 兼容早期浏览器 xmlhttp = new ActiveXObject('Microsoft.XMLHTTP') } // 2、发送请求 xmlhttp.open('GET', url, true) // 指定发送请求的操作 xmlhttp.send() // 发送 // 3、服务端响应 // 监听onreadystatechange 方法 xmlhttp.onreadystatechange = function () { // 事件处理函数 if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { var obj = JSON.parse(xmlhttp.responseText) // console.log(obj) callback(obj) // 将响应得到的数据传给回调,回调函数是自己定义并传入的 } } } var url = 'http://musicapi.xiecheng.live/personalized' ajax(url, res => { console.log(res) })
回调函数可规范调用的顺序,但是当代码层层嵌套越写越深,代码的可维护性、可读性都会降低,就会造成Callback Hell,下面将介绍ES6对异步操作的解决方案Promise来解决这类问题。
// 1 -> 2 -> 3
// callback hell
ajax('static/a.json', res => {
console.log(res)
ajax('static/b.json', res => {
console.log(res)
ajax('static/c.json', res => {
console.log(res)
})
})
})
所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可 以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 对象有以下两个特点。
Promise的精髓在于对于异步的状态管理
// 状态管理 // resolve 成功 // reject 失败 let p = new Promise((resolve, reject) => { setTimeout(() => { console.log('kakaDorothy') // resolve() // reject() // if(){ // resolve() // }else{ // reject() // } // resolve('成功') // 传参到then里面 reject('失败') }, 1000) }).then(res => { // then可传入两个函数作为参数,第一个为必须得参数,第二个可省略 // 异步操作成功之后进行的操作 console.log(res) }, err => { console.log(err) })
注意,如果Promise内部没有写任何异步操作,那么它是会立即执行的。then方法相当于promise的回调函数(它的微任务),待promise内的函数执行完成便执行。
let p = new Promise((resolve, reject) => {
console.log(1)
resolve() // 成功->可调用then方法进一步操作
})
console.log(2)
p.then(res => { // 想要调用then方法,在promise对象中的resolve或者reject方法是一定要写的
console.log(3)
})
//1 2 3
let p1 = new Promise((resolve, reject) => { resolve(1) }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve(2) // 模拟成功的状态 }, 1000) }) let p3 = new Promise((resolve, reject) => { setTimeout(() => { reject(3) // 模拟失败的状态 }, 1000) }) console.log(p1) // resolved console.log(p2) // pending console.log(p3) // pending // 待状态转换完成之后再输出 setTimeout(() => { console.log(p2) }, 2000) // resolved setTimeout(() => { console.log(p3) }, 2000) // rejected p1.then(res => { console.log(res) // 1 }) p2.then(res => { console.log(res) // 2 }) p3.catch(err => { console.log(err) // 3 })
Promise状态形成之后就无法再改变
let p = new Promise((resolve, reject) => {
reject(2)
resolve(1)
})
p.then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// 只输出2,promise状态形成之后就无法再改变
封装Ajax请求
// 传入成功回调与失败回调 function ajax(url, successCallback, failCallback) { // 1、创建XMLHttpRequest对象 var xmlhttp if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest() } else { // 兼容早期浏览器 xmlhttp = new ActiveXObject('Microsoft.XMLHTTP') } // 2、发送请求 xmlhttp.open('GET', url, true) xmlhttp.send() // 3、服务端响应 xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { var obj = JSON.parse(xmlhttp.responseText) // console.log(obj) successCallback && successCallback(obj) } else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) { failCallback && failCallback(xmlhttp.statusText) } } }
对于重复的方法,将它封装成一个函数,每次调用此方法时,返回一个新的Promise对象。
function getPromise(url) {
return new Promise((resolve, reject) => {
// 在Promise内部调用ajax函数
ajax(url, res => {
resolve(res)
}, err => {
reject(err)
})
})
}
调用getPromise方法(注意需要使用return,否则链式操作可能不起效)
// resolved成功示例 getPromise('static/a.json') .then(res => { console.log(res) return getPromise('static/b.json') }).then(res => { console.log(res) return getPromise('static/c.json') }).then(res => { console.log(res) }) // static文件夹下不存在aa.json,会进入rejected状态 getPromise('static/aa.json') .then(res => { console.log(res) return getPromise('static/b.json')// aa.json读取成功时向b.json发出请求 }, err => { console.log(err) // Not Found return getPromise('static/b.json') // aa.json读取失败时也向b.json发出请求 }).then(res => { console.log(res) // 输出b.json的内容 return getPromise('static/c.json') }).then(res => { console.log(res) // 输出c.json的内容 }) // 在最后加上catch方法,如出现错误便会抛出异常,而不会进入内部继续输出 getPromise('static/aa.json') .then(res => { console.log(res) return getPromise('static/b.json') }).then(res => { console.log(res) return getPromise('static/c.json') }).then(res => { console.log(res) }).catch(err => { console.log(err) // Not Found })
// Promise.resolve() let p1 = Promise.resolve('success') // console.log(p1) p1.then(res => { console.log(res) }) // Promise.reject() let p2 = Promise.reject('fail') console.log(p2) p2.catch(err => { console.log(err) }) function foo(flag) { if (flag) { return new Promise(resolve => { // 异步操作 resolve('success') }) } else { // return 'fail' // 因为下面是在then方法中调用此返回字符串方法 所以报错表示not a function return Promise.reject('fail') // 能够返回一个Promise对象 } } foo(false).then(res => { console.log(res) }, err => { console.log(err) })
Promise.all 方法接受一个数组作为参数, p1 、 p2 、 p3 都是 Promise 实例,如果不是,就会先调用 Promise.resolve 方法, 将参数转为 Promise 实例,再进一步处理。( Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p 的状态由 p1 、 p2 、 p3 决定,分成两种情况:
let p1 = new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { console.log(1) resolve('1成功') }, 2000) }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { console.log(2) // resolve('2成功') reject('2失败') }, 1000) }) let p3 = new Promise((resolve, reject) => { setTimeout(() => { console.log(3) resolve('3成功') }, 3000) }) // 传入数组作为参数, 数组内放入Promise对象 Promise.all([p1, p2, p3]).then(res => { // 需要等数组内的Promise对象都执行完成之后再执行此时的then方法 console.log(res) }, err => { // 如果出现错误 便直接进入err错误函数 而不会执行前面的res console.log(err) })
只要 p1 、 p2 、 p3 之中有一个实例率先改变状态, p 的状态就跟着改变,即只要其中一个promise对象状态转换完成了(无论是成功or失败),便认为整体的p完成了。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
Promise.race 方法的参数与 Promise.all 方法一样,如果不是 Promise 实例,就会先调用Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
Promise.race([p1, p2, p3]).then(res => {
console.log(res)
}, err => {
console.log(err)
})
// 结果为 2失败
const imgArr = ['1.jpg', '2.jpg', '3.jpg'] let promiseArr = [] imgArr.forEach(item => { promiseArr.push(new Promise((resolve, reject) => { // 图片上传的操作 resolve() })) }) // 控制图片全部上传成功 Promise.all(promiseArr).then(res => { // 插入数据库的操作 console.log('图片全部上传完成') }) // 加载图片 function getImg() { return new Promise((resolve, reject) => { let img = new Image() img.onload = function () { resolve(img) } img.src = 'http://www.xxx.com/xx.jpg' }) } // 定义定时器,判断两秒内是否成功 function timeout() { return new Promise((resolve, reject) => { setTimeout(() => { reject('图片请求超时') }, 2000) }) } // 如果图片为加载成功而定时器超时了->返回超时信息 // 如果两秒内图片加载成功-> 返回图片信息 // 这两个Promise对象中任意一个完成了 整个对象便算完成了 Promise.race([getImg(), timeout()]).then(res => { console.log(res) }).catch(err => { console.log(err) })
从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式, 定义不同的内部状态( yield 在英语里的意思就是“产出”)。
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
必须调用遍历器对象的 next 方法(next方法可以传递参数),使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
Generator是可以暂停的,需要调用next方法手动执行, yield指令只能在生成器内部使用。
// 普通函数 function foo() { for (let i = 0; i < 3; i++) { console.log(i) } } foo() // Generator function* foo() { for (let i = 0; i < 3; i++) { yield i } } console.log(foo()) // 输出一个Generator对象 但是并不会输出 因为它需要我们手动输出 let f = foo() console.log(f.next()) console.log(f.next()) console.log(f.next()) console.log(f.next()) // yield指令只能在生成器内部使用 function* gen(args) { args.forEach(item => { yield item + 1 }) }
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。 yield 表达式就是暂停标志。
遍历器对象的 next 方法的运行逻辑:
yield 表达式与 return 语句既有相似之处,也有区别。
function* gen(x) { let y = 2 * (yield(x + 1)) let z = yield(y / 3) return x + y + z } let g = gen(5) console.log(g.next()) // 6 console.log(g.next()) // NaN console.log(g.next()) // NaN let g = gen(5) console.log(g.next()) // 6 console.log(g.next(12)) // y=24 8 console.log(g.next(13)) // z=13 x=5 42 // 计数器 function* count(x = 1) { while (true) { // x为7的倍数时才暂停 if (x % 7 === 0) { yield x } x++ } } let n = count() console.log(n.next().value) // 7 console.log(n.next().value) // 14 console.log(n.next().value) // 21 console.log(n.next().value) // 28 console.log(n.next().value) // 35
同样以ajax为例:
function ajax(url, callback) { // 1、创建XMLHttpRequest对象 var xmlhttp if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest() } else { // 兼容早期浏览器 xmlhttp = new ActiveXObject('Microsoft.XMLHTTP') } // 2、发送请求 xmlhttp.open('GET', url, true) xmlhttp.send() // 3、服务端响应 xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { var obj = JSON.parse(xmlhttp.responseText) // console.log(obj) callback(obj) } } } // 封装请求方法,调用ajax function request(url) { ajax(url, res => { getData.next(res) // 调用next,使Generator对象继续执行 }) } //Generator函数 function* gen() { let res1 = yield request('static/a.json') console.log(res1) // 返回第一次请求的结果 let res2 = yield request('static/b.json') console.log(res2) // 返回第二次请求的结果 let res3 = yield request('static/c.json') console.log(res3) // 返回第三次请求的结果 } let getData = gen() getData.next()
http://es6.ruanyifeng.com/#docs/generator
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://juejin.cn/post/6969028296893792286
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。