JavaScript 代码执行效率对比工具

平时写些小页面小程序,一般不会出现性能问题,但是在大的工程,或者在写一个框架、类库的时候,代码的性能就需要提高一个优先级了。测试代码的性能有多种方案:

  • https://jsperf.com 上测试
  • 使用 console.time 来收集代码执行的时间

    console.time('Name');// code here...console.timeEnd('Name');
  • 自己写一个时间控制器

本文自然就是自己撸一个简单易用的测试工具,效果如下图:

JavaScript 代码执行效率对比工具

设计分析

可以先把代码下载下来,跑起来:

git clone https://github.com/barretlee/performance.gitcd performance/test;open index.html;

或者直接打开测试页面:https://barretlee.github.io/performance/test/

点击代码按钮,Performance 会循环执行 button 中的代码,持续时间是设定的 1000ms,每次执行完,都会计算出相对效率,100% 是效率最高的,剩下的自然就是效率比较低的,从而可以比较清晰地看出程序之间性能差异。

相关阅读

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/28/javascript-performance-tester/

Advertisements
JavaScript 代码执行效率对比工具

元素选择器 – Mini Query

寥寥几行代码,实现一个简单的元素选择器,兼容低版本 IE。

自 IE8 开始已经开始支持 querySelector 和 querySelectorAll 这两个十分有用的选择器函数,如果不考虑低版本浏览器,它们已经可以基本满足日常需求了。而在兼容低版本浏览器中,可以采用一些 hack 手段。

原理比较简单:通过 CSS Rule 给我们的目标元素添加特殊属性,然后遍历所有元素找到具备特殊属性的元素,当然,找到之后,移除这些特殊属性。

var firstStyleSheet = document.styleSheets[0] || document.createStyleSheet();  firstStyleSheet.addRule(query, 'Barret:Lee');for (var i = 0, len = document.all.length; i < len; i++) {  var item = document.all[i];  item.currentStyle.Barret && res.push(item);}firstStyleSheet.removeRule(0);

比如我们要获取 .box .item a.pink 元素,上面的代码是这么做的,

  • 给所有的 .box .item a.pink 元素添加 { Barret: Lee; } 这个 CSS 的样式
  • 遍历所有元素找到包含 Barret 这个 CSS 属性的元素
  • 移除属性

IE8 有些调皮,需要修复点小问题,源码地址:

代码预览:

function $(query) {  var res = [];  if (document.querySelectorAll) {    res = document.querySelectorAll(query);  } else {    var firstStyleSheet = document.styleSheets[0] || document.createStyleSheet();    firstStyleSheet.addRule(query, 'Barret:Lee');    for (var i = 0, len = document.all.length; i < len; i++) {      var item = document.all[i];      item.currentStyle.Barret && res.push(item);    }    firstStyleSheet.removeRule(0);  }  if(res.item) { /* Fuck IE8 */    var ret = [];    for(var i = 0, len = res.length; i < len; i++){      ret.push(res.item(i));    }    res = ret;  }  return res;};

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/28/mini-query/

元素选择器 – Mini Query

细说 CA 和证书

CA,Catificate Athuority,它的作用就是提供证书(即服务器证书,由域名、公司信息、序列号和签名信息组成)加强服务端和客户端之间信息交互的安全性,并证书运维相关服务。任何个体/组织都可以扮演 CA 的角色,只不过难以得到客户端的信任,能够受浏览器默认信任的 CA 大厂商有很多,其中 TOP5 是 Symantec、Comodo、Godaddy、GolbalSign 和 Digicert。

服务器证书分类

可以通过两个维度来分类,一个是商业角度,一个是业务角度。

.cert-eg th,.cert-eg td {text-align:center;}.cert-eg .align-left{text-align:left;}.cert-eg br{display: block;}

单域名 多域名 泛域名 多泛域名
DV 支持 不支持
OV 支持
EV 支持 不支持
举例 http://www.barretlee.com http://www.barretlee.comwww.xiaohuzige.comwww.barret.cc *.barretlee.com *.barretlee.com*.xiaohuzige.com*.barret.cc

需要强调的是,不论是 DV、OV 还是 EV 证书,其加密效果都是一样的! 它们的区别在于:

  • DV(Domain Validation),面向个体用户,安全体系相对较弱,验证方式就是向 whois 信息中的邮箱发送邮件,按照邮件内容进行验证即可通过;
  • OV(Organization Validation),面向企业用户,证书在 DV 证书验证的基础上,还需要公司的授权,CA 通过拨打信息库中公司的电话来确认;
  • EV(Extended Validation),打开 Github 的网页,你会看到 URL 地址栏展示了注册公司的信息,这会让用户产生更大的信任,这类证书的申请除了以上两个确认外,还需要公司提供金融机构的开户许可证,要求十分严格。

OV 和 EV 证书相当昂贵,使用方可以为这些颁发出来的证书买保险,一旦 CA 提供的证书出现问题,一张证书的赔偿金可以达到 100w 刀以上。

CA 的作用

前文 HTTPS证书生成原理和部署细节 提到如果本地生成公/私钥对和对应未签证的证书,如果使用的证书没有签证,或者未在浏览器受信的 CA 签证,你会看到下图的问题:

net:ERR_CERT_AUTHORITY_INVALID

上图出现的错误是 net:ERR_CERT_AUTHORITY_INVALID,我们生成证书和公/私钥对的流程都是正确的,但是浏览器不认这张证书,并且提示证书授权不通过;如果通过其他与 Common Name 不同的域名去访问,如我注册的时候使用的 localhost,但是访问的时候用的 127.0.0.1,还会报出这样的错误:

net:ERR_CERT_COMMON_NAME_INVALID

错误码为 net:ERR_CERT_COMMON_NAME_INVALID,意思是 Common Name 不匹配,具体校验流程可以在浏览器的 DevTools 中看到:

DevTools

从上面几张图,可以大致了解 CA 和证书会做哪些事情,证书由域名、公司信息、序列号和签名信息组成,当我们通过 HTTPS 访问页面时,浏览器会主动验证证书信息是否匹配,也会验证证书是否有效。

