JavaScriptCore的MacroTasks及MicroTasks源碼解析

《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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章