如何监听页面 DOM 变动并高效响应

2017/08/04 · JavaScript
· DOM

原文出处:
hijiangtao   

最近在做 chrome
插件开发,既然是插件那就难免不对现有页面做一些控制,比如事件监听、调整布局、对
DOM
元素的增删改查等等。其中有一个需求比较有意思,便整理一下顺便把涉及到的知识点复习一遍。

需求是这样的:在一个包含懒加载资源以及动态 DOM
元素生成的页面中,需要针对页面中存在的元素添加属性显示标签。

理解 JavaScript 的 async/await

2017/03/08 · JavaScript
· 1 评论 ·
async,
await

原文出处: 边城   

随着 Node 7
的发布,越来越多的人开始研究据说是异步编程终级解决方案的
async/await。我第一次看到这组关键字并不是在 JavaScript 语言里,而是在
c# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5
以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP
系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》
中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await
都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await
实现离不开 Task 或
Task<Result>
类,而 JavaScript 的 async/await
实现,也离不开Promise。

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

WebAssembly 对比 JavaScript 及其使用场景

2018/05/17 · JavaScript
· 滚动

原文出处: Alexander
Zlatkov   译文出处:Troland   

从 DOM 变动事件监听说起

首先假设大家已经知道 JavaScript
中事件的发生阶段(捕获-命中-冒泡),附上一张图带过这个内容,我们直接进入寻找解决方法的过程。

澳门微尼斯人手机版 1

Graphical representation of an event dispatched in a DOM tree using
the DOM event
flow

开始的时候我一直在 window
状态改变涉及到的事件中寻找,一圈搜寻下来发现也就 onload
事件最接近了,所以我们看看 MDN 对该事件的定义:

The load event is fired when a resource and its dependent resources
have finished loading.

怎么理解资源及其依赖资源已加载完毕呢?简单来说,如果一个页面涉及到图片资源,那么
onload
事件会在页面完全载入(包括图片、css文件等等)后触发。一个简单的监听事件用
JavaScript 应该这样书写(注意不同环境下 load 和 onload 的差异):

<script> window.addEventListener(“load”, function(event) {
console.log(“All resources finished loading!”); }); // or
window.onload=function(){ console.log(“All resources finished
loading!”); }; // HTML < body onload=”SomeJavaScriptCode”> //
jQuery $( window ).on( “load”, handler ) </script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
  window.addEventListener("load", function(event) {
    console.log("All resources finished loading!");
  });
  
  // or
  window.onload=function(){
    console.log("All resources finished loading!");
  };
  
  // HTML
< body onload="SomeJavaScriptCode">
  
  // jQuery
  $( window ).on( "load", handler )
</script>

当然,说到 onload 事件,有一个 jQuery 中相似的事件一定会被提及——
ready 事件。jQuery 中这样定义这个事件:

Specify a function to execute when the DOM is fully loaded.

需要知道的是 jQuery 定义的 ready 事件实质上是为 DOMContentLoaded
事件设计的,所以当我们谈论加载时应该区分的事件其实是 onload(接口
UIEvent) 以及 DOMContentLoaded(接口 Event),MDN 这样描述
DOMContentLoaded

当初始HTML文档被完全加载和解析时,DOMContentLoaded
事件被触发,而无需等待样式表、图像和子框架完成加载。另一个不同的事件
load 应该仅用于检测一个完全加载的页面。

所以可以知道,当一个页面加载时应先触发 DOMContentLoaded 然后才是
onload. 类似的事件及区别包括以下几类:

  • DOMContentLoaded:
    当初始HTML文档被完全加载和解析时,DOMContentLoaded
    事件被触发,而无需等待样式表、图像和子框架完成加载;
  • readystatechange: 一个document 的 Document.readyState
    属性描述了文档的加载状态,当这个状态发生了变化,就会触发该事件;
  • load: 当一个资源及其依赖资源已完成加载时,将触发load事件;
  • beforeunload:
    当浏览器窗口,文档或其资源将要卸载时,会触发beforeunload事件。
  • unload: 当文档或一个子资源正在被卸载时, 触发 unload事件。