CA 有权给所有的域名签发证书,如它可以私自给我的网站签发一张 http://www.barretlee.com 的证书,并且可以拿着新证书拦截网页流量(当然,前提是这个 CA 是浏览器认证的权威 CA),那我的网站可能就很不安全了,对拥新证书的人来说,我的网站等同于在 HTTP 下进行通讯。

评估 CA 供应商

CA 供应商很多,提供服务的侧重点可能也存在一些差异,比如很多 CA 都没有提供证书吊销的服务,这一点对于安全性要求很高的企业来说是完全不能接受的,那么对 CA 供应商的评估需要注意写什么呢?

1. 内置根

所谓内置根,就是 CA 的根证书内置到各种通用的系统/浏览器中,只有根证书的兼容性够强,它所能覆盖的浏览器才会越多。

2. 安全体系

两个指标可以判断 CA 供应商是否靠谱,一是看价格,价格高自然有它的理由,必然提供了全套的安全保障体系;二是看黑历史,该 CA 供应商有没有爆出过什么漏洞,比如之前的 DigiNotar,被伊朗入侵,签发了 500 多张未授权的证书,结果直接被各系统/浏览器将其根拉入黑名单,毫无疑问公司直接倒闭。

3. 核心功能和扩展功能

这就需要从业务上考虑了,不同的规模的企业、不同的业务对证书的要求不一样,比如证书是否会考虑无 SNI 支持的浏览器问题,是否支持在 reissue 的时候添加域名,是否支持 CAA,是否支持短周期证书等等。

4. 价格

企业完全没必要购买 Github 那样的 EV 证书,太昂贵,而且一般的企业也未必能够申请到这样的证书。供应商很大,价格可以好好评估下,不一定要最贵,最适合的就行。

自建 Root CA

OpenSSL 是一个免费开源的库,它提供了构建数字证书的命令行工具,其中一些可以用来自建 Root CA。

很多网站都希望用户知道他们建立的网络通道是安全的,所以会想 CA 机构购买证书来验证 domain,所以我们也可以在很多 HTTPS 的网页地址栏看到一把小绿锁。

然而在一些情况下,我们没必要去 CA 机构购买证书,比如在内网的测试环境中,为了验证 HTTPS 下的一些问题,我们不需要部署昂贵的证书,这个时候自建 Root CA,给自己办法证书就显得很有价值了。

本节内容较多,主要是代码演示生成证书和验证的过程,可以跳过看下一节,直接看 这里

首先找到一个放置证书的文件夹,比如 /root/ca 下,下方的测试也在改目录下,如果你要更换其他目录,记得替换下文中的目录地址。

创建 root pair

扮演 CA 角色,就意味着要管理大量的 pair 对,而原始的一对 pair 对叫做 root pair,它包含了 root key(ca.key.pen)和 root certificate(ca.cert.pem)。通常情况下,root CA 不会直接为服务器或者客户端签证,它们会先为自己生成几个中间 CA(intermediate CAs),这几个中间 CA 作为 root CA 的代表为服务器和客户端签证。

注意:一定要在绝对安全的环境下创建 root pair,可以断开网络、拔掉网线和网卡,当然,如果是测试玩一玩就不用这么认真了。

设定文件夹结构,并且配置好 openssl 设置:

# cd /root/ca# mkdir certs crl newcerts private# chmod 700 private# touch index.txt# echo 1000 > serial# wget -O /root/ca/openssl.cnf \     https://raw.githubusercontent.com/barretlee/autocreate-ca/master/cnf/root-ca

创建 root key,密码可为空,设定权限为只可读:

# cd /root/ca# openssl genrsa -aes256 -out private/ca.key.pem 4096Enter pass phrase for ca.key.pem: secretpasswordVerifying - Enter pass phrase for ca.key.pem: secretpassword# chmod 400 private/ca.key.pem

创建 root cert,权限设置为可读:

# cd /root/ca# openssl req -config openssl.cnf \      -key private/ca.key.pem \      -new -x509 -days 7300 -sha256 -extensions v3_ca \      -out certs/ca.cert.pemEnter pass phrase for ca.key.pem: secretpasswordYou are about to be asked to enter information that will be incorporatedinto your certificate request.-----Country Name (2 letter code) [XX]:CNState or Province Name []:ZhejiangLocality Name []:Organization Name []:Barret LeeOrganizational Unit Name []:Barret Lee Certificate AuthorityCommon Name []:Barret Lee Root CAEmail Address []:# chmod 444 certs/ca.cert.pem

验证证书:

# openssl x509 -noout -text -in certs/ca.cert.pem

正确的输出应该是这样的:

Certificate:    Data:        Version: 3 (0x2)        Serial Number:            87:e8:c0:a0:4b:e2:12:5d        Signature Algorithm: sha256WithRSAEncryption        Issuer: C=CN, ST=Zhejiang, O=Barret Lee, OU=Barret Lee Certificate Authority, CN=Barret Lee Root CA        Validity            Not Before: Apr 23 05:46:36 2016 GMT            Not After : Apr 18 05:46:36 2036 GMT        Subject: C=CN, ST=Zhejiang, O=Barret Lee, OU=Barret Lee Certificate Authority, CN=Barret Lee Root CA        Subject Public Key Info:            Public Key Algorithm: rsaEncryption            RSA Public Key: (4096 bit)                Modulus (4096 bit):                    // ...                Exponent: 65537 (0x10001)        X509v3 extensions:            X509v3 Subject Key Identifier:                E5:2D:B8:2B:DC:88:FE:CE:DA:93:D8:6F:2E:74:04:D2:39:E7:C8:03            X509v3 Authority Key Identifier:                keyid:E5:2D:B8:2B:DC:88:FE:CE:DA:93:D8:6F:2E:74:04:D2:39:E7:C8:03            X509v3 Basic Constraints: critical                CA:TRUE            X509v3 Key Usage: critical                Digital Signature, Certificate Sign, CRL Sign    Signature Algorithm: sha256WithRSAEncryption        // ...

