高性能Javascript阅读笔记

avatarplhDigital nomad

第六章:快速响应的用户界面

没有比页面点击没有响应更能令用户烦心的事情了。JavaScript是单线程,js脚本在解析过程中,用户点击界面没有响应。 下面这个例子,用户点击按钮的时候,他会触发UI触发两个进程,一个考虑css有没有添加:active等伪类属性,如果有就改变它的样式,另一个进程是js进程,即如下位于<script>标签内的代码,js代码创建一个新的div元素最后将他appendChild到body里面,这个时候会引发页面重绘+重排,进程如图 image

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <button onclick="handleClick()">click me</button>
  <script>
    function handleClick (){
      const div = document.createElement('div');
      div.innerHTML = 'Clicked!'
      document.body.appendChild(div)
    }
</script>
</body>
</html>

定时器基础(从定时器的角度去了解浏览器页面进程,GUI线程以及js线程和setTimeou线程以及事件线程组成的线程执行的先入后出的执行顺序)

对于js来说,多久运行时间算久,事实证明,超过100ms的都会引起用户的操作不适应,因此超过100ms就算久,那么应对业务需要,定时器应用而生。 下面这段代码,将在250ms后在UI队列中插入一个执行test()函数的JavaScript任务。

function test(){
  console.log('settimeout');
}
setTimeout(test,250);

总之,任何情况下创造一个定时器会造成UI线程暂停,因此定时器会重置所有有关浏览器的限制,包括长时间运行的脚本定时器,调用栈也在定时器的代码中重置为0,这一特性使得定时器成为长时间运行JavaScript代码理想的跨浏览器解决方案。setTimeoutsetInterval几乎相同,除了前者重复添加js任务到UI队列中,。他们最主要的区别就是如果UI队列中已经存在由同一个setInterval创建的任务,那么后续任务不会被添加到UI队列中。 尝试运行如下代码

setInterval(()=>console.log(1),0);

进程,线程简单说明 JavaScript是单线程的,这个是由于为了防止一个dom元素被绘制的时候同时又被修改 js单线程以及css对页面的渲染,这两个呈现互斥关系,css渲染则js被暂时挂起,js运行的时候,css渲染又被挂起,他们共同在UI多线程中来回切换,按顺序执行,其中还有setTimeout的定时器线程,setTimeout是独立于js单线程之外的一个线程,因此js在由setTimeout的延迟执行的时候才不会被阻塞后面的代码继续执行,这样子的js配合另一个setTimeout线程才能实现异步函数Promisse等的能力, 一般情况下浏览器至少有三个常驻线程(前三个)

  • GUI渲染线程(渲染页面)
  • JS引擎线程(处理脚本)
  • 事件触发线程(控制交互)
  • setTimeout(延迟执行)
  • AJAX线程(后台交互)

从下面这段代码了解js单线程与setTimeout线程执行关系,

setTimeout(function(){
    console.log('timer');
}, 0);
console.time('js耗时:')
for(var i = 0; i < 1e4; i++){
    console.log(1);
}
console.timeEnd('js耗时:')

image 为什么这样呢? 因为setTimeout已经被放入另外一个线程中执行,所以定时器的存在才不会对js单线程造成阻塞。所以setTimeout的延迟时间无论被设置成多少,他都会被放在js这个线程之后再执行。也许你会怀疑,因为某些浏览器默认setTimeout执行时间默认最少16ms,假设我将setTimeout后面的js代码执行1w次循环,浏览器耗时7s+才循环完毕。 但看上图我截图的,timer依然在他之后输出。这就说明了setTimeout必然是被放在了另外一个独立于js线程之外的线程,只有在js线程执行完毕后才回去执行setTimeout线程,(chrome的每一个网页都是一个独立的进程,因此才会在一个网页卡死后,另一个网页照样过可以流程运行打开,同时也防止了不同网页之间的相互干扰,因为不同进程之间在chrome里面被保护以至于他们之间相互隔离)

同样的道理,通过上面的例子可以看出,定时器所设置的延迟时间 的计算规则,它是从整一段js代码执行完毕之后开始计算的(包括我那段用了7s来循环一万次的代码,);定时器通过浏览器的时间戳来在js主线程代码执行完毕后延迟0s再执行。所以任何不报错代码但是执行时间很长的代码放入setTimeout中不会造成js阻塞。

事件循环,

通常发生在当你为浏览器滚动添加事件监听的时候,浏览器会高频触发事件,但是同样的道理,事件监听里面的代码也是被放入另一个事件触发线程之中,只有当你document触发屏幕滚动事件的时候才会触发这段代码,这和setTimeout有点不一致,但是和他的兄弟setInterval(间隔固定时间执行js代码)却很类似。他们都和js主线程相独立开来。

Node.js的Event Loop

image

hahah上面这张图好有意思,v8威武

