Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

高性能Javascript阅读笔记 #15

Open
plh97 opened this issue Mar 31, 2018 · 0 comments
Open

高性能Javascript阅读笔记 #15

plh97 opened this issue Mar 31, 2018 · 0 comments
Assignees
Labels
javaScript 关于js的一些事 博客 写一些前端技术记录 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思

Comments

@plh97
Copy link
Owner

plh97 commented Mar 31, 2018

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

没有比页面点击没有响应更能令用户烦心的事情了。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条优化网站建议

@plh97 plh97 self-assigned this Mar 31, 2018
@plh97 plh97 added 博客 写一些前端技术记录 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思 labels Mar 31, 2018
@plh97 plh97 added the javaScript 关于js的一些事 label Apr 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javaScript 关于js的一些事 博客 写一些前端技术记录 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思
Projects
None yet
Development

No branches or pull requests

1 participant