包含:

  • 数字签名(Signature Algorithm)
  • 有效时间(Validity)
  • 主体(Issuer)
  • 公钥(Public Key)
  • X509v3 扩展,openssl config 中配置了 v3_ca,所以会生成此项

创建 intermediate pair

目前我们已经拥有了 Root Pair,事实上已经可以用于证书的发放了,但是由于根证书很干净,特别容易被污染,所以我们需要创建中间 pair 作为 root pair 的代理,生成过程同上,只是细节略微不一样。

生成目录结构和 openssl 的配置,这里的配置是针对 intermediate pair 的:

# mkdir /root/ca/intermediate# cd /root/ca/intermediate# mkdir certs crl csr newcerts private# chmod 700 private# touch index.txt# echo 1000 > serial# echo 1000 > /root/ca/intermediate/crlnumber# wget -O /root/ca/openssl.cnf \    https://raw.githubusercontent.com/barretlee/autocreate-ca/master/cnf/intermediate-ca

创建 intermediate key,密码可为空,设定权限为只可读:

# cd /root/ca# openssl genrsa -aes256 \      -out intermediate/private/intermediate.key.pem 4096Enter pass phrase for intermediate.key.pem: secretpasswordVerifying - Enter pass phrase for intermediate.key.pem: secretpassword# chmod 400 intermediate/private/intermediate.key.pem

创建 intermediate cert,设定权限为只可读,这里需要特别注意的一点是 Common Name 不要与 root pair 的一样

# cd /root/ca# openssl req -config intermediate/openssl.cnf -new -sha256 \      -key intermediate/private/intermediate.key.pem \      -out intermediate/csr/intermediate.csr.pemEnter pass phrase for intermediate.key.pem: secretpasswordYou are about to be asked to enter information that will be incorporatedinto your certificate request.-----Country Name (2 letter code) [XX]:CNState or Province Name []:ZhejiangLocality Name []:Organization Name []:Barret LeeOrganizational Unit Name []:Barret Lee Certificate AuthorityCommon Name []:Barret Lee Intermediate CAEmail Address []:

使用 v3_intermediate_ca 扩展签名,密码可为空,中间 pair 的有效时间一定要为 root pair 的子集:

# cd /root/ca# openssl ca -config openssl.cnf -extensions v3_intermediate_ca \      -days 3650 -notext -md sha256 \      -in intermediate/csr/intermediate.csr.pem \      -out intermediate/certs/intermediate.cert.pemEnter pass phrase for ca.key.pem: secretpasswordSign the certificate? [y/n]: y# chmod 444 intermediate/certs/intermediate.cert.pem

此时 root 的 index.txt 中将会多出这么一条记录:

V 260421055318Z   1000  unknown .../CN=Barret Lee Intermediate CA

验证中间 pair 的正确性:

# openssl x509 -noout -text \      -in intermediate/certs/intermediate.cert.pem# openssl verify -CAfile certs/ca.cert.pem \      intermediate/certs/intermediate.cert.pemintermediate.cert.pem: OK

浏览器在验证中间证书的时候,同时也会去验证它的上一级证书是否靠谱,创建证书链,将 root cert 和 intermediate cert 合并到一起,可以让浏览器一并验证:

# cat intermediate/certs/intermediate.cert.pem \      certs/ca.cert.pem > intermediate/certs/ca-chain.cert.pem# chmod 444 intermediate/certs/ca-chain.cert.pem

创建服务器/客户端证书

终于到了这一步,生成我们服务器上需要部署的内容,上面已经解释了为啥需要创建中间证书。root pair 和 intermediate pair 使用的都是 4096 位的加密方式,一般情况下服务器/客户端证书的过期时间为一年,所以可以安全地使用 2048 位的加密方式。

# cd /root/ca# openssl genrsa -aes256 \      -out intermediate/private/www.barretlee.com.key.pem 2048# chmod 400 intermediate/private/www.barretlee.com.key.pem

创建 http://www.barretlee.com 的证书:

# cd /root/ca# openssl req -config intermediate/openssl.cnf \      -key intermediate/private/www.barretlee.com.key.pem \      -new -sha256 -out intermediate/csr/www.barretlee.com.csr.pemEnter pass phrase for www.barretlee.com.key.pem: secretpasswordYou are about to be asked to enter information that will be incorporatedinto your certificate request.-----Country Name (2 letter code) [XX]:CNState or Province Name []:ZhejiangLocality Name []:HangzhouOrganization Name []:Barret LeeOrganizational Unit Name []:Barret Lee's Personal WebsiteCommon Name []:www.barretlee.comEmail Address []:barret.china@gmail.com

使用 intermediate pair 签证上面证书:

# cd /root/ca# openssl ca -config intermediate/openssl.cnf \      -extensions server_cert -days 375 -notext -md sha256 \      -in intermediate/csr/www.barretlee.com.csr.pem \      -out intermediate/certs/www.barretlee.com.cert.pem# chmod 444 intermediate/certs/www.barretlee.com.cert.pem

可以看到 /root/ca/intermediate/index.txt 中多了一条记录:

V 170503055941Z   1000  unknown .../emailAddress=barret.china@gmail.com

验证证书:

# openssl x509 -noout -text \      -in intermediate/certs/www.barretlee.com.cert.pem# openssl verify -CAfile intermediate/certs/ca-chain.cert.pem \      intermediate/certs/www.barretlee.com.cert.pemwww.barretlee.com.cert.pem: OK

此时我们已经拿到了几个用于部署的文件:

添加信任 CA 和证书的调试

双击 /root/ca/intermediate/certs/ca-chain.cert.pem 将证书安装到系统中,目的是让本机信任这个 CA,将其当作一个权威 CA,安装 root pem 或者 intermediate chain pem 都是可以的,它们都具备验证能力。如果不执行这一步,浏览器依然会提示 net:ERR_CERT_AUTHORITY_INVALID