细心点会发现上面在介绍事件时提到了 UIEvent 以及
Event,这是什么呢?这些都是事件——可以被 JavaScript
侦测到的行为。其他的事件接口还包括 KeyboardEvent / VRDisplayEvent
(是的,没错,这就是你感兴趣且熟知的那个
VR)等等;如果在搜索引擎中稍加搜索,你会发现有些资料里写到事件可以分为以下几类:

  • UI事件
  • 焦点事件
  • 鼠标与滚轮事件
  • 键盘与文本事件
  • 复合事件
  • 变动事件
  • HTML5 事件
  • 设备事件
  • 触摸与手势事件

但这样写实在有些凌乱,其中一些是 DOM3
定义的事件,有一些是单独列出的事件,如果你觉得熟悉那么你会发现这是
JavaScript 高级程序设计里的叙述模式,在我看来,理解这些事件可以按照 DOM3
事件以及其他事件来做区分:其中,DOM3 级事件规定了以下几类事件 – UI 事件,
焦点事件, 鼠标事件, 滚轮事件, 文本事件, 键盘事件, 合成事件, 变动事件,
变动名称事件; 而剩下的例如 HTML5 事件可以单独做了解。而刚开始提到的
Event 作为一个主要接口,是很多事件的实现父类。有关 Web API
接口可以在这里查到,里面可以看到有很多
Event 字眼。

好吧,事件说了这么多,我们还是没有解决刚开始提出的问题,如果监听页面中动态生成的元素呢?想到动态生成的元素都是需要通过网络请求获取资源的,那么是否可以监听所有
HTTP 请求呢?查看 jQuery 文档可以知道每当一个Ajax请求完成,jQuery
就会触发 ajaxComplete 事件,在这个时间点所有处理函数会使用
.ajaxComplete() 方法注册并执行。但是谁能保证所有 ajax 都从 jQuery
走呢?所以应该在变动事件中做出选择,我们来看看 DOM2
定义的如下变动事件:

  • DOMSubtreeModified:
    在DOM结构发生任何变化的时候。这个事件在其他事件触发后都会触发;
  • DOMNodeInserted:
    当一个节点作为子节点被插入到另一个节点中时触发;
  • DOMNodeRemoved: 在节点从其父节点中移除时触发;
  • DOMNodeInsertedIntoDocument:
    在一个节点被直接插入文档或通过子树间接插入文档之后触发。这个事件在
    DOMNodeInserted 之后触发;
  • DOMNodeRemovedFromDocument:
    在一个节点被直接从文档移除或通过子树间接从文档移除之前触发。这个事件在
    DOMNodeRemoved 之后触发;
  • DOMAttrModified: 在特性被修改之后触发;
  • DOMCharacterDataModified: 在文本节点的值发生变化时触发;

所以,用 DOMSubtreeModified 好像没错。师兄旁边提醒,用
MutationObserver, 于是又搜到了一个新大陆。MDN 这样描述
MutationObserver:

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

DOM3 事件规范中的 Mutation 事件可以被简单看成是 DOM2 事件规范中定义的
Mutation 事件的一个扩展,但是这些都不重要了,因为他们都要被
MutationObserver 替代了。好了,那么来详细介绍一下 MutationObserver
吧。文章《Mutation Observer
API》对
MutationObserver
的用法介绍的比较详细,所以我挑几点能直接解决我们需求的说一说。

既然要监听 DOM 的变化,我们来看看 Observer 的作用都有哪些:

它等待所有脚本任务完成后,才会运行,即采用异步方式。

它把 DOM 变动记录封装成一个数组进行处理,而不是一条条地个别处理 DOM
变动。

它既可以观察发生在 DOM 的所有类型变动,也可以观察某一类变动。

MutationObserver
的构造函数比较简单,传入一个回调函数即可(回调函数接受两个参数,第一个是变动数组,第二个是观察器实例):

