一、Wind.js 是怎么实现的异步流程控制。
通常我们常见的代码是同步执行的。比如这样:
1
2
3
|
var
log = fs.readFile(
'log.txt'
);
console.log(
'log'
);
console.log(
'read finished.'
);
|
同步代码执行是顺序的,阻塞的,文件内容没有被读取完城,后面的两句 console.log 语句不会被执行。
当遇到异步代码时,情况则发生了变化:
1
2
3
4
5
6
7
|
fs.readFile(
'log.txt'
,
function
(err, content) {
if
(error)
return
;
var
log = content;
console.log(
'log'
);
console.log(
'read finished.'
);
});
console.log(
'reading...'
);
|
原本顺序执行的代码,必须非放置在 readFile 异步函数的回调内。读取文件函数执行后立即运行的是 console.log(‘reading…’)。很好,它不阻塞了,副作用是破坏了代码的执行顺序。
如果仔细观察两端代码,我们可以发现一些有迹可循的编码原则。当使用异步函数时,红色代码被移动到了回调函数内,其它地方则没有更多的变化。
Wind.js 就是利用了这个编码调制原则。
Wind.js 类库中 Wind.compile 方法先将可能执行的异步代码包装为匿名函数,通过参数传入。 Wind.compile 方法会将传入的函数 toString 后分析代码。函数体内的 $await 函数实际上是充当了代码块位置偏移的标示位,以便分析函数源码时候见到 $await 函数时,就将其后的代码块整体修正到异步回调函数内(实际上 Wind.js 处理的内容非常多,如 $await 函数前后的 JS 语法逻辑变更为异步函数逻辑等,具体可看 老赵 亲自写的讲解文章: )。
看一段简单的异步代码:
1
2
3
|
setTimeout(
function
() {
console.log(
'sleep finished.'
);
}, 1000);
|
1
2
3
4
5
|
var
test = eval(Wind.compile(
"async"
,
function
() {
$await(Wind.Async.sleep(1000));
console.log(
'sleep finished.'
);
}
}));
|
Wind.compile 函数先将匿名函数参数 toString,拆解源码字符串并重新进行语法整合。遇到 $await 函数后,就根据其参数,确定调用具体的何种异步代码模型。这里 Wind.Async.sleep 是对 setTimeout 函数的封装,它告知 $await 与 Wind.compile 需要按照 setTimeout 函数语法的回调形式来重新组织代码。于是, 被重构后的代码可能为如下源码字符串形式:
1
2
3
|
var
sourceCode =
"setTimeout(function() {
console.log('sleep finished.');
}, 1000);"
|
1
2
3
4
|
Wind.compile =
function
() {
// 语法重组处理 blablablabla ……
return
sourceCode;
}
|
之后,eval 此源码串,即可获得原始编写异步代码同效结果。
*注意下,这里仅是阐述原理,Wind.js 返回的代码是被包含在 “function(){ … }” 字符串内的源码,此代码被 eval 后变为真正的 function 对象,此 function 需要被执行才能真正开始执行异步流程控制。
二、$await 为什么是个函数而不是作为一个简单的语法标记存在?
其实这个问题刚才已经诠释过, Wind.compile 在处理代码时,遇到 $await 后需要知道调用何种方式的异步语法。如 setTimeout 异步函数的回调参数为首参,而 addEventListenter 异步函数的回调函数位置在次参数。
1
2
|
setTimeout(
function
() { ... }, timeout);
DOM.addEventListenter(‘click’,
function
() {},
false
);
|
Wind.compile 需要明确知道不同异步函数回调语法的确切位置才能准确无误的重组代码。因此 $await 就承担了告知任务,以函数形式来指明不同类型的异步语法。Wind.js 由此也获得了其他兼容异步模型的扩展能力,不同的异步语法可通过扩展绑定 Task 任务模型后交由 $await 识别处理。
三、为什么要用 eval 并且还没有封装它?
如上所述,Wind.js 将 Wind.compile 函数的第二个参数由 function 进行 toString 后得到源码字符串。分析源码字符串后,将此部分代码重新封装为一段异步调用形式的源码字符串。此时,它需要执行此字符串,使它们能成为 JS 执行流中的一部分。
于是,动态执行一段源码串在 JS 内不依赖宿主环境实现的 API 就只有 eval 和 Function 构造函数两种。
先说,Function 构造函数,它的特点是其内源码都在 Global 作用域下被执行。如果此时,源码需要 Wind.compile 函数所在作用域中的某个变量,那么将会出现非预期的问题。如:
1
2
3
4
5
|
var
a =
'globlal scope'
;
(
function
() {
var
a =
'local scope'
;
new
Function(
'console.log(a)'
)();
}());
|
直接调用 eval 函数在运行源码串,则可以避免由 Function 构造函数带来的问题。它的执行作用域在当前作用域内,就是说它能获取当前作用域内变量值以及上层闭包内变量。如:
1
2
3
4
5
|
var
a =
'globlal scope'
;
(
function
() {
var
a =
'local scope'
;
eval(
'console.log(a)'
);
}());
|
此处打印出的结果是 ‘local scope’。
需要注意的是,这里说明的是直接调用。如果 eval 被赋值到某个别名中,则为非直接调用。那么它就与 Function 构造函数运行方式相同,在 Global 作用域内执行代码。如:
1
2
3
4
5
6
|
var
a =
'globlal scope'
;
(
function
() {
var
a =
'local scope'
;
var
myEval = eval;
myEval(
'console.log(a)'
);
}());
|
此处打印出的结果是 ‘globlal scope’。
因此,直接的 eval 调用是 Wind.js 所预期的结果,它必须这用 eval 来达到功能实现的目的。
特殊说明:直接与非直接 eval 调用时 ES5 标准定义的,因此它有不同时代的浏览器实不一致问题。显然这也不是 Wind.js 所希望的。
四、为什么没有将 eval 封装在 Wind.js 的某个函数内,不必让用户知道它的存在呢?
同上分析,这么做也是不行的。因为,JS 的作用域是静态的,在代码被解析时,作用域已经被确定,运行时无非被修改(只有 JS 引擎级别 API 才可以动态修改作用域)。如果这么做,eval 所处的作用域将为封装它的函数所在作用域,而不是此函数被执行时的作用域,同样会出现非预期问题。如:
1
2
3
4
5
6
7
8
|
var
a =
'globlal scope'
;
function
run(code) {
return
eval(code);
}
(
function
() {
var
a =
'local scope'
;
run(
'console.log(a)'
);
}());
|
此处打印出的结果是 ‘globlal scope’。
因此,希望将 eval 封装起来使用是行不通的,那样做将无法获取代码所在作用域中变量值。
Wind.js 就说这么多,如果有兴趣,请访问它的 官网 ,或者在微博咨询作者 @老赵 。
下面开始简要说说 berserkJS。
五、berserkJS 是干什么的,原理是什么?
berserkJS 是一个前端页面性能自动化分析工具,它的特点是可以使用 JS 操作页面导向,获取所需性能数据,编写自己的检测分析规则。它基于 QT 开发,理论上可以跨平台使用,前提是在目标平台编译并部署 QT 运行环境。
针对 berserkJS 编写检测脚本十分简单,因为它是使用 JS 语法的,比如获取某页面的网络性能数据仅需要几行代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 开启监听
App.netListener(
true
);
// 访问页面
App.webview.open(
'http://weibo.com'
);
// 页面加载完成后获取数据
App.webview.addEventListener(
'load'
,
function
() {
// 获取数据并序列化
var
data = JSON.stringify(App.networkData(), undefined, 2);
// 写入文件
App.writeFile(App.path +
'demo1.txt'
, data);
// 关闭监听
App.netListener(
false
);
// 退出应用
App.close();
});
|
如果你细心可以发现,berserkJS 内写文件语法是同步的(node.js 是异步的),对于 webview 事件监听则是异步的。事实上,berserkJS 作为工具程序,追求的是编码简洁易维护调整,而非高性能(又不用它作为服务组件)。因此,实现时尽可能避免如今非常流行的异步 io 语法设计,将可能实现为同步的 API 都做了同步实现,如 readFile、writeFile、httpReuqest、process 等都为强制同步实现。
但是,唯独 webview 的事件机制无法采用同步 API(同步 UI 事件编码的想法简直是太匪夷所思鸟),这导致使用 berserkJS 在根据 UI 事件模拟用户操作时必须使用异步编码方法。它与最初追求的 ”编码简洁易维护调整“ 理念背道而驰,毕竟异步代码容易被拆的很散,无法维护。
比如一段登录微博账号的自动化操作代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 实现微博登录
var
loginWeibo =
function
() {
w.removeEventListener(
'load'
, loginWeibo);
// 输入账号等 1s 因为表单是异步的
setTimeout(
function
() {
w.execScript(
function
() {
// 输入测试账号密码
document.querySelectorAll(
'input[node-type=loginname]'
)[0].value =
'xxx'
;
document.querySelectorAll(
'input[node-type=password]'
)[0].value =
'123'
;
});
// 点下登录按键
w.sendMouseEvent(w.elementRects(
'a[node-type=submit]'
)[0]);
// 开启网络监听
w.netListener(
true
);
// ...
}
var
w = App.webview;
w.addEventListener(
'load'
, loginWeibo);
|
由于页面内登陆模块异步渲染等原因,登录微博操作变成一个异步执行流,回调嵌套使代码变得很凌乱。此时,berserkJS 急需一个异步控制类库来解决由于 UI 事件异步以及页面实现异步等原因导致的代码组织分散问题, 自然 Wind.js 就成了一个很好的选择。
叽歪:至于 berserkJS 的再具体功能内容请看官们去看下其 文档页 。
六、 berserkJS 中无缝使用 Wind.js 以及原理
前戏终于整完,絮絮叨叨好大一坨,现在终于到了关键点,两者怎么结合使用。
在 berserkJS 中挂载 Wind.js 十分简单:
1、你可以通过 App.httpRequest 方法抓取远程的 Wind.js 源码并用 eval 执行它。
2、也可以使用 App.readFile 方法读取本地 Wind.js 源码并用 eval 执行它。
3、当然,对于本地源码你还可以调用 App.loadScript 方法将本地 Wind.js 直接挂载进 berserkJS 内。
挂着完成 Wind.js 后,使用 $await 的辅助功能函数 Wind.Async.onEvent 就可以流程化 App.webview 的异步事件代码了。
比如,有这样一个检测需求:先登录到微博页面,点击某个连接,然后每隔 200 ms 对页面渲染情况经行截图保存,持续截取一段时间的页面渲染情况。如果使用原始代码编写,片段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// blablabla …… 之前还有些常量
var
COUNT = 15;
var
SCREEN = {width: 960, height: 600};
var
DELAY = 10000;
var
TIMEOUT = 200;
var
num = 0;
var
timeId = 0;
var
w = App.webview;
var
toImg =
function
() {
if
(num >= COUNT) {
clearInterval(timeId);
App.close();
return
;
}
num++;
w.saveImage((isBp ? BP_PATH : NOBP_PATH) + (num < 10 ? (
'0'
+ num) : num) +
'.png'
,
'png'
,
'100'
, {
x:0, y:0, width: SCREEN.width, height: SCREEN.height
});
};
var
clickLink =
function
() {
toImg();
w.sendMouseEvent(w.elementRects(
'a[bpfilter="main"][action-type="leftNavItem"]'
)[1]);
timeId = setInterval(toImg, TIMEOUT);
}
w.setCookiesFromUrl(App.readFile(WB_COOKIE),
'http://www.weibo.com'
);
w.open(isBp ? wb_bp : wb_nobp);
w.addEventListener(
'load'
,
function
(url) {
setTimeout(clickLink, DELAY);
});
|
原本 toImg 函数仅需实现保存页面截图功能,但是由于执行异步,它必须耦合一些截图次数的判断以及对定时器处理的脏逻辑。同样,clickLink 本应是实现模拟点击操作的而已,为了截图和启用定时,它也被迫加入其它代码逻辑。
加入 Wind.js 后,代码情况得以改善:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// 读取远程 Wind.js 源码字符串并 eval 执行。
// 提醒:其中 URL 是存在的,仅为代码示意
eval(App.httpRequest(
'get'
,
'http://www.windjs.org/wind.7.0.js'
));
// blablabla …… 一些常量定义
var
COUNT = 15;
var
SCREEN = {width: 960, height: 600};
var
TIMEOUT = 200;
var
timeId = 0;
var
num = 0;
var
w = App.webview;
var
toImg =
function
() {
w.saveImage((isBp ? BP_PATH : NOBP_PATH) + (num < 10 ? (
'0'
+ num) : num) +
'.png'
,
'png'
,
'100'
, {
x:0, y:0, width: SCREEN.width, height: SCREEN.height
});
};
w.setCookiesFromUrl(App.readFile(WB_COOKIE),
'http://www.weibo.com'
);
w.open(isBp ? wb_bp : wb_nobp);
var
test = eval(Wind.compile(
'async'
,
function
() {
$await(Async.onEvent(w,
'load'
));
$await(Async.sleep(DELAY));
toImg(num);
w.sendMouseEvent(w.elementRects(
'a[bpfilter="main"][action-type="leftNavItem"]'
)[1]);
for
(
var
i = 0; i < COUNT; ++i) {
$await(Wind.Async.sleep(TIMEOUT));
toImg(++num);
}
App.close();
}));
test().start();
|
toImg 函数功能变得纯粹,clickLink 完全可以去除原本的无用封装,蜕变为简明的 sendMouseEvent 调用。
同时,很奇妙不是么?Wind.Async.onEvent 对于 App.webview 事件是天然支持的。
显然这不是实现这两个完全不同功能方向的库事前商量好的。实际原因是由于 Wind.Async.onEvent 内会检测对象是否存在 W3C DOM 的标准事件监听方法 addEventListener。如果有,则将此操作进行异步流程控制。
恰巧,berserkJS 的 App.webview 事件处理方法,完全遵从了标准事件 API 设计,所以可被 Wind.js 无缝处理。
显然我们该庆幸 berserkJS 为 webview 对象实现了标准 API,如果同 JQuery 一样使用 on、bind 之类简化 API 名,自然就无法享受到与 Wind.js 天然衔接的好处。