一、语言

1)慎用全局变量

  当变量暴露在全局作用域中时,由于全局作用域比较复杂,因此查找会比较慢。

  并且还有可能污染window对象,覆盖之前所赋的值,发生意想不到的错误。

0 == ''     //true
0 == '0'    //true

3)简写

  简写的方式很多,此处只会列举其中的几种,例如用三目运算替代if-else语句,或用&&或||符号替代条件语句。

if (count > 1) {
  ++a;
} else {
  --a;
}
// 简写
count > 1 ? (++a) : (--a);

if (count) {
  ++a;
}
// 简写
count && (++a)

  利用ES6语法,可以用解构赋值,简洁明了。还有些小技巧包括用箭头函数表示回调,块级作用域变量等。

const { count } = obj;

  关于去除数组中的重复数,可以采用ES6最新的Set数据结构。

Array.from(new Set(arr));

4)减少魔法数

  魔法数是指意义不明的常量,例如直接在代码中使用一个数字1,其判断条件令人费解。

if(type == 1) { }

  而如果将该数字赋给一个语义化的常量后,就能明确其意图。

const ERROR_TYPE = 1;
if(type == ERROR_TYPE) { }

5)位运算

  用位运算取代纯数学操作,例如对2取模(digit%2)判断偶数与奇数。

if (digit & 1) {
  // 奇数(odd)
} else {
  // 偶数(even)
}

  位掩码技术,使用单个数字的每一位来判断选项是否成立。掩码中每个选项的值都是2的幂。

var OPTION_A = 1, OPTION_B = 2, OPTION_C = 4, OPTION_D = 8, OPTION_E = 16;
//用按位或运算创建一个数字来包含多个设置选项
var options = OPTION_A | OPTION_C | OPTION_D;
//接下来可以用按位与操作来判断给定的选项是否可用
//选项A是否在列表中
if(options & OPTION_A) {
  //...
}

  用按位左移(<<)做乘法,用按位右移做除法(>>),例如digit*2可以替换成digit<<2。

6)字符串拼接

  除了使用加号(+)或加等(+=)实现字符串拼接之外,还可以使用数组的join()和字符串的concat()方法。

["strick", "jane"].join("");
"strick".concat("jane");

  ES6提供的模板字面量是一种能够嵌入表达式的格式化字符串,也可以用来做字符串拼接。

str = "My name is \"" + name + "\". M y age is " + age + ".";       //传统拼接方式
str = `My name is "${name}". My age is ${age}.`;                 //模板字面量方式

7)正则优化

  正则优化包括:

  1. 减少分支数量,缩小分支范围;

  2. 使用非捕获数组;

  3. 只捕获感兴趣的文本以减少后期处理;

  4. 使用合适的量词;

  5. 化繁为简,分解复杂的正则。

8)惰性模式

  惰性模式用于减少每次代码执行时的重复性分支判断,通过对对象重定义来屏蔽原对象中的分支判断。

  惰性模式分为两种:第一种文件加载后立即执行对象方法来重定义,第二种是当第一次使用方法对象时来重定义。

var A = {};
//加载时 损失性能 第一次加载时 不损失性能
A.on = (function (dom, type, fn) {
  if (dom.addEventListener) {
    return function (dom, type, fn) {
      dom.addEventListener(type, fn, false);
    };
  } else if (dom.attachEvent) {
    return function (dom, type, fn) {
      dom.attachEvent("on" + type, fn);
    };
  } else {
    return function (dom, type, fn) {
      dom["on" + type] = fn;
    };
  }
})();
//加载时 不损失性能 第一次加载时 损失性能
A.on = function (dom, type, fn) {
  if (dom.addEventListener) {
    A.on = function (dom, type, fn) {
      dom.addEventListener(type, fn, false);
    };
  } else if (dom.attachEvent) {
    A.on = function (dom, type, fn) {
      dom.attachEvent("on" + type, fn);
    };
  } else {
    A.on = function (dom, type, fn) {
      dom["on" + type] = fn;
    };
  }
  //执行重定义on方法
  A.on(dom, type, fn);
};

9)使用缓存

  当执行for循环时,需要读取数组的长度,可以事先做缓存。

for (let i = 0, len = arr.length; i < len; i++) {}

  或者在事件处理程序或对象方法中缓存this指向。

var obj = {
  name: function () {
    let self = this;
  }
};
btn.addEventListener("click", function(event) {
  let self = this;
}, false);

