JavaScript概念辨析

如何理解宏任务、微任务?

异步处理

JS 引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

如何理解宏任务、微任务?

JavaScript代码执行顺序:

  1. 先执行同步代码
  2. 遇到异步宏任务则将放入宏任务队列中,遇到异步微任务则将放入微任务队列中
  3. 当所有同步代码执行完毕后
  4. 将异步微任务从队列中调入主线程执行
  5. 再将异步宏任务从队列中调入主线程执行(宏任务执行中会产生微任务,这些微任务会在下一个宏任务执行前就完成掉)
  6. 循环往复…

结论:在异步任务中:微任务先于宏任务

运行机制

在事件循环中,每进行一次循环操作称为 tick,关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

宏任务

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->…

宏任务具体包括:

1
2
3
4
5
6
7
8
script(整体代码)
计时器结束的回调,setTimeout、setInterval
事件回调、http 回调等绝大部分异步函数进入宏队列
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的

微任务

在当前 task 执行结束后立即执行的任务。在当前task任务后,下一个task之前,在渲染之前。

在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕

具体包括:

1
2
3
4
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

案例

1
2
3
4
5
6
7
8
9
10
setTimeout(_ => console.log(4))

new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})

console.log(2)
  1. 整体script作为第一个宏任务进入主线程,遇到setTimeout入栈处理,发现是异步函数(宏任务),出栈,移交给Web API处理,0秒等待后,将回调函数加到宏任务队列尾部;
  2. 遇到new Promise,入栈处理,发现是同步任务,直接执行,console输出1;
  3. 遇到then,入栈处理,发现是异步函数(微任务),出栈,移交给Web API处理,将回调函数加入微任务队列尾部;
  4. 遇到console.log(2),入栈处理,同步任务,直接console输出2, 出栈;
  5. 栈已清空,检查微任务队列;
  6. 取出第一个回调函数,入栈处理,发现是同步任务,直接console输出3, 出栈;
  7. 继续从取微任务队列中取下一个,发现微任务队列已清空,结束第一轮事件循环;
  8. 从宏任务队列中取出第一个宏任务,入栈处理,发现是同步任务,直接console输出4;

所以,最终输出结果为:1 > 2 > 3 > 4

JavaScript异步解决方案

  1. 回调函数,解决同步问题,但是带来回调地狱
  2. promise,解决回调地狱,但是无法取消promise,错误需要用回调函数来捕获
  3. generator,控制函数的执行
  4. async/await,代码清晰,不用写一堆then链

代理和反射

最后一遍说 bind、call、apply

区别一:call、apply是立即执行、bind是返回一个新的函数

bind 返回的是一个新的函数,你必须调用它才会被执行

1
2
3
obj.myFun.call(db);    // 德玛年龄 99
obj.myFun.apply(db);    // 德玛年龄 99
obj.myFun.bind(db)();   // 德玛年龄 99

区别二:

call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别

call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔

apply 的所有参数都必须放在一个数组里面传进去

bind 除了返回是函数以外,它 的参数和 call 一样

1
2
3
4
obj.myFun.call(db,'成都','上海');     // 德玛 年龄 99  来自 成都去往上海
obj.myFun.apply(db,['成都','上海']); // 德玛 年龄 99 来自 成都去往上海
obj.myFun.bind(db,'成都','上海')(); // 德玛 年龄 99 来自 成都去往上海
obj.myFun.bind(db,['成都','上海'])();   // 德玛 年龄 99 来自 成都, 上海去往 undefined

参考资料