这个另外写,你也可以看阮一峰的再谈Event Loop,或者nodejs官方介绍,也许以后我会再写吧。毕竟我自己也要去搞nodejs

document.addEventListener('scroll',()=>{
  console.log('scroll')
},false)

web Workers

web worker 的运行环境,由以下组成

  • navigator对象,4个属性,appName,appVersion,userAgent和platform。
  • 一个location对象,
  • 一个self对象,它指向window
  • 一个importScript()方法,用来加载worker所有用到的外部js文件(chrome v.65没看到)。
  • 所有ECMAscript方法,如Object,Array,Date等。
  • XMLHttpRequest构造器。
  • setTimeout() 和 setInterval()方法
  • 一个close()方法,他能立刻停止运行worker image

与web worker通信

他与网页通过实践接口进行通信 ,网页代码可以通过postMessage()方法给worker传递数据,

var work = new worker('code.js');
worker.onmessage = function(){
  alert('event data');
}
worker.postMessage('Nicholas');
// code.js内部代码
self.onmessage = function(event){
  self.postMessage('hello, ',+event.data+ "!");
}

最终的字符串在worker的onmessage事件处理器中构造完成。消息系统是网页和worker通讯的唯一途径。

worker加载外部文件,

worker内部由importScripts()方法加载外部文件,该方法可以同时接受多个文件。他的加载过程是阻塞的,但是由于worker在UI之外的线程运行,所以这种阻塞在这里却是优点,并不会造成UI响应,例如

// code.js内部代码
importScript('file.js','file2.js');
self.onmessage = function(){
  self.postMessage('hello, '+event.data+'!');
}

worker 的兼容性

// chrome 。。。。用不了,但看别人用的又没问题。。 image

worker 的实用价值,

原因,渲染dom的时候不能执行javascript代码,执行javascript代码的时候,UI界面会暂停响应。 那么和setTimeout相比,web worker脱离独立与UI进程,而setTimeout则被放入js接下来的UI线程,这个遵循先入后出原则,因此是会影响接下来的操作的,所以超过100ms的JSON解析都应该被放入web worker去解析,以免造成网页界面卡顿无响应现象发生。。。而任何小于100ms的代码都可以考虑放入setTimeout去执行,因为小于100ms,用户根本察觉不到,利用好setTimeout可以让js和谐运行,而运用好web worker则可以让UI线程中的队列和谐进行下去。 如果javascript代码执行时间很长,那么UI就会无响应,这就是所谓的页面卡死。Web Workers是 HTML5 提供的一个javascript多线程解决方案,我们可以将一些耗时的javascript代码交由web Worker运行而不冻结用户界面。也就是说web worker和UI界面是运行在不同线程中的。它可以被用来处理一些接近500kb的json文件数据,计算中。。。这种一般被放入另外的事件线程,要么放入setTimeout定时器线程,要么放入worker线程,因为他在UI线程之外,不影响任何web操作。

结论:js和用户界面更新在同一个进程中进行,因此一次只能处理一件事情。这就意味着js运行过程中,用户不能输入,高效管理UI进程就是要确保js不能运行太长时间。

  • 任何js代码都不应该超过100ms,过长会导致UI更新出现延迟,
  • js运行期间,浏览器响应用户交互行为存在差异,无论如何js长时间运行会导致用户体验变得混乱和脱节,
  • 定时器可以让代码延迟执行,它可以使得你的代码从长时间运行分解成一系列小的任务,
  • web worker 是新版浏览器支持的特性,它允许你在UI线程外部执行JavaScript代码,从而避免锁定UI。 web应用越复杂,积极主动的去管理UI进程就越重要,即使js代码再重要,也不能影响用户体验。

第七章Ajax

ajax是高性能JavaScript的基础,

数据传输

在不刷新页面前提下获取后台数据,从而更新dom内容

请求数据

五种常用技术用于向服务器请求数据:

  • XMLHttpRequest(XHR)
  • Dynamic script tag insertion 动态脚本注入
  • iframe
  • Comet
  • Multipart XHR 推荐使用XHR,动态脚本注入,Multipart XHR 不推荐使用iframe,Comet

XMLHttpRequest(XHR)

这里由jquery封装的ajax,有fetch(es6),有(axios,jsonp)等package, 这里就不过多介绍。

get 和 post

get请求的数据会被缓存起来,而post则不会,只有当你get请求的参数长度超过2048个字符的时候才应该使用post,尽管post比get安全,

动态脚本注入

var scriptElement = document.createElement('script');
scriptElement.src = 'https://amy-domain.com/js/lib.js'
document.head[0].appendChild(scriptElement);

但是和XML请求不同的是,你不能通过它设置请求的头信息,并且,参数传递也只能是get而不是post,并且她只能是纯粹的js代码,不能是json或者XML或者其他任何数据,无论任何代码都只能通过script.onload调用他们,尽管如此,这种技术速度却非常快,但是当他是不可控的时候,就是某种hack行为,因为无论这段动态注入的代码可以让网页重定向到其他网站,将用户密码发送出去,或者追踪用户操作并将他们发送到第三方服务器上面,因此引入的如果是外部不可控的js代码请格外小心。

