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源码解析中,有这么一段话:


JavaScript线程

“主线程的执行过程就是一个 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(vm));
    return jsCast(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(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(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(vm));

globalObject->queueMicrotask(createJSMicrotask(vm, job, jsCast(arguments)));

return JSValue::encode(jsUndefined());

}
其中JSC_HOST_CALL表明其可在Javascript端进行调用。queueMicrotask函数将创建的微任务加入JSVitureMachine(VM)的调用队列。

void VM::queueMicrotask(JSGlobalObject& globalObject, Ref&& task)
{
    m_microtaskQueue.append(std::make_unique(*this, &globalObject, WTFMove(task)));
}

而任务的执行在VM函数drainMicroTask:

void VM::drainMicrotasks()
{
    while (!m_microtaskQueue.isEmpty())
        m_microtaskQueue.takeFirst()->run();
}

其调用在JSLock中:

void JSLock::willReleaseLock()
{   
    RefPtr 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 DOMWindow::setTimeout(JSC::ExecState& state, std::unique_ptr action, int timeout, Vector>&& 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 = 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

你可能感兴趣的:(JavaScriptCore的MacroTasks及MicroTasks源码解析)