并发模型与事件循环

2019-09-09 15:59:08 浏览数 (1)

JavaScript进阶

#包管理器

#NPM

默认安装到项目目录下,-g安装到全局,-save在package.json写入dependencies字段,-save-dev相应写入devDependencies字段。

#YARN

yarn add添加包,yarn global add添加全局包,yarn add --dev添加dev依赖。yarn添加的依赖会默认保存到package.json里。

#import与require

import与require都提供引入一个模块的功能,但require是AMD规范下的引入,在运行时调用,而import是ES6规定的引入,编译时调用(因此实际上最早执行,)。require对应exports,import对应export。

代码语言:javascript复制
//CommonJS/AMD
const app = require("app")
module.exports = app
exports.app = app

//ES6规范,解构要求标识符对应(但可以用as重命名),引入export default这种的可以自定义变量名,一个文件可以同时export default和export xxx
import app from 'app'// export default xxx
import {login,logout} from 'app'//export const xxx or export function xxx or export {login,logout,...} or export * from 'xxx'
import * from 'xxx'
import {login as logIn} from 'app'
import app ,{login} from 'app'
import * as app from 'app'

#闭包

代码语言:javascript复制
function outer() {
        let a = 1
        let inner = () => { a  ; console.log(a) }
        return inner
      }

闭包利用了函数的执行环境,每次返回的inner都有不同的执行环境,意味着不同的inner分别拥有自己的a值。

PS:上次面头条,竟然没有写出来emm千万不要像我一样。

#constructor 构造函数

#原型链&继承

#Promise

#函数生成器

#async...await

#并发模型与事件循环

JavaScript的并发模型基于事件循环。

先同步,后异步。先执行微任务,后执行宏任务。

#Stack 栈

这里的栈指函数调用形成的执行栈。函数具有参数和局部变量,如果函数A调用了函数B,并且执行函数A,那么函数A会被先压入栈,调用B时,函数B被压入栈(位于A之上),到函数B返回,其被弹出。

函数被压入栈的实际过程是压入调用帧。

#Heap 堆

非结构化的存储区域,其中存储对象。

#Queue 队列

JavaScript维护一个待处理的消息队列,而每一个消息与处理它的函数关联。在事件循环中的某个环节,JavaScript按顺序处理Queue的消息。

每当调用处理消息的函数,其形成的调用帧被压入栈。该函数可能会调用其他函数,因此只有当执行栈为空,JavaScript才能继续处理下一个消息。最终,消息队列为空。

#事件循环

代码语言:javascript复制
while (queue.waitForMessage()){
  queue.processNextMessage();
}

瞧,这就是事件循环,因为它是一个处理消息的循环。其中waitForMessage是同步的,如果没有消息,它就会等。

#不打断地执行

如果你理解了队列的执行方式,那么你会明白这种处理方式意味着函数执行决不会被抢占。(相对于C/C 多线程,你不得不考虑函数被中断的情况)这为编程和分析带来了便利,但代价是消息处理函数可能会长时间阻塞其他事件,如用户的点击、滑动,在这种情况下,浏览器会提示无响应,用户可以选择等待或结束进程。

#不阻塞

MDN声称JavaScript“永不阻塞”,这当然是不对的,例如alert()与同步XHR的场景,但如此声称有它的理由。JavaScript中I/O通常采用事件回调的形式完成,这意味着I/O不会影响其余代码执行。

#添加消息

事件需要绑定监听器以被监听,否则事件将丢失。例如用户点击按钮并被监听到时,消息队列就多了一个消息。

setTimeout(handler, timeOut)允许向队列添加消息,并且设置最小触发延时。延时可能大于设定的时间,因为预定的时间内JavaScript可能正在处理其他消息(即使延时设置为0也一样,并且H5标准规定最小间隔为4ms)。一个简单的例子是,先设定一个定时执行的函数,再令JavaScript进入无限循环,无论何时被设定的函数都不会执行。

#同步代码

JavaScript的同步执行代码可以理解成第一条消息的处理函数,在它执行完前,不会有其他消息被处理。

#Runtime间通信

JavaScript虽然是单线程,但跨域iframe和web worker都是独立的runtime。他们能且只能用postMessage()发送消息,并监听message事件。

#宏任务与微任务

微任务和宏任务指的是setTimeout一样需要被加入队列执行的异步代码,而微任务一定位于宏任务之前。

先祭上这段常见代码:

代码语言:javascript复制
setTimeout(_ => console.log(4))

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

console.log(2)

Promise是同步代码,Promise.then才是异步代码,所以1,2的顺序是毫无疑问的。3,4都是异步任务,为什么3在4前面呢?如果以事件队列理解,4应该在3前面,但由于3是微任务,4是宏任务,3应该在4之前被处理。

宏任务和微任务都存在于事件循环,但微任务尽管添加时间可能比宏任务晚,仍然要在下一个宏任务执行前执行。事件循环处理消息相当于有两个步骤,第一步检查当前是否有微任务(微任务虽然也是异步代码,但可以看作不在消息队列中,因为它会“插队”),如果有先完成,第二步执行宏任务并在队列中寻找下一个消息。

如果在宏任务执行过程中添加微任务,那么它会在下一个宏任务执行前执行。

代码语言:javascript复制
setTimeout(_ => {
    Promise.resolve().then(_ => { console.log("Micro") });
    console.log("Macro")
})
//Macro
//Micro

let two = (date) => { while (Date.now() - date < 2000) { } }
let twoWithPromise = (date) => {
    Promise.resolve().then(_ => console.log('Promise'));
    while (Date.now() - date < 2000) { }
}
let fdd = () => {
    let d = Date.now();
    setTimeout(twoWithPromise, 0, d);
    setTimeout(two, 0, d);
    setTimeout(two, 0, d)
}
//2秒后输出Promise,说明twoWithPromise的确花了2s,之后Promise.then执行,再之后才是下一个setTimeout

我在掘金上看到有人说requestAnimationFrame()的触发要先于setTimeout(),他说这是因为修改DOM属性是同步操作,这显然是不对的,同步只是注册监听器。参考评论,理想情况下requestAnimationFrame对于60Hz的显示器来说每16.6ms执行一次,而setTimeout(handler,0)既可能是4ms执行一次,也可能由于页面重新渲染,最小间隔变为16ms。当屏幕刷新率变高,requestAnimationFrame将在setTimeout()之前。

#宏任务与微任务表格

函数/过程

宏任务

微任务

I/O

setTimeout

setInterval

Promise.then/catch/finally

setImmediate(NodeJS)

requestAnimationFrame(Browser)

process.nextTick(NodeJS)

MutationObserver(Browser)

0 人点赞