Ajax 性能指南

一旦你确定了选择某种数据格式来传输,(建议用Graphql),那么接下来考虑其他优化方式

缓存数据

最快的Ajax请求就是没有请求,下面两种方法都可以

  • 在服务端,设置http头信息以确保你的响应会被浏览器缓存(便于维护)
  • 在客户端,吧获取的信息储存到本地,从而避免再次请求(一般是localStorage)(给你最大控制权)
设置http头

如果你希望响应被浏览器缓存,那么用get请求,还必须响应中加入正确的header

Expires: Mon, 28 July 2014 23:30:00 GMT

第八章 编程实践

####避免多重求值 js提取一个包含代码的字符串有4中方法

  • eval(),这个太经典不用说
  • Function()构造函数
const sum = new Function('arg1', 'arg2', 'return arg1 + arg2');
console.log(sum(1, 2));
  • setTimeout()
var sum1 = 1;
var sum2 = 1;
setTimeout('sum = sum1 + sum2', 0);
  • setInterval()
var sum1 = 1;
var sum2 = 1;
setInterval('sum = sum1 + sum2', 0);

以上4中方法都比直接求值要慢很多,因为每次都要创建一个新的解释器/编译器

应该使用Objecty/Array直接量

这个自己领悟

避免重复

例如下面这段代码,同时兼容ie和chrome

function addHandler(target,event,handler){
  if(target.addEventListener){
    target.addEventListener(event,handler,false);
  }esle{
    target.attachEvent('on'+event,handler)
  }
}

应该避免多次检查addEventListener是否支持。 如下所示:初始化的时候就检测是否支持IE,

const isIE = target.addEventListener; 
function addHandler(target,event,handler){
  if(isIE){
    target.addEventListener(event,handler,false);
  }esle{
    target.attachEvent('on'+event,handler)
  }
}

延迟加载

第一种消除函数中重复工作的方法是延迟加载,意味着信息在被加载之前不会做任何事情,在函数调用之前,没有必要判断该用那个方法去绑定或者取消绑定事件,采用延迟加载的函数版本如下:

function addHandler(target, event, handler) {
  // 复写现有函数
  if (target.addEventListener) {
    addHandler = function (target, event, handler) {
      target.addEventListener(event, handler, false);
    };
  } else {
    addHandler = function (target, event, handler) {
      target.attachEvent(event, handler, false);
    };
  }
  addHandler(target, event, handler);
}

这个函数实现了延迟加载的加载模式,这两个方法第一次被调用中,先决定使用哪种方法调用,随后函数会被包含正确操作的新函数覆盖,最后一步调用新函数,随后每次调用addHandler()都不会再做检测,因为检测代码以及被新函数覆盖。 调用延迟加载函数的时候,第一次事件较长,因为它必须检测接着调用另一个函数完成任务,但随后调用函数会更快,因为不需要在做检测。当一个函数在页面不会立即调用执行时,延迟加载是最好的选择。

条件预加载

他会在脚本预加载期间提前检测,而不会等到函数被调用,检测操作依然只有一次,只是他在过程中来的更早,例如:

const addHandler = document.body.addEventListener ?
  function (target, event, handler) {
    target.addEventListener(event, handler, false);
  } :
  function (target, event, handler) {
    target.attachEvent(`on${event}`, handler);
  };

这个例子先检查addEventListener()是否存在,然后根据结果指定选择最佳函数,如果他们存在的话,三元运算符号,返回指定函数,这个三元函数在一开始就判断了,所以称之为条件预加载, 条件预加载确保所有函数调用消耗的时间相同,其代价是需要在脚本加载时就检测,而不是在加载后。条件预加载适用于一个函数马上就要用到,并且在整个页面的生命周期中频繁出现的场合。

使用速度快的部分

尽管js经常被指责运行慢,但它这个语言在某部分运行速度快的让人难以置信。

位操作符
1&2 //两个操作数对应位都是1,该位返回1
1|2 //两个操作数对应位一个是1,该位返回1
1^2 //两个操作数对应位只有一个是1,该位返回1
~1    // 遇0则返回1,反之亦然
const newArr = [1, 2, 3, 4, 5, 6].map((arr, i) => {
  if (i & 2) {
    return 'event';
  }
  return 'odd';
});
console.log(newArr);

这样以上代码可被改写

const newArr = [1, 2, 3, 4, 5, 6].map((arr, i) => {
  if (i % 2) {
    return 'event';
  }
  return 'odd';
});
console.log(newArr);

第十章 构建部署高性能JavaScript应用

这个建议看我翻译的Yahoo的35条优化网站建议