image.png
1、javascript引擎,主要包含堆栈调用栈
2、Web API由浏览器提供包括,DOM操作,事件监听,timeout的函数。
3、event loop(事件循环机制),提供javascript的执行机制。

一.变量的赋值

对于变量的赋值,有值赋值和引用赋值,值赋值var a =1,引用赋值var obj = {a:1},由于对象是保存在堆中,只能通过引用去访问。

  1. var obj_1={a:1};
  2. var obj_2= obj_1//这里发生了对象的浅拷贝

变量赋值需要注意的一个关键点是:变量提升

  1. console.log(a)//输出undefined;
  2. var a = 3

为什么,因为当var a = 3被引擎执行时候,是分三个步骤:

  • 首先创建上下文,在堆栈中申请空间a,此时a表示的内存为存放任何值,顾表示为undefined。
  • 上下文创建完成后,引擎开始执行,遇到a=3语句,就将a表示的内存值修改为3。

这种情况我们称为变量提升在变量对象的创建过程中,函数声明的执行优先级会比变量声明的优先级更高一点,而且同名的函数会覆盖函数与变量,但是同名的变量并不会覆盖函数。

二.执行上下文与作用域(动态作用域 词法作用域)

1.执行上下文生命周期

当一个浏览器启动时候,会先构建一个全局执行上下文,当一个函数被调用时候,会构建一个函数执行上下文。执行上下文的生命周期分为三个步骤:

  • 创建阶段:构建执行环境,比如生成VO(variable Object变量对象)、作用域链、this指向(this的值是动态绑定的)。上下文环境被压入调用栈
  • 执行阶段: 执行指令,对变量赋值,函数引用等。
  • 执行结束: 执行上下文出栈,释放内存空间。
  1. var global = 5;
  2. funciton fn(g){
  3. var k = 3;
  4. return g+k;
  5. }
  6. var a = fn(global);

引擎分析这段代码并构建全局执行上下文

  1. globalEC={//EC表示golbal execution
  2. VO:{
  3. global:undefined,
  4. fn:函数的地址,
  5. a:undefined
  6. },
  7. scopChain:[globalEC.Vo]//作用域链
  8. this:undefined
  9. }

构建完成后globalEC入栈,引擎根据上下文环境,开始执行代码:

  1. 执行->global=5;
  2. 修改 globalEC.VO.global:5
  3. 执行->function fn(){....}这是一个函数声明
  4. 执行-> a=fn(global);

这个时候构建fn的执行上下文环境:

  1. fnEC={
  2. VO:{
  3. arguments: [5],
  4. g: 5
  5. k: undefined
  6. },
  7. scopChain: [fnEC.VO ,globalEC.VO]//作用域链
  8. this: undefined
  9. }

同上执行fn()中的代码,并修改,由于fn()是在全局上下文中被调用的,this指向全局对象(如果是在浏览器中fnEC.this= windows)。

2. 要点

执行上下文的要点是,构建VO对象,和作用域链。作用域链保存了中执行上下文的VO,通过VO查询变量。这个机制使javascript具备了以下特性:

  • 变量覆盖:通过作用域链查询变量时候,当遇到匹配的变量就不再往下查询。
  • 闭包:当嵌套函数,被外部调用时候,他的作用域链,保持了对上层函数VO的引用,因此当上层函数执行完成并退栈的时候,并没有销毁VO的内存空间。

三. event loop机制

javascript是一个单线程语言,他的异步是通过事件循环机制模拟出来的。当浏览器启动的时候启动了如下进程:

  • 一个 Browser 进程:浏览器界面显示交互;其他进程管理;网络管理
  • 多个 Renderer进程:一个tab页一个进程;iframe由一个renderer进程管理;renderer是一个沙箱
  • 一个GPU进程

Renderer进程启动了如下线程:

  • GUI渲染线程:负责渲染浏览器界面,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • JS引擎线程:负责处理Javascript脚本程序。(例如V8引擎)JS引擎线程负责解析Javascript脚本,运行代码。一个Renderer进程中只有一个JS引擎线程,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程:归属于浏览器而不是JS引擎,用来控制事件循环(点击,鼠标移动这些都是浏览器事件)事件触发之后会加入到事件队列等待执行
  • 定时触发器线程:setInterval与setTimeout所在的线程。浏览器定时计数器并不是由JavaScript引擎计数的,是交给浏览器计时setTimeout(fn,ms) 指定某个任务在主线程最早可得的空闲时间执行,ms秒之后将fn函数加入到队列中W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。所以setTimeout的延时设置为0也不可能瞬发
  • 异步http请求线程:在XMLHttpRequest连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

1. java事件循环

image.png
javascript是单线程的,单线程就是一个任务完成,才能执行下一个任务。如果一个任务调用了WebAPI例如setTimeout,ajax等,因为耗时,因此将事件回调放入task queue中,主线程则继续执行。于是任务分为两种:同步任务,异步任务。

  • 同步任务由引擎的主线程执行,当前任务完成,才能执行下一个。
  • 异步任务,不由引擎的主线程处,而是调用其他线程处理,并将线程事件的回调函数放入,事件队列中,等待引擎主线程调用。比如调用ajax,将由异步http请求线程处理网络数据的获取,如果获取数据成功,将成功事件的回调函数放入事件队列中,等待javascript引擎执行。

image.png

在事件队列可以分为task(macro-task)和micro-task,根据HTML规范,可以看到渲染一次页面有如下步骤:

  • 执行事件队列中最后一个函数
  • 执行微任务队列中的所有回调函数
  • 渲染页面
    为了更好理解任务执行的过程,我们先看一个简单的例子
  1. console.log(1);
  2. setTimeout(()=>{console.log(2)},3000);
  3. console.log(3);
  4. 执行顺序:132

分析执行过程:
主线程执行,console.log(1)进栈,打印 1
主线程执行,setTimeout进栈,异步调用,seTimeout由定时触发器线程执行。3秒后将回调函数放入事件队列。
主线程执行,console.log(3),打印3
第一次主线程执行完成,开始事件循环。
事件队列有 回调()=>{console.log(2)}, 主线程执行,打印2j

继续下一个例子:

  1. console.log('main1');
  2. setTimeout(function() {
  3. console.log('setTimeout');
  4. Promise.resolve().then(function() {
  5. console.log('promise timeout');
  6. });
  7. }, 0);
  8. new Promise(function(resolve, reject) {
  9. console.log('promise');
  10. resolve();
  11. }).then(function() {
  12. console.log('promise then');
  13. });
  14. console.log('main2');
  15. 执行顺序:main1,promise,main2,promise then,setTimeout

引擎主线程执行整个代码可以看做是一个宏任务因此输出:main1,promise,main2。这里需要注意,主线程执行的new Promise(function(r,r)){..})这个构造函数的参数是一个函数表达式,会马上执行,因此会打印promise。

主线程执行完成后,事件循环开始,检查微任务队列,有new Promise.then注册的函数,取出放入主线程中执行。此时微任务队列清空,取出setTimeout注册到宏任务队列中的回调,交给主线程执行,输出setTimeout,然后发现Promise,将then中的回调放入微任务队列。

主线程执行完一个宏任务后,发现微任务队列中有函数,取出调用,打印promise timeout。

2. 要点