JS 演变、单线程、异步任务
一、JS 介绍与演变
-
JS 组成:ECMAScript标准、DOM、BOM
- ECMAScript 是JS语法标准(核心)
- DOM:文档对象模型(提供访问、操作页面内容的API)
- BOM:浏览器对象模型(提供与浏览器交互的API)
-
JS是一门脚本语言、解析型语言、弱类型语言、动态类型语言。
- 脚本语言:不用编译,一边解析一边执行
- 解析型语言:遇到一行代码就解析一行代码
- 弱类型语言( 声明变量都用var ),不受数据类型的约束
- 动态类型的语言:对象没有属性 或 对象没有方法,只需要通过.的方式进行添加就有了
-
历史演变:
- 始于1995年,前身为LiveScript,由网景公司 布兰登.艾奇 开发,主要处理 以前由服务端负责的数据验证
- 后更名为JavaScript,功能演变为:前后端数据交互、页面特效、服务端开发(nodeJS)
- JS的很多语法,和Java、C#语法相似,和Java毫无关系
-
JS 作为一门脚本语言,其运行环境:web浏览器、Node、Adobe Flash
-
DOM详解
- DOM应该理解为一个规范,定义了HTML和XML文档的逻辑结构和文档操作的编程接口
- DOM实际上是以面向对象的方式描述对象模型,将文档建模为一个个对象,以树状的结构组织
- 总结:DOM这个规范,提供了一些API,来操作 HTML/XML的中的DOM对象(标签),影响DOM对象的在浏览器中展现形式
二、JS 单线程
1. js 运行在浏览器中,是单线程的
- 即 js 代码始终在一个线程上执行,这个线程就是 js 引擎线程。
- 每个浏览器只有一个JS引擎线程
- js 的一大特点: 单线程,只能在一个线程上运行,即:js只能同时执行一个任务,其他任务要执行,需要排队。
- js 单线程并不代表 js 引擎只有一个线程;js 引擎有多个线程:一个主线程、其他后台配合主线程。
- js引擎线程 和 UI线程互斥:因为js可以操作DOM;导致js执行时会影响页面的渲染
- HTML5提出Web Worker标准,允许js脚本创建多个线程,但子线程完全受主线程的控制、且不能操作DOM元素。所以这并没有改变 js 单线程的本质
2. 浏览器是多线程的:
- JS引擎线程: 执行js (和UI线程互斥)
- UI渲染线程: 渲染页面 (和js引擎线程互斥)
- 浏览器事件触发线程: 用于控制交互,响应用户
- http请求线程: 用于处理请求,ajax是委托给浏览器新开的一个 http 线程
- EventLoop轮询的处理线程: 用于处理轮训消息队列
3. 浏览器中的 js 任务
- 执行js代码
- 对用户的输入(如:鼠标点击、键盘输入)作出反应
- 处理异步的网络请求
三、JS引擎线程中 工作原理
1. js主线程 ---> 由js引擎提供
- js 的一个的线程:执行js任务
2. js同步任务 ---> js引擎中,主线程上一个个排队执行的任务
- 主线程上排队执行的任务(排队执行)
3. js异步任务 ---> js引擎中
- 异步任务起初不会进入主线程,而是被放到 任务队列/消息队列 中
- 异步任务延迟执行:只有当主线程的任务全部执行完,js引擎将异步任务放到主线程执行栈中执行,事件循环
- 异步任务执行顺序:定时器到时间的先执行,除此的按顺序执行。
4. js任务队列 / 消息队列 ---> js引擎中,存放 异步任务
- 解析异步操作后,相关的异步任务( 事件、回调函数),就会被放到 js任务队列中,等待被处理
- 只有主线程中的任务全部执行完,任务队列中的异步任务才会被放到js主线程的执行栈中执行
5. 事件循环 EventLoop
- 主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
- JS 会创建一个 类似 while(true) 的循环,每执行一次循环体的过程叫 tick
- tick 过程:查看任务队列中是都有待处理的事件,如果有:则取出,放到js主线程中执行
- 由于js是运行在单线程上的,所有浏览器单独开启一个线程来处理事件消息的轮询,避免阻塞js的执行。
四、异步任务 执行机制
1. 异步任务是如何被执行的?
- 1、js主线程上有一个执行栈(execution context stack),用于执行任务
- 2、js主线程之外,还存在一个"任务队列"(task queue),用于存放 异步任务
- 3、一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列":看看里面有哪些事件?对应哪些异步任务?于是异步任务结束等待状态,进入主线程执行栈,开始执行。
- 4、主线程不断重复上面的第三步。
- 总之:异步任务执行有时间延迟,总是等同步任务执行完才执行。
2.常见的异步任务 ---> onclick 等事件的毁掉函数
- 当事件触发时,回调函数会被立即添加到任务队列中
- 由浏览器内核的 DOM Binding 模块处理
3. 常见的异步任务 ---> setTimeout / setInterval 等计时器 ( 时间延迟 )
- 当定时器时间到,就把该事件放到 任务队列中 等待处理
- 由浏览器内核的 timer 模块进行延时处理
4. 常见的异步任务 ---> ajax 请求
- 在网络请求完成返回之后,将回调函数添加到任务队列中
- 由浏览器内核的 network 模块处理
五、异步编程
1. 异步编程是有需求的
- 除了 onclick等事件、setTimeout、ajax 这些天生有异步任务
- 有些函数本身很耗时,其他函数依赖这个函数的执行结果,就有必要 手动进行异步编程了
2. 异步编程解决方案 ---> 回调函数
- 优点:简单,容易理解 和部署
- 缺点:层层嵌套,不利于js代码阅读、维护
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
f1(f2)
3. 异步编程解决方案 ---> 事件监听(事件驱动模式)
- 关键:任务的执行 不取决于 代码的顺序;而是取决于某个事件是否被出发
- f1.trigger('done')表示,执行f1函数完成后,立即触发done事件(从而执行f2)
- 优点:比较容易理解,去耦合
- 缺点:整个程序都要变成事件驱动型,运行流程变得不清晰
// 这里采用的jQuery的写法
f1.on('done', f2);
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
4. 异步编程解决方案 ---> 发布 / 订阅 (观察者模式)
- f1运行结束之后,像“消息中心”发出done信号,因f2之前向“消息中心”订阅了done信号,所以,此时f2开始执行。
- 优点:这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
jQuery.subscribe("done", f2);
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
5. 异步编程解决方案 ---> ES6中 Promises对象
- Promises对象是CommonJS提出的一种规范,目的是为异步编程提供统一接口。
- 每一个异步任务:都返回一个Promise对象
- 优点:回调函数变成了链式写法,程序的流程可以看得很清楚;如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。
function f1(){
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
dfd.resolve();
}, 500);
return dfd.promise;
}
f1().then(f2).then(f3);
//指定失败的回调函数
f1().then(f2).fail(f3);
6. 异步编程解决方案 ---> ES7 引入了像C#语言中的 await,async关键字
- async 定义函数;await 调用函数
// 这里认为 f2 是异步函数
async function f1 () {
console.log(1);
await f2();
console.log(2);
}
// 以上执行顺序是: console.log(1); f2(); console.log(2);