10)记忆函数

  记忆函数是指能够缓存先前计算结果的函数,避免重复执行不必要的复杂计算,是一种用空间换时间的编程技巧。

  具体的实施可以有多种写法,例如创建一个缓存对象,每次将计算条件作为对象的属性名,计算结果作为对象的属性值。

  下面的代码用于判断某个数是否是质数(质数又叫素数,是指一个大于1的自然数,除了1和它本身外,不能被其它自然数整除的数),在每次计算完成后,就将计算结果缓存到函数的自有属性digits内。

function prime(number) {
  if (!prime.digits) {
    prime.digits = {};     //缓存对象
  }
  if (prime.digits[number] !== undefined) {
    return prime.digits[number];
  }
  var isPrime = false;
  for (var i = 2; i < number; i++) {
    if (number % i == 0) {
      isPrime = false;
      break;
    }
  }
  if (i == number) {
    isPrime = true;
  }
  return (prime.digits[number] = isPrime);
}
prime(87);
prime(17);
console.log(prime.digits[87]);     //false
console.log(prime.digits[17]);     //true

11)闭包

  通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在于闭包的作用域中,因此对象无法被销毁。

function outter(count) {
  count++;
  // 闭包
  function inner() {
    return count + 1;
  }
  return inner();
}

  这意味着闭包需要更多的内存开销。在脚本编程中,要小心地使用闭包。

  推荐将跨作用域的变量存储到一个局部变量中,然后直接访问该局部变量,如下所示,将count作为参数传递给inner()函数。

function outter(count) {
  count++;
  // 闭包
  function inner(count) {
    return count + 1;
  }
  return inner(count);
}

12)节流和去抖动

  节流(throttle)是指预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。适用于mousemove事件、window对象的resize和scroll事件。

function throttle(fn, wait) {
  let start = 0;
  return () => {
    const now = +new Date();
    if (now - start > wait) {
      fn();
      start = now;
    }
  };
}

  去抖动(debounce)是指当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。适用于文本输入的keydown事件,keyup事件,做autocomplete等。

function debounce(fn, wait) {
  let start = null;
  return () => {
    clearTimeout(start);
    start = setTimeout(fn, wait);
  };
}

  节流与去抖动最大的不同的地方就是在计算最后执行时间的方式上。著名的开源工具库underscore中有内置了两个方法。

二、应用

1)合理放置脚本

  脚本会阻塞页面渲染,直至全部下载并执行完成后,页面渲染才会继续。浏览器在解析到body元素之前,不会渲染页面的任何部分。

  把脚本放在页面顶部会导致明显的延迟,通常表现为空白页面。因此推荐将所有script元素尽可能放到body元素底部。

2)无阻塞脚本

  为了解决阻塞的问题,script元素新增了两个布尔属性,分别是延迟(defer)和异步(async)。

  1. defer:延迟脚本执行,直到文档解析完成。

  2. async:尽快执行脚本,不会阻塞文档解析。

<script src="scripts/jquery.js" defer></script>
<script src="scripts/jquery.js" async></script>

3)动态脚本

  用JavaScript动态创建script元素,文件的下载和执行过程不会阻塞页面其它进程。

var hm = document.createElement("script");
hm.src = "//www.pwstrick.com/hm.js";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);

4)图像上传

  在上传图像时,可将其转换成Base64,相当于将图像做成字符串传送到后台。在下面的示例中用到了FileReader对象。

var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(e) {
  var img = new Image();
  img.src = this.result;
  console.log(this.result);
};

  注意,Base64图像会比原图要大。

5)原生方法

  JavaScript引擎提供的原生方法总是最快的。因为原生方法存在于浏览器中,并且都是用低级语言编写的。

  这意味着它们会被编译成机器码,成为浏览器的一部分,不会像自己写的JavaScript代码那样受到各种限制。

  CSS查询被JavaScript原生支持并被jQuery发扬光大。jQuery的选择器引擎虽然很快,但是仍然比原生方法慢。

  推荐使用原生的querySelector和querySelectorAll()作为选择器。

6)本地缓存

  在HTML5的本地缓存出现之前,都喜欢用cookie缓存数据。但cookie数据量只有4KB左右,并且每次都会携带在HTTP首部中,如果使用cookie保存过多数据会带来性能问题。

  而localStoragesessionStorage数据量一般在2.5M到10M之间(大部分是5M),并且不参与和服务器之间的通信,因此比较容易实现网页或应用的离线化。

7)重排和重绘

  当DOM的变化影响了元素的几何属性(宽和高)将会发生重排(reflow),发生重排的情况如下所列。

  1. 添加或删除可见的DOM元素

  2. 元素位置改变

  3. 元素尺寸改变(包括外边距、内边距、边框宽度、宽、高等属性)

  4. 内容改变,例如文本改变或图片被不同尺寸的替换掉。

  5. 页面渲染器初始化。

  6. 浏览器窗口尺寸改变。

  完成重排后,浏览器会重新绘制受影响的部分到屏幕中,此过程为重绘(repaint)。

  下面代码看上去会重排3次,但其实只会重排1次,大多数浏览器通过队列化修改和批量显示优化重排版过程。

