关于一文彻底吃透JavaScript的执行机制和javascript的执行原理的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于JavaScript一文彻底理解并掌握ES5继承、Javasc
关于一文彻底吃透 JavaScript 的执行机制和javascript的执行原理的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于JavaScript 一文彻底理解并掌握ES5继承、Javascript中的执行机制——Event Loop、JavaScript定时器与执行机制、JavaScript定时器与执行机制解析等相关知识的信息别忘了在本站进行查找喔。
本文目录一览:- 一文彻底吃透 JavaScript 的执行机制(javascript的执行原理)
- JavaScript 一文彻底理解并掌握ES5继承
- Javascript中的执行机制——Event Loop
- JavaScript定时器与执行机制
- JavaScript定时器与执行机制解析
一文彻底吃透 JavaScript 的执行机制(javascript的执行原理)
执行 & 运行
首先我们需要声明下,JavaScript
的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node
、浏览器、Ringo
等, JavaScript 在不同环境下的执行机制可能并不相同。而今天我们要讨论的 Event Loop
就是 JavaScript
的一种执行方式。所以下文我们还会梳理 node
的执行方式。而运行呢,是指JavaScript 的解析引擎。这是统一的。
关于 JavaScript
此篇文章中,这个小标题下,我们只需要牢记一句话: JavaScript 是单线程语言 ,无论HTML5
里面 Web-Worker
还是 node 里面的cluster
都是“纸老虎”,而且 cluster
还是进程管理相关。这里读者注意区分:进程和线程。
既然 JavaScript
是单线程语言,那么就会存在一个问题,所有的代码都得一句一句的来执行。就像我们在食堂排队打饭,必须一个一个排队点菜结账。那些没有排到的,就得等着~

概念梳理
在详解执行机制之前,先梳理一下 JavaScript
的一些基本概念,方便后面我们说到的时候大伙儿心里有个印象和大概的轮廓。
事件循环(Event Loop)

什么是 Event Loop?
其实这个概念还是比较模糊的,因为他必须得结合着运行机制来解释。
JavaScript
有一个主线程 main thread
,和调用栈 call-stack
也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
暂且,我们先理解为上图的大圈圈就是 Event Loop 吧!并且,这个圈圈,一直在转圈圈~ 也就是说,JavaScript
的 Event Loop
是伴随着整个源码文件生命周期的,只要当前 JavaScript
在运行中,内部的这个循环就会不断地循环下去,去寻找 queue
里面能执行的 task
。
任务队列(task queue)
task
,就是任务的意思,我们这里理解为每一个语句就是一个任务
console.log(1);
console.log(2);
如上语句,其实就是就可以理解为两个 task
。
而 queue
呢,就是FIFO
的队列!
所以 Task Queue
就是承载任务的队列。而 JavaScript
的 Event Loop
就是会不断地过来找这个 queue
,问有没有 task
可以运行运行。
同步任务(SyncTask)、异步任务(AsyncTask)
同步任务说白了就是主线程来执行的时候立即就能执行的代码,比如:
console.log(''this is THE LAST TIME'');
console.log(''Nealyang'');
代码在执行到上述 console
的时候,就会立即在控制台上打印相应结果。
而所谓的异步任务就是主线程执行到这个 task
的时候,“唉!你等会,我现在先不执行,等我 xxx 完了以后我再来等你执行” 注意上述我说的是等你来执行。
说白了,异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(()=>{
console.log(2)
});
console.log(1);
如上述代码,setTimeout
就是一个异步任务,主线程去执行的时候遇到setTimeout
发现是一个异步任务,就先注册了一个异步的回调,然后接着执行下面的语句console.log(1)
,等上面的异步任务等待的时间到了以后,在执行console.log(2)
。具体的执行机制会在后面剖析。

