整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HTML5技术 引用类型与基本类型

HTML5技术 引用类型与基本类型

文将从以下六个方面讲解引用类型和基本类型

1. 概念

2. 内存图

3. 引用类型和基本类型作为函数的参数体现的区别

4. 引用类型的优点:

5. 引用类型的赋值(对比基本类型)

6. 浅拷贝和深拷贝

以下为详细内容:

1. 概念:

基本类型也叫简单类型,存储的数据是单一的,如:学生的个数就是一个数字而已;引用类型也叫复杂类型,存储的数据是复杂的,如:学生,包括学号,姓名,性别,年龄等等很多信息。从内存(大家如果不懂内存,请查阅相关资料)的角度来说:基本类型只占用一块内存区域;引用类型占用两块内存区域。即定义基本类型的变量时,在内存中只申请一块空间,变量的值直接存放在该空间;定义引用类型的变量时(容易理解的是,我门看到new运算符,一般就是定义引用类型的变量),在内存中申请两块空间,第一块空间存储的是第二块空间的地址,第二块空间存储的是真正的数据;第一块空间叫作第二块空间的引用(地址),所以叫作引用类型。

javaScript中的基本类型包括:数字(Number),字符串(String),布尔(Boolean),Null,Undefined五种;

javascript的引用类型是:Object。而Array,Date是属于Obejct类型。

2. 内存图:

如下代码(都是定义了两个局部变量):

以上两行代码的内存图:

可以看到,num变量只占用了一块内存区域;arr变量占用了两块内存区域,arr变量在栈区(不懂栈区的人,先不要想太多)申请了一块内存区域,存储着地址,存储的地址是堆区的地址。而堆区中真正才存储着数据,所以说,arr变量占用了两块内存区域。这样看来,引用类型的变量好像还占用内存多了。哈哈,不要着急,后面了解了引用类型的优点后,你就会觉得这是问题了。

当我们读取num变量的值时,直接就能读到,但是当我们要读取arr里的值时,先找到arr中的地址,然后根据地址再找到对应的数据。

引用类型,类似于windows操作系统中的快捷方式。快捷方式就是一个地址,真正的内容是快捷方式所指向的路径的内容。如:我们把d:\t.txt文件创建一个快捷方式放在桌面上,那么,桌面上的快捷方式会占用桌面的空间,而d:\t.txt会占用d盘的空间,所以,占用了两块空间。

基本类型就相当于文件。

引用类型,类似于我们在入学报名填写报名表时,填写家庭地址,这个家庭地址就相当于第一块空间,真正你家(第二块内存空间)不在报名表上。学校要找你家,先在报名表上找到你家的地址,然后根据地址,才能找到你家去。

3. 引用类型的优点:

引用类型作为函数的参数时,优点特别明显,第一,形参传递给实参时,只需要传递地址,而不需要搬动大量的数据(节约了内存开销);第二,形参对应的数据改变时,实参对应的数据也在改变(很多时候,我们希望这样)。

如以下代码:

先定义函数(冒泡排序)



当调用冒泡排序时,

看看内存以上代码执行时的,内存变化:

图中,当执行,①对应的代码(var arr1=[250,2,290,35,12,99];)时,内存中会产生①对应的变化,即在栈中申请一块内存区域,起名为arr1,在堆区中申请内存空间放置250,2,290,35,12,99,并把堆区中的内存的地址赋给arr1的内存中;当执行②对应的代码bubble(arr1)时,调用函数。这时候会定义形参arr(内存中③对应的变化),即在栈中申请一块内存区域,起名为arr,并把arr1保存的地址赋给了arr(内存中②表示的赋值),这样,形参arr和实参arr1就指向了同一块内存区域。数组中的值250,2,290,35,12,99在内存中只有一份。即,不用把数组中每个元素的值再复制一份,节约了内存。如果对内存图看懂了,那么,当形参arr对应的数据顺序改变了,实参arr1对应的数据顺序也就改变了。即,实现了形参数据改变时,实参数据也改变了。所以,bubble函数不需要返回值,依然可以达到排序的目的。可以运行我示例中的代码,看看是不是达到了排序的效果。