上面申请测试证书时,我设置的 Common Name 为 http://www.barretlee.com,由于不在线上机器测试,可以将其添加到 hosts:

127.0.0.1 www.barretlee.com

执行下方测试代码:

// https-server.jsvar https = require('https');var fs = require('fs');var options = {  key: fs.readFileSync('/root/ca/intermediate/private/www.barretlee.com.key.pem'),  cert: fs.readFileSync('/root/ca/intermediate/certs/www.barretlee.com.cert.pem'),  passphrase: 'passoword' // 如果生成证书的时候设置了密码,请添加改参数和密码};https.createServer(options, function(req, res) {  res.writeHead(200);  res.end('hello world');}).listen(8000, function(){  console.log('Open URL: https://www.barretlee.com:8000');});

可以看到这样的效果:

小绿锁出来了

查看证书的详细信息:

证书的详细信息

回到最初的问题:

然而在一些情况下,我们没必要去 CA 机构购买证书,比如在内网的测试环境中,为了验证 HTTPS 下的一些问题,我们不需要部署昂贵的证书,这个时候自建 Root CA,给自己办法证书就显得很有价值了。

一般公司内网的电脑都会强制安装一些安全证书,此时就可以把我们自建自签名的证书导入/引导安装到用户的电脑中啦~

无 SNI 支持问题

很多公司由于业务众多,域名也是相当多的,为了方便运维,会让很多域名指向同样的 ip,然后统一将流量/请求分发到后端,此时就会面临一个问题:由于 TLS/SSL 在 HTTP 层之下,客户端和服务器握手的时候还拿不到 origin 字段,所以服务器不知道这个请求是从哪个域名过来的,而服务器这边每个域名都对应着一个证书,服务器就不知道该返回哪个证书啦。

SNI 就是用来解决这个问题的,官方解释是

SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的SSL/TLS扩展。一句话简述它的工作原理就是,在连接到服务器建立SSL链接之前先发送要访问站点的域名(Hostname),这样服务器根据这个域名返回一个合适的证书。

然后有将近 25% 的浏览器不支持该字段的扩展,这个问题有两个通用解决方案:

  • 使用 VIP 服务器,每个域名对应一个 VIP,然后 VIP 与统一接入服务对接,通过 ip 来分发证书,不过运维成本很高,可能也需要大量的 VIP 服务器
  • 采用多泛域名,将多个泛域名证书打包进一个证书,可以看看 淘宝 页面的证书taobao cert它的缺点是每次添加域名都需要更新证书。

几个细节知识点

1. 证书选择

证书有多张加密方式,不同的加密方式对 CPU 计算的损耗不同,安全级别也不同。TLS 在进行第一次握手的时候,客户端会向服务器端 say hello,这个时候会告诉服务器,它支持哪些算法,此时服务器可以将最适合的证书发给客户端。

2. 证书的吊销

CA 证书的吊销存在两种机制,一种是在线检查,client 端向 CA 机构发送请求检查 server 公钥的靠谱性;第二种是 client 端储存一份 CA 提供的证书吊销列表,定期更新。前者要求查询服务器具备良好性能,后者要求每次更新提供下次更新的时间,一般时差在几天。安全性要求高的网站建议采用第一种方案。

大部分 CA 并不会提供吊销机制(CRL/OCSP),靠谱的方案是为根证书提供中间证书,一旦中间证书的私钥泄漏或者证书过期,可以直接吊销中间证书并给用户颁发新的证书。中间证书的签证原理于上上条提到的原理一样,中间证书还可以产生下一级中间证书,多级证书可以减少根证书的管理负担。

很多 CA 的 OCSP Server 在国外,在线验证时间比较长,如果可以联系 CA 供应商将 Server 转移到国内,效率可以提升 10 倍左右。

3. PKI 体系

比较主流的两种方案是 HPKP 和 Certificate Transparency:

  • HPKP 就是用户第一次访问的时候记下 sign 信息,以后不匹配则拒绝访问,这存在很大的隐患,比如 Server 更新了证书,或者用户第一次访问的时候就被人给黑了
  • Certificate Transparency 意思就是让 CA 供应商透明化 CA 服务日志,防止 CA 供应商偷偷签证

小结

看了不少文章,对 CA 和证书相关的知识做了一些总结,可能不全面,也可能存在表述错误或者知识性错误,欢迎拍砖!

拓展阅读

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/24/detail-about-ca-and-certs/

细说 CA 和证书

JavaScript 被忽视的细节

《JavaScript 权威指南》这本书从第四版开始,一直到第六版,每个版本我都逐字逐句读过几遍,然而每一遍下来的感受却完全不一样。上上周的周一,再次翻开了这本犀牛书,这一次我是带着批判精神和研究精神过来的,所以看的时候也写下了一些感受和笔记,都是些容易被忽略的点,部分内容犀牛书上不一定有提到。

之前都发在 微博 上,稍微整理了一番,放在这里,方便阅读。

一些小点

语句/表达式

换个角度理解语句(statemaents)和表达式(expressions):表达式不会改变程序的运行状态,而语句会。还有一种叫做表达式语句,可以理解为表达式和语句的交集,如 ({a:1})"use strict;"等,我觉得没必要死扣,意义不大。

字符集

ES3 要求 JS 必须实现 Unicode 2.1 及后续版本,而 ES5 只要求支持 Unicode 3 及后续版本。Unicode 字符 2005 年超过了十万字符,至今仍在不断增修,最新版本是 8.0。

分号