主线程自上而下执行所有代码
同步任务直接进入到主线程被执行,而异步任务则进入到
Event Table
并注册相对应的回调函数异步任务完成后,
Event Table
会将这个函数移入Event Queue
主线程任务执行完了以后,会从
Event Queue
中读取任务,进入到主线程去执行。循环如上
上述动作不断循环,就是我们所说的事件循环(Event Loop
)。
小试牛刀
ajax({
url:www.Nealyang.com,
data:prams,
success:() => {
console.log(''请求成功!'');
},
error:()=>{
console.log(''请求失败~'');
}
})
console.log(''这是一个同步任务'');
ajax 请求首先进入到
Event Table
,分别注册了onError
和onSuccess
回调函数。主线程执行同步任务:
console.log(''这是一个同步任务'');
主线程任务执行完毕,看
Event Queue
是否有待执行的 task,这里是不断地检查,只要主线程的task queue
没有任务执行了,主线程就一直在这等着ajax 执行完毕,将回调函数
push
到Event Queue
。(步骤 3、4 没有先后顺序而言)主线程“终于”等到了
Event Queue
里有task
可以执行了,执行对应的回调任务。如此往复。
宏任务(MacroTask)、微任务(MicroTask)
JavaScript
的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask
)和微任务(MicroTask
)。
先说说 MacroTask
,所有的同步任务代码都是MacroTask
(这么说其实不是很严谨,下面解释),setTimeout
、setInterval
、I/O
、UI Rendering
等都是宏任务。
MicroTask
,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask
呢,因为我们仅仅需要记住几个 MicroTask
即可,排除法!别的都是 MacroTask
。MicroTask
包括:Process.nextTick
、Promise.then catch finally
(注意我不是说 Promise)、MutationObserver
。
浏览器环境下的 Event Loop
当我们梳理完哪些是 MicroTask
,除了那些别的都是 MacroTask
后,哪些是同步任务,哪些又是异步任务后,这里就应该彻底的梳理下JavaScript 的执行机制了。
如开篇说到的,执行和运行是不同的,执行要区分环境。所以这里我们将Event Loop
的介绍分为浏览器和 Node 两个环境下。
先放图镇楼!如果你已经理解了这张图的意思,那么恭喜你,你完全可以直接阅读 Node 环境下的 Event Loop
章节了!
setTimeout、setInterval
setTimeout
setTimeout
就是等多长时间来执行这个回调函数。setInterval
就是每隔多长时间来执行这个回调。
let startTime = new Date().getTime();
setTimeout(()=>{
console.log(new Date().getTime()-startTime);
},1000);
如上代码,顾名思义,就是等 1s 后再去执行 console
。放到浏览器下去执行,OK,如你所愿就是如此。
但是这次我们在探讨 JavaScript 的执行机制,所以这里我们得探讨下如下代码:
let startTime = new Date().getTime();
console.log({startTime})
setTimeout(()=>{
console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},1000);
for(let i = 0;i<40000;i++){
console.log(1)
}

如上运行,setTimeout
的回调函数等到 4.7s 以后才执行!而这时候,我们把 setTimeout
的 1s 延迟给删了:
let startTime = new Date().getTime();
console.log({startTime})
setTimeout(()=>{
console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},0);
for(let i = 0;i<40000;i++){
console.log(1)
}

结果依然是等到 4.7s 后才执行setTimeout 的回调。貌似 setTimeout 后面的延迟并没有产生任何效果!

其实这么说,又应该回到上面的那张 JavaScript 执行的流程图了。

setTimeout
这里就是简单的异步,我们通过上面的图来分析上述代码的一步一步执行情况
首先
JavaScript
自上而下执行代码遇到遇到赋值语句、以及第一个
console.log({startTime})
分别作为一个task
,压入到立即执行栈中被执行。遇到
setTImeout
是一个异步任务,则注册相应回调函数。(异步函数告诉你,js 你先别急,等 1s 后我再将回调函数:console.log(xxx)
放到Task Queue
中)OK,这时候 JavaScript 则接着往下走,遇到了 40000 个 for 循环的 task,没办法,1s 后都还没执行完。其实这个时候上述的回调已经在
Task Queue
中了。等所有的立即执行栈中的 task 都执行完了,在回头看
Task Queue
中的任务,发现异步的回调 task 已经在里面了,所以接着执行。
打个比方
其实上述的不仅仅是 timeout,而是任何异步,比如网络请求等。

