1+1=10

扬长避短 vs 取长补短

Qt中的JavaScript引擎小记

准备补充些JavaScript知识,所以先从Qt角度简单梳理一下。Qt对JavaScript支持,从Qt3时代就开始了,但由于各种原因,一直没有好好了解过:

  • QSA:Qt Script for Application。2003~2008,后被QtScript取代。
  • QtScript:2007(Qt4.3) ~ 2020 (Qt6.0),后被QJSEngine取代。(QtScript有两个版本,Qt经典版本和Qt4.6起基于JavaScriptCore的版本)
  • QJSEngine:2012(Qt5.0)~至今。(QJSEngine 也有两个版本,基于V8的版本和Qt5.2起Qt自己的V4版本)

QtWebEngine和早期QtWebkit中都包含javascript引擎,不在本文范围之内。

背景

Qt下javascript的故事,感觉和Qt下opengl的故事有一拼:变来变去,受外界影响很大。

Qt下引擎

早期Qt引入javascript是为了给QWidget引入脚本功能,后来改进引擎是为了支持QML。

QSA

QSA(Qt Script for Application),实现上,它是ECMAScript 3.0和4.0草案的复合。

  • 2003年7月发布
  • 2008年结束支持(被Qt4.3引入的功能更强的QtScript模块取代)。

QtScript

分两个版本(阶段):

  • QtScript (Classic):Qt4.3 ~ Qt4.5。这个模块的问题是,没有JIT功能,速度慢,不支持EMCAScript新特性。Qt 4.6时,QtScript 使用JavaScriptCore进行了重写。原有的这个模块,以QtScriptClassic的名字被放入到了Qt Solutions 中。
  • QtScript:Qt4.6 ~ Qt5.15。使用JavaScriptCore对QtScript进行重写的版本。但是JavaScriptCore作为Webkit的JavaScript引擎,并没有公开的API接口,不关注Webkit之外的应用场景,也不接受Qt提交的适配QtScript补丁。Qt需要维护自己的JavaScriptCore,在其内部挂钩子,但是上游变动很大,每次适配都需要大量工作,这从一开始也为废弃埋下伏笔。

时间线:

  • 2007年5月,Qt4.3 引入了QtScript模块(ECMAScript 3.0标准)。
  • 2009年12月,Qt4.6使用JavaScriptCore对QtScript进行了重写。(此时Qt在Nokia旗下,重写QtScript是为了推出QML/QtQuick,以更好支持Qt在塞班、meego等手机操作系统下的APP开发)。
  • 2015年7月,Qt5.5发布,QtScript被标识为废弃。
  • 2020年8月,Qt6.0发布,QtScript从Qt6中正式移除。

QJSEngine

分两个版本(阶段):

  • QJSEngine(V8):Qt5.0~Qt5.1。基于Google V8的 QJSEngine。V8作为JavaScript引擎,速度没得说,但是和QML配合起来,数据需要来回转换,性能受影响。而且V8和JavaScriptCore有同样的问题,不Care Qt下的使用场景,不接受Qt提交的补丁,需要Qt自行维护一个V8,比较痛苦。
  • QJSEngine:Qt5.2 ~ 至今。Qt自研的V4,兼容EMCAScript5.1(在Qt6.0中,至少支持到EMCAScript 7)。与V8相比,对QML支持更好,但是纯JavaScript的话,会稍差一些。

时间线:

  • 2012年,Qt5.0引入基于Google V8的 QJSEngine。
  • 2013年,Qt5.2引入自己的JavaScript引擎——V4VM,取代Google的V8。

QQmlEngine:QJSEngine的派生类,与QJSEngine都是在Qt5.0引入。注意,QtQuick1中引擎叫做QDeclarativeEngine。

JavaScript

对于JavaScript,网络上的资料就很多了。而且里面涉及多个公司的爱恨情仇,远比Qt的故事复杂...

JavaScript提交给ECMA指定标准,称之为ECMAScript,标准编号ECMA-262。本文中没有刻意区分 ECMAScript、ECMA-262和JavaScript。

  • ECMAScript (/ˈɛkməskrɪpt/),JavaScript在标准中的名字
  • ECMA-262,标准的名字
  • ISO/IEC 16262:2011,对应的ISO标准名字

