《Secrets of The Javascript Ninja》 中提到一段話:
“瀏覽器的Event Loop至少包含兩個隊列,Macrotasks隊列和Microtasks隊列。
Macrotasks包含生成dom對象、解析HTML、執行主線程js代碼、更改當前URL還有其他的一些事件如頁面加載、輸入、網絡事件和定時器事件。從瀏覽器的角度來看,macrotask代表一些離散的獨立的工作。當執行完一個task後,瀏覽器可以繼續其他的工作如頁面重渲染和垃圾回收。
Microtasks則是完成一些更新應用程序狀態的較小任務,如處理promise的回調和DOM的修改,這些任務在瀏覽器重渲染前執行。Microtask應該以異步的方式儘快執行,其開銷比執行一個新的macrotask要小。Microtasks使得我們可以在UI重渲染之前執行某些任務,從而避免了不必要的UI渲染,這些渲染可能導致顯示的應用程序狀態不一致。”
而在Vue源碼解析中,有這麼一段話:
“主線程的執行過程就是一個 tick,而所有的異步結果都是通過 “任務隊列” 來調度。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分爲兩大類,分別是 macro task 和 micro task,並且每個 macro task 結束後,都要清空所有的 micro task。”
實時上是不是這樣做的,我們從源碼上來找答案。首先來分析WebKit的代碼,代碼主要分爲兩部分:WebCore,JSScriptCore,JSScriptCore的源碼非常多,感謝戴銘的博客做的整理,摘錄如下:
API:JavaScriptCore 對外的接口類
assembler:不同 CPU 的彙編生成,比如 ARM 和 X86
b3:ftl 裏的 Backend
bytecode:字節碼的內容,比如類型和計算過程
bytecompiler:編譯字節碼
Configurations:Xcode 的相關配置
Debugger:用於測試腳本的程序
dfg:DFG JIT 編譯器
disassembler:反彙編
heap:運行時的堆和垃圾回收機制
ftl:第四層編譯
interpreter:解釋器,負責解析執行 ByteCode
jit:在運行時將 ByteCode 轉成機器碼,動態及時編譯。
llint:Low Level Interpreter,編譯四層裏的第一層,負責解釋執行低效字節碼
parser:詞法語法分析,構建語法樹
profiler:信息收集,能收集函數調用頻率和消耗時間。
runtime:運行時對於 js 的全套操作。
wasm:對 WebAssembly 的實現。
yarr:Yet Another Regex Runtime,運行時正則表達式的解析
注:代碼中有USE(CF) 以及USE(GLIB_EVENT_LOOP),其中,CF爲CoreFoudation的意思,在這裏我們僅注意Apple平臺的情況。
根據目錄做判斷,runtime爲引擎運行對外環境。應當在runtime目錄中,在runtime目錄順利找到搜索Microtasks,發現JSPromise.h、JSPromise.cpp兩個文件有代碼(PromiseDeferredTimer屬於Promise.defer內容,此處不做討論)。
JSPromise* JSPromise::resolve(JSGlobalObject& globalObject, JSValue value)
{
auto* exec = globalObject.globalExec();
auto& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* promiseResolveFunction = globalObject.promiseResolveFunction();
CallData callData;
auto callType = JSC::getCallData(vm, promiseResolveFunction, callData);
ASSERT(callType != CallType::None);
MarkedArgumentBuffer arguments;
arguments.append(value);
ASSERT(!arguments.hasOverflowed());
auto result = call(exec, promiseResolveFunction, callType, callData, globalObject.promiseConstructor(), arguments);
RETURN_IF_EXCEPTION(scope, nullptr);
ASSERT(result.inherits<JSPromise>(vm));
return jsCast<JSPromise*>(result);
}
很顯然,因爲做了對應綁定關係,Javascript中的每一個Promise對應JSPromise,假設我們有如下代碼。
new Promise(function(resolve,reject){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
那麼resolve,reject,then函數就是我們的考慮的重點函數。這裏,resolve函數由JSPromise注入其函數:
JSPromise* JSPromise::resolve(JSGlobalObject& globalObject, JSValue value)
而then函數由於需要保證其不被覆蓋重寫,其函數被安排在文件JSInternalPromise中:
JSInternalPromise* JSInternalPromise::then(ExecState* exec, JSFunction* onFulfilled, JSFunction* onRejected)
{
VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSObject* function = jsCast<JSObject*>(get(exec, vm.propertyNames->builtinNames().thenPublicName()));
RETURN_IF_EXCEPTION(scope, nullptr);
CallData callData;
CallType callType = JSC::getCallData(vm, function, callData);
ASSERT(callType != CallType::None);
MarkedArgumentBuffer arguments;
arguments.append(onFulfilled ? onFulfilled : jsUndefined());
arguments.append(onRejected ? onRejected : jsUndefined());
ASSERT(!arguments.hasOverflowed());
scope.release();
return jsCast<JSInternalPromise*>(call(exec, function, callType, callData, this, arguments));
}
當調用該then之後,onFulfilled與onRejected被寫入到該Promise上下文中等待被調用。事實上JavaScriptCore的runtime環境構建不僅僅只有runtime目錄中文件支撐,在上面的目錄中缺少了某個目錄的解釋,即:builtins。內置調用函數由該項目支撐,通過一系列宏頭部的聲明與C++變量產生關聯。promiseResolveFunction就是一個由宏定義構建的函數,宏定義編寫於BuiltinNames.h,BuiltinNames.cpp,與之產生聯繫的文件爲:PromiseOperations.js,promiseResolveFunction對應到以下函數:
function @resolve(resolution) {
if (alreadyResolved)
return @undefined;
alreadyResolved = true;
if (resolution === promise)
return @rejectPromise(promise, new @TypeError("Resolve a promise with itself"));
if (!@isObject(resolution))
return @fulfillPromise(promise, resolution);
var then;
try {
then = resolution.then;
} catch (error) {
return @rejectPromise(promise, error);
}
if (typeof then !== 'function')
return @fulfillPromise(promise, resolution);
@enqueueJob(@promiseResolveThenableJob, [promise, resolution, then]);
return @undefined;
}
@enqueueJob實際調用由C++代碼JSGlobalObject.cpp提供:
static EncodedJSValue JSC_HOST_CALL enqueueJob(ExecState* exec)
{
VM& vm = exec->vm();
JSGlobalObject* globalObject = exec->lexicalGlobalObject();
JSValue job = exec->argument(0);
JSValue arguments = exec->argument(1);
ASSERT(arguments.inherits<JSArray>(vm));
globalObject->queueMicrotask(createJSMicrotask(vm, job, jsCast<JSArray*>(arguments)));
return JSValue::encode(jsUndefined());
}
其中JSC_HOST_CALL表明其可在Javascript端進行調用。queueMicrotask函數將創建的微任務加入JSVitureMachine(VM)的調用隊列。
void VM::queueMicrotask(JSGlobalObject& globalObject, Ref<Microtask>&& task)
{
m_microtaskQueue.append(std::make_unique<QueuedTask>(*this, &globalObject, WTFMove(task)));
}
而任務的執行在VM函數drainMicroTask:
void VM::drainMicrotasks()
{
while (!m_microtaskQueue.isEmpty())
m_microtaskQueue.takeFirst()->run();
}
其調用在JSLock中:
void JSLock::willReleaseLock()
{
RefPtr<VM> vm = m_vm;
if (vm) {
vm->drainMicrotasks();
if (!vm->topCallFrame)
vm->clearLastException();
vm->heap.releaseDelayedReleasedObjects();
vm->setStackPointerAtVMEntry(nullptr);
if (m_shouldReleaseHeapAccess)
vm->heap.releaseAccess();
}
if (m_entryAtomicStringTable) {
Thread::current().setCurrentAtomicStringTable(m_entryAtomicStringTable);
m_entryAtomicStringTable = nullptr;
}
}
也就是完成了主線程工作之後調用。
setTimeout函數也是Vue提到的Macro Task,在JavaScriptCore中究竟是神馬錶現。其實,setTimeout在並不在JavascriptCore中,它屬於WebCore的內容,在瀏覽器中,setTimeout函數屬於window對象內置函數。What!ORG...
ExceptionOr<int> DOMWindow::setTimeout(JSC::ExecState& state, std::unique_ptr<ScheduledAction> action, int timeout, Vector<JSC::Strong<JSC::Unknown>>&& arguments)
{
auto* context = scriptExecutionContext();
if (!context)
return Exception { InvalidAccessError };
// FIXME: Should this check really happen here? Or should it happen when code is about to eval?
if (action->type() == ScheduledAction::Type::Code) {
if (!context->contentSecurityPolicy()->allowEval(&state))
return 0;
}
action->addArguments(WTFMove(arguments));
return DOMTimer::install(*context, WTFMove(action), Seconds::fromMilliseconds(timeout), true);
}
其中DomTimer爲計時器,時間到到達之後執行action,action即爲setTimeout函數參數包裝,其中一個execute爲
void ScheduledAction::execute(Document& document)
{
JSDOMWindow* window = toJSDOMWindow(document.frame(), m_isolatedWorld);
if (!window)
return;
RefPtr<Frame> frame = window->wrapped().frame();
if (!frame || !frame->script().canExecuteScripts(AboutToExecuteScript))
return;
if (m_function)
executeFunctionInContext(window, window->proxy(), document);
else
frame->script().executeScriptInWorld(m_isolatedWorld, m_code);
}
executeScriptInWorld最終交互的單例對象JSMainThreadExecState
JSValue returnValue = JSMainThreadExecState::profiledEvaluate(&exec, JSC::ProfilingReason::Other, jsSourceCode, &proxy, evaluationException);
直接將該setTimeout中定義的代碼執行在JavascriptCore當前的VM JS線程上。profiledEvaluate相當於產生了一個script代碼執行,而在之前執行之前會VM會有drainMicroTasks的調用,這也就是setTimeout函數在執行在最終執行的原因了。但是在JavascriptCore,源碼完全卻是沒有定義macroTasks的變量。另外因爲setTimeout屬於WebCore中屬於window的函數,所以在蘋果官方提供的JavaScriptCore中也是需要自行實現setTimeout函數的。
參考:
https://ming1016.github.io/2018/04/21/deeply-analyse-javascriptcore/
https://webkit.org/blog/6411/javascriptcore-csi-a-crash-site-investigation-story/#LLIntProbe