let observer = new MutationObserver(callback);

1
let observer = new MutationObserver(callback);

观察器实例使用 observe 方法来监听, disconnect
方法停止监听,takeRecords 方法来清除变动记录。

let article = document.body; let options = { ‘childList’: true,
‘attributes’:true } ; observer.observe(article, options);

1
2
3
4
5
6
7
8
let article = document.body;
 
let  options = {
  ‘childList’: true,
  ‘attributes’:true
} ;
 
observer.observe(article, options);

observe 方法中第一个参数是所要观察的变动 DOM
元素,第二个参数则接收所要观察的变动类型(子节点变动和属性变动)。变动类型包括以下几种:

  • childList:子节点的变动。
  • attributes:属性的变动。
  • characterData:节点内容或节点文本的变动。
  • subtree:所有后代节点的变动。

想要观察哪一种变动类型,就在 option 对象中指定它的值为
true。需要注意的是,如果设置观察 subtree 的变动,必须同时指定
childList、attributes 和 characterData 中的一种或多种。disconnect
方法和 takeRecords 方法则直接调用即可,无传入参数。

好的,我们已经搞定了 DOM
变动的监听,将代码刷新一下看下效果吧,因为页面由很多动态生成的商品组成,那么我应该在
body 上添加变动监听,所以 options 应该这样设置:

var options = { ‘attributes’: true, ‘subtree’: true }

1
2
3
4
5
var options = {
‘attributes’: true,
‘subtree’: true
}
 

咦?页面往下拉一小点就触发了 observer 几十次?这样 DOM
哪吃得消啊,查看了页面的变动记录发现每次新进的资源底层都调用了
Node.insertBefore()
方法…

async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而
await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个
function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async
函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async
函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个
async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

WebAssembly 对比 JavaScript 及其使用场景

这是 JavaScript 工作原理的第六章。

现在,我们将会剖析 WebAssembly 的工作原理,而最重要的是它和 JavaScript
在性能方面的比对:加载时间,执行速度,垃圾回收,内存使用,平台 API
访问,调试,多线程以及可移植性。

我们构建网页程序的方式正面临着改革-这只是个开始而我们对于网络应用的思考方式正在发生改变。

再聊聊 JavaScript 中的截流/节流函数

现在遇到的一个麻烦是, DOM
变动太频繁了,如果每次变动都监听那真是太耗费资源了。一个简单的解决办法是我就放弃监听了,而采用
setInterval
方法定时执行更新逻辑。是的,虽然方法原始了一点,但是性能上比 Observer
“改进”了不少。

这个时候,又来了师兄的助攻:“用用截流函数”。记起之前看《JavaScript
语言精粹》的时候看到是用
setTimeout 方法自调用来解决 setInteval
的频繁执行吃资源的现象,不知道两者是不是有关联。网上一查发现有两个“jie流函数”。需求来自于这里:

在前端开发中,页面有时会绑定scroll或resize事件等频繁触发的事件,也就意味着在正常的操作之内,会多次调用绑定的程序,然而有些时候javascript需要处理的事情特别多,频繁出发就会导致性能下降、成页面卡顿甚至是浏览器奔溃。

如果重复利用 setTimeout 和 clearTimeout
方法,我们好像可以解决这个频繁触发的执行。每次事件触发的时候我首先判断一下当前有没有一个
setTimeout 定时器,如果有的话我们先将它清除,然后再新建一个 setTimeout
定时器来延迟我的响应行为。这样听上去还不错,因为我们每次都不立即执行我们的响应,而频繁触发过程我们又能保持响应函数一直存在(且只存在一个),除了会有些延迟响应外,没什么不好的。是的这就是截流函数(debounce),有一篇博客用这个小故事介绍它:

形像的比喻是橡皮球。如果手指按住橡皮球不放,它就一直受力,不能反弹起来,直到松手。debounce
的关注点是空闲的间隔时间。

在我的业务中,在 observer 实例中调用下面写的这个截流函数就可以啦