如果你写 JS 代码不喜欢带分号,而又搞不清什么时候必须加分号,可以这么做:在以 “(“、”[“ 、”/“、”+”、”-“ 开头的语句前面都加上一个分号,如 ;(a + b).toString()

进制

ES5 严格模式中禁止使用八进制。目前各种引擎对 JS 的实现是存在差异的,部分支持八进制,部分不支持。八进制被禁止的原因:String 和 Number 之间经常被相互转换,而以 0 开头的八进制数据特别容易让人迷惑,也容易让机器迷惑,比如 09 是该被转换成 9 还是直接报错?十六进制不存在这个问题,如 0x98。更多信息参阅 这里

精度

JS 采用 IEEE-754 浮点数表示法,这是一种二进制表示法,由于精度原因 JS 不能表示所有的实数。它能展示的浮点数个数是有限的,比如它不能准确地表示三分之一的数值字面量。这也导致了它在浮点数的计算上存在误差,如 0.3-0.2 != 0.2-0.1,因为在计算的过程中,存在数据的溢出,丢失了精度。

null/undefined

系统级、出乎意料的或者类似错误的值的空缺使用 undefined,而程序级、正常的或意料之中的值的空缺使用 null。平时编程给变量赋值时,不要使用 undefined 而应该用 null。值得注意的是 ES3 中的 undefined 是可以被重新赋值的,ES5 修复了这个 bug。通常我们使用 void 0 来还原/代替 undefined 的值。

eval

eval 是个不好把握的东西,它在 ES3 中更像是 Function,而在 ES5 中更像是一个运算符(严格模式下不允许设置别名,否则报错,且将其作为保留字)。实际上 ES3 中也不允许给 eval 设置别名,然而很多实现却依然允许,并将其作为全局代码来执行,浏览器尤其是 IE 对它实现相当混乱,没有什么规律可循,不过 IE 中提供了一个 execScript 函数,类似全局的 eval,这个函数每次执行都会返回 null。

需要使用 eval 的场景并不多,尽量少用,一般需求使用 new Function 就能满足。

引用

删除属性存在的坑:a = {n: {x: 2}}, b = a.n; delete a.n; 这段代码执行之后,b.x 依然等于 2,原因是 {x:2} 这个对象被 a 和 b 同时引用,delete 指令只删除了 a 对它的引用,b 上的引用依然存在。这种问题有可能造成内存泄漏。

Object 扩展

Object 的 freeze 方法过于严格;defineGetter/lookupGetter 和对应的 Setter 是很好用的属性。

toLocalString

如图,你可能还不知道 JavaScript 的 toLocaleString 还可以这么玩。

this语义

this 上下文只存在两种语义,一种是被当作方法调用,this 指向调用它的对象;一种是作为函数调用,指向 Global 对象(严格模式下为 undefined)。它没有作用域的限制,如下图所示,a 由于是作为函数被调用,所以它指向的是 window,故而返回 false。

类型

JavaScript 可以被调用执行的均为 Function 类型,但是也存在可调用的 Object,如低版本 IE 中的一些宿主对象:document.getElementById、alert 等,在很多浏览器中 typeof RegExp 同样是 Object。这绝对是一个不标准的实现,在浏览器摒弃/修正这些错误类型之前应该尽量少依赖它们。

IE8 getter/setter

Object.defineProperty 虽然是 ES5 的东西,早在 IE8 就已经支持了,但支持得并不完善,比如 writable、enumerable、configurable 这些配置项设置就无效,IE8 下主要支持 getter/setter。

JSON.stringify

JSON.stringify 接受三个参数,很多人都知道第三个参数可以设置空白字符来美化输出,但是你可能不知道第二个参数的作用,它为 {Array|Function} 类型,如果为 Array 则用于过滤 key,如果为 Function 则可以对 value 做处理,如图所示。

Symbol

ES6 中添加了一种新的数据类型,Symbol,它是一种原始数据类型(图一),具备对象的特性(图二),并可以指向同一个引用(图三),能够作为对象的 key 但不可枚举(图四),内置的 Symbol 会影响程序的执行(图五),Symbol.iterator 是个举足轻重的符号,能够让元素具备迭代属性(图六),花样很多。

附图见:http://weibo.com/1812166904/DqMwR8O6z

伪数组添加 Symbol.iterator 的几个办法:鸭式辨型的 iterator 函数、yield 函数和直接使用 Array 的遍历符号。

附图见:http://weibo.com/1812166904/DqMBYebPw

Set/WeakSet

Set/WeakSet 这种数据结构,不能说没用,但确实也没啥大用,前者就是个不允许出现重复成员的数组,顺便还带了点 ES6 的特性,后者虽说可以一定程度上防止内存泄漏,但是也容易出错,比如某个引用已经被垃圾回收了,再去使用它可能就返回 null。它们都是 ES6 的配套产物。而 Map/WeakMap 倒是两个非常不错的设计,常规的 Object 结构都为 String-Val 键值对,而它扩展为 AllType-Val,任意类型都可以作为它的 Key,无论是服务端编程还是客户端编程,这个属性都带来了极大的便利性。

正则

理解正则零宽的含义:正则中所谓的零宽断言,类似于锚点字符,它们匹配指定的位置而不会匹配内容,如 ^ 匹配开头,$ 匹配结尾,\b 匹配单词边界;(?=p) 匹配「接下来的字符与 p 匹配」的位置,(?!p) 匹配「接下来的字符不与 p 匹配」的位置。\b 字符匹配单词边界,实际上就是匹配 \w 与 \W 之间的位置(\w 匹配 [a-zA-Z0-9])。很少会有人用到 \B,它匹配的是非单词边界位置,简单理解就是 \w & \w 之间位置或者 \W & \W 之间位置。

持续学习和分享…

内容都是片段化的分享,比较多,也比较杂,就没有全部列举出来,感兴趣的同学可以 follow 我的 微博,我的想法和笔记都会在上面同步。

感受

在这之前犀牛书已经翻阅了差不多六七遍,很多内容都已经深深地刻在了脑海里,但时间久了也会忘记些,时而巩固复习下,毕竟是前端最基础部分。

带着问题去看书,收获是完全不一样的。犀牛书不难啃,难的是你对这些知识点的理解深度。

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/18/javascript-detail/

JavaScript 被忽视的细节

Unix/Linux 系统中的 Operation Not Permitted 问题

多次在 Mac 使用过程中遇到 Operation Not Permitted 问题,之前都是略过,今天好好摸索了一把,搞明白了道理,记录下来。

好几次整理移动硬盘数据的时候,都遇到了 Operation Not Permitted 问题,文件移动不了,也删除不掉,第一次遇到没理会,第二次是打开虚拟机,在 Windows 中操作这些问题文件,今天又遇到了,决定消灭它。

OS X EI Catitan 的 SIP

Apple 在 OS X 10.11 以后的版本中默认启动了一项系统保护程序,叫做 System Integrity Protection,也被唤作 rootless(寓意让 root 弱一点),该程序意在保护电脑不被恶意程序攻击,但是对于我们这群程序员,很多保护是多余的,甚至给我们带来了很多麻烦。

SIP 会锁定几个系统文件目录:

/System/sbin/usr (/usr/local 除外)

在 SIP 的保护下,部分软件、功能、脚本都会失效,我们可以通过如下步骤关闭 SIP:

  • 重启电脑,按下 Command + R 直到听到开机声音,此时电脑会进入恢复模式(Recovery Mode)
  • 当 OSX 工具出现在屏幕中时,下拉工具(Utilities)菜单,选择终端(Terminal)
  • 键入 csrutil disable,回车
  • 电脑重启后,SIP 就关闭了

恢复 SIP 的方式同上,只不过终端中键入 csrutil enable。通过 csrutil status 可以检测系统当前 SIP 的启动状态:

$ csrutil statusSystem Integrity Protection status: enabled.

Linux 下的 file flags

可能你也遇到过在 Linux 下删除文件报错:

root@ubuntu:/home/barret/work# rm -f 1.md rm: cannot remove ‘1.md’: Operation not permitted

这个时候可以通过 lsattr 命令看看该文件是否被打了 flags:

root@ubuntu:/home/barret/work# lsattr 1.md----i--------e-- ./1.md

如果文件上存在 i 标记,那肯定是删不掉的,同样这个文件也不能被编辑。可以进入 root 模式,去除这个标记:

root@ubuntu:/home/barret/work# chattr -i 1.md

给保护文件添加标记的方式:

root@ubuntu:/home/barret/work# chattr +i 1.md

也比较简单。

小结

文本算是一个经验性的小科普,希望对你有帮助。

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/06/operation-not-permitted-problem-in-linux-or-unix-system/

Unix/Linux 系统中的 Operation Not Permitted 问题

淘宝首页性能优化实践

上文 《一起来看看淘宝首页的个性化》 中,带大家看了下弥散着个性化味道的新首页,前端面临着:

  • 数据来源多
  • 串行请求渲染一个模块
  • 运营数据和个性化数据匹配和管理
  • 数据兜底容灾

等多个问题。本次淘宝首页改版,虽已不再支持 IE6 和 IE7 等低版本的古董浏览器,但是依然存在多个影响首页性能的因素:

  • 依赖系统过多,数据的请求分为三块,其一是静态资源(如 js/css/image/iconfont 等);其二是推到 CDN 的静态数据(如运营填写的数据、前端配置信息等);其三是后端接口,不同的模块对应不同的业务,而且页面中还有不少的广告内容,粗略估计页面刚加载时首屏发出的接口请求就有 8 个,滚到最底下,得发出 20 多个请求。
  • 无法直接输出首屏数据,首屏很多数据是通过异步请求获取的,由于系统限制,这些请求不可避免,而且请求个数较多,十分影响首屏时间。
  • 模块过多,为了能够在后台隔离运营之间填写数据的权限,模块必须做细粒度的拆分,如下图所示:多模块的拆分一个简单的模块必须拆分成多个行业小模块,页面中其他位置也是如此,而且这些被拆分出来的模块还不一定会展现出来,需要让算法告诉前端展示哪些模块。
  • 图片过多,翻页往下滚动,很明显看到,页面整屏整屏的图片,有些图片是运营填写,有些图片由个性化接口提供,这些图片都没有固定的尺寸。

网页性能衡量指标

网页性能衡量指标有很多,倘若能够把握关键的几个,集中优化,性能自然也就上去了。

FPS

最能反映页面性能的一个指标是 FPS(frame per second),一般系统设定屏幕的刷新率为 60fps,当页面元素动画、滚动或者渐变时绘制速率小于 60,就会不流畅,小于 24 就会卡顿,小于 12 基本认定卡爆了。

1 帧的时长约 16ms,除去系统上下文切换开销,每一帧中只留给我们 10ms 左右的程序处理时间,如果一段脚本的处理时间超过 10ms,那么这一帧就可以被认定为丢失,如果处理时间超过 26ms,可以认定连续两帧丢失,依次类推。我们不能容忍页面中多次出现连续丢失五六帧的情况,也就是说必须想办法分拆执行时间超过 80ms 的代码程序,这个工作并不轻松。

页面在刚开始载入的时候,需要初始化很多程序,也可能有大量耗时的 DOM 操作,所以前 1s 的必要操作会导致帧率很低,我们可以忽略。当然,这是对 PC 而言,Mobile 内容少,无论是 DOM 还是 JS 脚本量都远小于 PC,1s 可能就有点长了。

DOMContentLoaded 和 Load

DOM 加载并且解析完成才会触发 DOMContentLoaded 事件,倘若源码输出的内容过多,客户端解析 DOM 的时间也会响应加长,不要小看这里的解析时间,如果 DOM 数量增加 2000 个并且嵌套层级较深,解析时间也会相应增加 50-200ms,这个消耗对大多数页面来说其实是没必要的,保证首屏输出即可,后续的内容只保留钩子,利用 JS 动态渲染。

Load 时间可以用来衡量首屏加载中,客户端接受的信息总量,如果在首屏中充满了大尺寸图片或者客户端与后端建立连接次数较多,Load 时间也会相应被拖长。

流畅度

流畅度是对 FPS 的视觉反馈,FPS 值越高,视觉呈现越流畅。为了保障页面的加载速度,很多内容不会在页面打开的时候全部加载到客户端。这里提到的流畅度是等待过程中的视觉缓冲,如下方是 Google Plus 页面的一个效果图:

Google Plus Item

墙内访问 google 的速度不是很快,上面元素中的的很多内容都是通过异步方式加载,而从上图可以看出 Google 并没有让用户产生等待的焦虑感。

淘宝首页的性能优化

由于平台限制,淘宝首页面临一个先天的性能缺陷,首屏的渲染需要从 7 个不同的后端取数据,这些数据请求是难以合并的,如果用户屏幕比较大,则首屏的面积也比较大,对应的后端平台数据接口就更多。数据是个性化内容或者为广告内容,故请求也不能缓存。

关键模块优先

不论用户首屏的面积有多大,保证关键模块优先加载。下面代码片段是初始化所有模块的核心部分:

$('.J_Module').each(function(mod) {  var $mod = $(mod);  var name = $mod.attr('tms');  var data = $mod.attr('tms-data');  if($mod.hasClass('tb-pass')) {    Reporter.send({      msg: "跳过模块 " + name    });    return;  }  // 保证首屏模块先加载  if (/promo|tmall|tanx|notice|member/.test(name)) {    window.requestNextAnimationFrame(function(){      // 最后一个参数为 Force, 强制渲染, 不懒加载处理      new Loader($mod, data, /tanx/.test(name));    });  } else {    // 剩下的模块进入懒加载队列    lazyQueue.push({      $mod: $mod,      data: data,      force: /fixedtool|decorations|bubble/.test(name)    });  }});

TMS 输出的模块都会包含一个 .J_Module 钩子,并且会预先加载 js 和 css 文件。

对于无 JS 内容的模块,会预先打上 tb-pass 的标记,初始化的时候跳过此模块;对于首屏模块关键模块,会直接进入懒加载监控:

// $box 进入浏览器视窗后渲染// new Loader($box, data) ->datalazyload.addCallback($box, function() {  self.loadModule($box, data);});// $box 立即渲染// new Loader($box, data, true) ->self.loadModule($box, data);

除必须立即加载的模块外,关键模块被加到懒加载监控,原因是,部分用户进入页面就可能急速往下拖拽页面,此时,没必要渲染这些首屏模块。

非关键模块统一送到 lazyQueue 队列,没有基于将非关键模块加入到懒加载监控,这里有两个原因:

  • 一旦加入监控,程序滚动就需要对每个模块做计算判断,模块太多,这里可能存在性能损失
  • 如果关键模块还没有加载好,非关键模块进入视窗就会开始渲染,这势必会影响关键模块的渲染

那么,什么时候开始加载非关键模块呢?

var __lazyLoaded = false;function runLazyQueue() {  if(__lazyLoaded) {    return;  }  __lazyLoaded = true;  $(window).detach("mousemove scroll mousedown touchstart touchmove keydown resize onload", runLazyQueue);  var module;  while (module = lazyQueue.shift()) {    ~function(m){      // 保证在浏览器空闲时间处理 JS 程序, 保证不阻塞      window.requestNextAnimationFrame(function() {        new Loader(m.$mod, m.data, m.force);      });    }(module);  }}$(window).on("mousemove scroll mousedown touchstart touchmove keydown resize onload", runLazyQueue);// 担心未触发 onload 事件, 5s 之后执行懒加载队列window.requestNextAnimationFrame(function() {  runLazyQueue();}, 5E3);

上面的代码应该十分清晰,两种请求下会开始将非关键模块加入懒加载监控:

  • 当页面中触发 mousemove scroll mousedown touchstart touchmove keydown resize onload 这些事件的时候,说明用户开始与页面交互了,程序必须开始加载。
  • 如果用户没有交互,但是页面已经 onload 了,程序当然不能浪费这个绝佳的空档机会,趁机加载内容;经测试,部分情况下,onload 事件没有触发(原因尚不知),所以还设定了一个超时加载,5s 之后,不论页面加载情况如何,都会将剩下的非关键模块加入到懒加载监控。

懒执行,有交互才执行

如果说上面的优化叫做懒加载,那么这里的优化可以称之为懒执行。

首页上有几个模块是包含交互的,如头条区域的 tab ,便民服务的浮层和主题市场的浮层,部分用户进入页面可能根本不会使用这些功能,所以程序上并没有对这些模块做彻底的初始化,而是等到用户 hover 到这个模块上再执行全部逻辑。

更懒的执行,刷新页面才执行

首屏中有两个次要请求,一个是主题市场的 hot 标,将用户最常逛的三个类目打标;第二个是个人中心的背景,不同的城市会展示不同的背景图片,这里需要请求拿到城市信息。

这两处的渲染策略都是,在程序的 idle(空闲)时期,或者 window.onload 十秒之后去请求,然后将请求的结果缓存到本地,当用户第二次访问淘宝首页时能够看到效果。这是一种更懒的执行,用户刷新页面才看得到.这种优化是产品能够接受,也是技术上合理的优化手段。

图片尺寸的控制和懒加载

不论图片链接的来源是运营填写还是接口输出,都难以保证图片具备恰当的宽高,加上如今 retina 的屏幕越来越多,对于这种用户也要提供优质的视觉体验,图片这块的处理并不轻松。

<img src='//g.alicdn.com/s.gif' data-src='//g.alicdn.com/real/path/to/img.png' />

阿里 CDN 是支持对图片尺寸做压缩处理的,如下图为 200×200 尺寸的图片:

200x200

加上 _100x100.jpg 的参数后,会变成小尺寸:

100x100

我们知道 webp 格式的图片比对应的 jpg 要小三分之一,如上图加上 _.webp 参数后:

100x100 webp(不支持 webp 格式的浏览器展示不出来这张图片)

视觉效果并没有什么折扣,但是图片体积缩小了三分之一,图片越大,节省的越明显。显然,淘宝首页的所有图片都做了如上的限制,针对坑位大小对图片做压缩处理,只是这里需要注意的是,运营填写的图片可能已经是压缩过的,如:

$img = '//g.alicdn.com/real/path/to/img.png_400x400.jpg';<img src='{{$img}}_100x100jpg_.webp' />

上面这种情况,图片是不会正确展示的。首页对所有的图片的懒加载都做了统一的函数处理:

src = src.replace(/\s/g, '');var arr;if (/(_\d{2,}x\d{2,}\w*?\.(?:jpg|png)){2,}/.test(src) && src.indexOf('_!!') == -1) {  arr = src.split('_');  if (arr[arr.length - 1] == '.webp') {    src = [arr[0], arr[arr.length - 2], arr[arr.length - 1]].join('_');  } else {    src = [arr[0], arr[arr.length - 1]].join('_');  }}if (src.indexOf('_!!') > -1) {  src = src.replace(/((_\d{2,}x\d{2,}[\w\d]*?|_co0)\.(jpg|png))+/, '$1');}WebP.isSupport(function(isSupportWebp) {  // https 协议访问存在问题 IE8,去 schema  if (/^http:/.test(src)) {    src = src.slice(5);  }  // 支持 webp 格式,并且 host 以 taobaocdn 和 alicdn 结尾,并且不是 s.gif 图片  if (isSupportWebp && /(taobaocdn|alicdn)\.com/.test(src) && (src.indexOf('.jpg') ||    src.indexOf('.png')) && !/webp/.test(src) && !ignoreWebP && !/\/s\.gif$/.test(src)) {    src += '_.webp';  }  $img.attr('src', src);});

模块去钩子,走配置

TMS 的模块在输出的时候会将数据的 id 放在钩子上:

<div class='J_Module' tms-datakey='2483'></div>

如果模块是异步展示的,可以通过 tms-datakey 找到模块数据,而首页的个性化是从几十上百个模块中通过算法选出几个,如果把这些模块钩子全部输出来,虽说取数据方便了很多,却存在大量的冗余,对此的优化策略是:将数据格式相同的模块单独拿出来,新建页面作为数据页。所以可以在源码中看到好几段这样的配置信息:

<textarea class="tb-hide">[{"backup":"false","baseid":"1","mid":"222726","name":"iFashion","per":"false","tid":"3","uid":"1000"},{"backup":"false","baseid":"3","mid":"222728","name":"美妆秀","per":"false","tid":"3","uid":"1001"},{"backup":"false","baseid":"4","mid":"222729","name":"爱逛街","per":"false","tid":"4","uid":"1002"},{"backup":"false","baseid":"2","mid":"222727","name":"全球购","per":"false","tid":"4","uid":"1003"}]</textarea>

减少了大量的源码以及对 DOM 的解析。

低频修改模块,缓存请求

有一些模块数据是很少被修改的,比如接口的兜底数据、阿里 APP 模块数据等,可以通过调整参数,设置模块的缓存时间,如:

io({  url: URL,  dataType: 'jsonp',  cache: true,  jsonpCallback: 'jsonp' + Math.floor(new Date / (1000 * 60)),  success: function() {    //...  }});

Math.floor(new Date / (1000 * 60)) 这个数值在一分钟内是不会发生变化的,也就是说将这个请求在本地缓存一分钟,对于低频修改模块,缓存时间可以设置为一天,即:

Math.floor(new Date / (1000 * 60 * 60 * 24))

当然,我们也可以采用本地储存的方式缓存这个模块数据:

offline.setItem('cache-moduleName', JSON.stringify(data), 1000 * 60 * 60 * 24);

缓存过期时间设置为 1 天,淘宝首页主要采用本地缓存的方式。

使用缓动效果减少等待的焦急感

这方面的优化不是很多,但是也有一点效果,很多模块的展示并不是干巴巴的 .show(),而是通过动画效果,缓动呈现,这方面的优化推荐使用 CSS3 属性去控制,性能消耗会少很多。

优化的思考角度

上文 《一起来看看淘宝首页的个性化》 中提到几个黄金法则:

  • 首屏一定要快
  • 滚屏一定要流畅
  • 能不加载的先别加载
  • 能不执行的先别执行
  • 渐进展现、圆滑展现

性能优化的切入角度不仅仅是上几个方面,对照 Chrome 的 Timeline 柱状图和折线图,我们可以找到几个优化的点:

淘宝首页 Chrome Timeline

  • 在 1.0s 左右存在一次 painting 阻塞,可能因为一次性展示的模块面积过大
  • 从 FPS 的柱状图可以看出,在 1.5s-2.0s 之间,存在几次 Render 和 JavaScript 丢帧
  • 从多出的红点可以看出页面 jank 次数,也能够定位到代码堆栈

在优化的过程中需要更多地思考,如何让阻塞的脚本分批执行,如何将长时间执行的脚本均匀地分配到时间线上。这些优化都体现在代码的细节上,宏观上的处理难以有明显的效果。当然,在宏观上,淘宝首页也有一个明显的优化:

// https://gist.github.com/miksago/3035015#file-raf-js(function() {  var lastTime = 0;  var vendors = ['ms', 'moz', 'webkit', 'o'];  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];    window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];  }  if (!window.requestAnimationFrame) {    window.requestAnimationFrame = function(callback, element) {      var currTime = new Date().getTime();      var timeToCall = Math.max(0, 16 - (currTime - lastTime));      var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);      lastTime = currTime + timeToCall;      return id;    };  }  if (!window.cancelAnimationFrame) {    window.cancelAnimationFrame = function(id) {      clearTimeout(id);    };  }})();

这段代码基本保证每个模块的初始化都是在浏览器空闲时期,减少了很多不必要的丢帧。这个优化也可以被应用到每个模块的细节代码之中,不过优化难度会更高。

小结

代码的性能优化是一个精细活,如果你要在一个庞大的未经优化的页面上做性能优化,可能会面临一次重构代码。本文从淘宝首页个性化引出的问题出发,从微观到宏观讲述了页面的优化实践,提出了几条可以借鉴的「黄金法则」,希望对你有所启发,后续会继续给大家带来淘宝首页稳定性保障的分享。

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/04/01/optimization-in-taobao-homepage/

淘宝首页性能优化实践