补充,基本类型作为函数参数的内存变化:

内存图:

4. 引用类型变量的赋值:

引用类型变量赋值时,赋的是地址。即两个引用类型变量里存储的是同一块地址,也就是说,两个引用类型变量都指向同一块内存区域。所以,两个引用类型变量对应的数据时一样的。

基本类型变量赋值时的内存变化。

5. 浅拷贝和深拷贝

先说对象的复制,上面说了,引用类型(对象)的赋值,只是赋的地址,那么要真正复制一份新的对象(即克隆)时,又该怎么办。



但是,当一个对象的属性又是一个引用类型时,会出现浅拷贝和深拷贝的问题。用一个自定义的object类型来说明问题。

如:

person2.name="张四"; //不会改变掉person1的name属性。

person2.address.country="北京";//会改变掉person1的address.country

大家注意看,person1和person2的name属性各有各的空间,但是person1.address.country和person2.address.country是同一块空间。所以,改变person2.address.country的值时,person1.address.country的值也会改变。这就说明拷贝(克隆)的不到位,这种拷贝叫作浅拷贝,而进一步把person1.address.country和person1.address.name也拷贝(克隆)了,就是深拷贝。要做到深拷贝,就需要对每个属性的类型进行判断,如果是引用类型,就再循环进行拷贝(需要用到递归)。

(未完待续)


千锋HTML5教学部 田江

avaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、浏览器js运行机制

简介

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。
复制代码

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

三. 浏览器的 Event Loop 事件循环

简介

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。为了更好地理解Event Loop,请看下图

事件循环可以简单描述为:

函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空; 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待; 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。

  • Event Loop是由javascript宿主环境(像浏览器)来实现的;
  • WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
  • JavaScript 的并发模型基于"事件循环";
  • Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数

接下来看一个异步函数执行的例子:

var start=new Date();
setTimeout(function cb(){
    console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
复制代码
  1. main(Script) 函数入栈,start变量开始初始化
  2. setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
  3. while循环入栈,开始阻塞1000ms;
  4. 500ms过后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
  5. 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',出栈,cb()出栈

四. 宏任务(Macrotasks)和微任务(Microtasks)

简介

JS的异步有一个机制的,就是会分为宏任务和微任务。宏任务和微任务会放到不同的event queue中,先将所有的宏任务放到一个event queue(macro-task),再将微任务放到一个event queue(micro-task)中。执行完宏任务之后,就会先从微任务中取这个回调函数执行。

讲的详细一点的话

最开始, 执行栈为空, 微任务队列为空, 宏任务队列有一个 script 标签(内含整体代码)

将第一个宏任务出队, 这里即为上述的 script 标签

整体代码执行过程中, 如果是同步代码, 直接执行(函数执行的话会有入栈出栈操作), 如果是异步代码, 会根据任务类型推入不同的任务队列中(宏任务或微任务)

当执行栈执行完为空时, 会去处理微任务队列的任务, 将微任务队列的任务一个个推入调用栈执行完

微任务执行完后,检查是否需要重新渲染 UI。

...往返循环直到宏任务和微任务队列为空

总结一下上述循环机制的特点:

出队一个宏任务 -> 调用栈为空后, 执行一队微任务 -> 更新界面渲染 -> 回到第一步

宏任务 macro-task(Task)

一个event loop有一个或者多个task队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来说task任务源:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任务 micro-task(Job)

microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差异

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

宏任务和微任务的区别

  • 宏队列可以有多个,微任务队列只有一个,所以每创建一个新的settimeout都是一个新的宏任务队列,执行完一个宏任务队列后,都会去checkpoint 微任务。
  • 一个事件循环后,微任务队列执行完了,再执行宏任务队列
  • 一个事件循环中,在执行完一个宏队列之后,就会去check 微任务队列

宏任务和微任务的运行

下图是一个事件循环的流程

举个简单的例子,假设一个script标签的代码如下:

Promise.resolve().then(function promise1 () {
       console.log('promise1');
    })
setTimeout(function setTimeout1 (){
    console.log('setTimeout1')
    Promise.resolve().then(function  promise2 () {
       console.log('promise2');
    })
}, 0)

setTimeout(function setTimeout2 (){
   console.log('setTimeout2')
}, 0)

运行过程:

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】从task队列中取出script任务,推入栈中执行。promise1列为microtask,setTimeout1列为task,setTimeout2列为task。
  • 【task队列:setTimeout1 setTimeout2;microtask队列:promise1】script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise1执行。

循环2:

  • 【task队列:setTimeout1 setTimeout2;microtask队列:】从task队列中取出setTimeout1,推入栈中执行,将promise2列为microtask。
  • 【task队列:setTimeout2;microtask队列:promise2】执行microtask checkpoint,取出microtask队列的promise2执行。(循环2中的 setTimeout2为什么不是跟在setTimeout1的后面输出? 这里我觉得应该是setTimeout1和setTimeout2不是在同一个task队列中, 是两个task队列。在执行完setTimeout1的task队列后, event loop去检查microtask队列是否有事件,并且把事推入到主栈。) 复制代码

循环3:

  • 【task队列:setTimeout2;microtask队列:】从task队列中取出setTimeout2,推入栈中执行。setTimeout2任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

注:有些文章说的一个事件循环的开始是先执行微任务再执行宏任务,有有些说的是先执行宏任务再执行微任务,我个人觉得这两种只是看法的角度不一致

  • 如果把script载入到主堆栈这一过程看成是执行了宏任务,那么就是宏任务先开始。
  • 如果不把这个script的运行当做是宏任务,只看异步函数中的宏任务(setTimeout)那么就是微任务先开始。

宏任务与微任务示例

EXP1 在主线程上添加宏任务与微任务

console.log('-------start--------');

setTimeout(()=> {
  console.log('setTimeout');  // 将回调代码放入另一个宏任务队列
}, 0);

new Promise((resolve, reject)=> {
  for (let i=0; i < 5; i++) {
    console.log(i);
  }
  resolve()
}).then(()=>{
  console.log('Promise'); // 将回调代码放入微任务队列
})

console.log('-------end--------');

运行结果:

-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout

由EXP1,我们可以看出,当JS执行完主线程上的代码,会去检查在主线程上创建的微任务队列,执行完微任务队列之后才会执行宏任务队列上的代码

运行顺序:

主线程=> 主线程上创建的微任务=> 主线程上创建的宏任务

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】从task队列中取出script任务,推入栈中执行。promise列为microtask,setTimeout列为task。
  • 【task队列:setTimeout ;microtask队列:promise】script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise执行。

循环2:

  • 【task队列:setTimeout ;microtask队列:】从task队列中取出setTimeout,推入栈中执行setTimeout任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

EXP2 在微任务中创建微任务

setTimeout(_=> console.log('setTimeout4'))

new Promise(resolve=> {
  resolve()
  console.log('Promise1')
}).then(_=> {
  console.log('Promise3')
  Promise.resolve().then(_=> {
    console.log('before timeout')
  }).then(_=> {
    Promise.resolve().then(_=> {
      console.log('also before timeout')
    })
  })
})

console.log(2)

运行结果:

Promise1
2
Promise3
before timeout
also before timeout
setTimeout4

由EXP2,我们可以看出,在微任务队列执行时创建的微任务,还是会排在主线程上创建出的宏任务之前执行(因为微任务只有一条,自增链不断的话 会一直往下执行微任务,不会被中断)

运行顺序:

主线程=> 主线程上创建的微任务1=> 微任务1上创建的微任务2=> 主线程上创建的宏任务

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】从task队列中取出script任务,推入栈中执行。promise3 列为microtask,setTimeout4 列为task。
  • 【task队列:setTimeout4;microtask队列:promise3】script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise3执行。将before timeout 列为 microtask。
  • 【task队列:setTimeout4;microtask队列:before timeout】before timeout 执行将also before timeout 列为microtask
  • 【task队列:setTimeout4;microtask队列:also before timeout】also before timeout 执行

循环2:

  • 【task队列:setTimeout4 ;microtask队列:before timeout】从task队列中取出setTimeout4,推入栈中执行setTimeout4任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

EXP3: 宏任务中创建微任务

// 宏任务队列 1
setTimeout(()=> {
  // 宏任务队列 1.1
  console.log('timer_1');
  setTimeout(()=> {
    // 宏任务队列 3
    console.log('timer_3')
  }, 0)
  new Promise(resolve=> {
    resolve()
    console.log('new promise')
  }).then(()=> {
    // 微任务队列 1
    console.log('promise then')
  })
}, 0)

setTimeout(()=> {
  // 宏任务队列 2.2
  console.log('timer_2')
}, 0)

console.log('==========Sync queue==========')

运行结果:

==========Sync queue==========timer_1
new promise
promise then
timer_2
timer_3

运行顺序:

主线程(宏任务队列 1)=> 宏任务队列 1.1=> 微任务队列 1=> 宏任务队列 3=>宏任务队列2.2

循环1:

  • 【task队列:script ;microtask队列:】从task队列中取出script任务,推入栈中执行。timer_1 列为task,timer_2 列为task。
  • 【task队列:timer_1,timer_2;microtask队列:】script任务执行完毕,执行microtask checkpoint,无microtask队列可执行。

循环2

  • 【task队列::timer_1,timer_2;microtask队列:】从task队列中取出 timer_1 推入栈中执行。将 timer_3 列为task,promise then 列为microtask
  • 【task队列:timer_2,timer_3;microtask队列:promise then】执行microtask checkpoint,取出microtask队列的promise then执行

循环3

  • 【task队列:timer_2,timer_3;microtask队列:】从task队列中取出timer_2,推入栈中执行
  • 【task队列:timer_3;microtask队列:】执行microtask checkpoint,无microtask队列可执行

循环4

  • 【task队列:timer_3;microtask队列:】从task队列中取出timer_3,推入栈中执行
  • 【task队列:;microtask队列:】执行microtask checkpoint,无microtask队列可执行

EXP4:微任务队列中创建的宏任务

// 宏任务1
new Promise((resolve)=> {
  console.log('new Promise(macro task 1)');
  resolve();
}).then(()=> {
  // 微任务1
  console.log('micro task 1');
  setTimeout(()=> {
    // 宏任务3
    console.log('macro task 3');
  }, 0)
})

setTimeout(()=> {
  // 宏任务2
  console.log('macro task 2');
}, 0)

console.log('==========Sync queue(macro task 1)==========');

运行结果:

==========Sync queue(macro task 1)==========new Promise(macro task 1)
micro task 1
macro task 2
 task 3
复制代码

异步宏任务队列只有一个,当在微任务中创建一个宏任务之后,他会被追加到异步宏任务队列上(跟主线程创建的异步宏任务队列是同一个队列)

运行顺序:

主线程=> 主线程上创建的微任务=> 主线程上创建的宏任务=> 微任务中创建的宏任务

循环1:

  • 【task队列:script ;microtask队列:】从task队列中取出script任务,推入栈中执行。macro task 2 列为task,micro task 1 列为microtask。
  • 【task队列:macro task 2;microtask队列:micro task 1】script任务执行完毕,执行microtask checkpoint,microtask队列中取出micro task 1 执行。执行micro task 1 的时候,把macro task 3列为task

循环2

  • 【task队列:macro task 2,macro task 3;microtask队列:】
  • 从task队列中取出 micro task2,推入栈中执行。执行microtask checkpoint,无microtask队列可执行

循环2

  • 【task队列:macro task 3;microtask队列:】从task队列中取出 micro task3,推入栈中执行。执行microtask checkpoint,无microtask队列可执行
  • 【task队列:;microtask队列:】

小总结

  • 微任务队列优先于宏任务队列执行,
  • 微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端,微任务队列中创建的微任务会被添加到微任务队列的尾端。
  • 只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行。

五. 当 Event Loop 遇上事件冒泡

手动触发

代码

<div class="outer">
  <div class="inner"></div>
</div>
var outer=document.querySelector('.outer');
var inner=document.querySelector('.inner');

function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

点击 inner,最终打印结果为:

click
promise
click
promise
timeout
timeout

分析

为什么打印结果是这样的呢?我们来分析一下: (0)将 script 标签内的代码(宏任务)放入执行栈执行,执行完后,宏任务微任务队列皆空。

(1)点击 inner,onClick 函数入执行栈执行,打印 "click"。执行完后执行栈为空,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入宏任务队列。

(2)遇到 setTimeout,在最小延迟时间后,将回调放入宏任务队列。遇到 promise,将 then 的任务放进微任务队列

(3)此时,执行栈再次为空。开始清空微任务,打印 "promise"

(4)此时,执行栈再次为空。从宏任务队列拿出一个任务执行,即前面提到的派发事件的任务,也就是冒泡。

(5)事件冒泡到 outer,执行回调,重复上述 "click"、"promise" 的打印过程。

(6)从宏任务队列取任务执行,这时我们的宏任务队列已经累计了两个 setTimeout 的回调了,所以他们会在两个 Event Loop 周期里先后得到执行。

可以看成是:

function onClick() {
  //模拟outer click事件
  setTimeout(function(){onClick1()},0)
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });
}
function onClick1() {
  console.log('click1');

  setTimeout(function() {
    console.log('timeout1');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise1');
  });
}
//模拟inner click事件
onClick()

