十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
JavaScript是一种程序设计语言,一般用在浏览器里边,而在网页力编译代码的形式出现来控制网页的效果。
目前成都创新互联已为上千多家的企业提供了网站建设、域名、虚拟空间、成都网站托管、企业网站设计、伊川网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
JavaScript是采用的JAVA编程语言的语法,但是比JAVA结构简单得多,比较容易掌握,但是其内部实现并不一样,所以有人说JAVA和javascript近亲,这是不对的
JSP,是java的web形式,也就是说,jsp也是用来做网页的,但是jsp里边写的是java代码,java代码编译后负责生成HTML代码,生成的代码到客户端也就是你的浏览器,你的浏览器看到的只是生成代码,而不是jsp源代码
由于JavaScript是在网页里边直接进行控制的,所以比如说你的某一个操作是不符合条件的,但是为了达到某种效果,而你这种操作不需要让服务器知道,你就可以使用这种方法。
相对的JSP的操作,你必须把你的操作转交到服务器去处理
比如说jsp里边
%
out.print("tr");
for( i=0; i5; i++)
{
out.print("td/td");
}
out.print("/tr");
%
而你看到的代码只能是
tr
td/td
td/td
td/td
td/td
td/td
/tr
而javascript如果写在页面里边,你是可以看到实际代码的.
在我们的工作和学习当中,到处充满了异步的身影,到底什么是异步,什么是异步编程,为什么要用异步编程,以及经典的异步编程有哪些,在工作中的场景又有什么,我们一点点深入的去学习。
什么是异步编程?
有必要了解一下,什么是异步编程,为什么要异步编程。
先说一个概念异步与同步。介绍异步之前,回顾一下,所谓同步编程,就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞后续代码的执行。
同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法后,需等待其响应返回,然后执行后续代码。
一般情况下,同步编程,代码按序依次执行,能很好的保证程序的执行,但是在某些场景下,比如读取文件内容,或请求服务器接口数据,需要根据返回的数据内容执行后续操作,读取文件和请求接口直到数据返回这一过程是需要时间的,网络越差,耗费时间越长。
如果按照同步编程方式实现,在等待数据返回这段时间,JavaScript是不能处理其他任务的,此时页面的交互,滚动等任何操作也都会被阻塞,这显然是及其不友好,不可接受的,而这正是需要异步编程大显身手的场景。
我们想通过Ajax请求数据来渲染页面,这是一个在我们前端当中很常见渲染页面的方式。基本每个页面都会都这样的过程。在这里用同步的方式请求页面会怎么样?浏览器锁死,不能进行其他操作。而且每当发送新的请求,浏览器都会锁死,用户体验极差。
在浏览器中同步执行将会是上面的这个样子,任务1做完才能做任务2,任务2做完才会做任务3。这里面体现出同步编程的有序的特点。只能1,2,3不能1,3,2。但是我们的代码逻辑中可以存在多任务同时执行的过程。在我们生活中,煮饭和烧水可以同时去做,同样在我们编程中也需要这样的逻辑。
在计算机中有多线程的概念,什么意思呢,每一个线程做一件事,像下面这样。
在不同的线程中可以执行不同的任务。
但是我们的JavaScript是单线程的,这里的单线程,强调的执行线程是单线程。后面也是有线程池的,线程以及线程池的概念在这里就不多说了。想了解的同学可以看看操作系统相关书籍。
JavaScript语言执行环境是单线程的,单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。
但是我们也需要类似多线程机制的这种执行方式。但是JavaScript还是单线程的,我们需要异步执行,异步执行会使得多个任务并发执行。
并行与并发。前文提到多线程的任务可以并行执行,而JavaScript单线程异步编程可以实现多任务并发执行,这里有必要说明一下并行与并发的区别。
并行,指同一时刻内多任务同时进行。边煮饭,边烧水,可以同时进行并发,指在同一时间段内,多任务同时进行着,但是某一时刻,只有某一任务执行。边吃饭边喝水,同一时间点只能喝水和吃饭。
接下来说一说异步机制
并发模型
目前,我们已经知道,JavaScript执行异步任务时,不需要等待响应返回,可以继续执行其他任务,而在响应返回时,会得到通知,执行回调或事件处理程序。那么这一切具体是如何完成的,又以什么规则或顺序运作呢?接下来我们需要解答这个问题。回调和事件处理程序本质上并无区别,只是在不同情况下,不同的叫法。
前文已经提到,JavaScript异步编程使得多个任务可以并发执行,而实现这一功能的基础是JavaScript拥有一个基于事件循环的并发模型。
堆栈与队列
介绍JavaScript并发模型之前,先简单介绍堆栈和队列的区别:
堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
队列(queue):先进先出顺序存储数据结构。
事件循环(EventLoop):JavaScript引擎负责解析,执行JavaScript代码,但它并不能单独运行,通常都得有一个宿主环境,一般如浏览器或Node服务器,前文说到的单线程是指在这些宿主环境创建单一线程,提供一种机制,调用JavaScript引擎完成多个JavaScript代码块的调度,执行(是的,JavaScript代码都是按块执行的),这种机制就称为事件循环(EventLoop)。
JavaScript执行环境中存在的两个结构需要了解:
消息队列(messagequeue),也叫任务队列(taskqueue):存储待处理消息及对应的回调函数或事件处理程序;
执行栈(executioncontextstack),也可以叫执行上下文栈:JavaScript执行栈,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执行上下文,通常称为执行栈帧(frame),存储着函数参数和局部变量,当该函数执行结束时,弹出该执行栈帧;
注:关于全局代码,由于所有的代码都是在全局上下文执行,所以执行栈顶总是全局上下文就很容易理解,直到所有代码执行完毕,全局上下文退出执行栈,栈清空了;也即是全局上下文是第一个入栈,最后一个出栈。
任务
分析事件循环流程前,先阐述两个概念,有助于理解事件循环:同步任务和异步任务。
任务很好理解,JavaScript代码执行就是在完成任务,所谓任务就是一个函数或一个代码块,通常以功能或目的划分,比如完成一次加法计算,完成一次ajax请求;很自然的就分为同步任务和异步任务。同步任务是连续的,阻塞的;而异步任务则是不连续,非阻塞的,包含异步事件及其回调,当我们谈及执行异步任务时,通常指执行其回调函数。
Web Worker 可能是在 JavaScript 中唯一可以真正实现多线程的方法了。我们需要按照下面的方式创建 worker :
?
1
const worker = newWorker("worker.js");
上面就定义了一个 Worker 实例,然后你可以通过 postMessage 与 worker 通信,就像和 iFrame
通信一样,只不过不存在跨域的问题,不需要验证跨域。
?
1
worker.postMessage(num);
在 worker 代码中,你需要监听这些事件:
?
1
2
3
onmessage = (e) = {
// e.data will contain the value passed
};
这种方式是双向的,所以你也可以从 worker 中 postMessage 给我们的主程序。
在 worker 代码中:
?
1
postMessage(result);
在主程序中:
?
1
worker.onmessage = (e) = {}
这就是 worker 最基本的用法。
异常处理
在你的 worker 代码中,有很多种方式来处理异常,比如你可以 catch 之后通过 postMessage
传递,这样可能需要多些一些代码,但是确实最有效也最安全的。
另一种方式是用 onerror 事件,这种方式可以捕捉所有未处理的异常,并且交给调用方来决定如何处理。调用方式很简单:
?
1
worker.onerror = (e) = {};
为了调试方便,异常对象中还有一些额外的字段比如:filename,lineno,colno.
回收
将不需要的 worker 回收是非常重要的,worker 会生成真正的操作系统线程,如果你发现与很多 worker
线程同时运行,你可以通过很简单的杀掉浏览器进程。
你有两种方式杀掉 worker 进程:在 worker 里和在 worker 外。我认为最好的处理 worker
生命周期的地方是在主页面里,但这也要取决于你代码的具体情况。
杀掉一个 worker 实例,在外部可以直接调用
terminate()方法,这种方法可以立即杀掉它,释放所有它正在使用的资源,如果它正在运行,也会立即终止。
如果你想要让 worker 自己去管理它的生命周期,可以直接在 worker 代码中调用stop()方法。
不管使用哪种方法,worker 都会停止,销毁所有资源。
如果你想使用一种“一次性”的 worker,比如需要做一些复杂运算之后就不再使用了,也要确保在 onerror
事件中去销毁它们,这样有利于规避一些难以发现的问题。
?
1
2
3
4
5
6
7
8
worker.onerror = (e) = {
worker.terminate();
reject(e);
};
worker.onmessage = (e) = {
worker.terminate();
resolve(e.data);
}
二、行内 Workers
有些时候将 worker 代码写到一个外部文件可能会使原本简单的问题变得复杂,幸运的是,workers 也可以用一个 Blob 来初始化。
写一个行内 worker ,参考如下代码段:
?
1
2
3
4
5
6
7
8
!-- --
script id="worker" type="javascript/worker"
// Put your worker code here
/script
const code = URL.createObjectURL(new Blob([
document.getElementById("worker").textContent
]));
const worker = new Worker(code);
这样你就创建了一个全局的 ObjectURL,但别忘了当不需要的时候要销毁它:
?
1
2
worker.terminate();
URL.revokeObjectURL(code);
三、Workers 嵌套
理论上,你可以嵌套使用 worker,就像在主线程中定义一个 worker 一样。这里有一个简单的 例子。但是不幸的是在 Chrome 中一直存在一个
bug ,让我们不能愉快的玩耍,或许以后这个 bug 会修复,但是目前来说还是没有太多进展,所以你最好不要使用。
数据传递
在 worker
数据传递的过程中有些需要注意的边缘情况。你可以传递数值,字符串,数组,也可以传递序列化/反序列化的对象。然而,你却不应该依赖序列化来保持数据结构,实际上,postMessage
用到了一种 数据克隆算法,它会生成一些额外的属性比如 RegExps 和 Blobs 以及一些循环引用。
这就是说,你需要将你要传递的数据最小化。你不可以传递 functions ,即使是支持的类型也会有一些限制,这些也很容易产生一些难以发现的
bug。如果你将你的 API 定义为只传递字符串,数值,数组和对象的话,那你可能会避过这些问题。
循环引用
如果你有一个很复杂的对象,那么里面很可能存在循环引用,这时如果你将它序列化成 JSON,你将会得到一个 TypeError:
Converting circular structure to JSON.
?
1
2
3
4
let a = {};
let b = {a};
a.b = b;
JSON.stringify({a,b}); // Error
然而你可以在 postMessage 中放心的使用,从而你就可以在 worker 中使用。
Transferable objects
为了防止同时修改同一变量的场景,你传递给 postMessage
的所有变量都会复制一份,这样确保了你多个线程不会修改同一个变量。但如果你想要传一个非常大的数据的话,你就会发现复制操作是很慢的。比如,如果你在做一些图片相关的运算,你可能会传递整个图片信息,就可能会遇到复制性能的瓶颈。
好在有 transferable object ,用 transfer 来代替 copy,比如ArrayBuffer
是transferable对象,而我们可以把任何类型的对象放在 ArrayBuffer 中。
如果你 transfer 一个对象,之前拥有它的线程被锁定权限,它确保了数据没有复制之前,不会被同时修改。
这时 postMessage 的代码段就有点尴尬了:
?
1
2
3
4
const ab = new ArrayBuffer(100);
console.log(ab.byteLength); // 100
worker.postMessage(ab, [ab]);
console.log(ab.byteLength); // 0
确保在 postMessage 中传递第二个参数,否则数据将会被复制。
?
1
2
3
4
const ab = new ArrayBuffer(100);
console.log(ab.byteLength); // 100
worker.postMessage(ab);
console.log(ab.byteLength); // 100
四、Webpack
在 Webpack 中使用 Web worker 时,你需要用 worker-loader。将它添加到 package.json 中的
devDependencies,然后运行 npm install,就可以了。
用到 worker 时,只需要 require 它。
?
1
2
3
const workerCode = require("worker!./worker.js");
...
const worker = new workerCode();
这样就初始化了 worker,然后就像上面讲的一样使用 worker。
如果需要使用行内 worker,你需要传递 inline 参数给 loader。
?
1
2
3
const workerCode = require("worker?inline!./worker.js");
...
const worker = new workerCode();
在 worker 中你也可以 import 模块。
?
1
2
3
import fibonacci from "./fibonacci.js";
...
const result = fibonacci(num);
缺点
在 Webpack 中使用 worker 很简单,但是在使用时也有一些坑值得你注意。
首先,无法将代码共用部分提取出来。如果你的 worker 中依赖一段共用代码,你只能把代码添加到 worker
中,不管其他地方是否也用到同样的代码。而且如果你多个 worker 要用同样的库,你也需要在每个 worker 中引入它们。
你可能会想如果你不用 worker-loader,然后用CommonsChunkPlugin指定一个新的入口,可能会解决这个问题。但是不幸的是
worker 不像是浏览器 window ,一些 feature 不可用,所以一些代码必须要引入。
同时,用行内 worker 也不会解决问题,共用的代码依然会出现在多个地方。
第二点缺点是,行内 worker 可能会导致
ObjectURLs内存泄露.它们被创建出来以后就不会被释放。这虽然不是一个大问题,但是如果你有很多“一次性” worker 的话,就会影响性能。
综上所述,我个人建议是使用标准的 worker,注意在 worker 中引入了什么。还要注意使用缓存。
五、IFrames Web worker
IFrames Web worker 和 IFrame 很像,而且印象中 IFrame 也可以实现多线程。但是 IFrame 存在一些不是线程安全
API,比如 DOM 相关,浏览器不能为他们生成新的线程,参考这里.
在 IFrame 跨域中,很多 API 它都没有权限,也只能通过 postMessage,就像 Web Worker
一样。理论上,浏览器可以在不同的线程中运行 IFrame,也就可以用 IFrame 实现多线程。
但是实际并非如此,它还是单线程的,浏览器不会给它们额外的线程。
async await都是通过promise 来实现,可以同时并行多个任务
直接同步方式的话
假设你要得到10本书的JSON文件采取同步方式,那么是等待书本一个一个的获取
但是如果是async await的话可以直接类似
async function book(u){
let data = await getJson(u)
$dom.parse(data)
}
lz可以跑下下列代码
var hold = function () {
return new Promise(function (resolve, reject) {
resolve();
})
};
async function count(i){
await hold()
console.log(i)
}
for(var i = 0 ;i 10 ; i++)
count(i);
console.log("run")
实际上是run 先跑
1、defer 和 async 在网络读取(脚本下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
2、两者的差别:在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的。defer是立即下载但延迟执行,加载后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。async是立即下载并执行,加载和渲染后续文档元素的过程将和js脚本的加载与执行并行进行(异步)。
3、关于 defer,我们还要记住的是它是按照加载顺序执行脚本的
4、标记为async的脚本并不保证按照指定它们的先后顺序执行。对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。
5、async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的。