【有书共读08】JS核心技术开发解密读书笔记11
5. Promise
同步与异步
同步是指当发起一个请求时,如果未得到请求结果,代码将会等待,直到结果出来,才会执行后面的代码。
异步是指当发起一个请求时,不会等待请求结果,而是直接继续执行后面的代码。
我们使用Promise模拟一个发起请求的函数,该函数在执行后,会在1s之后返回数值30。
function fn () { return new Promise(function (resolve, reject) { setTimeout(function () { resolve(30) }, 1000) }) } // 在该函数的基础上,可以使用async/await语法来模拟同步的效果 var foo = async function () { var t = await fn(); console.log(t); console.log('next code'); } foo(); // 1s之后输出30,next code
而异步效果则会有不同的输出结果。
var foo = function () { fn().then(function (resp) { console.log(resp); }); console.log('next code'); } foo(); // next code,1s之后输出30
Ajax
Ajax是网页与服务端进行数据交互的一种技术,我们可以通过服务端提供的接口,用Ajax想服务端请求我们需要的数据。
// 简单的Ajax原生实现 // 由服务端提供的接口 var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; var result;- var XHR = new XMLHttpRequest(); XHR.open('GET', url, true); XHR.send(); XHR.onreadystatechange = function () { if (XHR.readyState == 4 && XHR.status == 200) { retult = XHR.response; console.log(result); } }
在Ajax的原生实现中,利用了onreadystatechange事件,只有当该事件触发并且符合一定条件时,才能拿到我们想要的数据,之后才能开始处理数据。
但是,当Ajax中嵌套新的Ajax请求时,我们不得不不停地嵌套毁掉函数,以确保下一个接口所需要的参数的正确性。这样的灾难,我们称之为回调地狱。
此时,我们就需要一个叫做Promise的语法来解决这样的问题。
var tag = true; // new Promise创建一个Promis实例,Promise函数中的第一个参数为一个回调函数,通常情况下,在这个函数中,会执行发起请求操作 // 请求结果有三种状态,分别是pending(等待中,表示还没有得到结果),resolved(得到了我们想要的结果,可以继续执行),以及rejected(得到了错误的,或者不是我们期望的结果,拒绝执行) // 回调函数中,分别使用resolve与reject将状态修改为对应的resolved与rejected,resolve、reject是回调函数的两个参数,它们能将请求结果的具体数据传递出去 var p = new Promise(function (resolve, reject) { if (tag) { resolve('tag is true'); } else { reeject('tag is false'); } }) // Promise实例拥有的then方法,可用来处理当请求结果的状态变成resolved时的逻辑,then的第一个参数为一个回调函数,该函数的参数是resolve传递出来的数据,这里是tag is true // Promise实例拥有的catch方法,可用来处理当请求结果的状态变成rejected时的逻辑,catch的第一个参数为一个回调函数,该函数的参数是reject传递出来的数据,这里是tag is false p.then(function (result) { console.log(result); }).catch(function (error) { console.log(error); })
经过简单介绍,我们通过几个例子来感受一下Promise的用法。
例子1:
function fn (num) { return new Promise(function (resolve, reject) { if (typeof num == 'number') { resolve(); } else { reject(); } }).then(function () { console.log('参数是一个number值'); }).catch(function () { console.log('参数不是一个number值'); }) } fn('12'); console.log('next code'); //先输出 next code,再输出参数不是一个number值
例子2:
function fn (num) { return new Promise(function (resolve, reject) { setTimeout(function () { if (typeof num == 'number') { resolve(num); } else { var arr = num + ' is not a number'; reject(arr); } }, 2000) }).then(function (resp) { console.log(resp); }).catch(function (arr) { console.log(arr); }) } fn('abc'); console.log('next code'); // 先输出next code,2s后输出 abc is not a number
我们将最开始的Ajax原生请求进行简单的封装。
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; function getJSON (url) { return new Promise(function (resolve, reject) { var XHR = new XMLHttpRequest(); XHR.open('GET', url, true); XHR.send(); XHR.onreadystatechange = function () { if (XHR.readyState == 4) { if (XHR.status == 200) { try { var response = JSON.parse(XHR.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(XHR.statusText)); } } } }) } getJSON.then(function (resp) { console.log(resp); })
Promise.all
当有一个Ajax请求,它的参数需要另外两个甚至更多个请求都有返回结果之后才能确定时,就需要用到Promise.all来帮助我们应对这个场景。
Promise.all接收一个Promise对象组成的数组座位参数,当这个数组中所有的Promise对象状态都变成resolved或者rejected时,它才回去调用then方法。
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10'; function renderAll () { return Promise.all([getJSON(url), getJSON(url1)]); } renderAll.then(function (value) { console.log(value); })
Promise.race
与Promise.all相似的是,Promise.race也是以一个Promise对象组成的数组作为参数,不同的是,只要当数组中的其中一个Promise状态变成resolved或者rejected时,就可以调用then方法,而传递给then方法的值也会有所不同。
async/await
异步问题不仅可以使用前面学到的Promise解决,还可以用async/await来解决。
async/await是ES7中新增的语法,虽然现在最新的Chrome浏览器已经支持了该语法,但在实际使用中,仍然需要在构建工具中配置对该语法的支持才能放心使用。
async function fn () { return 30; } const fn = async () => { return 30; } console.log(fn()); // Promise {<resolved>: 30}
可以发现fn函数运行后返回的是一个标准的Promise对象,因此可以猜想到async其实是Promise的一个语法糖,目的是为了让写法更加简单,因此也可以使用Promise的相关语法来处理后续的逻辑。
fn().then(res => { console.log(res); // 30 })
await的含义是等待,意思就是代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码,这正是同步的效果。
需要注意的是,await关键字只能在async函数中使用,并且await后面的函数运行必须返回一个Promise对象才能实现同步的效果。
function fn () { return new Promise((resolve, reject) => { setTimeout(() => { resolve(30); }, 1000); }) } const foo = async () => { const t = await fn(); console.log(t); console.log('next code'); } foo(); // 先输出Promise {<pending>},然后输出30,最后输出next code
通过运行这个例子可以看出,在async函数中,当运行遇到await时,就会等待await后面的函数运行完毕,而不会直接执行next code。
6. 事件循环机制
先看两个简单的例子,例子1:
setTimeout(function () { console.log(1); }, 0); console.log(2); for (var i = 0; i < 5; i++) { console.log(3); } console.log(4); // 依次输出 2 3 3 3 3 3 4 1
例子2:
console.log(1); for (var i = 0; i < 5; i++) { setTimeout(function () { console.log('2-' + i); }, 0); } console.log(3); // 依次输出 1 3 2-5 2-5 2-5 2-5 2-5
很多人在运行之后可能感到困惑,为什么即使设置了setTimeout的延迟事件为0,它里面的代码仍然是最后执行的?
通常情况下,决定代码执行顺序的是函数调用栈。很明显这里的setTimeout中的执行顺序已经不是用函数调用栈能够解释清楚的了,这是因为队列。
JavaScript的一个特点是单线程,但是很多时候我们仍需要在不同的事件去执行不同的任务,例如给元素添加点击事件,设置一个定时器,或者发起Ajax请求。因此需要一个异步机制来达到这样的目的,事件循环机制也因此而来。
每一个JavaScript程序都拥有唯一的事件循环,大多数代码的执行顺序是可以根据函数调用栈的规则执行的,而setTimeout/setIInterval或者不同的事件绑定(click等)中的代码,则通过队列来执行。
setTimeout为任务源,或者任务分发器,由它们讲不通的任务分发到不同的任务队列中去。每个任务源都有对应的任务队列。
任务队列又分为宏任务(macro-task)与微任务(micro-task)两种,在浏览器中,宏任务包括script,setTimeout/setInterval,I/O,UI rendering等,微任务包括Promise。
JS是单线程,同步任务会在主线程上执行,但异步任务会被分配到被事件触发线程管理着的任务队列中,任务队列分为宏任务(macro-task)和微任务(micro-task)
macro-task:setTimeOut/setInterval、I/O、UI rendering
micro-task: Promise
第一次循环,先从主线程中同步代码开始执行,执行完毕后检查微任务队列中是否有任务,有则执行micro-task,第一次循环结束;
第二次循环,检查macro-task中是否有任务,有则执行,像setTimeOut中时间未到的会进入下一个队列。此过程中可能会产生微任务,在每个宏任务执行完毕后就会检查是否有微任务,有则执行完当下宏任务立即执行微任务,然后再执行下一个该队列中的宏任务
依次循环直到调用栈未空。
// 宏任务 setTimeout(() => { console.log('timeout') }, 1000) // Promise中的回调函数会立即执行,但是then()是一个微任务 new Promise((resolve, reject) => { console.log('promise1') for (var i = 0; i < 100; i++) { i === 99 && resolve() } console.log('promise2') }).then(() => { console.log('then') }) // 主任务 console.log('global') // 输出: // promise1 promise2 global then timeout
7. 对象与class
ES6针对对象的写法新增了一些语法简化的写法。
1)当属性与变量同名时
const name = 'Jane'; const age = 20; // ES6 const person = { name, age } // 等价于ES5 var person = { name: 'Jane', age: 20 }
这样的写法在很多地方都能见到。
const getName = () => person.name; const getAge = () => person.age; // commonJS的方式 module.exports = {getName, getAge} // ES6 modules的方式 export default {getName, getAge}
2)对象中方法的简写
// ES6 const person = { name, getName () { return this.name; } } // ES5 var person = { name: name, getName: function getName () { return this.name; } }
3)可以使用变量作为对象的属性,只需用中括号[]包裹即可
const name = 'Jane'; const age = 20; const person = { [name]: true, [age]: true }
class
ES6为创建对象提供了新的语法class。
// ES5 function Person (name, age) { this.name = name; this.age = age; } Person.prototype.getName = function () { return this.name; } // ES6 class Person { constructor (name, age) { // 构造方法 this.name = name; this.age = age; } getName () { // 原型方法 return this.name; } static a = 20; // 等同于Person.a = 20 c = 20; // 表示在构造函数中添加属性,在构造函数中等同于this.c = 20 getAge = () => this.age; // 箭头函数的写法表示在构造函数中添加方法,在构造函数中等同于this.getAge = function () {} }
继承
与ES5相比,ES6的继承要简单的多。
class Person { constructor(name, age) { this.name = name; this.age = age; } getName() { return this.name; } } class Student extends Person { constructor(name, age, gender, classes) { super(name, age); this.gender = gender; this.classes = classes; } getGender() { return this.gender; } } const s = new Student('Tom', 20, 1, 3); a.getName(); // Tom a.getGender(); // 1
子类的构造函数中必须调用super方法,它表示构造函数的继承。
8. 模块化
import
通过import指令,可以在当前模块中引入其他模块。
import registerServiceWorker from './registerServiceWorker'; registerServiceWorker();
- import表示引入/加载一个模块
- registryServiceWorker可以理解为这个模块的名字
- from表示模块来自于哪里
- 当引入.js文件时,可以省略文件后缀名
export
export提供对外接口,常配合import一起使用。
```
export const name1 = 'Tom';
export const name2 = 'Jake';
import {name1} from './xx';
还可以通过export default来对外提供接口,这种情况下,对外接口通常是一个对象。
const name1 = 'Tom';
const name2 = 'Jake';
export default {name1, name2}
```
关于ES6模块化的知识,更多请阅读阮一峰大神的ECMAScript 6入门。