//渲染树变化的排队和刷新
var ele = document.getElementById('myDiv');
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';
ele.style.padding = '5px';

  但下列操作将会强迫队列刷新并要求所有计划改变的部分立刻应用:

offsetTop, offsetLeft, offsetWidth, offsetHeight 
scrollTop, scrollLeft, scrollWidth, scrollHeight 
clientTop, clientLeft, clientWidth, clientHeight 
getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle) 

  像offsetHeight属性需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确的值。

  最小化重绘和重排的方式有两种:

  1. cssText和class,cssText可以一次设置多个CSS属性。class也可以一次性设置,并且更清晰,更易于维护,但有前提条件,就是不依赖于运行逻辑和计算的情况。

  2. 批量修改DOM,包括隐藏元素display:none,修改后重新显示display:block;使用文档片段fragment,在片段上操作节点,再拷贝回文档;将原始元素拷贝到一个脱离文档的节点中(例如position:absolute),修改副本,完成后再替换原始元素。

8)定时器

  为了不让一些复杂的JavaScript任务阻塞线程,就需要将其让出线程的控制权,即停止执行,可以通过定时器实现。

  当函数运行时间太长时,可以把它拆分成一系列更小的步骤,把每个独立的方法放到定时器中回调,如下所示,其中arguments.callee是指当前正在执行的函数。

let tasks = [openDocumnet, writeText, closeDocument, updateUI];
setTimeout(function() {
  //执行下一个任务
  let task = tasks.shift();
  task();
  //检查是否还有其他任务
  if (tasks.length > 0) {
    setTimeout(arguments.callee, 25);
  }
}, 25);

9)动画

  JavaScript早期的动画是用定时器实现的,但随着浏览器功能的不断完善,出现了一种更新、性能更高的方法:requestAnimationFrame()

  requestAnimationFrame()会在重绘之前更新下一帧的动画,注意,回调函数自身必须再次调用requestAnimationFrame(),如下所示。

function step(timestamp) {
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}
window.requestAnimationFrame(step);

10)Ajax

  最快的Ajax请求是没有请求,即避免发送不必要的请求,例如:

  1. 在服务端,设置HTTP首部信息以确保响应会被浏览器缓存。

  2. 在客户端,把获取到的信息缓存到本地。

  其它加速Ajax的技术包括:

  1. 数据格式采用轻量级的JSON,解析速度快,通用性与XML相当。

  2. 缩短页面加载时间,主要内容加载后,再用Ajax获取次要文件。

  3. 确保代码的健壮性,错误不会输出给用户。

11)DOMContentLoaded

  当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像等资源的完全加载。

document.addEventListener("DOMContentLoaded", function() { }, false);

  另一个load事件应该仅用于检测一个完全加载的页面。

  注意,DOMContentLoaded事件必须等待其所属script之前的样式表加载解析完成后才会触发。

12)事件委托

  事件委托(event delegation)是一种提高程序性能、降低内存空间的技术手段,它利用了事件冒泡的特性,只需在某个祖先元素上注册一个事件,就能管理其所有后代元素上同一类型的事件。

  通过事件对象的target属性,就能分辨出当前运行在哪个事件目标上,如下所示。

container.addEventListener("click", function(event) {
  event.target;
}, false);

  使用委托后就能避免对容器中的每个子元素注册事件,并且如果在容器中动态添加子元素,新加入的子元素也能使用容器元素上注册的事件,而不用再单独绑定一次事件处理程序。

13)SSR

  服务器端渲染(SSR)是指将单页应用(SPA)在服务器端渲染成HTML片段,发送到浏览器,然后交由浏览器为其绑定状态与事件,成为完全可交互页面的过程。

  其优点是:

  1. 更快的首屏加载速度,无需等待JavaScript完成下载且执行之后才显示内容。

  2. 更友好的SEO,爬虫可以直接抓取渲染之后的页面。

14)MVVM

  MVVM模式是指视图和数据之间的双向互通,视图的修改会反映给数据,反之亦然。

  目前市面上许多库和框架都会采用MVVM模式的思想,其提升的并不在于性能,而是开发效率,鼓励开发者操作数据更新视图,由库或框架最低限度的操作DOM,减少回流。