代码触发

代码

inner.click()

打印结果为:

click
click
promise
promise
timeout
timeout

分析

依旧分析一下:

(0)将 script(宏任务)放入执行栈执行,执行到 inner.click() 的时候,执行 onClick 函数,打印 "click"

(1)当执行完 onClick 后,此时的 script(宏任务)还没返回,执行栈不为空,不会去清空微任务,而是会将事件往上冒泡派发

...(关键步骤分析完后,续步骤就不分析了)

可以看成是:

function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });
}
onClick();
onClick();

总结

在一般情况下,微任务的优先级是更高的,是会优先于事件冒泡的,但如果手动 .click() 会使得在 script代码块 还没弹出执行栈的时候,触发事件派发。

Event Loop总结

浏览器进行事件循环工作方式

  1. 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。
  2. 将事件循环中的任务设置为已选择任务。
  3. 执行任务。
  4. 将事件循环中当前运行任务设置为null。
  5. 将已经运行完成的任务从任务队列中删除。
  6. microtasks步骤:进入microtask检查点。
  7. 更新界面渲染。
  8. 返回第一步

【执行进入microtask检查点时,浏览器会执行以下步骤:】

  • 设置microtask检查点标志为true。
  • 当事件循环microtask执行不为空时:选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。
  • 清理IndexDB事务
  • 设置进入microtask检查点的标志为false。