就好比,我六点钟下班了,可以安排下自己的活动了!
然后收拾电脑(同步任务)、收拾书包(同步任务)、给女朋友打电话说出来吃饭吧(必然是异步任务),然后女朋友说你等会,我先化个妆,等我画好了call你。
那我不能干等着呀,就接着做别的事情,比如那我就在改个 bug 吧,你好了通知我。结果等她一个小时后说我化好妆了,我们出去吃饭吧。不行!我 bug 还没有解决掉呢?你等会。。。。其实这个时候你的一小时化妆还是 5 分钟化妆都已经毫无意义了。。。因为哥哥这会没空~~
如果我 bug 在半个小时就解决完了,没别的任务需要执行了,那么就在这等着呀!必须等着!随时待命!。然后女朋友来电话了,我化完妆了,我们出去吃饭吧,那么刚好,我们在你的完成了请求或者 timeout 时间到了后我刚好闲着,那么我必须立即执行了。
setInterval
说完了 setTimeout
,当然不能错过他的孪生兄弟:setInterval
。对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入 Task Queue
,如果前面的任务耗时太久,那么同样需要等待。
这里需要说的是,对于 setInterval(fn,ms)
来说,我们制定没 xx ms
执行一次 fn
,其实是没 xx ms
,会有一个fn
进入到 Task Queue
中。一旦 setInterval 的回调函数fn
执行时间超过了xx ms,那么就完全看不出来有时间间隔了。 仔细回味回味,是不是那么回事?
Promise
关于 Promise
的用法,这里就不过过多介绍了,后面会在写《【THE LAST TIME】彻底吃透 JavaScript 异步》 一文的时候详细介绍。这里我们只说 JavaScript 的执行机制。
如上所说,promise.then
、catch
和 finally
是属于 MicroTask
。这里主要是异步的区分。展开说明之前,我们结合上述说的,再来“扭曲”梳理一下。
为了避免初学者这时候脑子有点混乱,我们暂时忘掉 JavaScript 异步任务! 我们暂且称之为待会再执行的同步任务。
有了如上约束后,我们可以说,JavaScript 从一开始就自上而下的执行每一个语句(Task
),这时候只能遇到立马就要执行的任务和待会再执行的任务。对于那待会再执行的任务等到能执行了,也不会立即执行,你得等js 执行完这一趟才行
再打个比方
就像做公交车一样,公交车不等人呀,公交车路线上有人就会停(农村公交!么得站牌),但是等公交车来,你跟司机说,我肚子疼要拉x~这时候公交不会等你。你只能拉完以后等公交下一趟再来(大山里!一个路线就一趟车)。
OK!你拉完了。。。等公交,公交也很快到了!但是,你不能立马上车,因为这时候前面有个孕妇!有个老人!还有熊孩子,你必须得让他们先上车,然后你才能上车!
而这些 孕妇、老人、熊孩子所组成的就是传说中的 MicroTask Queue
,而且,就在你和你的同事、朋友就必须在他们后面上车。
这里我们没有异步的概念,只有同样的一次循环回来,有了两种队伍,一种优先上车的队伍叫做MicroTask Queue
,而你和你的同事这帮壮汉组成的队伍就是宏队伍(MacroTask Queue
)。

一句话理解:一次事件循环回来后,开始去执行 Task Queue
中的task
,但是这里的 task
有优先级。所以优先执行 MicroTask Queue
中的 task,执行完后在执行MacroTask Queue
中的 task
小试牛刀
理论都扯完了,也不知道你懂没懂。来,期中考试了!
console.log(''script start'');
setTimeout(function() {
console.log(''setTimeout'');
}, 0);
Promise.resolve().then(function() {
console.log(''promise1'');
}).then(function() {
console.log(''promise2'');
});
没必要搞个 setTimeout 有加个 Promise,Promise 里面再整个 setTimeout 的例子。因为只要上面代码你懂了,无非就是公交再来一趟而已!
如果说了这么多,还是没能理解上图,那么公众号内回复【1】,手摸手指导!
Node 环境下的 Event Loop
Node中的Event Loop
是基于libuv
实现的,而libuv
是 Node 的新跨平台抽象层,libuv
使用异步,事件驱动的编程方式,核心是提供i/o
的事件循环和异步回调。libuv
的API
包含有时间,非阻塞的网络,异步文件操作,子进程等等。
Event Loop就是在libuv
中实现的。所以关于 Node 的 Event Loop
学习,有两个官方途径可以学习:
libuv 文档
官网的What is the Event Loop?.
在学习 Node 环境下的 Event Loop
之前呢,我们首先要明确执行环境,Node 和浏览器的Event Loop是两个有明确区分的事物,不能混为一谈。nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Node 的 Event Loop 分为 6 个阶段:
timers:执行
setTimeout()
和setInterval()
中到期的callback。pending callback: 上一轮循环中有少数的
I/O
callback会被延迟到这一轮的这一阶段执行idle, prepare:仅内部使用
poll: 最为重要的阶段,执行
I/O
callback,在适当的条件下会阻塞在这个阶段check: 执行
setImmediate
的callbackclose callbacks: 执行
close
事件的callback,例如socket.on(''close''[,fn])
、http.server.on(''close, fn)
上面六个阶段都不包括 process.nextTick()(下文会介绍)