/** * fn 执行函数 * context 绑定上下文 * timeout 延时数值 **/ let
debounce = function(fn, context, timeout) { let timer; //
利用闭包将内容传递出去 return function() { if (timer) { // 清除定时器
clearTimeout(timer); } // 设置一个新的定时器 timer =
setTimeout(function(){ fn.apply(context, arguments) }, timeout); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* fn 执行函数
* context 绑定上下文
* timeout 延时数值
**/
let debounce = function(fn, context, timeout) {
let timer;
    
    // 利用闭包将内容传递出去
return function() {
if (timer) {
    // 清除定时器
clearTimeout(timer);
}
// 设置一个新的定时器
timer = setTimeout(function(){
fn.apply(context, arguments)
}, timeout);
}
}

当然,解决了自己的问题,但还有一个概念没有说到——“节流函数”。同一篇博文里也使用了一个例子来说明它:

形像的比喻是水龙头或机枪,你可以控制它的流量或频率。throttle
的关注点是连续的执行间隔时间。

函数节流的原理也挺简单,一样还是定时器。当我触发一个时间时,先setTimout让这个事件延迟一会再执行,如果在这个时间间隔内又触发了事件,那我们就清除原来的定时器,再setTimeout一个新的定时器延迟一会执行。函数节流的出发点,就是让一个函数不要执行得太频繁,减少一些过快的调用来节流。这里引用
AlloyTeam
的节流代码实现来解释:

// 参数同上 var throttle = function(fn, delay, mustRunDelay){ var timer
= null; var t_澳门微尼斯人手机版,start; return function(){ var context = this, args =
arguments, t_curr = +new Date(); // 清除定时器 clearTimeout(timer); //
函数初始化判断 if(!t_start){ t_start = t_curr; } //
超时(指定的时间间隔)判断 if(t_curr – t_start >= mustRunDelay){
fn.apply(context, args); t_start = t_curr; } else { timer =
setTimeout(function(){ fn.apply(context, args); }, delay); } }; };

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
// 参数同上
var throttle = function(fn, delay, mustRunDelay){
var timer = null;
var t_start;
return function(){
var context = this, args = arguments, t_curr = +new Date();
// 清除定时器
clearTimeout(timer);
// 函数初始化判断
if(!t_start){
t_start = t_curr;
}
// 超时(指定的时间间隔)判断
if(t_curr – t_start >= mustRunDelay){
fn.apply(context, args);
t_start = t_curr;
}
else {
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
}
};
};

当然,AlloyTeam 那篇文章将这里所说的截流函数作为节流函数的 V1.0
版本,你也可以这样认为。毕竟,设置了必然触发执行的时间间隔(即
mustRunDelay
函数),可以使得截流函数不会在“疯狂事件”情况下无止境的循环下去。

Observer
和截流函数一结合,问题解决啦嘿嘿。当然还有很多坑,下次再开一篇说说吧。

async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return
语句返回我们想要的值,但是如果真是这样,似乎就没 await
什么事了。所以,写段代码来试试,看它到底会返回什么:

JavaScript

async function testAsync() { return “hello async”; } const result =
testAsync(); console.log(result);

1
2
3
4
5
6
async function testAsync() {
    return "hello async";
}
 
const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

Shell

c:\var\test> node –harmony_async_await . Promise { ‘hello async’
}

1
2
c:\var\test> node –harmony_async_await .
Promise { ‘hello async’ }

所以,async 函数返回的是一个 Promise
对象。从文档中也可以得到这个信息。async
函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise
对象,如果在函数中 return 一个直接量,async 会把这个直接量通过
Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await
获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个
Promise 对象,就像这样

JavaScript

testAsync().then(v => { console.log(v); // 输出 hello async });

1
2
3
testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async
函数没有返回值,又该如何?很容易想到,它会返回
Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async
函数,它会立即执行,返回一个 Promise
对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise
对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

首先,认识下 WebAssembly 吧

WebAssembly(又称 wasm) 是一种用于开发网络应用的高效,底层的字节码。

WASM 让你在其中使用除 JavaScript 的语言以外的语言(比如 C, C++, Rust
及其它)来编写应用程序,然后编译成(提早) WebAssembly。

构建出来的网络应用加载和运行速度都会非常快。

参考

  • 1 赞 3 收藏
    评论

澳门微尼斯人手机版 2

await 到底在等啥

一般来说,都认为 await 是在等待一个 async
函数完成。不过按语法说明,await
等待的是一个表达式,这个表达式的计算结果是 Promise
对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async
函数的返回值——这也可以说是 await 在等 async
函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等
Promise 对象,它可以等任意表达式的结果,所以,await
后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

JavaScript

function getSomething() { return “something”; } async function
testAsync() { return Promise.resolve(“hello async”); } async function
test() { const v1 = await getSomething(); const v2 = await testAsync();
console.log(v1, v2); } test();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getSomething() {
    return "something";
}
 
async function testAsync() {
    return Promise.resolve("hello async");
}
 
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
 
test();

加载时间

为了加载 JavaScript,浏览器必须加载所有文本格式的 js 文件。

浏览器会更加快速地加载 WebAssembly,因为 WebAssembly
只会传输已经编译好的 wasm 文件。而且 wasm
是底层的类汇编语言,具有非常紧凑的二进制格式。

await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise
对象,或者其它值,然后呢?我不得不先说,await
是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await
表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await
就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到
resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async
函数中的原因。async
函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise
对象中异步执行。

执行速度

如今 Wasm 运行速度只比原生代码
20%。无论如何,这是一个令人惊喜的结果。它是这样的一种格式,会被编译进沙箱环境中且在大量的约束条件下运行以保证没有任何安全漏洞或者使之强化。和真正的原生代码比较,执行速度的下降微乎其微。另外,未来将会更加快速。

更让人高兴的是,它具备很好的浏览器兼容特性-所有主流浏览器引擎都支持
WebAssembly 且运行速度相关无几。

为了理解和 JavaScript 对比,WebAssembly
的执行速度有多快,你应该首先阅读之前的 JavaScript
引擎工作原理的文章。

让我们快速浏览下 V8 的运行机制:

澳门微尼斯人手机版 3

V8 技术:懒编译

左边是 JavaScript 源码,包含 JavaScript
函数。首先,源码先把字符串转换为记号以便于解析,之后生成一个语法抽象树。

语法抽象树是你的 JavaScript 程序逻辑的内存中图示。一旦生成图示,V8
直接进入到机器码阶段。你基本上是遍历树,生成机器码然后获得编译后的函数。这里没有任何真正的尝试来加速这一过程。

现在,让我们看一下下一阶段 V8 管道的工作内容:

澳门微尼斯人手机版 4

V8 管道设计

现在,我们拥有 TurboFan ,它是
V8 的优化编译程序之一。当 JavaScript 运行的时候,大量的代码是在 V8
内部运行的。TurboFan
监视运行得慢的代码,引起性能瓶颈的地方及热点(内存使用过高的地方)以便优化它们。它把以上监视得到的代码推向后端即优化过的即时编译器,该编译器把消耗大量
CPU 资源的函数转换为性能更优的代码。

它解决了性能的问题,但是缺点即是分析代码及辨别哪些代码需要优化的过程也是会消耗
CPU 资源的。这也即意味着更多的耗电量,特别是在手机设备。

但是,wasm 并不需要以上的全部步骤-它如下所示插入到执行过程中:

澳门微尼斯人手机版 5

V8 管道设计 + WASM

wasm
在编译阶段就已经通过了代码优化。总之,解析也不需要了。你拥有优化后的二进制代码可以直接插入到后端(即时编译器)并生成机器码。编译器在前端已经完成了所有的代码优化工作。

由于跳过了编译过程中的不少步骤,这使得 wasm 的执行更加高效。

发表评论

电子邮件地址不会被公开。 必填项已用*标注