重点

总结以上规则为一条通俗好理解的:

  1. 顺序执行先执行同步方法,碰到MacroTask直接执行,并且把回调函数放入MacroTask执行队列中(下次事件循环执行);碰到microtask直接执行。把回调函数放入microtask执行队列中(本次事件循环执行)
  2. 当同步任务执行完毕后,去执行微任务microtask。(microtask队列清空)
  3. 由此进入下一轮事件循环:执行宏任务 MacroTask (setTimeout,setInterval,callback)

[总结]所有的异步都是为了按照一定的规则转换为同步方式执行。

备的一篇干货文章,对你有用的话就收藏或者转发吧!

HTML5 DOM 选择器

javascript 代码

阻止默认行为

javascript 代码

阻止冒泡

javascript 代码

鼠标滚轮事件

javascript 代码

检测浏览器是否支持svg

javascript 代码

检测浏览器是否支持canvas

javascript 代码

检测是否是微信浏览器

javascript 代码

常用的一些正则表达式

javascript 代码

js时间戳、毫秒格式化

javascript 代码

getBoundingClientRect() 获取元素位置

javascript 代码

HTML5全屏

javascript 代码

关注我

定期分享前端干货、新技术解读

如果你越学越迷茫,没思路

加我微信 webwula

备注“想提升自己

我教你学前端的方法