JS 演变、单线程、异步任务

时间:2022-05-29 11:38:50

一、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);