15)虚拟DOM

  虚拟DOM(Virtual DOM)是构建在真实DOM之上的一层抽象,它将DOM元素映射成内存中的JavaScript对象(即通过React.createElement()得到的React元素),形成一棵JavaScript对象树。

  虚拟DOM与模板引擎有些相似,将多次的DOM操作先在映射的JavaScript对象中处理,再将该对象一次性挂载到真实的DOM树上,避免因浏览器重排导致的大量无用计算。

  同构应用也是基于虚拟DOM实现的,虚拟DOM的思想还可应用于其它方面,例如JavaScript录像回放

16)帧

  大部分情况下,生成一个新的帧都是由 JavaScript 通过修改 DOM 或者 CSSOM 来触发的。

  一个大的原则就是让单个帧的生成速度变快,优化策略如下。

  1. 减少 JavaScript 脚本执行时间,不要一次霸占太久主线程,例如将执行的函数分解为多个任务。

  2. 避免强制同步布局,即避免 JavaScript 强制将计算样式和布局操作提前到当前的任务中。

  3. 避免布局抖动,即避免在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。

  4. 合理利用 CSS 合成动画,因为合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同。

  5. 避免频繁的垃圾回收,要尽量避免产生那些临时垃圾数据和小颗粒对象的产生。

三、HTML5

1)history

  浏览器中的历史浏览记录就像一堆层叠的卡片,在HTML4中,可以使用window.history对象来控制历史记录的跳转。

  HTML5引进了history.pushState()方法和history.replaceState()方法,允许逐条地添加和修改历史记录条目。这些方法可以协同window.onpopstate事件一起工作。

  利用全新的history对象,就能让Ajax就像重定向到新页面一样,拥有能够返回上一页或进入下一页的功能。

2)Web Worker

  Web Worker可以在主线程(通常是UI线程)之外运行代码,当在独立线程中执行费时的任务时,就能避免主线程被阻塞。

  注意,由于Web Worker没有绑定UI线程,因此它们不能访问浏览器的许多资源,例如从外部线程修改DOM会导致界面出现错误。

  由于Web Worker有着不同的全局运行环境,因此需要创建一个完全独立的JavaScript文件,其中包含了需要在Worker中运行的代码。

  例如下面的code.js,其中message事件用于接收信息,postMessage()方法用于发送信息。

var worker = new Worker("code.js");
worker.onmessage = function (event) {
  console.log(event.data);        //"hello strick"
};
worker.postMessage("strick");

// code.js的内部代码
self.onmessage = function (event) {
  var text = `hello ${event.data}`;
  self.postMessage(text);
};

  Worker通过importScripts()方法加载外部JavaScript文件,它的调用过程是阻塞式的,直到所有文件加载并执行完成之后,脚本才会继续运行。注意,不会影响UI响应。

importScripts("foo1.js", foo2.js);

  Web Worker的实际应用包括解析大JSON字符串,计算复杂数学运算(例如图像或视频处理),大数组排序,任何超过100ms的处理过程,都应该考虑Worker方案。

2)Service Worker

  Service Worker是谷歌发起的实现PWA(Progressive Web App,渐进式Web应用)的一个关键角色,它相当于Web应用与浏览器之间的一台代理服务器。

  Service Worker会在后台启动一条Worker线程(不能访问DOM),其工作是把一些资源缓存起来(跨域资源无法缓存),然后拦截页面的HTTPS请求,如果缓存中有,就从缓存里取,响应200,没有就走正常的请求流程。

  Service Worker结合Web App Manifest能完成离线使用、断网时返回200、将一个图标添加到桌面上等。

3)WebAssembly

  将繁重的计算(如Web游戏)任务抽离到WebAssembly(WASM)中,它是一种二进制指令格式,被设计为一种用高级语言(如C/C++/Rust)编译的可移植对象。

  WebAssembly的目的并不是替代JavaScript,而是与JavaScript共存,允许两者一起工作。

  通过使用WebAssembly的JavaScript接口,你可以把WebAssembly模块加载到一个JavaScript应用中,这样在同一个应用中就能同时享用WebAssembly的性能和JavaScript的灵活。

  下载一个simple.wasm示例,其内容如下所示。

(module
  (func $i (import "imports" "imported_func") (param i32))
  (func (export "exported_func")
    i32.const 42
    call $i
  )
)

  由于内部函数$i是从imports.imported_func导入的,因此需要创建一个对象来反映simple.wasm中的两级命名空间。

let importObject = {
  imports: {
    imported_func: (arg) => console.log(arg)
  }
};

  在加载wasm文件后,使其在Array Buffer中可用,然后就可以使用导出函数了。

fetch("simple.wasm")
  .then((res) => res.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, importObject))
  .then((results) => {
    results.instance.exports.exported_func();
  });

 

 posted on 2020-07-27 08:19  咖啡机(K.F.J)  阅读(379)  评论(2编辑  收藏  举报