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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355