标准化之前

  • 1995年,网景公司(NetScope)就在浏览器中嵌入Scheme还是Java,展开激烈讨论。最终布兰登·艾克(Brendan Eich)5月花了10天时间设计了一门语言。取名Mocha,9月改名LiveScript,1995年年底定名JavaScript(当时网景公司与昇阳电脑公司组成的开发联盟为了让这门语言搭上Java这个编程语言“热词”,将其临时改名为JavaScript,日后这成为大众对这门语言有诸多误解的原因之一)。
  • 1996年8月,微软发布的 IE3.0 包含了JScript。JScript系通过对网景的解释器进行逆向而创建。(浏览器大战)
  • 1996年11月,网景正式向ECMA(欧洲计算机制造商协会)提交语言标准。ECMA以JavaScript语言为基础制定了ECMAScript标准规范——ECMA-262。

注:1996年,微软在IE浏览器中也搞了自己VBScript脚本,但在IE寿终正寝之前,基本就已经没人在web中使用了,毕竟大家都不喜欢打开网页看到一句“请在IE浏览器下使用”。VBScript作为Windows系统的脚本语言,也已经于2023年10月10日进入微软的弃用名单。

标准化 1.0~3.0

  • 1997年6月,ECMAScript 1.0
  • 1998年6月,ECMAScript 2.0
  • 1999年12月,ECMAScript 3.0

ECMAScript 3.0获取巨大成功,得到广泛支持。

标准化 4.0 3.1 5.x(ISO/IEC)

ECMAScript 4.0 并不存在,ECMAScript 3.1 也不存在,因为

  • 2000年,ECMAScript 4.0开始酝酿
  • 2007年10月, ECMAScript 4.0草案发布,各方对此有严重分歧
  • 2008年7月,终止ECMAScript4.0开发

ECMA会议决定:将ECMAScript4.0草案中涉及现有功能改善的部分,发布为ECMAScript3.1,但会后不久又将其改为ECMAScript5.0;将草案中改动大的部分,放入JavaScript.next(即后面的ES6)或JavaScript.next.next。

  • 2009年12月,ECMAScript5.0发布
  • 2011年6月,ECMAScript5.1发布(并成为ISO国际标准 ISO/IEC 16262:2011)

标准化 6~ 至今(成熟阶段?)

  • 2015年6月,ECMA262的第6版发布。如果从2000年4.0酝酿开始算,这一版本用时15年(比从C++98和难产的C++0x即后来的C++11跨度都大)。从这一版本开始,官方开始用年份命名:ECMAScript2015,简称ES2015(尽管很多人都在用ES6)。

从ES2015开始,ECMA-262开挂了,每年6月份一个版本:

  • 2016年6月,ECMA262 第7版,ECMAScript 2016
  • 2017年6月,ECMA262 第8版,ECMAScript 2017
  • 2018年6月,ECMA262 第9版,ECMAScript 2018
  • 2019年6月,ECMA262 第10版,ECMAScript 2019
  • 2020年6月,ECMA262 第11版,ECMAScript 2020
  • 2021年6月,ECMA262 第12版,ECMAScript 2021
  • 2022年6月,ECMA262 第13版,ECMAScript 2022
  • 2023年6月,ECMA262 第14版,ECMAScript 2023
  • ...

Babel

  • https://babeljs.io

Babel 是一个广泛使用的 JavaScript 编译器,它可以将最新的 ECMAScript 语言标准(如 ES2015+)编译为向后兼容的 JavaScript 版本,以便在旧版本的浏览器或环境中运行。

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法。要使用新的API,需要使用某种Polyfill才行。

Babel的插件babel-polyfill包含 core-js

引擎环境

ECMAScript规范的语言本身,而其执行需要环境:

  • 浏览器下:ECMAScript + WebAPIs(BOM + DOM)
  • Node.js:ECMAScript + Node APIs(fs + net + ...)

同样:

  • Qt下:ECMAScript + Qt APIs

QJSEngine

  • https://doc.qt.io/qt-6/qtjavascript.html
  • https://doc.qt.io/qt-6/qtqml-javascript-functionlist.html
  • https://doc.qt.io/qt-6/qtqml-javascript-hostenvironment.html
  • https://wiki.qt.io/V4

扯了这么多,终于可以回到正题了。

从QTBUG-47735在Qt5.12修复来看,QJSEngine 应该支持 ECMAScript2016 (ES7)。EMCA各个版本的特性可以如下地址查看:

  • https://www.w3schools.com/js/js_versions.asp

ECMAScript2017(ES8)还不支持,见QTBUG-58620,Add async/await support to V4。可是,六七年过去了,没都有动静。这也使得我们一直不能用 async/await。