整体的执行机制如上图所示,下面我们具体展开每一个阶段的说明
timers 阶段
timers 阶段会执行 setTimeout
和 setInterval
回调,并且是由 poll 阶段控制的。
在 timers 阶段其实使用一个最小堆而不是队列来保存所有的元素,其实也可以理解,因为timeout的callback是按照超时时间的顺序来调用的,并不是先进先出的队列逻辑)。而为什么 timer 阶段在第一个执行阶梯上其实也不难理解。在 Node 中定时器指定的时间也是不准确的,而这样,就能尽可能的准确了,让其回调函数尽快执行。
以下是官网给出的例子:
const fs = require(''fs'');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile(''/path/to/file'', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
当进入事件循环时,它有一个空队列(fs.readFile()
尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()
完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。
pending callbacks 阶段
pending callbacks 阶段其实是 I/O
的 callbacks 阶段。比如一些 TCP 的 error 回调等。
举个栗子:如果TCP socket ECONNREFUSED
在尝试connect
时receives
,则某些* nix系统希望等待报告错误。这将在pending callbacks阶段执行。
poll 阶段
poll 阶段主要有两个功能:
执行
I/O
回调处理 poll 队列(poll queue)中的事件
当时Event Loop 进入到 poll 阶段并且 timers 阶段没有任何可执行的 task 的时候(也就是没有定时器回调),将会有以下两种情况
如果 poll queue 非空,则 Event Loop就会执行他们,知道为空或者达到system-dependent(系统相关限制)
如果 poll queue 为空,则会发生以下一种情况
如果setImmediate()有回调需要执行,则会立即进入到 check 阶段
相反,如果没有setImmediate()需要执行,则 poll 阶段将等待 callback 被添加到队列中再立即执行,这也是为什么我们说 poll 阶段可能会阻塞的原因。
一旦 poll queue 为空,Event Loop就回去检查timer 阶段的任务。如果有的话,则会回到 timer 阶段执行回调。
check 阶段
check 阶段在 poll 阶段之后,setImmediate()
的回调会被加入check队列中,他是一个使用libuv API
的特殊的计数器。
通常在代码执行的时候,Event Loop 最终会到达 poll 阶段,然后等待传入的链接或者请求等,但是如果已经指定了setImmediate()并且这时候 poll 阶段已经空闲的时候,则 poll 阶段将会被中止然后开始 check 阶段的执行。
close callbacks 阶段
如果一个 socket 或者事件处理函数突然关闭/中断(比如:socket.destroy()
),则这个阶段就会发生 close
的回调执行。否则他会通过 process.nextTick()
发出。
setImmediate() vs setTimeout()
setImmediate()
和 setTimeout()
非常的相似,区别取决于谁调用了它。
setImmediate
在 poll 阶段后执行,即check 阶段setTimeout
在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段
计时器的执行顺序将根据调用它们的上下文而有所不同。如果两者都是从主模块中调用的,则时序将受到进程性能的限制。
例如,如果我们运行以下不在I / O
周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log(''timeout'');
}, 0);
setImmediate(() => {
console.log(''immediate'');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
如果在一个I/O
周期内移动这两个调用,则始终首先执行立即回调:
// timeout_vs_immediate.js
const fs = require(''fs'');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log(''timeout'');
}, 0);
setImmediate(() => {
console.log(''immediate'');
});
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
所以与setTimeout()
相比,使用setImmediate()
的主要优点是,如果在I / O
周期内安排了任何计时器,则setImmediate()
将始终在任何计时器之前执行,而与存在多少计时器无关。
nextTick queue
可能你已经注意到process.nextTick()
并未显示在图中,即使它是异步API的一部分。所以他拥有一个自己的队列:nextTickQueue
。
这是因为process.nextTick()
从技术上讲不是Event Loop的一部分。相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理nextTickQueue
。
如果存在 nextTickQueue
,就会清空队列中的所有回调函数,并且优先于其他 microtask
执行。
setTimeout(() => {
console.log(''timer1'')
Promise.resolve().then(function() {
console.log(''promise1'')
})
}, 0)
process.nextTick(() => {
console.log(''nextTick'')
process.nextTick(() => {
console.log(''nextTick'')
process.nextTick(() => {
console.log(''nextTick'')
process.nextTick(() => {
console.log(''nextTick'')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
process.nextTick() vs setImmediate()
从使用者角度而言,这两个名称非常的容易让人感觉到困惑。
process.nextTick()
在同一阶段立即触发setImmediate()
在事件循环的以下迭代或“tick”中触发
貌似这两个名称应该互换下!的确~官方也这么认为。但是他们说这是历史包袱,已经不会更改了。
这里还是建议大家尽可能使用setImmediate。因为更加的让程序可控容易推理。
至于为什么还是需要 process.nextTick
,存在即合理。这里建议大家阅读官方文档:why-use-process-nexttick。
Node与浏览器的 Event Loop 差异
一句话总结其中:浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

上图来自浪里行舟
最后
来~期末考试了
console.log(''1'');
setTimeout(function() {
console.log(''2'');
process.nextTick(function() {
console.log(''3'');
})
new Promise(function(resolve) {
console.log(''4'');
resolve();
}).then(function() {
console.log(''5'')
})
})
process.nextTick(function() {
console.log(''6'');
})
new Promise(function(resolve) {
console.log(''7'');
resolve();
}).then(function() {
console.log(''8'')
})
setTimeout(function() {
console.log(''9'');
process.nextTick(function() {
console.log(''10'');
})
new Promise(function(resolve) {
console.log(''11'');
resolve();
}).then(function() {
console.log(''12'')
})
})
评论区留下你的答案吧~~老铁!
参考文献
Tasks, microtasks, queues and schedules
libuv 文档
The Node.js Event Loop, Timers, and process.nextTick()
node 官网
async/await 在chrome 环境和 node 环境的 执行结果不一致,求解?
更快的异步函数和 Promise
一次弄懂Event Loop(彻底解决此类面试问题)
这一次,彻底弄懂 JavaScript 执行机制
不要混淆nodejs和浏览器中的event loop
完
本文分享自微信公众号 - 编程微刊(wangxiaoting678)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
JavaScript 一文彻底理解并掌握ES5继承
继承的目的是什么?结果是什么?
子类继承父类,子类实例拥有和父类实例相同的属性
父类实例的属性来自于哪里?
- 构造函数
- 原型对象
因此继承的实现应该做到以下两件事情
- 继承父类构造函数设置的属性: 借用父类构造函数使用apply/call绑定this为子类实例,使得子类实例具有父类实例相同属性
- 继承父类的原型属性: 将父类的原型属性设置到子类原型上去,通过new关键字调用继承父类原型属性
原型继承
思路是继承将父类实例放在构造函数原型上,父类实例作为子类原型
缺陷是所有子类共用一个原型对象,引用类型属性的地址相同。
只继承了原型属性,但缺失了调用父类构造函数为子类实例设置属性的这一过程,导致继承的属性全部来自于原型
function object(o){
function F(){}
F.prototype = o
return new F()
}
const o = {a:[1,2,3]}
const newO = object(o)
newO.a.push(4)
console.log(newO,o); // [1,2,3,4]
组合继承
原型继承+借用父类构造函数设置实例属性
用的最多,缺陷是调用了两次父类构造函数
function SuperClass() { }
function SubClass() {
SuperClass.apply(this, Array.from(arguments)) // 借用构造函数
}
SubClass.prototype = new SuperClass() // 继承原型属性
寄生式继承
寄生式继承基于原型继承, 主要体现在给实例做拓展与与增强
缺陷在于增强实例的过程中不能做到函数复用,使得效率降低,这一点和构造函数设置实例属性是一致的
function object(o){
function F(){}
F.prototype = o
const obj = new F()
// 拓展、增强
obj.fn = ()=>console.log('fn');
return obj
}
寄生组合式继承
寄生组合式继承解决了组合继承调用两次父类构造函数使得开销过大的问题
function extend(SubClass, SuperClass) {
function F() { }
F.prototype = SuperClass.prototype
SubClass.prototype = new F()
SubClass.prototype.constructor = SubClass
SubClass.SuperClass = SuperClass
}
// 使用
function Child() {
Child.SuperClass.apply(this, arguments)
}
function Parent() { this.a = 1 }
extend(Child, Parent)
console.log(new Child());
总结
原型继承,所有实例共用一个原型对象,继承了原型属性,但缺失了调用父类构造函数为子类实例设置属性的这一过程,导致继承的属性全部来自于原型,缺少了文章开头提到的第1点
组合继承,用的最多的方案,虽然1、2都满足,但是存在调用了两次父类构造函数的问题
寄生式继承,在原型继承的基础上,在实例创建的过程中给实例做拓展和增强,缺陷为并没有调用父类构造函数设置实例属性,复用性差,不满足1
寄生组合式继承,通过一个中间函数F,解决了组合继承中父类构造函数调用两次的问题;在子类实例创建过程中调用父类构造函数,解决了原型继承和寄生式继承中没有用父类构造函数为子类实例设置属性的问题。 1、2都满足。
Javascript中的执行机制——Event Loop
众所周知,Javascript是单线程语言, 这就意味着,所有的任务都必须按照顺序执行,只有等前面的一个任务执行完毕了,下一个任务才能执行。如果前面一个任务耗时很长,后一个任务就得一直等着,因此,为了实现主线程的不阻塞,就有了Event Loop。
1、javascript事件循环
首先,我们先了解一下同步任务和异步任务,同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
为了更好的了解执行机制,看下图
以上图说明主线程在执行的时候产生堆(内存分配)和堆栈(执行上下文),JavaScript是单线程的,意味着当执行环境的堆栈中的一个任务(task)在执行的时候,其它的任务都要处于等待状态。当主进程执行到异步操作的时候就会将异步操作对应的task放到event table,指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行,因为这个过程是不断重复的,所以称为Event Loop(事件循环),接下来,我们用几个例子进行分析
eg1:
console.log(1);
setTimeout(function () {
console.log(2);
})
console.log(3);
//执行结果:1、3、2
我们来分析一下这段代码,首先,根据执行上下文可知,执行环境栈中就有了一个task——console.log(1),输出1。接着往下执行,因为setTimeout是异步函数,所以将setTimeout进入event table,注册了一个回调函数在event queue,我们暂且称为fun1,此时的流程如下图:
接着往下执行,执行环境栈中会创建一个console.log(3)的task,并执行它,输出3,此时,执行环境已经没有任务了,则去Event Queue读取对应的函数,fun1被发现,进入主线程输出2,整个过程已经完成,所以输出的结果是1、3、2。
eg2:
setTimeout(function () {
console.log(1)
}, 3)
setTimeout(function () {
console.log(2)
})
输出2,1
我们再来简单的分析一下这个列子,我们暂且称第一个setTimeout为Time1,第二个为Time2。由于两个都是异步函数,按照执行顺序,先将Time放到event Table,接着将Time移到event Table,因为Time在event Table指定要3秒后才执行,所以Time2先于Time1到注册回调函数到event queue,所以输出的结果是2,1。
2、macro-task(宏任务)、micro-task(微任务)
MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver
任务又分为宏任务和微任务两种,在同一个上下文中,总的执行顺序为“同步代码—>microTask—>macroTask”,根据上面event loop的流程图,我们用列子来做进一步的了解:
eg1:
setTimeout(function () {
console.log(1);
},0);
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log(''end'');
//输出2、4、end、3、5、1、6
本例参考《JavaScript中的执行机制》,里面有详细的解释,大家可以参考下。
3、优先级
我们将上面的例子稍微改一下,将process.nextTick移到promise的后面,看下面的代码:
setTimeout(function () {
console.log(1);
},0);
console.log(2);
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
process.nextTick(() => {
console.log(3);
});
setImmediate(function () {
console.log(6)
})
console.log(''end'');
按照前面的分析,先执行同步代码,先输出“2,4,end”;然后是微任务promise输出5,process.nextTick输出3;最后的宏任务输出1,6。所以结果为2,4,end,5,3,1,6,然后事实并非如此,结果还是输出2、4、end、3、5、1、6,这是因为process.nextTick注册的函数优先级高于Promise**。
关于Event Loop的其他特殊情况,大家可参考文章一篇文章教会你Event loop——浏览器和Node和Event Loop的规范和实现,里面有更详细的介绍。
4、补充
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
setTimeout(() => {
console.log(999)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
JavaScript定时器与执行机制
JS执行机制
浏览器(或者说JS引擎)执行JS的机制是基于事件循环。
由于JS是单线程,所以同一时间只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。
为了避免因为某些长时间任务造成的无意义等待,JS引入了异步的概念,用另一个线程来管理异步任务。
JS定时器
JS的定时器目前有三个:setTimeout、setInterval和setImmediate。
定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。
JS定时器非常实用,做动画的肯定都用到过,也是最常用的异步模型之一。
有时候一些奇奇怪怪的问题,加一个setTimeout(fn, 0)(以下简写setTimeout(0))就解决了。不过,如果对定时器本身不熟悉,也会产生一些奇奇怪怪的问题。
setTimeout
setTimeout(fn, x)表示延迟x毫秒之后执行fn。
使用的时候千万不要太相信预期,延迟的时间严格来说总是大于x毫秒的,至于大多少就要看当时JS的执行情况了。
另外,多个定时器如不及时清除(clearTimeout),会存在干扰,使延迟时间更加捉摸不透。所以,不管定时器有没有执行完,及时清除已经不需要的定时器是个好习惯。
HTML5规范规定最小延迟时间不能小于4ms,即x如果小于4,会被当做4来处理。 不过不同浏览器的实现不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms。
setTimeout注册的函数fn会交给浏览器的定时器模块来管理,延迟时间到了就将fn加入主进程执行队列,如果队列前面还有没有执行完的代码,则又需要花一点时间等待才能执行到fn,所以实际的延迟时间会比设置的长。如在fn之前正好有一个超级大循环,那延迟时间就不是一丁点了。

JavaScript定时器与执行机制详细介绍
结果是:setTimeout: 335.187ms,远远不止10ms。
setInterval
setInterval的实现机制跟setTimeout类似,只不过setInterval是重复执行的。
对于setInterval(fn, 100)容易产生一个误区:并不是上一次fn执行完了之后再过100ms才开始执行下一次fn。 事实上,setInterval并不管上一次fn的执行结果,而是每隔100ms就将fn放入主线程队列,而两次fn之间具体间隔多久就不一定了,跟setTimeout实际延迟时间类似,和JS执行情况有关。

JavaScript定时器与执行机制详细介绍
输出

JavaScript定时器与执行机制详细介绍
可见,虽然每次fn执行时间都很长,但下一次并不是等上一次执行完了再过100ms才开始执行的,实际上早就已经等在队列里了。
另外可以看出,当setInterval的回调函数执行时间超过了延迟时间,已经完全看不出有时间间隔了。
如果setTimeout和setInterval都在延迟100ms之后执行,那么谁先注册谁就先执行回调函数。
setImmediate
这算一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他浏览器未测试。
从API名字来看很容易联想到setTimeout(0),不过setImmediate应该算是setTimeout(0)的替代版。
在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。不过在Nodejs中,两者谁先执行都有可能,原因是Nodejs的事件循环和浏览器的略有差异。

JavaScript定时器与执行机制详细介绍
Edge输出:setImmediate: 0.555 毫秒
很明显,setImmediate设计来是为保证让代码在下一次事件循环执行,以前setTimeout(0)这种不可靠的方式可以丢掉了。
其他常用异步模型
requestAnimationFrame
requestAnimationFrame并不是定时器,但和setTimeout很相似,在没有requestAnimationFrame的浏览器一般都是用setTimeout模拟。
requestAnimationFrame跟屏幕刷新同步,大多数屏幕的刷新频率都是60Hz,对应的requestAnimationFrame大概每隔16.7ms触发一次,如果屏幕刷新频率更高,requestAnimationFrame也会更快触发。基于这点,在支持requestAnimationFrame的浏览器还使用setTimeout做动画显然是不明智的。
在不支持requestAnimationFrame的浏览器,如果使用setTimeout/setInterval来做动画,最佳延迟时间也是16.7ms。 如果太小,很可能连续两次或者多次修改dom才一次屏幕刷新,这样就会丢帧,动画就会卡;如果太大,显而易见也会有卡顿的感觉。
有趣的是,第一次触发requestAnimationFrame的时机在不同浏览器也存在差异,Edge中,大概16.7ms之后触发,而Chrome则立即触发,跟setImmediate差不多。按理说Edge的实现似乎更符合常理。

JavaScript定时器与执行机制详细介绍
Edge输出:requestAnimationFrame: 16.66 毫秒
Chrome输出:requestAnimationFrame: 0.698ms
但相邻两次requestAnimationFrame的时间间隔大概都是16.7ms,这一点是一致的。当然也不是绝对的,如果页面本身性能就比较低,相隔的时间可能会变大,这就意味着页面达不到60fps。
Promise
Promise是很常用的一种异步模型,如果我们想让代码在下一个事件循环执行,可以选择使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。
而且Promise的延迟比setImmediate更低,意味着Promise比setImmediate先执行。

JavaScript定时器与执行机制详细介绍
Edge输出:Promise: 0.33 毫秒 setImmediate: 1.66 毫秒
尽管setImmediate的回调函数比Promise先注册,但还是Promise先执行。
可以肯定的是,在各JS环境中,Promise都是最先执行的,setTimeout(0)、setImmediate和requestAnimationFrame顺序不确定。
process.nextTick
process.nextTick是Nodejs的API,比Promise更早执行。
事实上,process.nextTick是不会进入异步队列的,而是直接在主线程队列尾强插一个任务,虽然不会阻塞主线程,但是会阻塞异步任务的执行,如果有嵌套的process.nextTick,那异步任务就永远没机会被执行到了。
使用的时候要格外小心,除非你的代码明确要在本次事件循环结束之前执行,否则使用setImmediate或者Promise更保险。
JavaScript定时器与执行机制解析
从JS执行机制说起
浏览器(或者说JS引擎)执行JS的机制是基于事件循环。
由于JS是单线程,所以同一时间只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。
为了避免因为某些长时间任务造成的无意义等待,JS引入了异步的概念,用另一个线程来管理异步任务。
同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程。等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如ajax请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环。
JS定时器
JS的定时器目前有三个:setTimeout、setInterval和setImmediate。
定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。
JS定时器非常实用,做动画的肯定都用到过,也是最常用的异步模型之一。
有时候一些奇奇怪怪的问题,加一个setTimeout(fn, 0)(以下简写setTimeout(0))就解决了。不过,如果对定时器本身不熟悉,也会产生一些奇奇怪怪的问题。
setTimeout
setTimeout(fn, x)表示延迟x毫秒之后执行fn。
使用的时候千万不要太相信预期,延迟的时间严格来说总是大于x毫秒的,至于大多少就要看当时JS的执行情况了。
另外,多个定时器如不及时清除(clearTimeout),会存在干扰,使延迟时间更加捉摸不透。所以,不管定时器有没有执行完,及时清除已经不需要的定时器是个好习惯。
HTML5规范规定最小延迟时间不能小于4ms,即x如果小于4,会被当做4来处理。 不过不同浏览器的实现不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms。
setTimeout注册的函数fn会交给浏览器的定时器模块来管理,延迟时间到了就将fn加入主进程执行队列,如果队列前面还有没有执行完的代码,则又需要花一点时间等待才能执行到fn,所以实际的延迟时间会比设置的长。如在fn之前正好有一个超级大循环,那延迟时间就不是一丁点了。
(function testSetTimeout() {
const label = ''setTimeout'';
console.time(label);
setTimeout(() => {
console.timeEnd(label);
}, 10);
for(let i = 0; i < 100000000; i++) {}
})();
setInterval结果是:setTimeout: 335.187ms,远远不止10ms。
setInterval
setInterval的实现机制跟setTimeout类似,只不过setInterval是重复执行的。
对于setInterval(fn, 100)容易产生一个误区:并不是上一次fn执行完了之后再过100ms才开始执行下一次fn。 事实上,setInterval并不管上一次fn的执行结果,而是每隔100ms就将fn放入主线程队列,而两次fn之间具体间隔多久就不一定了,跟setTimeout实际延迟时间类似,和JS执行情况有关。
(function testSetInterval() {
let i = 0;
const start = Date.now();
const timer = setInterval(() => {
i += 1;
i === 5 && clearInterval(timer);
console.log(`第${i}次开始`, Date.now() - start);
for(let i = 0; i < 100000000; i++) {}
console.log(`第${i}次结束`, Date.now() - start);
}, 100);
})();
第1次开始 100 第1次结束 1089 第2次开始 1091 第2次结束 1396 第3次开始 1396 第3次结束 1701 第4次开始 1701 第4次结束 2004 第5次开始 2004 第5次结束 2307 |
可见,虽然每次fn执行时间都很长,但下一次并不是等上一次执行完了再过100ms才开始执行的,实际上早就已经等在队列里了。输出
另外可以看出,当setInterval的回调函数执行时间超过了延迟时间,已经完全看不出有时间间隔了。
如果setTimeout和setInterval都在延迟100ms之后执行,那么谁先注册谁就先执行回调函数。
setImmediate
这算一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他浏览器未测试。
从API名字来看很容易联想到setTimeout(0),不过setImmediate应该算是setTimeout(0)的替代版。
在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。不过在Nodejs中,两者谁先执行都有可能,原因是Nodejs的事件循环和浏览器的略有差异。
(function testSetImmediate() {
const label = ''setImmediate'';
console.time(label);
setImmediate(() => {
console.timeEnd(label);
});
})();
很明显,setImmediate设计来是为保证让代码在下一次事件循环执行,以前setTimeout(0)这种不可靠的方式可以丢掉了。Edge输出:setImmediate: 0.555 毫秒
其他常用异步模型
requestAnimationFrame
requestAnimationFrame并不是定时器,但和setTimeout很相似,在没有requestAnimationFrame的浏览器一般都是用setTimeout模拟。
requestAnimationFrame跟屏幕刷新同步,大多数屏幕的刷新频率都是60Hz,对应的requestAnimationFrame大概每隔16.7ms触发一次,如果屏幕刷新频率更高,requestAnimationFrame也会更快触发。基于这点,在支持requestAnimationFrame的浏览器还使用setTimeout做动画显然是不明智的。
在不支持requestAnimationFrame的浏览器,如果使用setTimeout/setInterval来做动画,最佳延迟时间也是16.7ms。 如果太小,很可能连续两次或者多次修改dom才一次屏幕刷新,这样就会丢帧,动画就会卡;如果太大,显而易见也会有卡顿的感觉。
有趣的是,第一次触发requestAnimationFrame的时机在不同浏览器也存在差异,Edge中,大概16.7ms之后触发,而Chrome则立即触发,跟setImmediate差不多。按理说Edge的实现似乎更符合常理。
(function testRequestAnimationFrame() {
const label = ''requestAnimationFrame'';
console.time(label);
requestAnimationFrame(() => {
console.timeEnd(label);
});
})();
Chrome输出:requestAnimationFrame: 0.698msEdge输出:requestAnimationFrame: 16.66 毫秒
但相邻两次requestAnimationFrame的时间间隔大概都是16.7ms,这一点是一致的。当然也不是绝对的,如果页面本身性能就比较低,相隔的时间可能会变大,这就意味着页面达不到60fps。
Promise
Promise是很常用的一种异步模型,如果我们想让代码在下一个事件循环执行,可以选择使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。
而且Promise的延迟比setImmediate更低,意味着Promise比setImmediate先执行。
function testSetImmediate() {
const label = ''setImmediate'';
console.time(label);
setImmediate(() => {
console.timeEnd(label);
});
}
function testPromise() {
const label = ''Promise'';
console.time(label);
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.timeEnd(label);
});
}
testSetImmediate();
testPromise();
尽管setImmediate的回调函数比Promise先注册,但还是Promise先执行。Edge输出:Promise: 0.33 毫秒 setImmediate: 1.66 毫秒
可以肯定的是,在各JS环境中,Promise都是最先执行的,setTimeout(0)、setImmediate和requestAnimationFrame顺序不确定。
process.nextTick
process.nextTick是Nodejs的API,比Promise更早执行。
事实上,process.nextTick是不会进入异步队列的,而是直接在主线程队列尾强插一个任务,虽然不会阻塞主线程,但是会阻塞异步任务的执行,如果有嵌套的process.nextTick,那异步任务就永远没机会被执行到了。
使用的时候要格外小心,除非你的代码明确要在本次事件循环结束之前执行,否则使用setImmediate或者Promise更保险。
关于一文彻底吃透 JavaScript 的执行机制和javascript的执行原理的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于JavaScript 一文彻底理解并掌握ES5继承、Javascript中的执行机制——Event Loop、JavaScript定时器与执行机制、JavaScript定时器与执行机制解析的相关信息,请在本站寻找。
本文标签: