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