简单测试

使用ES2016中引入指数操作符(** )试一试:

#include <QCoreApplication>
#include <QJSEngine>
#include <QJSValueList>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QJSEngine engine1;
    //! [0]
    qDebug() << engine1.evaluate("2.5**3").toNumber();
    //! [0]
    //! [1]
    qDebug() << engine1.evaluate("let a = 2.5; let b= 3; a**b").toNumber();
    //! [1]
    //! [2]
    engine1.globalObject().setProperty("a", 2.5);
    engine1.globalObject().setProperty("b", 3);
    qDebug() << engine1.evaluate("a**b").toNumber();
    //! [2]
    //! [3]
    auto ret = engine1.evaluate("(function(a, b) { return a**b; })");
    qDebug() << ret.call(QJSValueList{2.5, 3.0}).toNumber();
    //! [3]
    //! [4]
    ret = engine1.evaluate("((a, b) => a**b)");
    qDebug() << ret.call(QJSValueList{2.5, 3.0}).toNumber();
    //! [4]
    return 0;
}

输出结果:

15.625
15.625
15.625
15.625
15.625

使用ES2017添加的Object.entries()试试看:

    QJSEngine engine2;
    engine2.globalObject().setProperty("e", engine2.newQObject(&engine2));
    qDebug() << engine2.evaluate("Object.entries(e)").toString();

另外,在 qtdeclarative/tests/auto/qml/ecmascripttests下,有ecma262的测试用例,以及Qt用于跑这些数据的 qjstest 程序。

小例子

点击按钮,调用javascript改变其属性:

#include <QApplication>
#include <QPushButton>
#include <QFile>
#include <QJSEngine>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    auto btn = new QPushButton("Hello world!");
    QJSEngine engine;
    engine.globalObject().setProperty("btn", engine.newQObject(btn));
    btn->connect(btn, &QPushButton::clicked, &engine, [&engine]() {
        engine.evaluate("btn.text=\"Hello from js\"");
    });

    btn->show();
    return a.exec();
}

使用module,实现上述功能:

#include <QApplication>
#include <QPushButton>
#include <QFile>
#include <QJSEngine>

const char *js = R"js(
export function hello() {
    btn.text = "Hello from js!";
}
)js";

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    {
        QFile f("test.mjs");
        f.open(QFile::WriteOnly);
        f.write(js);
    }

    auto btn = new QPushButton("Hello world!");
    QJSEngine engine;
    engine.globalObject().setProperty("btn", engine.newQObject(btn));
    auto m = engine.importModule("test.mjs");
    btn->connect(btn, &QPushButton::clicked, [m](){m.property("hello").call();});

    btn->show();
    return a.exec();
}

参考

  • https://www.qt.io/blog/2013/04/15/evolution-of-the-qml-engine-part-1
  • https://bugreports.qt.io/browse/QTBUG-47735
  • https://bugreports.qt.io/browse/QTBUG-71329
  • https://bugreports.qt.io/browse/QTBUG-58620
  • https://doc.qt.io/qt-6/qtqml-javascript-hostenvironment.html
  • https://doc.qt.io/qt-5/qtscript-index.html
  • https://doc.qt.io/qt-5/qtqml-javascript-hostenvironment.html
  • https://doc.qt.io/archives/4.3/qtscript.html#language-overview
  • https://doc.qt.io/archives/qt-4.8/porting-qsa.html
  • https://www.qt.io/blog/2013/09/30/qt-5-2-alpha-available
  • https://wiki.qt.io/New_Features_in_Qt_5.5
  • https://development.qt-project.narkive.com/hzVG9b6f/qtscript-vs-qml-qjsengine
  • https://en.wikipedia.org/wiki/QtScript
  • https://web.archive.org/web/20131202231949/http://blog.qt.digia.com/blog/2007/01/05/say-hello-to-qtscript/
  • https://web.archive.org/web/20080119082451/http://doc.trolltech.com/qsa-1.2.1/getting-started.html
  • https://tc39.es/ecma262/
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript
  • https://ecma-international.org/publications-and-standards/standards/ecma-262/
  • https://www.w3schools.com/js/js_versions.asp
  • https://en.wikipedia.org/wiki/ECMAScript
  • https://en.wikipedia.org/wiki/JavaScript
  • https://en.wikipedia.org/wiki/ECMAScript_version_history
  • ES6 标准入门
  • https://es6.ruanyifeng.com/

Comments