异步
- 出现的原因
- JavaScript语言的执行环境是"单线程"
- 所谓"单线程",就是指一次只能只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
- 单线程这种模式好处是实现起来比较简单,执行环境单一。坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段JavaScript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
- 为了解决这个问题,JavaScript语言将任务的执行模式分成两种,同步和异步。
- 异步的使用场景
- Ajax操作
- 异步编程的实现
-
回调函数
- 优点:简单丶很容易理解和部署。
- 缺点:不利于阅读和维护,各部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。
- 代码实现
// 假如有两个函数f1和f2 后者等待前者的执行结果 // 如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数 function f2(c) { console.log(c, 'hehe'); } function f1(callback) { setTimeout(function() { // f1任务代码 console.log('f1执行完'); callback('f2开始执行', 'hehe'); }, 1000); } f1(f2);
-
事件监听
- 代码实现
- 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。
- 缺点:整个程序都变成事件驱动,运行流程会变得很不流畅。
f1.on('done', f2);
-
发布/订阅
- 定义:我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"整个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式",又称"观察者模式"。
- 优点:
- 这种方法的性质和"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号丶每个信号有多少订阅者,从而监控程序的运行。
- 一是时间上的解耦,而是对象上的解耦
- 即可用于异步编程中,也可以用帮助我们完成更松耦合的代码编写
- 缺点:
- 创建订阅者本身需要消耗一定的时间和内存
- 当订阅一个消息时,也许次消息并没有发生,但这个订阅者会始终存在内存中
- 观察者模式弱化了对象之间的联系,这本是好事,但如果过度使用,对象与对象之间的联系也会隐藏的很深,会导致项目的难以追踪维护和理解。
- 使用场景
- DOM事件
- 自定义事件
- 代码实现
function Event() { // 存储不同的事件类型对应不同的处理函数,保证后续emmit可以执行。 this.cache = {}; } // 绑定事件 Event.prototype.on = function(type, handle) { if(!this.cache[type]) { this.cache[type] = [handle]; }else { this.cache[type].push(handle); } } // 事件触发 Event.prototype.emmit = function() { var type = arguments[0], arg = [].slice.call(arguments, 1); for(var i = 0; i < this.cache[type].length; i++) { this.cache[type][i].apply(this, arg); if(this.cache[type][i].flag) { this.cache[type].splice(i, 1); if(this.cache[type][i].flag) { this.cache[type].splice(i, 1); } } } } // 解除某个事件类型 Event.prototype.empty = function(type) { this.cache[type] = []; } // 解除某个事件 Event.prototype.remove = function(type, handle) { this.cache[type] = this.cache[type].filter((ele) => ele != handle); } // 绑定一次事件 Event.prototype.once = function(type, handle) { if(!this.cache[type]) { this.cache[type] = []; } // 做标记 handle.flag = true; this.cache[type].push(handle); } function detail1(time) { console.log('overtime1' + time); } function detail2(time) { console.log('overtime2' + time); } var oE = new Event(); oE.on('over', detail1); oE.on('over', detail2); oE.emmit('over', '2019-11-11'); oE.remove('over', detail2); oE.emmit('over', 'second-11-11');
-
Promise对象
- 定义
- 是CommonJS工作组提出的一种规范,目的是为了异步编程提供统一接口。
- 简单来说,它的思想就是,每一个异步任务都返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
- 优点:
- 回调函数变成链式写法,程序的流程可以看得很清楚,而且有一整套流程的配套方法。
- 而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不要担心是否错过了某个事件或信号。
- 缺点:就是编写和理解,相对比较难。
- 定义
-
Promise详解
- 定义:Promise是异步编程的一种解决方案,所谓的Promise简单来说就是一个容器,里面保存着未来才会结束的事件的结果。
- 特点
- 对象的状态不受外界影响,Promise对象代表一种异步操作,有三种状态:Pending(进行中)丶Resolve(已完成)丶Rejected(已失败),只有异步操作的结果可以改变状态,其它的任何操作都不能改变状态。
- 一旦状态改变了,就不会再变了,任何时候都可以得到这个结果。Promise对象的状态只有两种可能:Pending->Resolve或Pending->Resolve,只要这两种情况发生了,并且会一直保持这个结果,这与事件监听不同,事件的特点是不同时间。
- 缺点:
- 首先无法取消Promise,一旦创建它就会立即执行,中途无法取消。
- 其次,如果还不设置回调函数,Promise内部抛出的错误,不会反映到外部
- 最后,当处于Pending状态时,无法得知目前进展到哪一个阶段了。
- 源码实现
- 功能
- 同步
- 异步
- then链式操作
- 处理返回普通值
- 处理返回Promise值
- then异步操作
- then捕捉错误
- 空then
- Promise.all():全部成功才成功,一个失败全部失败
- Promise.race():哪个状态改变了,P的状态就改变了
function MyPromise(executor) { var self = this; self.status = 'pending'; self.resolveValue = null; self.rejectReason = null; self.resolveCallBackList = []; self.RejectCallBackList = []; function resolve(value) { if(self.status === 'pending') { self.status = 'Fulfilled'; self.resolveValue = value; self.resolveCallBackList.forEach(function(ele) { ele(); }); } } function reject(reason) { if(self.status === 'pending') { self.status = 'Rejected'; self.rejectReason = reason; self.RejectCallBackList.forEach(function(ele) { ele(); }) } } try { executor(resolve, reject); }catch(e) { reject(e); } } function ResolutionReturnPromise(nextPromise, returnValue, res, rej) { if(returnValue instanceof MyPromise) { returnValue.then(function() { res(val); }, function(reason) { rej(reason); }) }else { res(returnValue); } } MyPromise.prototype.then = function(onFulfilled, onRejected) { if(!onFulfilled) { onFulfilled = function(val) { return val; } } if(!onRejected) { onRejected = function(reason) { throw new Error(reason); } } var self = this; var nextPromise = new MyPromise(function(res, rej) { if(self.status === 'Fulfilled') { setTimeout(function() { try { var nextResolveValue = onFulfilled(self.resolveValue); ResolutionReturnPromise(nextPromise, nextResolveValue, res, rej); }catch(e) { rej(e); } }, 0); } if(self.status === 'Rejected') { setTimeout(function() { try{ var nextResolveValue = onRejected(self.rejectReason); ResolutionReturnPromise(nextPromise, nextRejectValue, res, rej); }catch(e) { rej(e); } }) } if(self.status === 'pending') { self.resolveCallBackList.push(function() { setTimeout(function() { try { var nextResolveValue = onFulfilled(self.resolveValue); ResolutionReturnPromise(nextPromise, nextResolveValue, res, rej); }catch(e) { rej(e); } }, 0) }) } }) return nextPromise; } MyPromise.race = function(promiseArr) { return new Promise(function(resolve, reject) { promiseArr.forEach(function(promise, index) { promise.then(resolve, reject); }) }) }
- 功能
- 使用场景
- Ajax请求
- 定义:
- Ajax是Asynchronous javascript and xml的缩写,用JavaScript以异步的形式操作XML(现在操作的是JSON)。随着谷歌地图的横空出世,这种不需要刷新页面就可以与服务器通讯的方式很快被人们所知。在传统的Web模型中,客户端向服务端发送一个请求,服务端会返回整个页面。
- 我们前面学习的form表单来传输数据的方式就属于传统的Web模型,当我们点击submit按钮之后,整个页面就会被刷新一下。form表单有三个很重要的属性,分别是method丶action和enctype。method是数据传输的方式,一般是GET或者POST,action是我们要把数据传送到的地址,enctype的默认值是"application/x-www-form-urlencoded",即在发送前编码所有字符,这个属性值即使我们不写也是默认这个的。但是当我们在使用包含文件上传控件的表单的时候,这个值就必须更改成"multipart/form-data",即不对字符进行编码。而在Ajax模型中,数据在客户端与服务器之间独立传输,服务器不再返回整个页面。
- 优点
- 页面无刷新,在页面内与服务器进行通信,给用户的体验更好。
- 使用异步的形式与服务器进行通信,不需要打断用户的操作,给用户的体验更好。
- 减轻服务器的负担。
- 不需要插件或者小程序
- 缺点
- 不支持浏览器的后退机制
- 安全问题,跨站点脚本攻击丶sql注入攻击
- 对搜索引擎支持较弱
- 不支持移动端设备
- 违背了url和资源定位的初衷
- 对象属性
- onreadystatechange:状态改变触发器
- readyState:对象状态
- 0:表示为初始化,此时已经创建了一个XMLHttpRequest对象
- 1:表示读取中,此时代码已经调用XMLHttpRequest的open方法并且XMLHttpRequest已经将请求发送到服务器。
- 2:表示已读取,此时已经通过open方法把一个请求发送到服务端,但是还没收到。
- 3:代表交互中,此时已经收到http响应头部信息,但是消息主体信息还没有完全接收。
- 4:代表完成,此时响应已经被完全接受
- responseText:服务器进程返回数据的文本版本
- responseXML:服务器进程返回数据的兼容DOM的XML文本对象
- status:服务器返回的状态码
- 代码实现
function AJAX(json) { var url = json.url, method = json.method, flag = json.flag, data = json.data, callBack = json.callBack, xhr = null; // 1. 创建异步对象 if(window.XMLHttpRequest) { // 一般主流浏览器支持这个 xhr = new window.XMLHttpRequest(); }else { // IE6以下用这个 xhr = new ActiveXObject('Microsoft.XMLHTTP'); } // 2. 让异步对象监听接收服务器的响应数据 xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { // 数据已经可用了 callBack(xhr.responseText); } } // 建立对服务器的调用 if(method === 'get') { url += '?' + data + new Data().getTime(); // 3. 设置请求方式 xhr.open('get', url, flag); xhr.send(); }else { xhr.open('post', url, flag); // 4. 设置请求头 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // 5. 设置请求体 xhr.send(data); } }
- 定义:
- 自定义弹窗处理
- 图片加载
- Ajax请求
Generator
- 定义:生成器,本身是函数,执行后返回迭代对象,函数内部要配合yeild使用Generator函数,会分段执行,遇到yeild即暂停。
- 特点
- function和函数名之间需要带*
- 函数体内部yeild表达式,产出不同的内部状态(值)
- 可以交出函数的执行权
- 作用:配合Promise解决回调地狱问题
- 配合库
- Co
- 使用场景
- 可以在任意对象上部署Iterator接口,配合Promise和Co库解决回调地狱的问题。
async
- 定义:是Generator语法糖,通过babel编译后可以看出它就是Generator+Promise+Co递归思想实现的,配合await使用。
- 目的:优雅解决异步操作的问题
- 异步编程会出现问题
- 回调地狱
// 这样能层层回调的事情,你会发现要写多个回调函数,一堆.then不够优雅, // 有没有写法写起来像同步很优雅,最后也是通过异步的方式来搞定层层回调的结果。 function readFile(path) { return new Promise((res, rej) => { fs.readFile(path, 'utf-8', (err, data) => { if(err) { rej(err); }else { res(data); } }) }) }; readFile('https://api.github.com/users/superman66') .then((val) => { return readFile(val); }, () => {}).then((val) => { return readFile(val); }) .then((val) => { console.log(val); }); // async写法 async function read(url) { let val1 = await readFile(url); let val2 = await readFile(val1); let val3 = await readFile(val2); } read('https://api.github.com/users/superman66') .then((val) => console.log(val));
- 解决了try catch可以异步的方式捕获异常
async function read(url) { try { let val1 = await readFile(url); let val2 = await readFile(val1); let val3 = await readFile(val2); }catch(e) { console.log(e); } } readFile('./data/number.txt').then((val) => console.log(val));
- 解决同步并发异步的结果
- Promise.all()
// 之前通过解决同步并发异步的结果是使用Promise.all()去解决的,首先Promise.all()使用的时候需要传递多个promise对象 // 其次他是全部成功才成功,一个失败全部失败 Promise.all([readFile('./data/number1.txt), readFile('./readFile/number2.txt'), readFile('./data/number3.txt')]) .then((val) => console.log(val), (reason) => console.log(reason));
- async
async function read1() { let val1 = null; try{ val1 = await readFile('./data/number1.txt'); console.log(val1); }catch(e) { console.log(e); } } async function read2() { let val2 = null; try{ val2 = await readFile('./data/number2.txt'); console.log(val2); }catch(e) { console.log(e); } } async function read3() { let val3 = null; try { val3 = await readFile('./data/number3.txt'); console.log(val3); }catch(e) { console.log(e); } } function readAll(...args) { args.forEach((ele) => { ele(); }) } readAll(read1, read2, read3);
- Promise.all()
- 回调地狱
- 异步编程会出现问题
- 异步编程的最高境界,就是根本不用关心它是不是异步。一句话,async函数就是Generator函数的语法糖。
- 优点:
- 内置执行器
- Genarator函数的执行必须依靠执行器,所以才有Co函数库,而async函数自带执行器,也就是说,async函数的执行,与普通的函数一模一样,只要一行。
- 更好的语义化
- async和await比起星号与yield语义化更清楚了,async表示函数里有异步操作,async表示紧跟在后面的表达式需要等待结果。
- 更广的适应性
- co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值(数值丶字符串丶布尔值 但这时等同于同步操作)
- 返回值是Promise
- async函数返回值是Promise对象,比Generator函数返回的Iterator对象方便,可以直接使用then()方法进行调用。
- 内置执行器
- async函数是非常新的语法功能,新到都不属于ES6,而是属于ES7。目前,它任处于提案阶段,但是转码器Babel和regenerator已经支持,转码后就能使用。
- 注意点
- await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
- await命令只能用在async函数中,如果用在普通函数,就会报错,上面的代码会报错,因为await函数用在普通函数之中了,但是,如果将foreach方法的参数改成async函数,也会有问题。上面的代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行,正确的写法是采用for循环
- 如果确实希望多个请求并发执行,可以使用Promise.all方法。
Rest参数
- 目的:
- Rest参数用于获取函数的多余参数,组成一个数组,放在形参的最后,这样就不需要arguments对象了。
- 主要用于处理不定参数
funtion fn(a, b, ...args) { // ... }
- Rest参数和arguments对象区别:
- rest参数只包括那些没有给出名称的参数,arguments包含所有参数。
- arguments对象不是真正的数组,而rest参数是数组实例,可以直接应用sort,map,等方法
- arguments对象拥有一些自己额外的功能(比如callee)
- Rest参数简化了使用arguments获取多余参数的方法
- 注意:
- rest参数之后不能再有其他参数,否则会报错。
- 函数的length属性,不包括rest参数
- Rest参数可以被结构化(通俗一点,将rest参数的数据解析后一一对应),不要忘记参数用[]括起来,因为它是数组。
function fn(...[a, b, c]) { console.log(a + b + c); } fn(1); fn(1, 2, 3); fn(1, 2, 3, 4);
Iterator
- 目的:ES6引入这个Iterator是了这些数据有统一的遍历或者进行for of ...Array.from等操作,方便我们写代码的时候不用大范围的重构。
- 定义
- Iterator的思想取自于我们的迭代模式
- forEach可以迭代数组,for in可以迭代对象,$.each只能迭代数组和对象,Iterator还能迭代Set和Map。
- Set和Map原型上都有Symbol.Iterator这个迭代口,只要有这个接口都可以被for..of迭代
// 把不可迭代数据变成可迭代数据 let obj = { 0: 'a', 1: 'b', 2: 'c', length: 3, [Symbol.iterator]: function() { let curIndex = 0; let next = () => { return { value: this[curIndex], done: this.length == ++ curIndex } } return { next } } } console.log([...obj]) for(let p of obj) { console.log(p); }
- Generator要去生成一个迭代对象,为什么生成迭代对象呢,就是根据Symbol.iterator。
- 分类
- 内部迭代器
- Array.prototype.forEach就是我们所说的迭代器,它就是用迭代模式的思想做出的函数,本质是内部迭代器。
- 外部迭代器
- 代码实现(ES5实现)
function OuterInterator() { let curIndex = 0; let next = () => { return { value: o[curIndex], done: o.length == ++ curIndex; } } return { next } } let oIt = OuterIterator(arr);
- 代码实现(ES5实现)
- 内部迭代器
Symbol
- 特点
- 唯一性
- arr丶Set丶Map丶arguments丶nodelist都有这个属性Symbol.iterator 这个属性等于iterator迭代函数
异步加载JavaScript
- JS加载的缺点:加载工具方法没必要阻塞文档,过多的JS加载会影响页面效率,一旦网速不好,那么整个网站都将等待JS加载而不进行后续等渲染工作
- 目的:有些工具方法需要按需加载,用到再加载,不用不加载。
- JavaScript异步加载的三种方案
- defere异步加载
- 但要等到dom文档全部解析完才会被执行,只有IE能用。(执行时不阻塞页面加载)(支持IE)
- async异步加载
- 加载完就执行,async只能加载外部脚本,不能把JS写在script标签里面(执行时不阻塞页面加载)
- 创建script,插入到DOM中,加载完callBack
- 代码实现
function loadScript(url, callback) { var script = document.createElement('script'), script.type = 'text/javaScript'; if(script.readyState) { // IE if(script.onreadystatechange === 'complete' || script.onreadystatechange === 'loaded') { callback(); } }else { // FireFox丶Safari丶Chrome and Opera script.onload = function() { callback(); } } script.url = url; document.head.appendChild(script); }
- 代码实现
- defere异步加载
异步加载CSS
- 通过JS动态插入link标签来异步载入CSS代码
var myLink = document.createElement('link'); myLink.rel = 'stylesheet'; 'myLink.href = './index.css'; documemt.head.insertBefore(myLink, document.head.childNodes[document.head.childNodes.length - 1].nextSibling);
- 利用link上的media属性
- 将它设置为和用户当前浏览器环境不匹配的值,比如:media="print",甚至可以设置为一个完全无效的值media="jscourse"之类的。
- 这样的话,浏览器就会认为CSS文件优先级非常低,就会在不阻塞的情况下进行加载。但是为了让CSS规则失效,最后还是要将media值改对才行。
<link rel="style" href="css.style" media="jscourse" onload="this.media='all'">
- rel="preload"
- 通过preload属性值就是告诉浏览器这个资源随后会用到,请提前加载好。所以你看它加载完毕后,还是需要将rel值改回去,这才能让CSS生效。
- 语义更好一些
- as="style"这个属性,所以preload不仅仅可以用在CSS文件上,而是可以用在绝大多数的资源文件上。比如JS文件
<link rel="preload" href="sccriptfile.js" as="script"> // 要用的时候 就创建一个script标签加载它,这个时候直接从缓存中拿到这个文件了,因为提前加载好了 var oScript = document.createElement('script'); script.src = 'index.js'; document.body.appendChild(script);
- Chrome完美支持,其他不支持。