Quantcast
Channel: IT瘾javascript推荐
Viewing all 148 articles
Browse latest View live

前后端完全分离之API设计

$
0
0

背景

API就是开发者使用的界面。我的目标不仅是能用,而且好用, 跨平台(PC, Android, IOS, etc…)使用; 本文将详细介绍API的设计及异常处理, 并将异常信息进行封装友好地反馈给前端.

上篇文章 前后端完全分离初探只是讲了些宽泛的概念, 接下来的文章将直接上干货, 干货的源码会挂在 github上.

前后端完全分离后, 前端和后端如何交互?

答: 通过双方协商好的API.

接下来我分享我自己设计的API接口, 欢迎各位朋友指教.

API设计理念

  1. 将涉及的实体抽象成资源, 即按 id访问资源, 在 url上做文章, 以后再也不用为 url起名字而苦恼了.
  2. 使用 HTTP动词对资源进行 CRUD(增删改查); get->查, post->增, put->改, delete->删.
  3. URL命名规则, 对于资源无法使用一个单数名词表示的情况, 我使用中横线( -)连接.
    • 资源采用名词命名, e.g: 产品 -> product
    • 新增资源, e.g: 新增产品, url -> /product , verb -> POST
    • 修改资源, e.g: 修改产品, url -> /products/{id} , verb -> PUT
    • 资源详情, e.g: 指定产品详情, url -> /products/{id} , verb -> GET
    • 删除资源, e.g: 删除产品, url -> /products/{id} , verb -> DELETE
    • 资源列表, e.g: 产品列表, url -> /products , verb -> GET
    • 资源关联关系, e.g: 收藏产品, url -> /products/{id}/star , verb -> PUT
    • 资源关联关系, e.g: 删除收藏产品, url -> /products/{id}/star , verb -> DELETE

目前我API的设计只涉及这两点, 至于第三点 HATEOAS(Hypermedia As The Engine Of Application State)那就由读者自己去选择了.

项目地址

本文中只涉及了设计的理念, 具体的实现请下载源码 https://github.com/arccode/rest-api, 项目内写了比较详细的注释.

项目实战

实战将从业务场景出发, 详细介绍如何使用HTTP verb对资源进行操作( 状态转移), 使用JSON返回结果( 资源表述), 并定义JSON的基础结构.

JSON结构

requestParams:

1     
2
{     
}

responseBody:

1     
2
3
4
5
6
{     
"meta": {
},
"data": {
}
}

meta中封装操作成功或失败的消息, data中封装返回的具体数据.

当新建商品或更新产品时, 相关属性封装在JSON中, 通过POST或PUT发送,

1     
2
3
4
{     
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}

当用户对商品进行操作后, 将得到响应结果,

GET, POST, PUT操作成功, 返回如下结果

1     
2
3
4
5
6
7
8
9
10
11
{     
"meta": {
"code": 201,
"message": "创建成功"
},
"data": {
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}
}

DELETE操作成功, 返回如下结果

1     
2
3
4
5
6
{     
"meta": {
"code": 204,
"message": "删除成功"
}
}

业务场景一

电商网站的管理员对商品进行新增,编辑,删除,浏览的操作; 暂时不考虑认证授权, 只关注对商品的操作.

为了以后便于做分布式, 所有资源id(表主键)均采用uuid.

新增商品

1, url: /api/product

2, method: POST

3, requestParams:

1     
2
3
4
{     
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}

4, responseBody

1     
2
3
4
5
6
7
8
9
10
11
{     
"meta": {
"code": 201,
"message": "创建成功"
},
"data": {
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}
}

编辑商品

1, url: /api/products/{id}

2, method: PUT

3, requestParams:

1     
2
3
4
{     
"name": "iPhone 6",
"description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
}

4, responseBody

1     
2
3
4
5
6
7
8
9
10
11
{     
"meta": {
"code": 200,
"message": "修改成功"
},
"data": {
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "iPhone 6",
"description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
}
}

删除商品

1, url: /api/products/{id}

2, method: DELETE

3, responseBody

1     
2
3
4
5
6
7
{     
"meta": {
"code": 204,
"message": "删除成功"
},
"data": {}
}

获取商品详情

1, url: /api/products/{id}

2, method: GET

3, responseBody

删除前

1     
2
3
4
5
6
7
8
9
10
11
{     
"meta": {
"code": 200,
"message": "查询成功"
},
"data": {
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}
}

删除后

1     
2
3
4
5
6
{     
"meta": {
"code": 404,
"message": "指定产品不存在"
}
}

获取商品列表(未分页)

1, url: /api/products

2, method: GET

3, responseBody

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{     
"meta": {
"code": 200,
"message": "获取全部商品成功"
},
"data": [
{
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
},
{
"id": "9db1992a-c342-4ff0-a2a4-aeb3dbfd93f6",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
},
{
"id": "4481619b-45c5-4729-9539-f93bb01f10d8",
"name": "Apple Watch SPORT",
"description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}
]
}

业务场景二

业务场景一中只涉及了单个资源的操作, 但实际场景中还有些关联操作; 如用户去电商网站浏览商品, 并收藏了一些商品, 之后又取消收藏了部分商品.

暂时不考虑用户认证授权, 以后加了 token后, 用户信息可以从中获取.

收藏商品

1, url: /api/products/{id}/star

2, method: PUT

3, responseBody

1     
2
3
4
5
6
7
8
9
10
11
12
13
{     
"meta": {
"code": 200,
"message": "收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功"
},
"data": [
{
"id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
"name": "iPhone 6",
"description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
}
]
}

取消收藏商品

1, url: /api/products/{id}/star

2, method: DELETE

3, responseBody

1     
2
3
4
5
6
7
{     
"meta": {
"code": 200,
"message": "删除收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功"
},
"data": []
}

自定义异常和异常处理

所有自定义异常继承RuntimeException, 在业务层抛出, 统一在Controller层进行处理.

异常分为全局异常和局部异常, 例如http method unsupported(405), unauthorized(401), accessDenied(403), not found(404)等属于全局异常; 针对对独立业务的一些异常属于局部异常, 例如产品编辑出错;

异常在Controller中进行处理, 并封装成json返回给前端, 封装后的数据如下, 相关实现见 源码;

1     
2
3
4
5
6
{     
"meta": {
"code": 404,
"message": "指定产品不存在"
}
}
1     
2
3
4
5
6
{     
"meta": {
"code": 405,
"message": "Request method 'POST' not supported"
}
}

项目运行截图部分

本系列文章

  • 前后端完全分离初探
  • 前后端完全分离之API设计
  • 前后端完全分离之安全认证与授权-上
  • 前后端完全分离之安全认证与授权-下
  • 前后端完全分离之前端模块化开发
  • 前后端完全分离之前端路由系统
  • 前后端完全分离之后端面向服务的模块化开发

JavaScript 异步机制及应用 入门教程

$
0
0

1. 异步与同步 技术研究

(1). 概念介绍

异步: asynchronous 简写async
同步: synchronous 简写sync

用比方来比喻
异步就是: N个人同时起跑, 起点和出发时间相同, 在起跑时不去关心其他人会啥时候跑完~尼玛这不废话吗?大家都才起跑怎么知道别人多就跑完.
同步就是: N个人接力跑, 起点和出发时间不同, 且后一个人会等待前一个人跑完才能继续跑, 也就是要关心前一个人的结果(上一行代码的返回值).


(2). JS里面的异步/同步

JS运行场景多数是在用户浏览器上, 程序效率优劣会直接影响用户的体验交互. 比如一个网站, 在用户注册时, 会ajax校验输入再发提交表单, 如果用同步就可能会一直卡着等待ajax响应, 好几秒结束后再跳到注册结果页, 这个体验将是非常糟糕的.

说到JS的异步, 不得不提及一个非常有代表意义函数了.

JavaScriptvar url = '/action/';
var data = 'i=1';
xmlHTTP = new XMLHttpRequest();
xmlHTTP.nonce = nonce;
xmlHTTP.open("POST", url);
xmlHTTP.onreadystatechange = function(a) {
    if(a.target.readyState!=4)return false;
    try{
        console.log(a.target.responseText)
    }catch(e){
        return false;
    }
};
xmlHTTP.send(data);

或者在jQuery写作:

JavaScript$.ajax({
    url: '/action/',
    type: 'POST',
    data: 'i=1',
    success: function(responseText){
        console.log(responseText);
    }
})

上面的无论是 xmlHTTP.onreadystatechange, 还是 success, 在JavaScript中均称为 回调方法,
以原生JS的 XMLHttpRequest为例, xmlHTTP变量是个 XMLHttpRequest对象, 他的 onreadystatechange是在每次请求响应状态发生变化时会触发的一个函数/方法, 然后在发出请求 xmlHTTP.send(data)的时候, JS并不会理会 onreadystatechange方法, 而当改送请求到达服务器, 开始响应或者响应状态改变时会调用 onreadystatechange方法:

也就是
1) 请求发出
2) 服务器开始响应数据
3) 执行回调方法, 可能执行多次

以jQuery版为例, $.ajax本身是个函数, 唯一一个参数是{...} 这个对象, 然后回调方法 success是作为这个对象的一个属性传入$.ajax的.
$.ajax()先将数据post到'/action/', 返回结果后再调用 success(如果发生错误会调用 error).
也就是

 1) 请求发出
 2) 服务器开始响应数据
 3) 响应结束执行回调方法

然后作为函数$.ajax, 是函数就应该有返回值(哪怕没有return也会返回undefined), 他本身的返回值是多少呢?
分为 async:trueasync:false两个版本:

async:true版本:

JavaScript$.ajax({'url':'a.html', type:'GET', async:true})> Object {readyState: 1}

async:false版本:

JavaScript$.ajax({'url':'robots.txt', type:'GET', false})> Object {readyState: 4, responseText: "<!DOCTYPE HTML PUBLIC ...", status: 200, statusText: "OK"}

我们可以直接看到, async:true异步模式下, jquery/javascript未将结果返回... 而async:false就将结果返回了.

然后问题就来了, 为什么async:true未返回结果呢?
答案很简单:
因为在返回的时候, 程序不可能知道结果. 异步就是指不用等此操作执行出结果再往下执行, 也就是返回的值中未包含结果.

留下一个问题, 我们是不是为了程序流程的简单化而使用同步呢?


(3). 异步的困惑

先帖一段代码:
a.php

php<?php
sleep(1);      // 休息一秒钟
echo '{}';

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    $.ajax({
    url: 'a.php',
    type: 'POST',
    dataType: 'json',
    data: {data: i},
    async: true,         // 默认即为异步
    success: function(json) {
            console.log(i + ': ' + json); // 打印
        }
    });
}

你们猜猜 打印的那行会最终打印出什么内容?

1: {}
2: {}
3: {}
4: {}

吗?

错!

输出的将是:

4: {}
4: {}
4: {}
4: {}

你TM在逗我?
没有, 这并不是JS的BUG, 也不是jQuery的BUG.
这是因为, PHP休息了一秒, 而js异步地循环从1到4, 远远用不到1秒.
然后在1秒钟后, 才开始返回数据, 触发 success, 此时此刻 i已经自增成了4.
自然而然地, 第一次 console.log(i...)就是4, 第二次也是, 第三次也是, 第四次也是.
那么如果我们希望程序输出也1,2,3,4这样输出怎么办呢?

两种方案:

1) 让后端输出i

a.php

php<?php
sleep(1);
echo '{i: ' . $_POST['data'] . '}'; // 这一行改了

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    $.ajax({
    url: 'a.php',
    type: 'POST',
    dataType: 'json',
    data: {data: i},
    async: true,
    success: function(json) {
            console.log(json.i + ': ' + json); // 这一行改了
        }
    });
}

2) 给回调的事件对象赋属性

a.php

php保持原代码不变

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    ajaxObj = $.ajax({          // 将ajax赋给ajaxObj
        url: 'a.php',
        type: 'POST',
        dataType: 'json',
        data: {data: i},
        async: true,
        success: function(json, status, obj) {    // 增加回调参数, jQuery文档有说第三个参数就是ajax方法产生的对象.
                console.log(obj.i + ': ' + json); // 从jQuery.ajax返回的对象中取i
        }
    });
    ajaxObj.i = i;            // 给ajaxObj赋属性i 值为循环的i 
}
有可能你会感到困惑, 为何可以给ajaxObj设置一个i属性然后在回调时用第三个回调参数的i属性呢?

jQuery.ajax文档中写到:

jQuery.ajax( [settings ] )
settings
...
success: Function( Anything data, String textStatus, jqXHR jqXHR )
第1个参数就是响应的文本/HTML/XML/数据/json之类的, 跟你的dataType设置有关
第2个参数就是status状态, 如success
第3个参数就是jqXHR, 也就是jQuery的XMLHttpRequest对象, 当然, 在这里就是$.ajax()生成的对象, 也就是事件的触发者本身, 
给本身设置一个属性(ajaxObj.i = i), 然后再调用本身的回调时, 使用本身的那个属性(obj.i), 当然会保持一致了.

然后
1)输出的结果将是

1: {i:1}
2: {i:2}
3: {i:3}
4: {i:4}

2)输出的结果将是

1: {}
2: {}
3: {}
4: {}

虽然略有区别, 但两者均可达到要求. 若要论代码的逼格, 相信你一定会被第二个方案给震惊的.
凭什么你给ajaxObj赋个属性就可以在 success中用了呢?

请看 (4). 异步的回调机制


(4). 异步的回调机制 ------ 事件

一个有经验的JavaScript程序员一定会将js回调用得得心应手.
因为JavaScript天生异步, 异步的好处是顾及了用户的体验, 但坏处就是导致流程化循环或者递归的逻辑明明在别的语言中无任何问题, 却在js中无法取得期待的值...
而JavaScript异步在设计之初就将这一点考虑到了. 任何流行起来的JS插件方法, 如jQuery的插件, 一定考虑到了这一点了的.

举个例子.

ajaxfileupload插件, 实现原理是将选择的文件$.clone到一个form中, form的target设置成了一个页面中的iframe, 然后定时取iframe的contents().body, 即可获得响应的值.
如果要支持multiple文件上传(一些现代化的浏览器支持), 还是得要用`XMLHttpRequest`

如下面代码:

$('input#file').on('change', function(e){
    for(i = 0; i < e.target.files.length; i++ ){
        var data = new FormData();
        data.append("file", e.target.files[i]);
        xmlHTTP = new XMLHttpRequest();
        xmlHTTP.open("POST", s.url);
        xmlHTTP.onreadystatechange = function(a) { // a 为 事件event对象
            if(a.target.readyState!=4)return false; // a.target为触发这个事件的对象 即xmlHTTP (XMLHttpRequest) 对象
            try{
                console.log(a.target.responseText);
            }catch(e){
                return false;
            }
        };
        xmlHTTP.send(data);
    }
})

你可以很明显地知道, 在 onreadystatechange调用且走到 console.log(a.target.responseText)时, 如果服务器不返回文件名, 我们根本并不知道返回的是哪个文件的URL. 如果根据i去取的话, 那么很容易地, 我们只会取到始终1个或几个, 并不能保证准确.
那么我们应该怎么去保证在 console.log(a.target.responseText)时能知道我信上传的文件的基本信息呢?

$('input#file').on('change', function(e){
    for(i = 0; i < e.target.files.length; i++ ){
        var data = new FormData();
        data.append("file", e.target.files[i]);
        xmlHTTP = new XMLHttpRequest();
        xmlHTTP.file = e.target.files[i];
        xmlHTTP.open("POST", s.url);
        xmlHTTP.onreadystatechange = function(a) {
            if(a.target.readyState!=4)return false;
            try{
                console.log(a.target.file);         //这儿是上面`xmlHTTP.file = e.target.files[i]` 赋进去的
                console.log(a.target.responseText);
            }catch(e){
                return false;
            }
        };
        xmlHTTP.send(data);
    }
})

是不是很简单?


2. 展望

(1). Google对同步JavaScript的态度

在你尝试在chrome打开的页面中执行 async: false的代码时, chrome将会警告你:

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/.

clipboard.png

(2). 职场展望

异步和事件将是JavaScript工程师必备技能

[完]
Reference:

1.《Javascript异步编程的4种方法》     http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
2.《什么是 Event Loop?》            http://www.ruanyifeng.com/blog/2013/10/event_loop.html
3.《JavaScript 运行机制详解:再谈Event Loop》 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

WebPack常用功能介绍

$
0
0

WebPack常用功能介绍

概述

Webpack是一款用户打包前端模块的工具。主要是用来打包在浏览器端使用的javascript的。同时也能转换、捆绑、打包其他的静态资源,包括css、image、font file、template等。个人认为它的优点就是易用,而且常用功能基本都有,另外可以通过自己开发loader和plugin来满足自己的需求。这里就尽量详细的来介绍下一些基本功能的使用。

安装

npm install webpack 

运行webpack

webpack需要编写一个config文件,然后根据这个文件来执行需要的打包功能。我们现在来编写一个最简单的config。新建一个文件,命名为webpack-config.js。config文件实际上就是一个Commonjs的模块。内容如下:

var webpack = require('webpack');
var path = require('path');
var buildPath = path.resolve(__dirname,"build");
var nodemodulesPath = path.resolve(__dirname,'node_modules');

var config = {
    //入口文件配置
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]//当requrie的模块找不到时,添加这些后缀
    },
    //文件导出的配置
    output:{
        path:buildPath,
        filename:"app.js"
    }
}

module.exports = config;

我的目录结构是这样的:


webpack
    |---index.html
    |---webpack-config.js
    |---src
         |---main.js
         |---js
              |---a.js

main.js文件内容如下:


var a = require('./js/a');
a();
console.log('hello world');
document.getElementById("container").innerHTML = "<p>hello world</p>";

a.js文件内容如下:

module.exports = function(){
    console.log('it is a ');
}

然后我们执行如下的命令:


webpack --config webpack-config.js --colors

这样我们就能在目录里面看到一个新生成的目录build,目录结构如下:


webpack
    |---index.html
    |---webpack-config.js
    |---build
         |---app.js

然后引用app.js就Ok啦。main.js和模块a.js的内容就都打包到app.js中了。这就演示了一个最简单的把模块的js打包到一个文件的过程了。

介绍webpack config文件

webpack是根据config里面描述的内容对一个项目进行打包的。接着我们来解释下config文件中的节点分别代表什么意思。一个config文件,基本都是由以下几个配置项组成的。

  • entry

配置要打包的文件的入口;可以配置多个入口文件,下面会有介绍。

  • resolve

    配置文件后缀名,除了js,还有jsx、coffee等等。除了这个功能还可以配置其他有用的功能,由于我还不完全了解,有知道的朋友欢迎指教。

  • output

    配置输出文件的路径,文件名等。

  • module(loaders)

配置要使用的loader。对文件进行一些相应的处理。比如babel-loader可以把es6的文件转换成es5。
大部分的对文件的处理的功能都是通过loader实现的。loader就相当于gulp里的task。loader可以用来处理在入口文件中require的和其他方式引用进来的文件。loader一般是一个独立的node模块,要单独安装。

loader配置项:

    test: /\.(js|jsx)$/,//注意是正则表达式,不要加引号,匹配要处理的文件
    loader: 'eslint-loader',//要使用的loader,"-loader"可以省略
    include: [path.resolve(__dirname, "src/app")],//把要处理的目录包括进来
    exclude: [nodeModulesPath]//排除不处理的目录

目前已有的loader列表:
https://webpack.github.io/docs/list-of-loaders.html

一个module的例子:

module: {
    preLoaders: [
      {
        test: /\.(js|jsx)$/,
        loader: 'eslint-loader',
        include: [path.resolve(__dirname, "src/app")],
        exclude: [nodeModulesPath]
      },
    ],
    loaders: [
      {
        test: /\.(js|jsx)$/, //正则表达式匹配 .js 和 .jsx 文件
        loader: 'babel-loader?optional=runtime&stage=0',//对匹配的文件进行处理的loader 
        exclude: [nodeModulesPath]//排除node module中的文件
      }
    ]
}
  • plugins

    顾名思义,就是配置要使用的插件。不过plugin和loader有什么差别还有待研究。

来看一个使用plugin的例子:

plugins: [
    //压缩打包的文件
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        //supresses warnings, usually from module minification
        warnings: false
      }
    }),
    //允许错误不打断程序
    new webpack.NoErrorsPlugin(),
    //把指定文件夹xia的文件复制到指定的目录
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ]

目前已有的plugins列表:
http://webpack.github.io/docs/list-of-plugins.html

如何压缩输出的文件

plugins: [
    //压缩打包的文件
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        //supresses warnings, usually from module minification
        warnings: false
      }
    })]

如何copy目录下的文件到输出目录

copy文件需要通过插件"transfer-webpack-plugin"来完成。

安装:

npm install transfer-webpack-plugin  -save

配置:

var TransferWebpackPlugin = require('transfer-webpack-plugin');
//其他节点省略    
plugins: [
    //把指定文件夹下的文件复制到指定的目录
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ]

打包javascript模块

支持的js模块化方案包括:

  • ES6 模块

    import MyModule from './MyModule.js';

  • CommonJS

    var MyModule = require('./MyModule.js');

  • AMD

    define(['./MyModule.js'], function (MyModule) {
    });

上面已经演示了打包js模块,这里不再重复。ES6的模块需要配置babel-loader来先把处理一下js文件。
下面展示下打包ES模块的配置文件:


var webpack = require('webpack');
var path = require('path');
var buildPath = path.resolve(__dirname, 'build');
var nodeModulesPath = path.resolve(__dirname, 'node_modules');
var TransferWebpackPlugin = require('transfer-webpack-plugin');

var config = {
  entry: [path.join(__dirname, 'src/main.js')],
  resolve: {
    extensions: ["", ".js", ".jsx"]
    //node_modules: ["web_modules", "node_modules"]  (Default Settings)
  },
  output: {
    path: buildPath,    
    filename: 'app.js'  
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.NoErrorsPlugin(),
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ],
  module: {
    preLoaders: [
      {
        test: /\.(js|jsx)$/,
        loader: 'eslint-loader',
        include: [path.resolve(__dirname, "src/app")],
        exclude: [nodeModulesPath]
      },
    ],
    loaders: [
      {
        test: /\.js$/, //注意是正则表达式,不要加引号
        loader: 'babel-loader?optional=runtime&stage=0',//babel模块相关的功能请自查,这里不做介绍
        exclude: [nodeModulesPath]
      }
    ]
  },
  //Eslint config
  eslint: {
    configFile: '.eslintrc' //Rules for eslint
  },
};

module.exports = config;

打包静态资源

  • css/sass/less

安装css-loader和style-loader


npm install css-loader --save -dev
npm install style-loader --save -dev

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',
            exclude:nodemodulesPath
        }]
    }
}

style-loader会把css文件嵌入到html的style标签里,css-loader会把css按字符串导出,这两个基本都是组合使用的。打包完成的文件,引用执行后,会发现css的内容都插入到了head里的一个style标签里。
如果是sass或less配置方式与上面类似。

  • images

可以通过url-loader把较小的图片转换成base64的字符串内嵌在生成的文件里。
安装:


npm install url-loader --save -dev

config配置:

var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',//
            exclude:nodemodulesPath
        },
        { test:/\.png$/,loader:'url-loader?limit=10000'}//限制大小小于10k的
        ]
    }
}

css文件内容:


#container{
    color: #f00;
    background:url(images/logo-201305.png);
    /*生成完图片会被处理成base64的字符串 注意:不要写'/images/logo-201305.png',否则图片不被处理*/
}
  • iconfont

内嵌iconfont的使用方法其实和上述处理png图片的方法一致。通过url-loader来处理。

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',//
            exclude:nodemodulesPath
        },
        { test:/\.(png|woff|svg|ttf|eot)$/,loader:'url-loader?limit=10000'}//限制大小小于10k的
        ]
    }
}

css文件内容:


@font-face {font-family: 'iconfont';
src: url('fonts/iconfont.eot'); /* IE9*/
src: url('fonts/iconfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/iconfont.woff') format('woff'), /* chrome、firefox */
url('fonts/iconfont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('fonts/iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */
}

执行打包后会把字体文件都转换成base64字符串内容到文件里.
这里有个头疼的问题,就是每个浏览器支持的字体格式不一样,由于把全部格式的字体打包进去,造成不必要的资源浪费。

打包template

我一大包handlebars的模块为例,来演示下打包模块的过程。有的模板对应的loader,有可能没有现车的,恐怕要自己实现loader。

先安装必须的node模块

npm install handlebars-loader --save -dev
npm install handlebars -save//是必须的

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[
        { test: /\.html$/, loader: "handlebars-loader" }
        ]
    }
}

新建一个模板文件tb.html,目录结构:

webpack
    |---index.html
    |---webpack-config.js
    |---src
         |---template
         |         |---tb.html
         |---main.js

main.js中调用模块的代码如下:


var template = require("./template/tp.html");
var data={say_hello:"it is handlebars"};
var html = template(data);
document.getElementById('tmpl_container').innerHTML = html;         

公用的模块分开打包

这需要通过插件“CommonsChunkPlugin”来实现。这个插件不需要安装,因为webpack已经把他包含进去了。
接着我们来看配置文件:

var config = {
    entry:{app:path.resolve(__dirname,'src/main.js'),
            vendor: ["./src/js/common"]},//【1】注意这里
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',
            exclude:nodemodulesPath
        }
        ]
    },
    plugins:[
        new webpack.optimize.UglifyJsPlugin({
             compress: {
                warnings: false
             }
        }),
        //【2】注意这里  这两个地方市用来配置common.js模块单独打包的
        new webpack.optimize.CommonsChunkPlugin({
            name: "vendor",//和上面配置的入口对应
            filename: "vendor.js"//导出的文件的名称
        })
    ]
}

目录结构现在是这样的:


webpack
  |---index.html
  |---webpack-config.js
  |---src
     |---main.js
     |---js
          |---a.js    //a里面require了common
          |---common.js

执行webpack会生成app.js和common.js两个文件.

多个入口

config配置:

var config = {    
    entry:{
        m1:path.resolve(__dirname,'src/main.js'),
         m2:path.resolve(__dirname,'src/main1.js')
    },//注意在这里添加文件的入口
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"[name].js"//注意这里使用了name变量
    }    
}

webpack-dev-server

在开发的过程中个,我们肯定不希望,每次修改完都手动执行webpack命令来调试程序。所以我们可以用webpack-dev-server这个模块来取代烦人的执行命令。它会监听文件,在文件修改后,自动编译、刷新浏览器的页面。另外,编译的结果是保存在内存中的,而不是实体的文件,所以是看不到的,因为这样会编译的更快。它就想到与一个轻量的express服务器。
安装:


npm install webpack-dev-server --save -dev

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    //Server Configuration options
    devServer:{
        contentBase: '',  //静态资源的目录 相对路径,相对于当前路径 默认为当前config所在的目录
        devtool: 'eval',
        hot: true,        //自动刷新
        inline: true,    
        port: 3005        
    },
    devtool: 'eval',
    output:{
        path:buildPath,
        filename:"app.js"
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),//这个好像也是必须的,虽然我还没搞懂它的作用
        new webpack.NoErrorsPlugin()
    ]
}

我的目录结构:


webpack
  |---index.html
  |---webpack-config.js//我把静态资源目录配置在了这里
  |---src
     |---main.js
     |---js
          |---a.js
          |---common.js

执行命令:


webpack-dev-server --config webpack-dev-config.js  --inline --colors

默认访问地址: http://localhost:3000/index.html(根据配置会不一样)

有一点需要声明,在index.html(引用导出结果的html文件)里直接引用“app.js”,不要加父级目录,因为此时app.js在内存里与output配置的目录无关:

<script type="text/javascript" src="app.js"></script>

详细文档在这里查看:
http://webpack.github.io/docs/webpack-dev-server.html

你有必要知道的 25 个 JavaScript 面试题

$
0
0

你有必要知道的 25 个 JavaScript 面试题

1、使用 typeof bar === "object"判断 bar是不是一个对象有神马潜在的弊端?如何避免这种弊端?

使用 typeof的弊端是显而易见的(这种弊端同使用 instanceof):

let obj = {};
let arr = [];

console.log(typeof obj === 'object');  //true
console.log(typeof arr === 'object');  //true
console.log(typeof null === 'object');  //true

从上面的输出结果可知, typeof bar === "object"并不能准确判断 bar就是一个 Object。可以通过 Object.prototype.toString.call(bar) === "[object Object]"来避免这种弊端:

let obj = {};
let arr = [];

console.log(Object.prototype.toString.call(obj));  //[object Object]
console.log(Object.prototype.toString.call(arr));  //[object Array]
console.log(Object.prototype.toString.call(null));  //[object Null]

另外,为了珍爱生命,请远离 ==

珍爱生命

[] === false是返回 false的。

2、下面的代码会在 console 输出神马?为什么?

(function(){
  var a = b = 3;
})();

console.log("a defined? " + (typeof a !== 'undefined'));   
console.log("b defined? " + (typeof b !== 'undefined'));

这跟变量作用域有关,输出换成下面的:

console.log(b); //3
console,log(typeof a); //undefined

拆解一下自执行函数中的变量赋值:

b = 3;
var a = b;

所以 b成了全局变量,而 a是自执行函数的一个局部变量。

3、下面的代码会在 console 输出神马?为什么?

var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log("outer func:  this.foo = " + this.foo);
        console.log("outer func:  self.foo = " + self.foo);
        (function() {
            console.log("inner func:  this.foo = " + this.foo);
            console.log("inner func:  self.foo = " + self.foo);
        }());
    }
};
myObject.func();

第一个和第二个的输出不难判断,在 ES6 之前,JavaScript 只有函数作用域,所以 func中的 IIFE 有自己的独立作用域,并且它能访问到外部作用域中的 self,所以第三个输出会报错,因为 this在可访问到的作用域内是 undefined,第四个输出是 bar。如果你知道闭包,也很容易解决的:

(function(test) {
            console.log("inner func:  this.foo = " + test.foo);  //'bar'
            console.log("inner func:  self.foo = " + self.foo);
}(self));

如果对闭包不熟悉,可以戳此: 从作用域链谈闭包

4、将 JavaScript 代码包含在一个函数块中有神马意思呢?为什么要这么做?

换句话说,为什么要用立即执行函数表达式(Immediately-Invoked Function Expression)。

IIFE 有两个比较经典的使用场景,一是类似于在循环中定时输出数据项,二是类似于 JQuery/Node 的插件和模块开发。

for(var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

上面的输出并不是你以为的0,1,2,3,4,而输出的全部是5,这时 IIFE 就能有用了:

for(var i = 0; i < 5; i++) {
    (function(i) {
      setTimeout(function() {
        console.log(i);  
      }, 1000);
    })(i)
}

而在 JQuery/Node 的插件和模块开发中,为避免变量污染,也是一个大大的 IIFE:

(function($) { 
        //代码
 } )(jQuery);

5、在严格模式('use strict')下进行 JavaScript 开发有神马好处?

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript做好铺垫。

6、下面两个函数的返回值是一样的吗?为什么?

function foo1()
{
  return {
      bar: "hello"
  };
}

function foo2()
{
  return
  {
      bar: "hello"
  };
}

在编程语言中,基本都是使用分号(;)将语句分隔开,这可以增加代码的可读性和整洁性。而在JS中,如若语句各占独立一行,通常可以省略语句间的分号(;),JS 解析器会根据能否正常编译来决定是否自动填充分号:

var test = 1 + 
2
console.log(test);  //3

在上述情况下,为了正确解析代码,就不会自动填充分号了,但是对于 returnbreakcontinue等语句,如果后面紧跟换行,解析器一定会自动在后面填充分号(;),所以上面的第二个函数就变成了这样:

function foo2()
{
  return;
  {
      bar: "hello"
  };
}

所以第二个函数是返回 undefined

7、神马是 NaN,它的类型是神马?怎么测试一个值是否等于 NaN?

NaN是 Not a Number 的缩写,JavaScript 的一种特殊数值,其类型是 Number,可以通过 isNaN(param)来判断一个值是否是 NaN

console.log(isNaN(NaN)); //true
console.log(isNaN(23)); //false
console.log(isNaN('ds')); //true
console.log(isNaN('32131sdasd')); //true
console.log(NaN === NaN); //false
console.log(NaN === undefined); //false
console.log(undefined === undefined); //false
console.log(typeof NaN); //number
console.log(Object.prototype.toString.call(NaN)); //[object Number]

ES6 中, isNaN()成为了 Number 的静态方法: Number.isNaN().

8、解释一下下面代码的输出

console.log(0.1 + 0.2);   //0.30000000000000004
console.log(0.1 + 0.2 == 0.3);  //false

JavaScript 中的 number 类型就是浮点型,JavaScript 中的浮点数采用IEEE-754 格式的规定,这是一种二进制表示法,可以精确地表示分数,比如1/2,1/8,1/1024,每个浮点数占64位。但是,二进制浮点数表示法并不能精确的表示类似0.1这样 的简单的数字,会有舍入误差。

由于采用二进制,JavaScript 也不能有限表示 1/10、1/2 等这样的分数。在二进制中,1/10(0.1)被表示为 0.00110011001100110011……注意 0011是无限重复的,这是舍入误差造成的,所以对于 0.1 + 0.2 这样的运算,操作数会先被转成二进制,然后再计算:

0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)

双精度浮点数的小数部分最多支持 52 位,所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100…因浮点数小数位的限制而截断的二进制数字,这时候,再把它转换为十进制,就成了 0.30000000000000004。

对于保证浮点数计算的正确性,有两种常见方式。

一是先升幂再降幂:

function add(num1, num2){
  let r1, r2, m;
  r1 = (''+num1).split('.')[1].length;
  r2 = (''+num2).split('.')[1].length;

  m = Math.pow(10,Math.max(r1,r2));
  return (num1 * m + num2 * m) / m;
}
console.log(add(0.1,0.2));   //0.3
console.log(add(0.15,0.2256)); //0.3756

二是是使用内置的 toPrecision()toFixed()方法, 注意,方法的返回值字符串。

function add(x, y) {
    return x.toPrecision() + y.toPrecision()
}
console.log(add(0.1,0.2));  //"0.10.2"

9、实现函数 isInteger(x)来判断 x 是否是整数

可以将 x转换成10进制,判断和本身是不是相等即可:

function isInteger(x) { 
    return parseInt(x, 10) === x; 
}

ES6 对数值进行了扩展,提供了静态方法 isInteger()来判断参数是否是整数:

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

JavaScript能够准确表示的整数范围在 -2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限,并提供了 Number.isSafeInteger()来判断整数是否是安全型整数。

10、在下面的代码中,数字 1-4 会以什么顺序输出?为什么会这样输出?

(function() {
    console.log(1); 
    setTimeout(function(){console.log(2)}, 1000); 
    setTimeout(function(){console.log(3)}, 0); 
    console.log(4);
})();

这个就不多解释了,主要是 JavaScript 的定时机制和时间循环,不要忘了,JavaScript 是单线程的。详解可以参考 从setTimeout谈JavaScript运行机制

11、写一个少于 80 字符的函数,判断一个字符串是不是回文字符串

function isPalindrome(str) {
    str = str.replace(/\W/g, '').toLowerCase();
    return (str == str.split('').reverse().join(''));
}

这个题我在 codewars上碰到过,并收录了一些不错的解决方式,可以戳这里: Palindrome For Your Dome

12、写一个按照下面方式调用都能正常工作的 sum 方法

console.log(sum(2,3));   // Outputs 5
console.log(sum(2)(3));  // Outputs 5

针对这个题,可以判断参数个数来实现:

function sum() {
  var fir = arguments[0];
  if(arguments.length === 2) {
    return arguments[0] + arguments[1]
  } else {
    return function(sec) {
       return fir + sec;
    }
  }
}

13、根据下面的代码片段回答后面的问题

for (var i = 0; i < 5; i++) {
  var btn = document.createElement('button');
  btn.appendChild(document.createTextNode('Button ' + i));
  btn.addEventListener('click', function(){ console.log(i); });
  document.body.appendChild(btn);
}

1、点击 Button 4,会在控制台输出什么?

2、给出一种符合预期的实现方式

1、点击5个按钮中的任意一个,都是输出5

2、参考 IIFE。

14、下面的代码会输出什么?为什么?

var arr1 = "john".split(''); j o h n
var arr2 = arr1.reverse(); n h o j
var arr3 = "jones".split(''); j o n e s
arr2.push(arr3);
console.log("array 1: length=" + arr1.length + " last=" + arr1.slice(-1));
console.log("array 2: length=" + arr2.length + " last=" + arr2.slice(-1));

会输出什么呢?你运行下就知道了,可能会在你的意料之外。

MDN 上对于 reverse()的描述是酱紫的:

Description

The reverse method transposes the elements of the calling array object in place, mutating the array, and returning a reference to the array.

reverse()会改变数组本身,并返回原数组的引用。

slice的用法请参考: slice

15、下面的代码会输出什么?为什么?

console.log(1 +  "2" + "2");
console.log(1 +  +"2" + "2");
console.log(1 +  -"1" + "2");
console.log(+"1" +  "1" + "2");
console.log( "A" - "B" + "2");
console.log( "A" - "B" + 2);

输出什么,自己去运行吧,需要注意三个点:

  • 多个数字和数字字符串混合运算时,跟操作数的位置有关
console.log(2 + 1 + '3'); / /‘33’
console.log('3' + 2 + 1); //'321'
  • 数字字符串之前存在数字中的正负号(+/-)时,会被转换成数字
console.log(typeof '3');   // string
console.log(typeof +'3');  //number

同样,可以在数字前添加 '',将数字转为字符串

console.log(typeof 3);   // number
console.log(typeof (''+3));  //string
  • 对于运算结果不能转换成数字的,将返回 NaN
console.log('a' * 'sd');   //NaN
console.log('A' - 'B');  // NaN

这张图是运算转换的规则

运算符转换

16、如果 list 很大,下面的这段递归代码会造成堆栈溢出。如果在不改变递归模式的前提下修善这段代码?

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        nextListItem();
    }
};

原文上的解决方式是加个定时器:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};

解决方式的原理请参考第10题。

17、什么是闭包?举例说明

可以参考此篇: 从作用域链谈闭包

18、下面的代码会输出什么?为啥?

for (var i = 0; i < 5; i++) {
  setTimeout(function() { console.log(i); }, i * 1000 );
}

请往前面翻,参考第4题,解决方式已经在上面了

19、解释下列代码的输出

console.log("0 || 1 = "+(0 || 1));
console.log("1 || 2 = "+(1 || 2));
console.log("0 && 1 = "+(0 && 1));
console.log("1 && 2 = "+(1 && 2));

逻辑与和逻辑或运算符会返回一个值,并且二者都是短路运算符:

  • 逻辑与返回第一个是 false的操作数 或者 最后一个是 true的操作数
console.log(1 && 2 && 0);  //0
console.log(1 && 0 && 1);  //0
console.log(1 && 2 && 3);  //3

如果某个操作数为 false,则该操作数之后的操作数都不会被计算

  • 逻辑或返回第一个是 true的操作数 或者 最后一个是 false的操作数
console.log(1 || 2 || 0); //1
console.log(0 || 2 || 1); //2
console.log(0 || 0 || false); //false

如果某个操作数为 true,则该操作数之后的操作数都不会被计算

如果逻辑与和逻辑或作混合运算,则逻辑与的优先级高:

console.log(1 && 2 || 0); //2
console.log(0 || 2 && 1); //1
console.log(0 && 2 || 1); //1

在 JavaScript,常见的 false值:

0, '0', +0, -0, false, '',null,undefined,null,NaN

要注意 空数组([])空对象({}):

console.log([] == false) //true
console.log({} == false) //false
console.log(Boolean([])) //true
console.log(Boolean({})) //true

所以在 if中, []{}都表现为 true

if

20、解释下面代码的输出

console.log(false == '0')
console.log(false === '0')

请参考前面第14题运算符转换规则的图。

21、解释下面代码的输出

var a={},
    b={key:'b'},
    c={key:'c'};

a[b]=123;
a[c]=456;

console.log(a[b]);

输出是 456,参考原文的解释:

The reason for this is as follows: When setting an object property, JavaScript will implicitly stringify the parameter value. In this case, since b and c are both objects, they will both be converted to [object Object]. As a result, a[b] anda[c] are both equivalent to a[[object Object]] and can be used interchangeably. Therefore, setting or referencing a[c] is precisely the same as setting or referencing a[b].

22、解释下面代码的输出

console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));

结果是10的阶乘。这是一个递归调用,为了简化,我初始化 n=5,则调用链和返回链如下:

递归

23、解释下面代码的输出

(function(x) {
    return (function(y) {
        console.log(x);
    })(2)
})(1);

输出1,闭包能够访问外部作用域的变量或参数。

24、解释下面代码的输出,并修复存在的问题

var hero = {
    _name: 'John Doe',
    getSecretIdentity: function (){
        return this._name;
    }
};

var stoleSecretIdentity = hero.getSecretIdentity;

console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());

getSecretIdentity赋给 stoleSecretIdentity,等价于定义了 stoleSecretIdentity函数:

var stoleSecretIdentity =  function (){
        return this._name;
}

stoleSecretIdentity的上下文是全局环境,所以第一个输出 undefined。若要输出 John Doe,则要通过 callapplybind等方式改变 stoleSecretIdentitythis指向(hero)。

第二个是调用对象的方法,输出 John Doe

25、给你一个 DOM 元素,创建一个能访问该元素所有子元素的函数,并且要将每个子元素传递给指定的回调函数。

函数接受两个参数:

  • DOM
  • 指定的回调函数

原文利用 深度优先搜索(Depth-First-Search) 给了一个实现:

function Traverse(p_element,p_callback) {
   p_callback(p_element);
   var list = p_element.children;
   for (var i = 0; i < list.length; i++) {
       Traverse(list[i],p_callback);  // recursive call
   }
}

文章参考:

25 Essential JavaScript Interview Questions

JavaScript中的立即执行函数表达式

Javascript 严格模式详解

声明:文中对问题的回答仅代表博主个人观点

转载请注明: 淡忘~浅思» 你有必要知道的 25 个 JavaScript 面试题

懒人必备的移动端定宽网页适配方案

$
0
0

本文最初发布于我的个人博客: 咀嚼之味

如今移动设备的分辨率纷繁复杂。以前仅仅是安卓机拥有各种各样的适配问题,如今 iPhone 也拥有了三种主流的分辨率,而未来的 iPhone 7 可能又会玩出什么新花样。如何以不变应万变,用简简单单的几行代码就能支持种类繁多的屏幕分辨率呢?今天就给大家介绍一种懒人必备的移动端定宽网页适配方法。

首先看看下面这行代码:

<meta name="viewport" content="width=device-width, user-scalabel=no">

有过移动端开发经验的同学是不是对上面这句代码非常熟悉?它可能最常见的响应式设计的 viewport设置之一,而我今天介绍的这种方法也是利用了 meta 标签设置 viewport来支持大部分的移动端屏幕分辨率。

目标

  • 仅仅通过配置 <meta name="viewport">使得移动端网站只需要按照固定的宽度设计并实现,就能在任何主流的移动设备上都能看到符合设计稿的页面,包括 Android 4+、iPhone 4+。

测试设备

  • 三星 Note II (Android 4.1.2) - 真机

  • 三星 Note III (Android 4.4.4 - API 19) - Genymotion 虚拟机

  • iPhone 6 (iOS 9.1) - 真机

iPhone

iPhone 的适配比较简单,只需要设置 width即可。比如:

<!-- for iPhone --><meta name="viewport" content="width=320, user-scalable=no" />

这样你的页面在所有的 iPhone 上,无论是 宽 375 像素的 iPhone 6 还是宽 414 像素的 iPhone 6 plus,都能显示出定宽 320 像素的页面。

Android

Android 上的适配被戏称为移动端的 IE,确实存在着很多兼容性问题。Android 以 4.4 版本为一个分水岭,首先说一说相对好处理的 Android 4.4+

Android 4.4+

为了兼容性考虑,Android 4.4 以上抛弃了 target-densitydpi属性,它只会在 Android 设备上生效。如果对这个被废弃的属性感兴趣,可以看看下面这两个链接:

我们可以像在 iPhone 上那样设置 width=320以达到我们想要的 320px 定宽的页面设计。

<!-- for Android 4.4+ --><meta name="viewport" content="width=320, user-scalable=no" />

Android 4.0 ~ 4.3

作为 Android 相对较老的版本,它对 meta 中的 width 属性支持得比较糟糕。以三星 Note II 为例,它的 device-width 是 360px。如果设置 viewport 中的 width (以下简称 vWidth ) 为小于等于 360 的值,则不会有任何作用;而设置 vWidth为大于 360 的值,也不会使画面产生缩放,而是出现了横向滚动条。

想要对 Android 4.0 ~ 4.3 进行支持,还是不得不借助于 页面缩放,以及那个被废除的属性: target-densitydpi

target-densitydpi

target-densitydpi 一共有四种取值:low-dpi (0.75), medium-dpi (1.0), high-dpi (1.5), device-dpi。在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。我使用手头上的三星 Note II 设备 (Android 4.1.2) 进行了一系列实验,得到了下面这张表格:

target-densitydpiviewport: widthbody width屏幕可视范围宽度
low-dpi (0.75)vWidth <= 320270270
vWidth > 320vWidth*270
medium-dpi (1.0)vWidth <= 360360360
vWidth > 360vWidth*360
high-dpi (1.5)vWidth <= 320540540
320 < vWidth <= 540vWidth*vWidth*
vWidth > 540vWidth*540
device-dpi (2.0)**vWidth <= 320720720
320 < vWidth <= 720vWidth*vWidth*
vWidth > 720vWidth*720
  • vWidth*:指的是与 viewport 中设置的 width 的值相同。

  • device-dpi (2.0)**:在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。

首先可以看到 320px是个特别诡异的临界值,低于这个临界值后就会发生超出我们预期的事情。综合考虑下来,还是采用 target-densitydpi = device-dpi这一取值。如果你想要以 320px 作为页面的宽度的话,我建议你针对安卓 4.4 以下的版本设置 width=321

如果 body 的宽度超过屏幕可视范围的宽度,就会出现水平的滚动条。这并不是我们期望的结果,所以我们还要用到缩放属性 initial-scale。计算公式如下:

Scale = deviceWidth / vWidth

这样的计算式不得不使用 JS 来实现,最终我们就能得到适配 Android 4.0 ~ 4.3定宽的代码:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
}

其中, TARGET_WIDTH就是你所期望的宽度,注意这段代码仅在 320-720px之间有效哦。

缩放中的坑

如果是 iPhone 或者 Android 4.4+ 的机器,在使用 scale 相关的属性时要非常谨慎,包括 initial-scale, maximum-scaleminimum-scale
要么保证 Scale = deviceWidth / vWidth,要么就尽量不用。来看一个例子:

Android 4.4+ 和 iPhone 在缩放时的行为不一致

在缩放比不能保证的情况下,即时设置同样的 widthinitial-scale后,两者的表现也是不一致。具体两种机型采用的策略如何我还没有探索出来,有兴趣的同学可以研究看看。最省事的办法就是在 iPhone 和 Android 4.4+ 上不设置 scale 相关的属性。

总结

结合上面所有的分析,你可以通过下面这段 JS 代码来对所有 iPhone 和 Android 4+ 的手机屏幕进行适配:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
} else {
    document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH);
}

如果你不希望你的页面被用户手动缩放,你还可以加上 user-scalable=no。不过需要注意的是,这个属性在部分安卓机型上是无效的哦。

其他参考资料

  1. Supporting Different Screens in Web Apps - Android Developers

  2. Viewport target-densitydpi support is being deprecated

附录 - 测试页面

有兴趣的同学可以拿这个测试页面来测测自己的手机,别忘了改 viewport哦。

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=250, initial-scale=1.5, user-scalable=no"><title>Document</title><style>
        body {
            margin: 0;
        }

        div {
            background: #000;
            color: #fff;
            font-size: 30px;
            text-align: center;
        }

        .block {
            height: 50px;
            border-bottom: 4px solid #ccc;
        }

        #first  { width: 100px; }
        #second { width: 200px; }
        #third  { width: 300px; }
        #fourth { width: 320px; }
        #log { font-size: 16px; }
    </style></head><body><div id="first" class="block">100px</div><div id="second" class="block">200px</div><div id="third" class="block">300px</div><div id="fourth" class="block">320px</div><div id="log"></div><script>
        function log(content) {
            var logContainer = document.getElementById('log');
            var p = document.createElement('p');
            p.textContent = content;
            logContainer.appendChild(p);
        }

        log('body width:' + document.body.clientWidth)
        log(document.querySelector('[name="viewport"]').content)</script></body></html>

JavaScript性能优化小知识总结

$
0
0

JavaScript的性能问题不容小觑,这就需要我们开发人员在编写JavaScript程序时多注意一些细节,本文非常详细的介绍了一下JavaScript性能优化方面的知识点,绝对是干货。

前言

一直在学习javascript,也有看过《犀利开发Jquery内核详解与实践》,对这本书的评价只有两个字犀利,可能是对javascript理解的还不够透彻异或是自己太笨,更多的是自己不擅于思考懒得思考以至于里面说的一些精髓都没有太深入的理解。

鉴于想让自己有一个提升,进不了一个更加广阔的天地,总得找一个属于自己的居所好好生存,所以平时会有意无意的去积累一些使用jQuerry的常用知识,特别是对于性能要求这一块,总是会想是不是有更好的方式来实现。

下面是我总结的一些小技巧,仅供参考。(我先会说一个总标题,然后用一小段话来说明这个意思 再最后用一个demo来简单言明)

避免全局查找

在一个函数中会用到全局对象存储为局部变量来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快些

        function search() {
            //当我要使用当前页面地址和主机域名
            alert(window.location.href + window.location.host);
        }
        //最好的方式是如下这样  先用一个简单变量保存起来
        function search() {
            var location = window.location;
            alert(location.href + location.host);
        }

定时器

如果针对的是不断运行的代码, 不应该使用setTimeout,而应该是用setInterval,因为setTimeout每一次都会初始化一个定时器,而setInterval只会在开始的时候初始化一个定时器

        var timeoutTimes = 0;
        function timeout() {
            timeoutTimes++;
            if (timeoutTimes < 10) {
                setTimeout(timeout, 10);
            }
        }
        timeout();
        //可以替换为:
        var intervalTimes = 0;
        function interval() {
            intervalTimes++;
            if (intervalTimes >= 10) {
                clearInterval(interv);
            }
        }
        var interv = setInterval(interval, 10);

字符串连接

如果要连接多个字符串,应该少使用+=,如

s+=a;

s+=b;

s+=c;

应该写成s+=a + b + c;

而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存,使用JavaScript数组来收集,最后 使用join方法连接起来

        var buf = [];
        for (var i = 0; i < 100; i++) {
            buf.push(i.toString());
        }
        var all = buf.join("");

避免with语句

和函数类似 ,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度,由于额外的作用域链的查找,在with语句中执行的代码肯定会比外面执行的代码要慢,在 能不使用with语句的时候尽量不要使用with语句

 with (a.b.c.d) {
            property1 = 1;
            property2 = 2;
        }
        //可以替换为:
        var obj = a.b.c.d;
        obj.property1 = 1;
        obj.property2 = 2;

数字转换成字符串

般最好用”" + 1来将数字转换成字符串,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:

(“” +) > String() > .toString() > new String()

浮点数转换成整型

很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()

各种类型转换

var myVar = "3.14159",
        str = "" + myVar, //  to string  
        i_int = ~ ~myVar,  //  to integer  
        f_float = 1 * myVar,  //  to float  
        b_bool = !!myVar,  /*  to boolean - any string with length 
                                and any number except 0 are true */
        array = [myVar];  //  to array

如果定义了toString()方法来进行类型转换的话,推荐 显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高

多个类型声明

在JavaScript中所有变量都可以使用单个var语句来声明,这样就是组合在一起的语句,以减少整个脚本的执行时间,就如上面代码一样,上面代码格式也挺规范,让人一看就明了。

插入迭代器

如var name=values[i]; i++;前面两条语句可以写成var name=values[i++]

使用直接量

var aTest = new Array(); //替换为
        var aTest = [];
        var aTest = new Object; //替换为
        var aTest = {};
        var reg = new RegExp(); //替换为
        var reg = /../;
        //如果要创建具有一些特性的一般对象,也可以使用字面量,如下:
        var oFruit = new O;
        oFruit.color = "red";
        oFruit.name = "apple";
        //前面的代码可用对象字面量来改写成这样:
        var oFruit = { color: "red", name: "apple" };

使用DocumentFragment优化多次append

一旦需要更新DOM,请考虑使用文档碎片来构建DOM结构,然后再将其添加到现存的文档中。

for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替换为:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用一次innerHTML赋值代替构建dom元素

对于大的DOM更改,使用innerHTML要比使用标准的DOM方法创建同样的DOM结构快得多。

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //可以替换为:
        var html = [];
        for (var i = 0; i < 1000; i++) {
            html.push('<p>' + i + '</p>');
        }
        document.body.innerHTML = html.join('');

通过模板元素clone,替代createElement

很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低,如果需要直接插入HTML,可以找一个容器元素,比如指定一个div或者span,并设置他们的innerHTML来将自己的HTML代码插入到页面中。通常我们可能会使用字符串直接写HTML来创建节点,其实这样做,1无法保证代码的有效性2字符串操作效率低,所以应该是用document.createElement()方法,而如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //替换为:
        var frag = document.createDocumentFragment();
        var pEl = document.getElementsByTagName('p')[0];
        for (var i = 0; i < 1000; i++) {
            var el = pEl.cloneNode(false);
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用firstChild和nextSibling代替childNodes遍历dom元素

        var nodes = element.childNodes;
        for (var i = 0, l = nodes.length; i < l; i++) {
            var node = nodes[i];
            //……
        }
        //可以替换为:
        var node = element.firstChild;
        while (node) {
            //……
            node = node.nextSibling;

删除DOM节点

删除dom节点之前,一定要删除注册在该节点上的事件,不管是用observe方式还是用attachEvent方式注册的事件,否则将会产生无法回收的内存。另外,在removeChild和innerHTML=’’二者之间,尽量选择后者. 因为在sIEve(内存泄露监测工具)中监测的结果是用removeChild无法有效地释放dom节点

使用事件代理

任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理,使用这个知识就可以将事件处理程序附加到更高的地方负责多个目标的事件处理,同样, 对于内容动态增加并且子节点都需要相同的事件处理函数的情况,可以把事件注册提到父节点上,这样就不需要为每个子节点注册事件监听了。另外,现有的js库都采用observe方式来创建事件监听,其实现上隔离了dom对象和事件处理函数之间的循环引用,所以应该尽量采用这种方式来创建事件监听

重复使用的调用结果,事先保存到局部变量

        //避免多次取值的调用开销
        var h1 = element1.clientHeight + num1;
        var h2 = element1.clientHeight + num2;
        //可以替换为:
        var eleHeight = element1.clientHeight;
        var h1 = eleHeight + num1;
        var h2 = eleHeight + num2;

注意NodeList

最小化访问NodeList的次数可以极大的改进脚本的性能

        var images = document.getElementsByTagName('img');
        for (var i = 0, len = images.length; i < len; i++) {

        }

编写JavaScript的时候一定要知道何时返回NodeList对象,这样可以最小化对它们的访问

  • 进行了对getElementsByTagName()的调用
  • 获取了元素的childNodes属性
  • 获取了元素的attributes属性
  • 访问了特殊的集合,如document.forms、document.images等等

要了解了当使用NodeList对象时,合理使用会极大的提升代码执行速度

优化循环

可以使用下面几种方式来优化循环

  • 减值迭代

大多数循环使用一个从0开始、增加到某个特定值的迭代器,在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效

  • 简化终止条件

由于每次循环过程都会计算终止条件,所以必须保证它尽可能快,也就是说避免属性查找或者其它的操作,最好是将循环控制量保存到局部变量中,也就是说对数组或列表对象的遍历时,提前将length保存到局部变量中,避免在循环的每一步重复取值。

        var list = document.getElementsByTagName('p');
        for (var i = 0; i < list.length; i++) {
            //……
        }

        //替换为:
        var list = document.getElementsByTagName('p');
        for (var i = 0, l = list.length; i < l; i++) {
            //……
        }
  • 简化循环体

循环体是执行最多的,所以要确保其被最大限度的优化

  • 使用后测试循环

在JavaScript中,我们可以使用for(;;),while(),for(in)三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以,就应该尽量少用。for(;;)和while循环,while循环的效率要优于for(;;),可能是因为for(;;)结构的问题,需要经常跳转回去。

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0;
        for (var i = 0, l = arr.length; i < l; i++) {
            sum += arr[i];
        }

        //可以考虑替换为:

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0, l = arr.length;
        while (l--) {
            sum += arr[l];
        }

最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。

展开循环

当循环次数是确定的,消除循环并使用多次函数调用往往会更快。

避免双重解释

如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串,也就是

  • 尽量少使用eval函数

使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间,而且使用Eval带来的安全性问题也是不容忽视的。

  • 不要使用Function构造器

不要给setTimeout或者setInterval传递字符串参数

        var num = 0;
        setTimeout('num++', 10);
        //可以替换为:
        var num = 0;
        function addNum() {
            num++;
        }
        setTimeout(addNum, 10);

缩短否定检测

       if (oTest != '#ff0000') {
            //do something
        }
        if (oTest != null) {
            //do something
        }
        if (oTest != false) {
            //do something
        }
        //虽然这些都正确,但用逻辑非操作符来操作也有同样的效果:
        if (!oTest) {
            //do something
        }

条件分支

  • 将条件分支,按可能性顺序从高到低排列:可以减少解释器对条件的探测次数
  • 在同一条件子的多(>2)条件分支时,使用switch优于if:switch分支选择的效率高于if,在IE下尤为明显。4分支的测试,IE下switch的执行时间约为if的一半。
  • 使用三目运算符替代条件分支
        if (a > b) {
            num = a;
        } else {
            num = b;
        }
        //可以替换为:
        num = a > b ? a : b;

使用常量

  • 重复值:任何在多处用到的值都应该抽取为一个常量
  • 用户界面字符串:任何用于显示给用户的字符串,都应该抽取出来以方便国际化
  • URLs:在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL
  • 任意可能会更改的值:每当你用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化,如果答案是“是”,那么这个值就应该被提取出来作为一个常量。

避免与null进行比较

由于JavaScript是弱类型的,所以它不会做任何的自动类型检查,所以如果看到与null进行比较的代码,尝试使用以下技术替换

  • 如果值应为一个引用类型,使用instanceof操作符检查其构造函数
  • 如果值应为一个基本类型,作用typeof检查其类型
  • 如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上

避免全局量

全局变量应该全部字母大写,各单词之间用_下划线来连接。尽可能避免全局变量和函数, 尽量减少全局变量的使用,因为在一个页面中包含的所有JavaScript都在同一个域中运行。所以如果你的代码中声明了全局变量或者全局函数的话,后面的代码中载入的脚本文件中的同名变量和函数会覆盖掉(overwrite)你的。

//糟糕的全局变量和全局函数
var current = null;
function init(){
//...
}
function change() {
    //...
}
function verify() {
    //...
}
//解决办法有很多,Christian Heilmann建议的方法是:
//如果变量和函数不需要在“外面”引用,那么就可以使用一个没有名字的方法将他们全都包起来。
(function(){
var current = null;
function init() {
    //...
}
function change() {
    //...
}
function verify() {
    //...
}
})();
//如果变量和函数需要在“外面”引用,需要把你的变量和函数放在一个“命名空间”中
//我们这里用一个function做命名空间而不是一个var,因为在前者中声明function更简单,而且能保护隐私数据
myNameSpace = function() {
    var current = null;

    function init() {
        //...
    }

    function change() {
        //...
    }

    function verify() {
        //...
    }

//所有需要在命名空间外调用的函数和属性都要写在return里面
    return {
        init: init,
        //甚至你可以为函数和属性命名一个别名
        set: change
    };
};

尊重对象的所有权

因为JavaScript可以在任何时候修改任意对象,这样就可以以不可预计的方式覆写默认的行为,所以如果你不负责维护某个对象,它的对象或者它的方法,那么你就不要对它进行修改,具体一点就是说:

  • 不要为实例或原型添加属性
  • 不要为实例或者原型添加方法
  • 不要重定义已经存在的方法
  • 不要重复定义其它团队成员已经实现的方法,永远不要修改不是由你所有的对象,你可以通过以下方式为对象创建新的功能:
  • 创建包含所需功能的新对象,并用它与相关对象进行交互
  • 创建自定义类型,继承需要进行修改的类型,然后可以为自定义类型添加额外功能

循环引用

如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。内存泄露的后果是在浏览器关闭前,即使是刷新页面,这部分内存不会被浏览器释放。

简单的循环引用:

        var el = document.getElementById('MyElement');
        var func = function () {
            //…
        }
        el.func = func;
        func.element = el;

但是通常不会出现这种情况。通常循环引用发生在为dom元素添加闭包作为expendo的时候。

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();

init在执行的时候,当前上下文我们叫做context。这个时候,context引用了el,el引用了function,function引用了context。这时候形成了一个循环引用。

下面2种方法可以解决循环引用:

1)  置空dom对象

       function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替换为:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            el = null;
        }
        init();

将el置空,context中不包含对dom对象的引用,从而打断循环应用。

如果我们需要将dom对象返回,可以用如下方法:

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            return el;
        }
        init();
        //可以替换为:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            try {
                return el;
            } finally {
                el = null;
            }
        }
        init();

2)  构造新的context

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替换为:
        function elClickHandler() {
            //……
        }
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = elClickHandler;
        }
        init();

把function抽到新的context中,这样,function的context就不包含对el的引用,从而打断循环引用。

通过javascript创建的dom对象,必须append到页面中

IE下,脚本创建的dom对象,如果没有append到页面中,刷新页面,这部分内存是不会回收的!

        function create() {
            var gc = document.getElementById('GC');
            for (var i = 0; i < 5000; i++) {
                var el = document.createElement('div');
                el.innerHTML = "test";
                //下面这句可以注释掉,看看浏览器在任务管理器中,点击按钮然后刷新后的内存变化
                gc.appendChild(el);
            }
        }

释放dom元素占用的内存

将dom元素的innerHTML设置为空字符串,可以释放其子元素占用的内存。

在rich应用中,用户也许会在一个页面上停留很长时间,可以使用该方法释放积累得越来越多的dom元素使用的内存。

释放javascript对象

在rich应用中,随着实例化对象数量的增加,内存消耗会越来越大。所以应当及时释放对对象的引用,让GC能够回收这些内存控件。

对象:obj = null

对象属性:delete obj.myproperty

数组item:使用数组的splice方法释放数组中不用的item

避免string的隐式装箱

对string的方法调用,比如’xxx’.length,浏览器会进行一个隐式的装箱操作,将字符串先转换成一个String对象。推荐对声明有可能使用String实例方法的字符串时,采用如下写法:

var myString = new String(‘Hello World’);

松散耦合

1、解耦HTML/JavaScript

JavaScript和HTML的紧密耦合:直接写在HTML中的JavaScript、使用包含内联代码的<script>元素、使用HTML属性来分配事件处理程序等

HTML和JavaScript的紧密耦合:JavaScript中包含HTML,然后使用innerHTML来插入一段html文本到页面

其实应该是保持层次的分离,这样可以很容易的确定错误的来源,所以我们应确保HTML呈现应该尽可能与JavaScript保持分离

2、解耦CSS/JavaScript

显示问题的唯一来源应该是CSS,行为问题的唯一来源应该是JavaScript,层次之间保持松散耦合才可以让你的应用程序更加易于维护,所以像以下的代码element.style.color=”red”尽量改为element.className=”edit”,而且不要在css中通过表达式嵌入JavaScript

3、解耦应用程序/事件处理程序

将应用逻辑和事件处理程序相分离:一个事件处理程序应该从事件对象中提取,并将这些信息传送给处理应用逻辑的某个方法中。这样做的好处首先可以让你更容易更改触发特定过程的事件,其次可以在不附加事件的情况下测试代码,使其更易创建单元测试

性能方面的注意事项

1、尽量使用原生方法

2、switch语句相对if较快

通过将case语句按照最可能到最不可能的顺序进行组织

3、位运算较快

当进行数字运算时,位运算操作要比任何布尔运算或者算数运算快

4、 巧用||&&布尔运算符

        function eventHandler(e) {
            if (!e) e = window.event;
        }
        //可以替换为:
        function eventHandler(e) {
            e = e || window.event;
        }
        if (myobj) {
            doSomething(myobj);
        }
        //可以替换为:
        myobj && doSomething(myobj);

避免错误应注意的地方

1、每条语句末尾须加分号

在if语句中,即使条件表达式只有一条语句也要用{}把它括起来,以免后续如果添加了语句之后造成逻辑错误

2、使用+号时需谨慎

JavaScript 和其他编程语言不同的是,在 JavaScript 中,’+'除了表示数字值相加,字符串相连接以外,还可以作一元运算符用,把字符串转换为数字。因而如果使用不当,则可能与自增符’++’混淆而引起计算错误

        var valueA = 20;
        var valueB = "10";
        alert(valueA + valueB);     //ouput: 2010 
        alert(valueA + (+valueB));  //output: 30 
        alert(valueA + +valueB);    //output:30 
        alert(valueA ++ valueB);     //Compile error

3、使用return语句需要注意

一条有返回值的return语句不要用()括号来括住返回值,如果返回表达式,则表达式应与return关键字在同一行,以避免压缩时,压缩工具自动加分号而造成返回与开发人员不一致的结果

        function F1() {
            var valueA = 1;
            var valueB = 2;
            return valueA + valueB;
        }
        function F2() {
            var valueA = 1;
            var valueB = 2;
            return
            valueA + valueB;
        }
        alert(F1());  //output: 3 
        alert(F2());  //ouput: undefined

==和===的区别

避免在if和while语句的条件部分进行赋值,如if (a = b),应该写成if (a == b),但是在比较是否相等的情况下,最好使用全等运行符,也就是使用===和!==操作符会相对于==和!=会好点。==和!=操作符会进行类型强制转换

        var valueA = "1";
        var valueB = 1;
        if (valueA == valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Equal"
        if (valueA === valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Not equal"

不要使用生偏语法

不要使用生偏语法,写让人迷惑的代码,虽然计算机能够正确识别并运行,但是晦涩难懂的代码不方便以后维护

函数返回统一类型

虽然JavaScript是弱类型的,对于函数来说,前面返回整数型数据,后面返回布尔值在编译和运行都可以正常通过,但为了规范和以后维护时容易理解,应保证函数应返回统一的数据类型

总是检查数据类型

要检查你的方法输入的所有数据,一方面是为了安全性,另一方面也是为了可用性。用户随时随地都会输入错误的数据。这不是因为他们蠢,而是因为他们很忙,并且思考的方式跟你不同。用typeof方法来检测你的function接受的输入是否合法

何时用单引号,何时用双引号

虽然在JavaScript当中,双引号和单引号都可以表示字符串, 为了避免混乱,我们建议在HTML中使用双引号,在JavaScript中使用单引号,但为了兼容各个浏览器,也为了解析时不会出错,定义JSON对象时,最好使用双引号

部署

  • 用JSLint运行JavaScript验证器来确保没有语法错误或者是代码没有潜在的问
  • 部署之前推荐使用压缩工具将JS文件压缩
  • 文件编码统一用UTF-8
  • JavaScript 程序应该尽量放在 .js 的文件中,需要调用的时候在 HTML 中以 <script src=”filename.js”> 的形式包含进来。JavaScript 代码若不是该 HTML 文件所专用的,则应尽量避免在 HTML 文件中直接编写 JavaScript 代码。因为这样会大大增加 HTML 文件的大小,无益于代码的压缩和缓存的使用。另外,<script src=”filename.js”> 标签应尽量放在文件的后面,最好是放在</body>标签前。这样会降低因加载 JavaScript 代码而影响页面中其它组件的加载时间。

永远不要忽略代码优化工作,重构是一项从项目开始到结束需要持续的工作,只有不断的优化代码才能让代码的执行效率越来越好

样式表的载入会延迟DOM载入事件

$
0
0

绝大多数情况下我们总是让JavaScript在DOM载入后再开始执行。 不管是直接用 DOM API 实现还是使用 jQuery,最终都是 DOMContentLoaded事件在起作用。 本文讨论一个我们习以为常却很少了解的问题: 样式文件的载入会延迟脚本执行,以及 DOMContentLoaded事件的触发。

DOMContentLoaded事件

页面文档(DOM)完全加载并解析完毕之后,会触发 DOMContentLoaded事件, HTML文档不会等待样式文件,图片文件,Iframe页面的加载。 但DOM树已被创建,多数JavaScript已经操作DOM并完成功能了。

This (DOMContentLoaded) event fires after the HTML code has been fully retrieved from the server, the complete DOM tree has been created and scripts have access to all elements via the DOM API. – molily.de

然而在绝大多数场景下,样式文件的载入会延迟 DOMContentLoaded事件的触发。 其实这样的行为正是开发者所希望的,为什么呢?

事实上,老版本的IE中 DOMContentLoaded事件存在兼容性问题。 参见: 兼容所有浏览器的 DOM 载入事件

浏览器为何延迟DOMContentLoaded

对于很多脚本而言,它们被编写时就希望在样式载入之后再开始执行。 JavaScript的作者往往会假设CSS规则已经生效,尤其是在进行一些显示相关的操作时, 比如需要得到DOM元素的位置和大小的场景。

事实上,在多数浏览器中 DOMContentLoaded事件的触发会考虑到外部样式文件的载入, 以及在HTML中脚本标签和样式标签的相对位置。 如果脚本位于样式之后,浏览器通常会认为该脚本依赖于样式的渲染结果, 也就更倾向于延迟脚本的执行(直到样式渲染结束)。

不同浏览器的行为

既然浏览器有时会延迟 DOMContentLoaded事件, 但是何时会延迟 DOMContentLoaded事件,还取决于行内脚本还是外部脚本,以及脚本与样式标签的相对位置。 不同的浏览器渲染引擎也有不同的行为。 在 http://molily.de/domcontentloaded/一文中对该问题有详尽的阐述和测试, 本文取其结论。

下表描述了各种情况下脚本是否会被延迟执行,进而延迟触发 DOMContentLoaded事件。

渲染引擎样式表之前的脚本样式表之后的外部脚本样式表之后的行内脚本
Presto (Opera)
Webkit (Safari, Chrome)
Gecko (Firefox)
Trident (MSIE) 

HTML5标准及最佳实践

其实 DOMContentLoaded是Firefox中最先提出的, 此后JavaScript社区发现它确实比 load事件(要求所有资源完全载入)更好, 于是Apple和Opera相继开始支持该事件。 但不同浏览器的实现方式有所区别,于是产生了上表所示的复杂情况。

在HTML5标准中情况有所好转: DOMContentLoaded是一个纯DOM事件,与样式表无关。 与此同时,HTML5要求:

  • 脚本执行前,出现在当前 <script>之前的 <link rel="stylesheet">必须完全载入。
  • 脚本执行会阻塞DOM解析。

这样的话,假如脚本和样式一起放在HTML <head>中, DOM解析到 <script>标签时会阻塞DOM解析,开始如下操作:

  1. 获取当前 <script>的脚本文件;
  2. 获取并载入前面的所有 <link rel="stylesheet">
  3. 执行当前脚本文件。

这些操作完成之后才能继续进行DOM解析,解析完毕时再触发 DOMContentLoaded事件。 如果将样式和脚本都放到 <head>中,会使浏览器在渲染 <body>前载入并执行所有样式和脚本。 页面的显示延迟会很高,多数情况下用户体验都很糟糕。 因此在HTML5标准的HTML页面中,最佳实践应当是: 所有样式放在 <head>中;所有脚本放在 <body>最后。

jQuery文档中也推荐这样的实践方式。

参考阅读

异步脚本载入提高页面性能

$
0
0

可能很多人都知道JavaScript的载入和渲染会暂停DOM解析,但可能仍缺乏直观的体验。 本文通过几个例子详述脚本对页面渲染的影响,以及如何使用异步脚本载入策略提供页面性能和用户体验。 包括在脚本载入缓慢或错误时尽早显示整个页面内容,以及早点结束浏览器忙提示(进度条、旋转图标、状态栏等)。

DOM 渲染流程

要理解异步脚本载入的用处首先要了解浏览器渲染DOM的流程,以及各阶段用户体验的差别。 一般地,一个包含外部样式表文件和外部脚本文件的HTML载入和渲染过程是这样的:

  1. 浏览器下载HTML文件并开始解析DOM。
  2. 遇到样式表文件 link[rel=stylesheet]时,将其加入资源文件下载队列,继续解析DOM。
  3. 遇到脚本文件时,暂停DOM解析并立即下载脚本文件。
  4. 下载结束后立即执行脚本,在脚本中可访问当前 <script>以上的DOM。
  5. 脚本执行结束,继续解析DOM。
  6. 整个DOM解析完成,触发 DOMContentLoaded事件。

上述步骤只是大致的描述,你可能还会关心下面两个问题:

  • 资源文件下载队列。样式表、图片等资源文件的下载不会暂停DOM解析。浏览器会并行地下载这些文件,但通常会限制并发下载数,一般为3-5个。可以在开发者工具的Network标签页中看到。
  • 执行脚本文件前,浏览器可能会等待该 <script>之前的样式下载完成并渲染结束。详见 外部样式表与DOMContentLoaded事件延迟一文。

脚本载入暂停DOM渲染

脚本载入真的会暂停DOM渲染吗?非常真切。 比如下面的HTML中,在脚本后面还有一个 <h1>标签。

<!DOCTYPE html><html><body><h1>Hello</h1><script src="/will-not-stop-loading.js"></script> <h1>World!</h1></body></html>

我们编写服务器端代码(见本文最后一章),让 /will-not-stop-loading.js始终处于等待状态。 此时页面的显示效果:

js block dom render

脚本等待下载完成的过程中,后面的 World不会显示出来。直到该脚本载入完成或超时。 试想如果你在 <head>中有这样一个下载缓慢的脚本,整个 <body>都不会显示, 势必会造成空白页面持续相当长的时间。 所以 较好的实践方式是将脚本放在 <body>尾部。

很多被墙的网站加载及其缓慢就是因为DOM主体前有脚本被挡在墙外了。

DOMContentLoaded 延迟

既然脚本载入会暂停DOM渲染,OK我们把脚本都放在 <body>尾部。 这时页面可以被显示出来了, 但是在脚本载入前, DOMContentLoaded事件仍然不会触发。 请看:

<!DOCTYPE html><html><body><h1>Hello</h1><h1>World!</h1><script>
    document.addEventListener('DOMContentLoaded', function(){
      alert('DOM loaded!');
    });</script><script src="/will-not-stop-loading.js"></script> </body></html>

这时 Wrold!会显示,但浏览器忙指示器仍在旋转。 这是因为 DOM 仍然没有解析完成,毕竟最后一个 <script>标签还未获取到嘛! 当然 DOMContentLoaded事件也就不会触发。 DOM loaded!对话框也不会弹出来。

dom not loaded with script pending

直到超时错误发生, DOMContentLoaded才会触发(在我的Chrome里超时用了好几分钟!), 此时对话框也会弹出:

dom-loaded-as-script-timeout

浏览器忙提示

本文关心的核心问题是页面性能和用户体验,现在来考虑一个问题:

对于非必须的页面脚本,在它的载入过程中如何取消浏览器的忙提示。

首先想到的办法一定是从HTML中干掉那些 <script>,然后在JavaScript中动态插入 <script>标签。 比如:

var s = document.createElement('script');
s.src = "/will-not-stop-loading.js";
document.body.appendChild(s);

不贴图了,标签页上的图标确实在旋转,和上一小节中的图一样 :(

那么等 DOMContentLoaded会后再来插入呢?

document.addEventListener('DOMContentLoaded', function(){
    var s = document.createElement('script');
    s.src = "/will-not-stop-loading.js";
    document.body.appendChild(s);
});

上述代码仍然无法阻止浏览器忙提示。这充分说明浏览器JavaScript执行是单线程的,DOM事件机制也不例外。

异步加载脚本

为了阻止浏览器忙提示,应当可以使用异步加载脚本的策略。先看一个简单的示例:

setTimeout(function(){
    var s = document.createElement('script');
    s.src = "/will-not-stop-loading.js";
    document.body.appendChild(s);
});

setTimeout未指定第二个参数(延迟时间),会立即执行第一个参数传入的函数。 但是JavaScript引擎会将该函数插入到执行队列的末尾。 这意味着正在进行的DOM渲染过程完全结束后(此时浏览器忙提示当然会消失),才会调用上述函数。 看图:

async script loading

其中 /will-not-stop-loading.js仍处于 pending状态,但浏览器忙提示已经消失。 然而在Chrome中,如果插入 <script>时仍有其他资源正在载入,那么上述做法仍然达不到效果 (浏览器会判别为页面仍未完全载入)。 总之: 异步加载脚本来禁止浏览器忙提示的关键在于让DOM先加载完毕

最佳实践

不要沮丧,在实际的项目中有两种成熟的办法可以禁止浏览器忙提示。

AJAX + Eval

使用AJAX获取脚本内容,并用Eval来运行它。 因为AJAX一般不会触发浏览器忙提示,脚本执行只可能让浏览器暂停响应也不会触发忙提示。

首先在需要异步加载的脚本设置 type="text/defered-script",并用 data-src代替 src防止浏览器直接去获取:

<script type="text/async-script" data-src="http://foo.com/bar.js">

然后在站点的公共代码中加入『异步脚本加载器』:

$('[type="text/defered-script"]').each(function(idx, el){
    $.get(el.dataset.src, eval);
});

注意:使用AJAX GET脚本文件时不要设置 Content-Type: "application/javascript" (包括 jQuery.getScript)。 这会使浏览器发现你是在加载脚本,进而触发忙提示指示器。 当然,如果此时页面已然载入完毕,任何AJAX都不会触发忙提示了。

上述方法的缺点在于,一旦被引入的JavaScript中需要以相对路径的方式载入其他JavaScript就会引发错误。 因为被Eval的脚本中,当前路径变成了页面所在路径,不再是原来的 <script>src所指的路径。 这在使用第三方库时非常常见。

Load 事件

既然禁止浏览器忙指示器的关键在于让DOM加载完毕,那就绑定页面载入完毕的事件: load。 例如:

$(window).load(function(){
    $('script[type="text/async-script"]').each(function(idx, el){
        var $script = $('<script>');
        if(el.dataset.src) $script.attr('src', el.dataset.src);
        else $script.html(el.text);
        $script.appendTo('body');
        el.remove();
    });
});
  • 对于外部 <script>,生成一个新的包含正确 src<script>
  • 对于行内 <script>,生成一个新的包含正确内容的 <script>type默认即为 "application/javascript"

该方法采用DOM中 <script>加载的方式,没有AJAX+Eval改变脚本中当前路径的缺点。 http://harttleland.com中的Google Analytics、MathJax等脚本都采用这种处理方式。

服务器工具

本文所做实验服务器端都使用Node.js写成:

const http = require("http");
const fs = require('fs');
const port = 4001;

var server = http.createServer(function(req, res) {
    switch (req.url) {
        case '/':
            var html = fs.readFileSync('./index.html', 'utf8');
            res.setHeader("Content-Type", "text/html");
            res.end(html);
            break;
        case '/will-not-stop-loading.js':
            break;
    }
});

server.listen(port, e =>
    console.log(`listening to port: ${port}`));

参考阅读


[总结贴] 十个 JavaScript 中易犯的小错误

$
0
0

序言

在今天,JavaScript已经成为了网页编辑的核心。尤其是过去的几年,互联网见证了在SPA开发、图形处理、交互等方面大量JS库的出现。

如果初次打交道,很多人会觉得js很简单。确实,对于很多有经验的工程师,或者甚至是初学者而言,实现基本的js功能几乎毫无障碍。但是JS的真实功能却比很多人想象的要更加多样、复杂。JavaScript的许多细节规定会让你的网页出现很多意想不到的bug,搞懂这些bug,对于成为一位有经验的JS开发者很重要。

常见错误一:对于this关键词的不正确引用

我曾经听一位喜剧演员说过:

“我从未在这里,因为我不清楚这里是哪里,是除了那里之外的地方吗?”

这句话或多或少地暗喻了在js开发中开发者对于this关键字的使用误区。This指代的是什么?它和日常英语口语中的this是一个意思吗?

随着近些年js编程不断地复杂化,功能多样化,对于一个程序结构的内部指引、引用也逐渐变多起来

下面让我们一起来看这一段代码:

Game.prototype.restart = function () {   this.clearLocalStorage(); 

    this.timer = setTimeout(function(){     this.clearBoard();        }, 0);

 };

运行上面的代码将会出现如下错误:

Uncaught TypeError: undefined is not a function

这是为什么?this的调用和它所在的环境密切相关。之所以会出现上面的错误,是因为当你在调用 setTimeout()函数的时候, 你实际调用的是window.setTimeout(). 因此,在 setTimeout() 定义的函数其实是在window背景下定义的,而window中并没有 clearBoard() 这个函数方法。

下面提供两种解决方案。第一种比较简单直接的方法便是,把this存储到一个变量当中,这样他就可以在不同的环境背景中被继承下来:

Game.prototype.restart = function () {   this.clearLocalStorage();  

 var self = this;

this.timer = setTimeout(function(){     self.clearBoard();}, 0); };

第二种方法便是用bind()的方法,不过这个相比上一种要复杂一些,对于不熟悉bind()的同学可以在微软官方查看它的使用方法: https://msdn.microsoft.com/zh-cn/library/ff841995

Game.prototype.restart = function () {   this.clearLocalStorage(); 

this.timer = setTimeout(this.reset.bind(this), 0); };      

Game.prototype.reset = function(){     this.clearBoard();};

上面的例子中,两个this均指代的是Game.prototype。

常见错误二:传统编程语言的生命周期误区

另一种易犯的错误,便是带着其他编程语言的思维,认为在JS中,也存在生命周期这么一说。请看下面的代码:

for (var i = 0; i < 10; i++) {   /* ... */ } console.log(i);

如果你认为在运行console.log() 时肯定会报出 undefined 错误,那么你就大错特错了。我会告诉你其实它会返回 10吗。

当然,在许多其他语言当中,遇到这样的代码,肯定会报错。因为i明显已经超越了它的生命周期。在for中定义的变量在循环结束后,它的生命也就结束了。但是在js中,i的生命还会继续。这种现象叫做 variable hoisting。

而如果我们想要实现和其他语言一样的在特定逻辑模块中具有生命周期的变量,可以用let关键字。

常见错误三:内存泄露

内存泄露在js变成中几乎是一个无法避免的问题。如果不是特别细心的话,在最后的检查过程中,肯定会出现各种内存泄露问题。下面我们就来举例说明一下:

var theThing = null; 

var replaceThing = function () { 



    var priorThing = theThing; 

    var unused = function () { 

              if (priorThing) {       console.log("hi");     }   

   }; 

   theThing = {     longStr: new Array(1000000).join('*'),  // 

              someMethod: function () {       console.log(someMessage);     }   
   }; 
};   
setInterval(replaceThing, 1000); 

如果运行上面的代码,你会发现你已经造成了大量的内存泄露,每秒泄露1M的内存,显然光靠GC(垃圾回收器)是无法帮助你的了。由上面的代码来看,似乎是longstr在每次replaceThing调用的时候都没有得到回收。这是为什么呢?

每一个theThing结构都含有一个longstr结构列表。每一秒当我们调用 replaceThing, 它就会把当前的指向传递给 priorThing. 但是到这里我们也会看到并没有什么问题,因为 priorThing 每回也是先解开上次函数的指向才会接受新的赋值。并且所有的这一切都是发生在 replaceThing 函数体当中,按常理来说当函数体结束之后,函数中的本地变量也将会被GC回收,也就不会出现内存泄露的问题了,但是为什么会出现上面的错误呢?

这是因为longstr的定义是在一个闭包中进行的,而它又被其他的闭包所引用,js规定,在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。关于在JS中的内存泄露问题可以查看 http://javascript.info/tutorial/memory-leaks#memory-management-in-java...

常见错误四:比较运算符

JavaScript中一个比较便捷的地方,便是它可以给每一个在比较运算的结果变量强行转化成布尔类型。但是从另一方面来考虑,有时候它也会为我们带来很多不便,下面的这些例子便是一些一直困扰很多程序员的代码实例:

console.log(false == '0'); 

console.log(null == undefined); 

console.log(" \t\r\n" == 0); 

console.log('' == 0);  // And these do too! 

if ({}) // ... 

if ([]) // ...

最后两行的代码虽然条件判断为空(经常会被人误认为转化为false),但是其实不管是{ }还是[ ]都是一个实体类,而任何的类其实都会转化为true。就像这些例子所展示的那样,其实有些类型强制转化非常模糊。因此很多时候我们更愿意用 === 和 !== 来替代== 和 !=, 以此来避免发生强制类型转化。. ===和!== 的用法和之前的== 和 != 一样,只不过他们不会发生类型强制转换。另外需要注意的一点是,当任何值与 NaN 比较的时候,甚至包括他自己,结果都是false。因此我们不能用简单的比较字符来决定一个值是否为 NaN 。我们可以用内置的 isNaN() 函数来辨别:

console.log(NaN == NaN);    // false 

console.log(NaN === NaN);   // false 

console.log(isNaN(NaN));    // true 

常见错误五:低效的DOM操作

js中的DOM基本操作非常简单,但是如何能有效地进行这些操作一直是一个难题。这其中最典型的问题便是批量增加DOM元素。增加一个DOM元素是一步花费很大的操作。而批量增加对系统的花销更是不菲。一个比较好的批量增加的办法便是使用 document fragments :

var div = document.getElementsByTagName("my_div");  

var fragment = document.createDocumentFragment(); 

 for (var e = 0; e < elems.length; e++) { fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true)); 

直接添加DOM元素是一个非常昂贵的操作。但是如果是先把要添加的元素全部创建出来,再把它们全部添加上去就会高效很多。

常见错误六:在for循环中的不正确函数调用

请大家看以下代码:

var elements = document.getElementsByTagName('input');

var n = elements.length; 

for (var i = 0; i < n; i++) {     

elements[i].onclick = function() {         

console.log("This is element #" + i);     }; } 

运行以上代码,如果页面上有10个按钮的话,点击每一个按钮都会弹出 “This is element #10”! 。这和我们原先预期的并不一样。这是因为当点击事件被触发的时候,for循环早已执行完毕,i的值也已经从0变成了。

我们可以通过下面这段代码来实现真正正确的效果:

var elements = document.getElementsByTagName('input'); 

var n = elements.length; 

var makeHandler = function(num) {  // outer function

      return function() { 

console.log("This is element #" + num);      }; }; 

for (var i = 0; i < n; i++) 

{     elements[i].onclick = makeHandler(i+1); }

在这个版本的代码中, makeHandler 在每回循环的时候都会被立即执行,把i+1传递给变量num。外面的函数返回里面的函数,而点击事件函数便被设置为里面的函数。这样每个触发函数就都能够是用正确的i值了。

常见错误七:原型继承问题

很大一部分的js开发者都不能完全掌握原型的继承问题。下面具一个例子来说明:

BaseObject = function(name) {     

if(typeof name !== "undefined") 

{         this.name = name;     } 

else 

{         this.name = 'default'     } }; 

这段代码看起来很简单。如果你有name值,则使用它。如果没有,则使用 ‘default’:

var firstObj = new BaseObject(); 

var secondObj = new BaseObject('unique');  

console.log(firstObj.name);  // -> 结果是'default' 

console.log(secondObj.name); // -> 结果是 'unique'

但是如果我们执行delete语句呢:

delete secondObj.name; 

我们会得到:

console.log(secondObj.name); // -> 结果是 'undefined' 

但是如果能够重新回到 ‘default’状态不是更好么? 其实要想达到这样的效果很简单,如果我们能够使用原型继承的话:

BaseObject = function (name) 

{     if(typeof name !== "undefined") 

{         this.name = name;     } };  

BaseObject.prototype.name = 'default'; 

在这个版本中, BaseObject 继承了原型中的name 属性, 被设置为了 'default'.。这时,如果构造函数被调用时没有参数,则会自动设置为 default。相同地,如果name 属性被从BaseObject移出,系统将会自动寻找原型链,并且获得 'default'值:

 var thirdObj = new BaseObject('unique'); 

 console.log(thirdObj.name);  

 delete thirdObj.name;

 console.log(thirdObj.name);  // -> 结果是 'default' 

常见错误八:为实例方法创建错误的指引

我们来看下面一段代码:

var MyObject = function() {} 

 MyObject.prototype.whoAmI = function() {     

console.log(this === window ? "window" : "MyObj"); }; 

 var obj = new MyObject(); 

现在为了方便起见,我们新建一个变量来指引 whoAmI 方法, 因此我们可以直接用 whoAmI() 而不是更长的obj.whoAmI():

var whoAmI = obj.whoAmI;

接下来为了确保一切都如我们所预测的进行,我们可以将 whoAmI 打印出来:

console.log(whoAmI); 

结果是:

function () {     console.log(this === window ? "window" : "MyObj"); } 

没有错误!

但是现在我们来查看一下两种引用的方法:

obj.whoAmI();  // 输出 "MyObj" (as expected) 

whoAmI();      // 输出 "window" (uh-oh!) 

哪里出错了呢?

原理其实和上面的第二个常见错误一样,当我们执行 var whoAmI = obj.whoAmI;的时候,新的变量 whoAmI 是在全局环境下定义的。因此它的this 是指window, 而不是obj!

正确的编码方式应该是:

var MyObject = function() {}  

MyObject.prototype.whoAmI = function() {     

      console.log(this === window ? "window" : "MyObj"); }; 

var obj = new MyObject(); 

obj.w = obj.whoAmI;   // still in the obj namespace  obj.whoAmI();  // 输出 "MyObj" (as expected) 

obj.w();       // 输出 "MyObj" (as expected) 

常见错误九:用字符串作为setTimeout 或者 setInterval的第一个参数

首先我们要声明,用字符串作为这两个函数的第一个参数并没有什么语法上的错误。但是其实这是一个非常低效的做法。因为从系统的角度来说,当你用字符串的时候,它会被传进构造函数,并且重新调用另一个函数。这样会拖慢程序的进度。

setInterval("logTime()", 1000); 

setTimeout("logMessage('" + msgValue + "')", 1000);

另一种方法是直接将函数作为参数传递进去:

setInterval(logTime, 1000);   

setTimeout(function() { 

logMessage(msgValue); }, 1000); 

常见错误十:忽略 “strict mode”的作用

“strict mode” 是一种更加严格的代码检查机制,并且会让你的代码更加安全。当然,不选择这个模式并不意味着是一个错误,但是使用这个模式可以确保你的代码更加准确无误。

下面我们总结几条“strict mode”的优势:

  1. 让Debug更加容易:在正常模式下很多错误都会被忽视掉,“strict mode”模式会让Debug极致更加严谨。

  2. 防止默认的全局变量:在正常模式下,给一个为经过声明的变量命名将会将这个变量自动设置为全局变量。在strict模式下,我们取消了这个默认机制。

  3. 取消this的默认转换:在正常模式下,给this关键字指引到null或者undefined会让它自动转换为全局。在strict模式下,我们取消了这个默认机制。

  4. 防止重复的变量声明和参数声明:在strict模式下进行重复的变量声明会被抱错,如 (e.g., var object = {foo: "bar", foo: "baz"};) 同时,在函数声明中重复使用同一个参数名称也会报错,如 (e.g., function foo(val1, val2, val1){}),

  5. 让eval()函数更加安全。

  6. 当遇到无效的delete指令的事后报错:delete指令不能对类中未有的属性执行,在正常情况下这种情况只是默默地忽视掉,而在strict模式是会报错的。

结语

正如和其他的技术语言一样,你对JavaScript了解的的越深,知道它是如何运作,为什么这样运作,你才会熟练地掌握并且运用这门语言。相反地,如果你缺少对JS模式的认知的话,你就会碰上很多的问题。了解JS的一些细节上的语法或者功能将会有助于你提高编程的效率,减少变成中遇到的问题。


原文地址: http://www.toptal.com/javascript/10-most-common-javascript-mistakes
译文地址: http://1ke.co/course/136?utm_source=segment&utm_medium=1&utm_c...

使用Benchmark.js和jsPerf分析代码性能

$
0
0

前言

前端开发中,掌握好浏览器的特性进行有针对性的性能调优是一项基本工作,同时,比较不同代码的执行速度也是一项关键的工作。

比如,当我们想比较 RegExptest方法和 String对象的 indexOf方法查找字符串谁的速度更快的话, js代码在不同的浏览器,不同的操作系统环境运行的效率可能是不一样的,这就是为什么我们需要对其进行基准测试,在做基准测试方面,我们可以使用 Benchmark.js和使用 jsPerf(一个基于 JSLitmus的基准测试库)。我们可以使用 jsPerf来分享你的基准测试。

Benchmark.js 的使用

github 地址: https://github.com/bestiejs/benchmark.js

我们在很多 github开源项目中,往往都能看到 benchmark文件夹,比如下面这个:

图片描述

于是 Google之,发现这是用来做基准测试的。于是乎:

首先我们在系统根目录下,通过 npm intsall benchmark来安装 benchmark。该模块会被写入 node_modules文件夹中,我们在 test.js文件中通过 require方法引入该模块。

将如下代码写入 test.js文件,该文件置于系统根目录下:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;

// 添加测试
suite.add('RegExp#test', function() {
    /o/.test('Hello World!');
})
    .add('String#indexOf', function() {'Hello World!'.indexOf('o') > -1;
    })
// add listeners
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').pluck('name'));
    })
// run async
    .run({ 'async': true });

然后在终端执行 node test.js可见输出结果如下:

➜  ~ git:(master) ✗ node test.js
RegExp#test x 9,847,928 ops/sec ±1.47% (83 runs sampled)
String#indexOf x 23,366,017 ops/sec ±0.91% (96 runs sampled)
Fastest is String#indexOf

结果最快的就是 String对象的 indexOf方法,其中, Ops/sec测试结果以每秒钟执行测试代码的次数( Ops/sec)显示,这个数值越大越好。除了这个结果外,同时会显示测试过程中的统计误差,以及相对最好的慢了多少(%)

call和apply的比较

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var arr1 = function (str) {
    return [].slice.apply(str);
};
var str2 = function (str) {
    return [].slice.call(str);
};
// 添加测试
suite.add('arr1', function() {
    arr1('test');
})
    .add('str2', function() {
        str2('test');
    })
// add listeners
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').pluck('name'));
    })
// run async
    .run({ 'async': true });

输出如下内容:

arr1 x 596,505 ops/sec ±1.14% (95 runs sampled)
str2 x 627,822 ops/sec ±1.27% (92 runs sampled)
Fastest is str2

jsPerf 的使用

jsPerf提供了一个简便的方式来创建和共享测试用例,并可以比较不同 JavaScript代码段的性能。 jsPerf也是基于 Benchmark来运行的。

打开 jsPerf站点: http://jsperf.com/,先将必填的项目填了。其中, slug是短名称,会生成一个网址,因此不可与别人的重复。然后在 Code snippets to compare区域填入 title和用于测试的code。最后点击 save test case完成验证即可。浏览器会自动跳转到测试页面

Async选项框是用来测试一些异步调用的性能的,我们的代码没有使用异步方法,所以不必勾选。

运行测试

点击“ Run tests”按钮开始测试两种算法的性能。建议在运行性能测试之前,关闭无关的浏览器页面,关闭其他程序,退出不必要的后台进程,以保证结果不受其他环境的影响。你也可以通过点击个别测试用例的名字单独运行这个例子

点击该链接查看性能比较: http://jsperf.com/huang

jsPerf还会统计所有运行过这个测试用例的浏览器的比较结果,显示在下方的 Browserscope区域,可以通过它直观地看出各个版本浏览器的性能横向和纵向比较情况。

图片描述

可以看到 Firefox下的执行速度明显高于 Chrome

查看别人的测试用例

我们可以通过 http://jsperf.com/browse浏览最新提交的250项最新测试用例。我们也可以使用底部的 Revisions来查看不同的版本,也就是不同浏览器的测试用例情况。

总结

John Resig在其博文 JavaScript 基准测试的质量中提到,应该尽量考虑到每个测试结果的误差并去减小它。扩大测试的样本值,健全的测试执行,都能够起到减少误差的作用。

前端性能优化指南

$
0
0

前端性能优化指南

AJAX优化

  • 缓存 AJAX

    • 异步并不等于 即时

  • 请求使用 GET

    • 当使用 XMLHttpRequest时,而URL长度不到 2K,可以使用 GET请求数据, GET相比 POST更快速。

      • POST类型请求要发送两个 TCP数据包。

        • 先发送文件头。

        • 再发送数据。

      • GET类型请求只需要发送一个 TCP数据包。

        • 取决于你的 cookie数量。

COOKIE专题

  • 减少 COOKIE的大小。

  • 使用无 COOKIE的域。

    • 比如图片 CSS等静态文件放在静态资源服务器上并配置单独域名,客户端请求静态文件的时候,减少 COOKIE反复传输时对主域名的影响。

DOM优化

  • 优化节点修改。

    • 使用 cloneNode在外部更新节点然后再通过 replace与原始节点互换。

  • 优化节点添加
    >多个节点插入操作,即使在外面设置节点的元素和风格再插入,由于多个节点还是会引发多次reflow。

    • 优化的方法是创建 DocumentFragment,在其中插入节点后再添加到页面。

      • JQuery中所有的添加节点的操作如 append,都是最终调用 DocumentFragment来实现的,

            createSafeFragment(document) {
                 var list = nodeNames.split( "|" ),
                     safeFrag = document.createDocumentFragment();
  • 优化 CSS样式转换。
    >如果需要动态更改CSS样式,尽量采用触发reflow次数较少的方式。

    • 如以下代码逐条更改元素的几何属性,理论上会触发多次 reflow

          element.style.fontWeight = 'bold' ;
          element.style.marginLeft= '30px' ;
          element.style.marginRight = '30px' ;
    • 可以通过直接设置元素的 className直接设置,只会触发一次 reflow

  • 减少 DOM元素数量

    • console中执行命令查看 DOM元素数量。

          `document.getElementsByTagName( '*' ).length`
    • 正常页面的 DOM元素数量一般不应该超过 1000

    • DOM元素过多会使 DOM元素查询效率,样式表匹配效率降低,是页面性能最主要的瓶颈之一。

  • DOM操作优化。

    • DOM操作性能问题主要有以下原因。

      • DOM元素过多导致元素定位缓慢。

      • 大量的 DOM接口调用。

        • JAVASCRIPTDOM之间的交互需要通过函数 API接口来完成,造成延时,尤其是在循环语句中。

      • DOM操作触发频繁的 reflow(layout)repaint

      • layout发生在 repaint之前,所以layout相对来说会造成更多性能损耗。

        • reflow(layout)就是计算页面元素的几何信息。

        • repaint就是绘制页面元素。

      • DOM进行操作会导致浏览器执行回流 reflow

    • 解决方案。

      • JAVASCRIPT执行时间是很短的。

      • 最小化 DOM访问次数,尽可能在js端执行。

      • 如果需要多次访问某个 DOM节点,请使用局部变量存储对它的引用。

      • 谨慎处理 HTML集合( HTML集合实时连系底层文档),把集合的长度缓存到一个变量中,并在迭代中使用它,如果需要经常操作集合,建议把它拷贝到一个数组中。

      • 如果可能的话,使用速度更快的API,比如 querySelectorAllfirstElementChild

      • 要留意重绘和重排。

      • 批量修改样式时, 离线操作 DOM树。

      • 使用缓存,并减少访问布局的次数。

      • 动画中使用绝对定位,使用拖放代理。

      • 使用事件委托来减少事件处理器的数量。

  • 优化 DOM交互
    >在 JAVASCRIPT中, DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分。

    • 最小化 现场更新

      • 当需要访问的 DOM部分已经已经被渲染为页面中的一部分,那么 DOM操作和交互的过程就是再进行一次 现场更新

        • 现场更新是需要针对 现场(相关显示页面的部分结构)立即进行更新,每一个更改(不管是插入单个字符还是移除整个片段),都有一个性能损耗。

        • 现场更新进行的越多,代码完成执行所花的时间也越长。

    • 多使用 innerHTML

      • 有两种在页面上创建 DOM节点的方法:

        • 使用诸如 createElement()appendChild()之类的 DOM方法。

        • 使用 innerHTML

          • 当使用 innerHTML设置为某个值时,后台会创建一个 HTML解释器,然后使用内部的 DOM调用来创建 DOM结构,而非基于 JAVASCRIPTDOM调用。由于内部方法是编译好的而非解释执行,故执行的更快。

  • 回流 reflow

    • 发生场景。

      • 改变窗体大小。

      • 更改字体。

      • 添加移除stylesheet块。

      • 内容改变哪怕是输入框输入文字。

      • CSS虚类被触发如 :hover。

      • 更改元素的className。

      • 当对DOM节点执行新增或者删除操作或内容更改时。

      • 动态设置一个style样式时(比如element.style.width="10px")。

      • 当获取一个必须经过计算的尺寸值时,比如访问offsetWidth、clientHeight或者其他需要经过计算的CSS值。

    • 解决问题的关键,就是限制通过DOM操作所引发回流的次数。

      • 在对当前DOM进行操作之前,尽可能多的做一些准备工作,保证N次创建,1次写入。

      • 在对DOM操作之前,把要操作的元素,先从当前DOM结构中删除:

        • 通过removeChild()或者replaceChild()实现真正意义上的删除。

        • 设置该元素的display样式为“none”。

      • 每次修改元素的style属性都会触发回流操作。

        
                element.style.backgroundColor = "blue";
        • 使用更改 className的方式替换 style.xxx=xxx的方式。

        • 使用 style.cssText = '';一次写入样式。

        • 避免设置过多的行内样式。

        • 添加的结构外元素尽量设置它们的位置为 fixedabsolute

        • 避免使用表格来布局。

        • 避免在 CSS中使用 JavaScript expressions(IE only)

      • 将获取的 DOM数据缓存起来。这种方法,对获取那些会触发回流操作的属性(比如 offsetWidth等)尤为重要。

      • 当对HTMLCollection对象进行操作时,应该将访问的次数尽可能的降至最低,最简单的,你可以将length属性缓存在一个本地变量中,这样就能大幅度的提高循环的效率。

eval优化

  • 避免 eval

    • eval会在时间方面带来一些效率,但也有很多缺点。

      • eval会导致代码看起来更脏。

      • eval会需要消耗大量时间。

      • eval会逃过大多数压缩工具的压缩。

HTML优化

  • 插入 HTML

    • JavaScript中使用 document.write生成页面内容会效率较低,可以找一个容器元素,比如指定一个 div,并使用 innerHTML来将 HTML代码插入到页面中。

  • 避免空的 srchref

    • link标签的 href属性为空、 script标签的 src属性为空的时候,浏览器渲染的时候会把当前页面的 URL作为它们的属性值,从而把页面的内容加载进来作为它们的值。

  • 为文件头指定 Expires

    • 使内容具有缓存性,避免了接下来的页面访问中不必要的HTTP请求。

  • 重构HTML,把重要内容的优先级提高。

  • Post-load(次要加载)不是必须的资源。

  • 利用预加载优化资源。

  • 合理架构,使DOM结构尽量简单。

  • 利用 LocalStorage合理缓存资源。

  • 尽量避免CSS表达式和滤镜。

  • 尝试使用defer方式加载Js脚本。

  • 新特性:will-change,把即将发生的改变预先告诉浏览器。

  • 新特性Beacon,不堵塞队列的异步数据发送。

  • 不同之处:网络缓慢,缓存更小,不令人满意的浏览器处理机制。

  • 尽量多地缓存文件。

  • 使用HTML5 Web Workers来允许多线程工作。

  • 为不同的Viewports设置不同大小的Content。

  • 正确设置可Tap的目标的大小。

  • 使用响应式图片。

  • 支持新接口协议(如HTTP2)。

  • 未来的缓存离线机制:Service Workers。

  • 未来的资源优化Resource Hints(preconnect, preload, 和prerender)。

  • 使用Server-sent Events。

  • 设置一个Meta Viewport。

JITGC优化

  • untyped(无类型)。

    • JAVASCRIPT是个无类型的语言,这导致了如 x=y+z这种表达式可以有很多含义。

      • yz是数字,则 +表示加法。

      • yz是字符串,则 +表示字符串连接。

        而JS引擎内部则使用“ 细粒度”的类型,比如:

      • 32-bit* integer。

      1. 64-bit* floating-point。

        这就要求js类型-js引擎类型,需要做“boxed/unboxed(装箱/解箱)”,在处理一次 x=y+z这种计算,需要经过的步骤如下。

      2. 从内存,读取 x=y+z的操作符。

      3. 从内存,读取 yz

      4. 检查y,z类型,确定操作的行为。

      5. unbox y,z

      6. 执行操作符的行为。

      7. box x

      8. x写入内存。

        只有第 5步骤是真正有效的操作,其他步骤都是为第 5步骤做准备/收尾, JAVASCRIPTuntyped特性很好用,但也为此付出了很大的性能代价。

  • JIT

    • 先看看 JITuntyped的优化,在 JIT下,执行 x=y+z流程。

      1. 从内存,读取 x=y+z的操作符。

      2. 从内存,读取 yz

      3. 检查 yz类型,确定操作的行为。

      4. unbox y,z

      5. 执行 操作符 的行为。

      6. box x

      7. x写入内存。

    • 新引擎还对“对象属性”访问做了优化,解决方案叫 inline caching,简称: IC。简单的说,就是做 cache。但如果当 list很大时,这种方案反而影响效率。

  • Type-specializing JIT
    > Type-specializing JIT引擎用来处理 typed类型(声明类型)变量,但 JAVASCRIPT都是 untype类型的。

    • Type-specializing JIT的解决方案是:

      • 先通过扫描,监测类型。

      • 通过编译优化(优化对象不仅仅只是“类型”,还包括对JS代码的优化,但核心是类型优化),生成类型变量。

      • 再做后续计算。

    • Type-specializing JIT的执行 x=y+z流程:

        • 从内存,读取 x=y+z的操作符。

        • 从内存,读取 yz

        • 检查 yz类型,确定操作的行为。

        • unbox y,z

        • 执行操作符的行为。

        • box x

        • x写入内存。

          代价是:
          
      • 前置的扫描类型

      • 编译优化。

        所以·Type-specializing JIT·的应用是有选择性,选择使用这个引擎的场景包括:
      • 热点代码。

      • 通过启发式算法估算出来的有价值的代码。

        另外,有2点也需要注意:
      • 当变量类型 发生变化时,引擎有2种处理方式:

        • 少量变更,重编译,再执行。

        • 大量变更,交给JIT执行。

      • 数组object properties, 闭包变量 不在优化范畴之列。

js载入优化

  • 加快JavaScript装入速度的工具:

    • Lab.js

      • 借助LAB.js(装入和阻止JavaScript),你就可以并行装入JavaScript文件,加快总的装入过程。此外,你还可以为需要装入的脚本设置某个顺序,那样就能确保依赖关系的完整性。此外,开发者声称其网站上的速度提升了2倍。

  • 使用适当的CDN:

    • 现在许多网页使用内容分发网络(CDN)。它可以改进你的缓存机制,因为每个人都可以使用它。它还能为你节省一些带宽。你很容易使用ping检测或使用Firebug调试那些服务器,以便搞清可以从哪些方面加快数据的速度。选择CDN时,要照顾到你网站那些访客的位置。记得尽可能使用公共存储库。

  • 网页末尾装入JavaScript:

    • 也可以在头部分放置需要装入的一些JavaScript,但是前提是它以异步方式装入。

  • 异步装入跟踪代码:
    >脚本加载与解析会阻塞HTML渲染,可以通过异步加载方式来避免渲染阻塞,步加载的方式很多,比较通用的方法如下。

       var _gaq = _gaq || []; 
           _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']); 
           _gaq.push(['_trackPageview']); 
       (function() { 
           var ga = document.createElement('script'); ga.type = 'text/JavaScript'; ga.async = true; 
           ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 
           var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 
       })();

或者

    function loadjs (script_filename){
         var script = document.createElement( 'script' );
         script.setAttribute( 'type' , 'text/javascript' );
         script.setAttribute( 'src' , script_filename);
         script.setAttribute( 'id' , 'script-id' );

         scriptElement = document.getElementById( 'script-id' );
         if (scriptElement){
             document.getElementsByTagName( 'head' )[0].removeChild(scriptElement);
         }
         document.getElementsByTagName( 'head' )[0].appendChild(script);
    }
    var script = 'scripts/alert.js' ;
    loadjs(script);
  • 把你的JavaScript打包成PNG文件

    • 将JavaScript/css数据打包成PNG文件。之后进行拆包,只要使用画布API的getImageData()。可以在不缩小数据的情况下,多压缩35%左右。而且是无损压缩,对比较庞大的脚本来说,在图片指向画布、读取像素的过程中,你会觉得有“一段”装入时间。

  • 设置Cache-Control和Expires头

    通过Cache-Control和Expires头可以将脚本文件缓存在客户端或者代理服务器上,可以减少脚本下载的时间。
    >
    Expires格式:
    >

       Expires = "Expires" ":" HTTP-date
       Expires: Thu, 01 Dec 1994 16:00:00 GMT
       Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the
       Expires field.

    >
    Cache-Control格式:
    >

       Cache-Control   = "Cache-Control" ":" 1#cache-directive
       Cache-Control: public

具体的标准定义可以参考http1.1中的定义,简单来说Expires控制过期时间是多久,Cache-Control控制什么地方可以缓存 。

with优化

  • 尽可能地少用 with语句,因为它会增加 with语句以外的数据的访问代价。

  • 避免使用 with

    >
    `with`语句将一个新的可变对象推入作用域链的头部,函数的所有局部变量现在处于第二个作用域链对象中,从而使局部变量的访问代价提高。
    

变量专题

  • 全局变量

    • 当一个变量被定义在全局作用域中,默认情况下 JAVASCRIPT引擎就不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。

    • 全局变量缺点。

      • 使变量不易被回收。

        • 多人协作时容易产生混淆。

      • 在作用域链中容易被干扰。

    • 可以通过包装函数来处理 全局变量

  • 局部变量。

    • 尽量选用局部变量而不是全局变量。

    • 局部变量的访问速度要比全局变量的访问速度更快,因为全局变量其实是 window对象的成员,而局部变量是放在函数的栈里的。

  • 手工解除变量引用

    • 在业务代码中,一个变量已经确定不再需要了,那么就可以手工解除变量引用,以使其被回收。

  • 变量查找优化。

    • 变量声明带上 var,如果声明变量忘记了 var,那么 JAVASCRIPT引擎将会遍历整个作用域查找这个变量,结果不管找到与否,都会造成性能损耗。

      • 如果在上级作用域找到了这个变量,上级作用域变量的内容将被无声的改写,导致莫名奇妙的错误发生。

      • 如果在上级作用域没有找到该变量,这个变量将自动被声明为全局变量,然而却都找不到这个全局变量的定义。

    • 慎用全局变量。

      • 全局变量需要搜索更长的作用域链。

      • 全局变量的生命周期比局部变量长,不利于内存释放。

      • 过多的全局变量容易造成混淆,增大产生bug的可能性。
        >

    • 具有相同作用域变量通过一个var声明。

            jQuery.extend = jQuery.fn.extend = function () {
                var options, 
                    name, 
                    src, 
                    copy, 
                    copyIsArray, 
                    clone,target = arguments[0] || {},
                    i = 1,
                    length = arguments.length,
                    deep = false ;
            }
    • 缓存重复使用的全局变量。

      • 全局变量要比局部变量需要搜索的作用域长

      • 重复调用的方法也可以通过局部缓存来提速

      • 该项优化在IE上体现比较明显

  • 善用回调。

    • 除了使用闭包进行内部变量访问,我们还可以使用现在十分流行的回调函数来进行业务处理。

            function getData(callback) {
              var data = 'some big data';
              callback(null, data);
            }
            getData(function(err, data) {
              console.log(data);
            });
      • 回调函数是一种后续传递风格( Continuation Passing Style, CPS)的技术,这种风格的程序编写将函数的业务重点从返回值转移到回调函数中去。而且其相比闭包的好处也有很多。

        • 如果传入的参数是基础类型(如字符串、数值),回调函数中传入的形参就会是复制值,业务代码使用完毕以后,更容易被回收。

        • 通过回调,我们除了可以完成同步的请求外,还可以用在异步编程中,这也就是现在非常流行的一种编写风格。

        • 回调函数自身通常也是临时的匿名函数,一旦请求函数执行完毕,回调函数自身的引用就会被解除,自身也得到回收。

常规优化

  • 传递方法取代方法字符串

    一些方法例如 setTimeout()setInterval(),接受 字符串或者 方法实例作为参数。直接传递方法对象作为参数来避免对字符串的二次解析。

      • 传递方法

           setTimeout(test, 1);
    • 传递方法字符串

  • 使用原始操作代替方法调用

    方法调用一般封装了原始操作,在性能要求高的逻辑中,可以使用原始操作代替方法调用来提高性能。

    • 原始操作

          var min = a<b?a:b;
    • 方法实例

  • 定时器

    如果针对的是不断运行的代码,不应该使用 setTimeout,而应该是用 setIntervalsetTimeout每次要重新设置一个定时器。

  • 避免双重解释

JAVASCRIPT代码想解析 JAVASCRIPT代码时就会存在双重解释惩罚,双重解释一般在使用 eval函数、 new Function构造函数和 setTimeout传一个字符串时等情况下会遇到,如。

    eval("alert('hello world');");
    var sayHi = new Function("alert('hello world');");
    setTimeout("alert('hello world');", 100);

> 上述 alert('hello world');语句包含在字符串中,即在JS代码运行的同时必须新启运一个解析器来解析新的代码,而实例化一个新的解析器有很大的性能损耗。

我们看看下面的例子:

    var sum, num1 = 1, num2 = 2;
    /**效率低**/
    for(var i = 0; i < 10000; i++){
        var func = new Function("sum+=num1;num1+=num2;num2++;");
        func();
        //eval("sum+=num1;num1+=num2;num2++;");
    }
    /**效率高**/
    for(var i = 0; i < 10000; i++){
        sum+=num1;
        num1+=num2;
        num2++;
    }

第一种情况我们是使用了new Function来进行双重解释,而第二种是避免了双重解释。

  • 原生方法更快

    • 只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。

  • 最小化语句数

    JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式

    • 多个变量声明

            /**不提倡**/
            var i = 1;
            var j = "hello";
            var arr = [1,2,3];
            var now = new Date();
            /**提倡**/
            var i = 1,
                j = "hello",
                arr = [1,2,3],
                now = new Date();
    • 插入迭代值

            /**不提倡**/
            var name = values[i];
            i++;
            /**提倡**/
            var name = values[i++];
      
    • 使用数组和对象字面量,避免使用构造函数Array(),Object()

            /**不提倡**/
            var a = new Array();
            a[0] = 1;
            a[1] = "hello";
            a[2] = 45;
            var o = new Obejct();
            o.name = "bill";
            o.age = 13;
            /**提倡**/
            var a = [1, "hello", 45];
            var o = {
                name : "bill",
                age : 13
            };
  • 避免使用属性访问方法

    • JavaScript不需要属性访问方法,因为所有的属性都是外部可见的。

    • 添加属性访问方法只是增加了一层重定向 ,对于访问控制没有意义。

      使用属性访问方法示例
      
           function Car() {
              this .m_tireSize = 17;
              this .m_maxSpeed = 250;
              this .GetTireSize = Car_get_tireSize;
              this .SetTireSize = Car_put_tireSize;
           }
      
           function Car_get_tireSize() {
              return this .m_tireSize;
           }
      
           function Car_put_tireSize(value) {
              this .m_tireSize = value;
           }
           var ooCar = new Car();
           var iTireSize = ooCar.GetTireSize();
           ooCar.SetTireSize(iTireSize + 1);
      
      直接访问属性示例
      
           function Car() {
              this .m_tireSize = 17;
              this .m_maxSpeed = 250;
           }
           var perfCar = new Car();
           var iTireSize = perfCar.m_tireSize;
           perfCar.m_tireSize = iTireSize + 1;
      
  • 减少使用元素位置操作

    • 一般浏览器都会使用增量reflow的方式将需要reflow的操作积累到一定程度然后再一起触发,但是如果脚本中要获取以下属性,那么积累的reflow将会马上执行,已得到准确的位置信息。

代码压缩

  • 代码压缩工具

    > 精简代码就是将代码中的 空格注释去除,也有更进一步的会对变量名称 混淆精简。根据统计精简后文件大小会平均减少 21%,即使 Gzip之后文件也会减少 5%

    • YUICompressor

    • Dean Edwards Packer

    • JSMin

    • GZip压缩

      • GZip缩短在浏览器和服务器之间传送数据的时间,缩短时间后得到标题是 Accept-Encoding: gzip, deflate的一个文件。不过这种压缩方法同样也有缺点。

        • 它在服务器端和客户端都要占用处理器资源(以便压缩和解压缩)。

        • 占用磁盘空间。

      • Gzip通常可以减少70%网页内容的大小,包括脚本、样式表、图片等任何一个文本类型的响应,包括 XMLJSONGzipdeflate更高效,主流服务器都有相应的压缩支持模块。

      • Gzip的工作流程为

        • 客户端在请求 Accept-Encoding中声明可以支持 Gzip

        • 服务器将请求文档压缩,并在 Content-Encoding中声明该回复为 Gzip格式。

        • 客户端收到之后按照 Gzip解压缩。

    • Closure compiler

代码优化

  • 优化原则:

  • JS优化总是出现在大规模循环的地方:

    这倒不是说循环本身有性能问题,而是循环会迅速放大可能存在的性能问题,所以第二原则就是以大规模循环体为最主要优化对象。
    

    以下的优化原则,只在大规模循环中才有意义,在循环体之外做此类优化基本上是没有意义的。

    目前绝大多数JS引擎都是解释执行的,而解释执行的情况下,在所有操作中,函数调用的效率是较低的。此外,过深的prototype继承链或者多级引用也会降低效率。JScript中,10级引用的开销大体是一次空函数调用开销的1/2。这两者的开销都远远大于简单操作(如四则运算)。

  • 尽量避免过多的引用层级和不必要的多次方法调用:

    特别要注意的是,有些情况下看似是属性访问,实际上是方法调用。例如所有DOM的属性,实际上都是方法。在遍历一个NodeList的时候,循环 条件对于nodes.length的访问,看似属性读取,实际上是等价于函数调用的。而且IE DOM的实现上,childNodes.length每次是要通过内部遍历重新计数的。(My god,但是这是真的!因为我测过,childNodes.length的访问时间与childNodes.length的值成正比!)这非常耗费。所以 预先把nodes.length保存到js变量,当然可以提高遍历的性能。
    

    同样是函数调用,用户自定义函数的效率又远远低于语言内建函数,因为后者是对引擎本地方法的包装,而引擎通常是c,c++,java写的。进一步,同样的功能,语言内建构造的开销通常又比内建函数调用要效率高,因为前者在JS代码的parse阶段就可以确定和优化。

  • 尽量使用语言本身的构造和内建函数:

动画优化

  • 动画效果在缺少硬件加速支持的情况下反应缓慢,例如手机客户端。

    • 特效应该只在确实能改善用户体验时才使用,而不应用于炫耀或者弥补功能与可用性上的缺陷。

    • 至少要给用户一个选择可以禁用动画效果。

    • 设置动画元素为absolute或fixed。

      • position: staticposition: relative元素应用动画效果会造成频繁的 reflow

      • position: absoluteposition: fixed的元素应用动画效果只需要 repaint

    • 使用一个 timer完成多个元素动画。

      • setIntervalsetTimeout是两个常用的实现动画的接口,用以间隔更新元素的风格与布局。。

    • 动画效果的帧率最优化的情况是使用一个 timer完成多个对象的动画效果,其原因在于多个 timer的调用本身就会损耗一定性能。

            setInterval(function() {
              animateFirst('');
            }, 10);
            setInterval(function() {
              animateSecond('');
            }, 10);
      
       使用同一个`timer`。
      
  • 以脚本为基础的动画,由浏览器控制动画的更新频率。

对象专题

  • 减少不必要的对象创建:

    • 创建对象本身对性能影响并不大,但由于 JAVASCRIPT的垃圾回收调度算法,导致随着对象个数的增加,性能会开始严重下降(复杂度 O(n^2))。

      • 如常见的字符串拼接问题,单纯的多次创建字符串对象其实根本不是降低性能的主要原因,而是是在对象创建期间的无谓的垃圾回收的开销。而 Array.join的方式,不会创建中间字符串对象,因此就减少了垃圾回收的开销。

    • 复杂的 JAVASCRIPT对象,其创建时时间和空间的开销都很大,应该尽量考虑采用缓存。

    • 尽量作用 JSON格式来创建对象,而不是 var obj=new Object()方法。前者是直接复制,而后者需要调用构造器。

  • 对象查找

    • 避免对象的嵌套查询,因为 JAVASCRIPT的解释性, a.b.c.d.e嵌套对象,需要进行 4次查询,嵌套的对象成员会明显影响性能。

    • 如果出现嵌套对象,可以利用局部变量,把它放入一个临时的地方进行查询。

  • 对象属性

    • 访问对象属性消耗性能过程( JAVASCRIPT对象存储)。

      • 先从本地变量表找到 对象

      • 然后遍历 属性

      • 如果在 当前对象属性列表里没找到。

      • 继续从 prototype向上查找。

      • 且不能直接索引,只能遍历。

服务端优化

  • 避免404。

    • 更改404错误响应页面可以改进用户体验,但是同样也会浪费服务器资源。

    • 指向外部 JAVASCRIPT的链接出现问题并返回404代码。

      • 这种加载会破坏并行加载。

      • 其次浏览器会把试图在返回的404响应内容中找到可能有用的部分当作JavaScript代码来执行。

  • 删除重复的 JAVASCRIPTCSS

    • 重复调用脚本缺点。

      • 增加额外的HTTP请求。

      • 多次运算也会浪费时间。在IE和Firefox中不管脚本是否可缓存,它们都存在重复运算 JAVASCRIPT的问题。

  • ETags配置 Entity标签。

    • ETags用来判断浏览器缓存里的元素是否和原来服务器上的一致。

      • last-modified date相比更灵活。

  • 权衡DNS查找次数

    • 减少主机名可以节省响应时间。但同时也会减少页面中并行下载的数量。

      • IE浏览器在同一时刻只能从同一域名下载两个文件。当在一个页面显示多张图片时, IE用户的图片下载速度就会受到影响。

  • 通过Keep-alive机制减少TCP连接。

  • 通过CDN减少延时。

  • 平行处理请求(参考BigPipe)。

  • 通过合并文件或者Image Sprites减少HTTP请求。

  • 减少重定向( HTTP 301和40x/50x)。

类型转换专题

  • 把数字转换成字符串。

    • 应用 ""+1,效率是最高。

      • 性能上来说: ""+字符串> String()> .toString()> new String()

        • String()属于内部函数,所以速度很快。

        • .toString()要查询原型中的函数,所以速度略慢。

        • new String()最慢。

  • 浮点数转换成整型。

    • 错误使用使用 parseInt()

      • parseInt()是用于将 字符串转换成 数字,而不是 浮点数整型之间的转换。

    • 应该使用 Math.floor()或者 Math.round()

      • Math是内部对象,所以 Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。

逻辑判断优化

  • switch语句。

    • 若有一系列复杂的 if-else语句,可以转换成单个 switch语句则可以得到更快的代码,还可以通过将 case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。

内存专题

  • JAVASCRIPT的内存回收机制

    • 以Google的 V8引擎为例,在 V8引擎中所有的 JAVASCRIPT对象都是通过 来进行内存分配的。当我们在代码中 声明变量赋值时, V8引擎就会在 堆内存中分配一部分给这个 变量。如果已申请的 内存不足以存储这个 变量时, V8引擎就会继续申请 内存,直到 的大小达到了 V8引擎的内存上限为止(默认情况下, V8引擎的 堆内存的大小上限在 64位系统中为 1464MB,在 32位系统中则为 732MB)。

    • 另外, V8引擎对 堆内存中的 JAVASCRIPT对象进行 分代管理

      • 新生代。

        • 新生代即存活周期较短的 JAVASCRIPT对象,如临时变量、字符串等

      • 老生代。

        • 老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

  • 垃圾回收算法。

    • 垃圾回收算法一直是编程语言的研发中是否重要的​​一环,而 V8引擎所使用的垃圾回收算法主要有以下几种。

      • Scavange算法:通过复制的方式进行内存空间管理,主要用于新生代的内存空间;

      • Mark-Sweep算法和 Mark-Compact算法:通过标记来对堆内存进行整理和回收,主要用于老生代对象的检查和回收。

  • 对象进行回收。

    • 引用

      • 当函数执行完毕时,在函数内部所声明的对象 不一定就会被销毁。

      • 引用( Reference)是 JAVASCRIPT编程中十分重要的一个机制。

          • 是指 代码对对象的访问这一抽象关系,它与 C/C++的指针有点相似,但并非同物。引用同时也是 JAVASCRIPT引擎在进行 垃圾回收中最关键的一个机制。

                var val = 'hello world';
                function foo() {
                  return function() {
                    return val;
                  };
                }
                global.bar = foo();
        • 当代码执行完毕时,对象 valbar()并没有被回收释放, JAVASCRIPT代码中,每个 变量作为单独一行而不做任何操作, JAVASCRIPT引擎都会认为这是对 对象的访问行为,存在了对 对象的引用。为了保证 垃圾回收的行为不影响程序逻辑的运行, JAVASCRIPT引擎不会把正在使用的 对象进行回收。所以判断 对象是否正在使用中的标准,就是是否仍然存在对该 对象引用

  • JAVASCRIPT引用是可以进行 转移的,那么就有可能出现某些引用被带到了全局作用域,但事实上在业务逻辑里已经不需要对其进行访问了,这个时候就应该被回收,但是 JAVASCRIPT引擎仍会认为程序仍然需要它。

  • IE下闭包引起跨页面内存泄露。

  • JAVASCRIPT的内存泄露处理

    • DOM对象添加的属性是一个对象的引用。

          var MyObject = {};
          document.getElementByIdx_x('myDiv').myProp = MyObject;
      
      解决方法:在window.onunload事件中写上: 
      
          document.getElementByIdx_x('myDiv').myProp = null;
    • DOM对象与JS对象相互引用。

           function Encapsulator(element) {
              this.elementReference = element;
              element.myProp = this;
           }
           new Encapsulator(document.getElementByIdx_x('myDiv'));
      
      解决方法:在onunload事件中写上: 
      
          document.getElementByIdx_x('myDiv').myProp = null;
    • 给DOM对象用attachEvent绑定事件。

           function doClick() {}
           element.attachEvent("onclick", doClick);
      
      解决方法:在onunload事件中写上: 
      
          element.detachEvent('onclick', doClick);
    • 从外到内执行appendChild。这时即使调用removeChild也无法释放。

           var parentDiv =   document.createElement_x("div");
           var childDiv = document.createElement_x("div");
           document.body.appendChild(parentDiv);
           parentDiv.appendChild(childDiv);
      
      解决方法:从内到外执行appendChild:
      
           var parentDiv =   document.createElement_x("div");
           var childDiv = document.createElement_x("div");
           parentDiv.appendChild(childDiv);
           document.body.appendChild(parentDiv);
    • 反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。

           for(i = 0; i < 5000; i++) {
              hostElement.text = "asdfasdfasdf";
           }
  • 内存不是 缓存

    • 不要轻易将 内存当作 缓存使用。

    • 如果是很重要的资源,请不要直接放在 内存中,或者制定 过期机制,自动销毁 过期缓存

  • CollectGarbage

    • CollectGarbageIE的一个特有属性,用于释放内存的使用方法,将该变量或引用对象设置为 nulldelete然后在进行释放动作,在做 CollectGarbage前,要必需清楚的两个必备条件:(引用)。

      • 一个对象在其生存的上下文环境之外,即会失效。

      • 一个全局的对象在没有被执用(引用)的情况下,即会失效

事件优化

  • 使用事件代理

    • 当存在多个元素需要注册事件时,在每个元素上绑定事件本身就会对性能有一定损耗。

    • 由于DOM Level2事件模 型中所有事件默认会传播到上层文档对象,可以借助这个机制在上层元素注册一个统一事件对不同子元素进行相应处理。

捕获型事件先发生。两种事件流会触发DOM中的所有对象,从document对象开始,也在document对象结束。

<ul id="parent-list"><li id="post-1">Item 1<li id="post-2">Item 2<li id="post-3">Item 3<li id="post-4">Item 4<li id="post-5">Item 5<li id="post-6">Item 6</li></ul>
    // Get the element, add a click listener...
    document.getElementById("parent-list").addEventListener("click",function(e) {
        // e.target is the clicked element!
        // If it was a list item
        if(e.target && e.target.nodeName == "LI") {
            // List item found!  Output the ID!
            console.log("List item ",e.target.id.replace("post-")," was clicked!");
        }
    });

数组专题

  • 当需要使用数组时,可使用 JSON格式的语法

    • 即直接使用如下语法定义数组: [parrm,param,param...],而不是采用 new Array(parrm,param,param...)这种语法。使用 JSON格式的语法是引擎直接解释。而后者则需要调用 Array的构造器。

  • 如果需要遍历数组,应该先缓存数组长度,将数组长度放入局部变量中,避免多次查询数组长度。

    • 根据字符串、数组的长度进行循环,而通常这个长度是不变的,比如每次查询 a.length,就要额外进行一个操作,而预先把 var len=a.length,则每次循环就少了一次查询。

同域跨域

  • 避免跳转

    • 同域:注意避免反斜杠 “/” 的跳转;

    • 跨域:使用Alias或者mod_rewirte建立CNAME(保存域名与域名之间关系的DNS记录)

性能测试工具

  • js性能优化和内存泄露问题及检测分析工具

      • 性能优化ajax工具 diviefirebug

      • [web性能分析工具YSlow]

        • performance性能评估打分,右击箭头可看到改进建议。

        • stats缓存状态分析,传输内容分析。

        • components所有加载内容分析,可以查看传输速度,找出页面访问慢的瓶颈。

        • tools可以查看js和css,并打印页面评估报告。

      • 内存泄露检测工具 sIEve

        • sIEve是基于 IE的内存泄露检测工具,需要下载运行,可以查看dom孤立节点和内存泄露及内存使用情况。

          1. 列出当前页面内所有dom节点的基本信息(html id style 等)

          2. 页面内所有dom节点的高级信息 (内存占用,数量,节点的引用)

          3. 可以查找出页面中的孤立节点

          4. 可以查找出页面中的循环引用

          5. 可以查找出页面中产生内存泄露的节点

      • 内存泄露提示工具 leak monitor

        • leak monitor在安装后,当离开一个页面时,比如关闭窗口,如果页面有内存泄露,会弹出一个文本框进行即时提示。

      • 代码压缩工具

        • YUI压缩工具

        • Dean Edwards Packer

        • JSMin

    • Blink/Webkit浏览器

      • Blink/Webkit浏览器中( Chrome, Safari, Opera),我们可以借助其中的 Developer ToolsProfiles工具来对我们的程序进行内存检查。

  • Node.js中的内存检查

    • Node.js中,我们可以使用 node-heapdumpnode-memwatch模块进​​行内存检查。

          var heapdump = require('heapdump');
          var fs = require('fs');
          var path = require('path');
          fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);

      在业务代码中引入 node-heapdump之后,我们需要在某个运行时期,向 Node.js进程发送 SIGUSR2信号,让 node-heapdump抓拍一份堆内存的快照。

         $ kill -USR2 (cat app.pid)
  • 分析浏览器提供的Waterfall图片来思考优化入口。

  • 新的测试手段(Navigation, Resource, 和User timing。

循环专题

  • 循环是一种常用的流程控制。

    • JAVASCRIPT提供了三种循环。

      • for(;;)

        • 推荐使用for循环,如果循环变量递增或递减,不要单独对循环变量赋值,而应该使用嵌套的 ++–-运算符。

        • 代码的可读性对于for循环的优化。

        • -=1

        • 从大到小的方式循环(这样缺点是降低代码的可读性)。

      • while()

        • for(;;)while()循环的性能基本持平。

      • for(in)

        • 在这三种循环中 for(in)内部实现是构造一个所有元素的列表,包括 array继承的属性,然后再开始循环,并且需要查询hasOwnProperty。所以 for(in)相对 for(;;)循环性能要慢。

  • 选择正确的方法

    • 避免不必要的属性查找。

      • 访问 变量数组O(1)操作。

      • 访问 对象上的 属性是一个 O(n)操作。

        对象上的任何属性查找都要比访问变量或数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,即属性查找越多,执行时间越长。所以针对需要多次用到对象属性,应将其存储在局部变量。
        
    • 优化循环。

      • 减值迭代。

        • 大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。

      • 简化终止条件。

        • 由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它O(n)的操作。

      • 简化循环体。

        • 循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。

      • 使用后测试循环。

        • 最常用的for和while循环都是前测试循环,而如do-while循环可以避免最初终止条件的计算,因些计算更快。

    • 展开循环。

      • 当循环的次数确定时,消除循环并使用多次函数调用往往更快。

      • 当循环的次数不确定时,可以使用Duff装置来优化。

        • Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。

  • 避免在循环中使用 try-catch

    • try-catch-finally语句在catch语句被执行的过程中会动态构造变量插入到当前域中,对性能有一定影响。

    • 如果需要异常处理机制,可以将其放在循环外层使用。

        • 循环中使用try-catch

              for ( var i = 0; i < 200; i++) {
                try {} catch (e) {}
              }
      • 循环外使用try-catch

                 try {
                   for ( var i = 0; i < 200; i++) {}
                 } catch (e) {}
  • 避免遍历大量元素:

    • 避免对全局 DOM元素进行遍历,如果 parent已知可以指定 parent在特定范围查询。

           var elements = document.getElementsByTagName( '*' );
           for (i = 0; i < elements.length; i++) {
              if (elements[i].hasAttribute( 'selected' )) {}
           }
      
      如果已知元素存在于一个较小的范围内,
      

原型优化

  • 通过原型优化方法定义。

    • 如果一个方法类型将被频繁构造,通过方法原型从外面定义附加方法,从而避免方法的重复定义。

    • 可以通过外部原型的构造方式初始化值类型的变量定义。(这里强调值类型的原因是,引用类型如果在原型中定义,一个实例对引用类型的更改会影响到其他实例。)

      • 这条规则中涉及到 JAVASCRIPT中原型的概念,构造函数都有一个 prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。可以把那些不变的属性和方法,直接定义在 prototype对象上。

        • 可以通过对象实例访问保存在原型中的值。

        • 不能通过对象实例重写原型中的值。

        • 在实例中添加一个与实例原型同名属性,那该属性就会屏蔽原型中的属性。

        • 通过delete操作符可以删除实例中的属性。

运算符专题

  • 使用运算符时,尽量使用 +=-=*=\=等运算符号,而不是直接进行赋值运算。

  • 位运算

    • 当进行数学运算时 位运算较快, 位运算操作要比任何 布尔运算算数运算快,如 取模逻辑与逻辑或也可以考虑用 位运算来替换。

重绘专题

  • 减少页面的 重绘

    • 减少页面 重绘虽然本质不是 JAVASCRIPT优化,但 重绘往往是由 JAVASCRIPT引起的,而 重绘的情况直接影响页面性能。

           var str = "<div>这是一个测试字符串</div>";
           /**效率低**/
           var obj = document.getElementsByTagName("body");
           for(var i = 0; i < 100; i++){
               obj.innerHTML += str + i;
           }
           /**效率高**/
           var obj = document.getElementsByTagName("body");
           var arr = [];
           for(var i = 0; i < 100; i++){
               arr[i] = str + i;
           }
           obj.innerHTML = arr.join("");

    一般影响页面重绘的不仅仅是innerHTML,如果改变元素的样式,位置等情况都会触发页面重绘,所以在平时一定要注意这点。

  • 使用HTML5和CSS3的一些新特性。

  • 避免在HTML里面缩放图片。

  • 避免使用插件。

  • 确保使用正确的字体大小。

  • 决定当前页面是不是能被访问。

字符串专题

  • 对字符串进行循环操作。

    • 替换、查找等操作,使用正则表达式。

      • 因为 JAVASCRIPT的循环速度较慢,而正则表达式的操作是用 C写成的 API,性能比较好。

  • 字符串的拼接。

    • 字符串的拼接在我们开发中会经常遇到,所以我把其放在首位,我们往往习惯的直接用 +=的方式来拼接字符串,其实这种拼接的方式效率非常的低,我们可以用一种巧妙的方法来实现字符串的拼接,那就是利用数组的 join方法,具体请看我整理的: Web前端开发规范文档中的 javaScript书写规范倒数第三条目。

    • 不过也有另一种说法,通常认为需要用 Array.join的方式,但是由于 SpiderMonkey等引擎对字符串的“ +”运算做了优化,结果使用 Array.join的效率反而不如直接用“ +”,但是如果考虑 IE6,则其他浏览器上的这种效率的差别根本不值一提。具体怎么取舍,诸君自定。

作用域链和闭包优化

  • 作用域。

    • 作用域( scope)是 JAVASCRIPT编程中一个重要的 运行机制,在 JAVASCRIPT同步和异步编程以及 JAVASCRIPT内存管理中起着至关重要的作用。

    • JAVASCRIPT中,能形成作用域的有如下几点。

      • 函数的调用

      • with语句

        • with会创建自已的作用域,因此会增加其中执行代码的作用域的长度。

      • 全局作用域。

        以下代码为例:
        
             var foo = function() {
               var local = {};
             };
             foo();
             console.log(local); //=undefined
        
             var bar = function() {
               local = {};
             };
             bar();
             console.log(local); //={}
        
            /**这里我们定义了foo()函数和bar()函数,他们的意图都是为了定义一个名为local的变量。在foo()函数中,我们使用var语句来声明定义了一个local变量,而因为函数体内部会形成一个作用域,所以这个变量便被定义到该作用域中。而且foo()函数体内并没有做任何作用域延伸的处理,所以在该函数执行完毕后,这个local变量也随之被销毁。而在外层作用域中则无法访问到该变量。而在bar()函数内,local变量并没有使用var语句进行声明,取而代之的是直接把local作为全局变量来定义。故外层作用域可以访问到这个变量。**/
        
  • 作用域链

    • JAVASCRIPT编程中,会遇到多层函数嵌套的场景,这就是典型的作用域链的表示。

           function foo() {
             var val = 'hello';
             function bar() {
               function baz() {
                 global.val = 'world;'
               };
               baz();
               console.log(val); //=hello
             };
             bar();
           };
           foo();
  • 减少作用域链上的查找次数

    • JAVASCRIPT代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要遍历当前执行环境的作用域链,而遍历是从这个作用域链的前端一级一级的向后遍历,直到全局执行环境。

  • 闭包

    • JAVASCRIPT中的标识符查找遵循从内到外的原则。

            function foo() {
              var local = 'Hello';
              return function() {
                return local;
              };
            }
            var bar = foo();
            console.log(bar()); //=Hello
      
            /**这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到`延伸`。foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。**/
      • 闭包是 JAVASCRIPT的高级特性,因为把带有​​内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。

      • 良好的闭包管理。

        • 循环事件绑定、私有属性、含参回调等一定要使用闭包时,并谨慎对待其中的细节。

          • 循环绑定事件,我们假设一个场景:有六个按钮,分别对应六种事件,当用户点击按钮时,在指定的地方输出相应的事件。

  • 避开闭包陷阱

    • 闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。

    • 闭包的性能不如使用内部方法,更不如重用外部方法。

      • 由于 IE 9浏览器的 DOM节点作为 COM对象来实现, COM内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦 DOM引用了闭包(例如 event handler),闭包的上层元素又引用了这个 DOM,就会造成循环引用从而导致内存泄漏。

  • 善用函数

    • 使用一个匿名函数在代码的最外层进行包裹。

       ;(function() {
         // 主业务代码
       })();
      

有的甚至更高级一点:

    ;(function(win, doc, $, undefined) {
      // 主业务代码
    })(window, document, jQuery);

甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:

    /**RequireJS**/
    define(['jquery'], function($) {
      // 主业务代码
    });
    /**SeaJS**/
    define('m​​odule', ['dep', 'underscore'], function($, _) {
      // 主业务代码
    });

被定义在全局作用域的对象,可能是会一直存活到进程退出的,如果是一个很大的对象,那就麻烦了。比如有的人喜欢在JavaScript中做模版渲染:

<?php
      $db = mysqli_connect(server, user, password, 'myapp');
      $topics = mysqli_query($db, "SELECT * FROM topics;");
    ?><!doctype html><html lang="en"><head><meta charset="UTF-8"><title>你是猴子请来的逗比么?</title></head><body><ul id="topics"></ul><script type="text/tmpl" id="topic-tmpl"><li class="topic"><h1><%=title%></h1><p><%=content%></p></li></script><script type="text/javascript">
        var data = <?php echo json_encode($topics); ?>;
        var topicTmpl = document.querySelector('#topic-tmpl').innerHTML;
        var render = function(tmlp, view) {
          var complied = tmlp
            .replace(/\n/g, '\\n')
            .replace(/<%=([\s\S]+?)%>/g, function(match, code) {
              return '" + escape(' + code + ') + "';
            });
          complied = ['var res = "";','with (view || {}) {','res = "' + complied + '";','}','return res;'
          ].join('\n');
          var fn = new Function('view', complied);
          return fn(view);
        };
        var topics = document.querySelector('#topics');
        function init()
          data.forEach(function(topic) {
            topics.innerHTML += render(topicTmpl, topic);
          });
        }
        init();</script></body></html>

在从数据库中获取到的数据的量是非常大的话,前端完成模板渲染以后,data变量便被闲置在一边。可因为这个变量是被定义在全局作用域中的,所以 JAVASCRIPT引擎不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。可是如果我们作出一些很简单的修改,在逻辑代码外包装一层函数,这样效果就大不同了。当UI渲染完成之后,代码对data的引用也就随之解除,而在最外层函数执行完毕时, JAVASCRIPT引擎就开始对其中的对象进行检查,data也就可以随之被回收。

GITHUB: 前端性能优化指南

参考和借鉴了大家的经验,收集整理了这一篇开发规范,感谢所有的原作者,众人拾柴火焰高,技术无国界,持续更新中。

404错误处理:重定向还是直接404?

$
0
0

小型网站开发通常会使用某种Web应用框架,比如类似Spring、Express、Django等框架。 这些框架会给出自定义错误页面的方式。当404发生时Web框架会渲染并返回对应的错误页面。 这是最自然和直接的错误处理方式,但有时我们希望错误页面可以单独Serve,比如放到CDN上。 本文档依据RFC 2616(HTTP 1.1)比较几种常见的404错误处理方法:

  • 返回具有404信息的页面,同时给出404状态码。

    Google、Github、Facebook、Amazon、Linkedin。

  • 重定向(302/303)至错误URL,该URL给出具有404信息的页面。

    百度、淘宝、腾讯

状态码及其语义

使用最广的HTTP标准当属1999年发布的HTTP/1.1, R. T. Fielding 在2000年的博士论文 “Architectural Styles and the Design of Network-based Software Architectures” 中重申如何正确使用HTTP语义以及REST架构风格。 符合标准的HTTP消息拥有透明的、自描述的语义,通信方遵循一致的标准才使得整个互联网得以高效地运行。

HTTP状态码对用户不可见,因为Web页面返回给用户之后页面代码并非直接显示给用户, 而是经由用户代理(User Agent,比如浏览器、爬虫等)处理。 状态码的重要性在于它告诉用户代理当前页面的状态,借此用户代理才能做出正确的操作: 比如刷新当前页面,或者访问另一个URL,重置或重新提交表单等等。

HTTP状态码有很多,本文档只关心其中的404,200,以及302/303状态码。

200

请求成功。对于GET,应当返回被请求资源的实体;对于POST,应当返回操作的结果。

RFC 2616: 10.2.1

The request has succeeded. The information returned with the response
is dependent on the method used in the request, for example:

GET   an entity corresponding to the requested resource is sent in
          the response;
POST  an entity describing or containing the result of the action;

302

被请求的资源暂时位于另一个URI处,并且对于非HEAD/GET请求, 用户代理在重定向前必须询问用户确认。

RFC 2616: 10.3.3

The requested resource resides temporarily under a different URI.
Since the redirection might be altered on occasion, the client SHOULD
continue to use the Request-URI for future requests. This response
is only cacheable if indicated by a Cache-Control or Expires header
field.

RFC 1945 和 RFC 2068 规定客户端不允许更改请求的方法。但很多浏览器会将302当做303来处理。

303

被请求的资源暂时位于另一个URI处,并且应当以GET方法去请求那个资源。

RFC 2616: 10.3.4

The response to the request can be found under a different URI and
SHOULD be retrieved using a GET method on that resource. This method
exists primarily to allow the output of a POST-activated script to
redirect the user agent to a selected resource. The new URI is not a
substitute reference for the originally requested resource. The 303
response MUST NOT be cached, but the response to the second
(redirected) request might be cacheable.

404

服务器未能找到URI所标识的资源。也常被用于服务器希望隐藏请求被拒绝的具体原因。 例如403、401可能会被统一处理为404。

RFC 2616: 10.4.5

The server has not found anything matching the Request-URI. No
indication is given of whether the condition is temporary or
permanent. 

This status code is commonly used when the server does not wish to
reveal exactly why the request has been refused, or when no other
response is applicable.

直接404

直接404是常见Web框架的通用做法:对于每一个用户请求遍历所有可能的路由, 如果任何一个控制器都不处理该请求,则Web服务器返回一个具有404状态码的HTTP响应。

404状态码告诉用户代理(User Agent)请求中的URI所标识的资源不存在, 用户代理将该HTTP响应的主体(往往是HTML)显示给用户, 该HTML页面是终端用户可读的,通常也会包含Not Found信息,以及一些有用的链接。

爬虫是一种特殊的用户代理,通常用于搜索引擎。当搜索引擎的爬虫发现某URI返回404状态码时, 会认为该URI已经失效而不对它进行索引,或者将该URI标识的已索引资源移除。 所以是网站迁移时,如果一个旧的URI会位于一个新的URI处,应当使用301重定向来告知搜索引擎: 该资源并未失效只是位置发生变化。

当我们无法控制服务器返回301时,也可在返回的 HTML中使用特殊标记 来告知用户代理这是一个301,这在迁移静态站点时非常有用。

404的一个问题在于不支持CDN。如果为了提升性能使用CDN服务, 将 404.html文件托管到CDN提供商,访问该文件显然会返回200状态码。 因为CDN服务器认为你所访问的文件存在。

重定向至404页面

在国内网站中更常见的方式是将所有错误重定向至错误页面,比如 error.html。 当用户代理访问 error.html时服务器返回状态码为200,这便是神奇的 200 Not Found。 200 Not Found显然不符合HTTP语义标准,下面从搜索引擎和CDN两个方面评价该方法的优劣:

搜索引擎非常不友好。当一个页面Not Found时爬虫并不知情,因为它收到的状态码是303。 于是跟随重定向并索引了错误页面 error.html。这意味着该网站会有大量的URL都拥有同样的内容(404页面), 网站会因此受到搜索引擎的惩罚而排名下降。

另外作为程序员吐槽一下200 Not Found给调试带来的不便:

  • 脚本、样式或 AJAX 发生 404 时 Console 不显示任何错误;
  • Accept为JSON或JavaScript的AJAX请求会得到HTML响应体;
  • <script>标签404会使整个HTML失效。

国内不少公司选择重定向的错误处理方式也必有其优点:

  • 固定的错误页面可以直接托管于CDN,通过CDN统计和脚本的方式来统计错误。
  • 固定的URL可以支持更加松耦合的架构,只需约定重定向URL即可构建前后端通用的错误处理。

参考

JavaScript 方法的4种调用模式

$
0
0

函数(Function)是JavaScript的基本模块单元,JavaScript的代码重用, 信息隐藏,对象组合等都可以借助函数来实现。 JavaScript中的函数有4种调用模式:

  • 方法调用(Method Invocation Pattern)
  • 函数调用(Function Invocation Pattern)
  • 构造函数调用(Constructor Invocation Pattern)
  • apply调用(Apply Invocation Pattern)

与其他语言不通,JavaScript的函数是一种特殊的对象 (事实上,JavaScript函数是一级对象)。 这意味着函数可以有属性,可以有方法,也可以传参、返回、赋值给变量、加入数组。 与对象不同的是函数可以被调用。

既然是对象便会有原型。我们知道通过花括号语法创建的对象的原型是 Object.prototype。 通过函数语法( function关键字或 new Function())创建的函数,其原型自然是 Function.prototype。 而 Function.prototype的原型是 Object.prototype

注意上述『原型』指的是原型链上的隐式原型,区别于 prototype原型属性。

调用模式

我们知道在函数里可见的名称包括:函数体内声明的变量、函数参数、来自外部的闭包变量。 此外还有两个: thisarguments

this在面向对象程序设计中非常重要,而它的值在JavaScript中取决于 调用模式。 JavaScript中的函数有4种调用模式:方法调用、函数调用、构造函数调用、apply调用。

arguments是一个类数组变量(array like),拥有 length属性并可以取下标, 它存着所有参数构成的有序数组。 在JavaScript中,函数调用与函数签名不一致(个数不正确、类型不正确定) 时不会产生运行时错。少了的参数会被置为 undefined,多了的参数会被忽略。

方法调用模式

在面向对象程序设计中,当函数(Function)作为对象属性时被称为方法(Method)。 方法被调用时 this会被绑定到对应的对象。在JavaScript中有两种语法可以完成方法调用: a.func()a['func']()

var obj = {
    val: 0,
    count: function(i){
        this.val += i;
        console.log(this.val);
    }
};

obj.count();    // 1
obj.count();    // 2

值得注意的是, thisobj的绑定属于极晚绑定(very late binding), 绑定发生在调用的那一刻。这使得JavaScript函数在被重用时有极大的灵活性。

函数调用模式

当函数不是对象属性时,它就会被当做函数来调用,比如 add(2,3)。 此时 this绑定到了全局对象 global

在[那些 JavaScript 的优秀特性][elegant-js]一文中曾提到, JavaScript的编译(预处理)需要 global对象来链接所有全局名称。

其实 this绑定到 global是JavaScript的一个设计错误(可以说是最严重的错误), 它导致了对象方法不能直接调用内部函数来做一些辅助工作, 因为内不函数里的 this的绑定到了 global。 所以如果要重新设计语言,方法调用的 this应该绑定到上一级函数的 this

然而共有方法总是需要调用内部辅助函数,于是产生了这样一个非常普遍的解决方案:

man.love = function(){
    var self = this;
    function fuck(){
        self.happy++;
    }
    function marry(){
        self.happy--;
    }
    fuck() && marry();
}

有些场景下用 Function.prototype.bind会更加方便:

man.love = function(){
    function fuck(girl1, girl2, ...){
        this.happy++;
    }
    fuck.bind(this)();
    ...
}

构造函数调用模式

Classically inspired syntax obscures the language’s true prototypal natur.

JavaScript中,那些用来 new对象的函数成为构造函数。

JavaScript采用原型继承方式。这意味着一个对象可以从另一个对象直接继承属性, JavaScript是class free的~ 但JavaScript为了迎合主流的基于类声明的继承方式, 同时也给出了构造函数机制:使用 new关键字,便会创建一个对象, 根据 prototype属性创建原型链,并以该对象为 this执行指定的(构造)函数。

function Man(name, age){
    this.sex = 'man';
    this.name = name;
    this.age = age;
}
Man.prototype.fuck = function(girl1, girl2, ...){}
var man = new Man();
man.fuck();

当构造函数有很多参数时,它们的顺序很难记住,所以通常使用对象进行传参:

var man = new Man({
    name: 'bob',
    age: 18
});

给参数起名字以达到顺序无关的做法在Python中也存在,但JavaScript的对象传参还将带来另一个好处: JSON兼容。因为JavaScript常常需要数据库(例如MongoDB)或网络( application/json)传来的JSON数据,这一点使得对象构造非常方便。

Apply 调用模式

JavaScript函数是一种特殊的对象,而对象可以有属性和方法。 其中的 apply方法提供了一个更加特殊的调用方式。 它接受两个参数:第一个是该函数要绑定的 this,第二个是参数数组。

var args = [girl1, girl2];
var animal = new Animal();
Man.prototype.fuck.apply(animal, args);

Apply使得一个方法可以用不同的对象对象来调用,比如 animal也可以用 Man的方式来 fuck

[elegant-js]: {% post_url 2016-01-18-elegant-javascript %}

工作中经常用到github上优秀、实用、轻量级、无依赖的插件和库

$
0
0

原文收录在我的 GitHub博客 ( https://github.com/jawil/blog),喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴。

按照格式推荐好用的插件有福利哦,说不定会送1024论坛邀请码,好自为之,你懂的,嘿嘿嘿。

由于github的issues没有TOC菜单栏导航,所以这里方便大家查看,先安利一款Chrome浏览器的插件,感谢github用户@BBcaptain 推荐。 点击我呀,进入商店,自备梯子,如果不会翻墙,赶紧转行。。。

效果预览,是不是很方便,图片较多,建议等待一会或者多刷新几下:

Echo.js – 简单易用的图片延迟加载插件

github: https://github.com/toddmotto/...

官方网站: https://toddmotto.com/echo-js...

star:3k+

Install:

npm:npm install echo-js
bower:bower install echojs

大小:2KB

功能介绍:
  Echo.js 是一个独立的延迟加载图片的 JavaScript 插件。Echo.js 不依赖第三方库,压缩后不到1KB大小。 延迟加载是提高网页首屏显示速度的一种很有效的方法,当图片元素进入窗口可视区域的时候,它就会改变图像的 src 属性,从服务端加载所需的图片,这也是一个异步的过程。
  
Demo:
效果预览地址: https://jawil.github.io/demo/...
Demo源码: https://github.com/jawil/jawi...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Lazyr.js – 延迟加载图片(Lazy Loading)

github: https://github.com/callmecavs...

官方网站: http://callmecavs.com/layzr.j...

star:5k+

Install:

npm:npm install layzr.js --save

大小:2.75 KB

功能介绍:
  Lazyr.js 是一个小的、快速的、现代的、相互间无依赖的图片延迟加载库。通过延迟加载图片,让图片出现在(或接近))视窗才加载来提高页面打开速度。这个库通过保持最少选项并最大化速度。

Demo:
跟上面的Echo.js用法类似,喜欢的可以自行去尝试,这里就不再演示了,我一般用Echo.js。






better-scroll.js – 小巧,灵活的 JavaScript 模拟滚动条的插件

github: https://github.com/ustbhuangy...

官方网站: https://ustbhuangyi.github.io...

star:300+

Install:

npm install better-scroll --save-dev
import BScroll from 'better-scroll';

大小:24 KB

功能介绍:
  better-scroll 是一个只有24.8KB的 JavaScript 模拟浏览器自带滚动条的插件,是在 iscroll开源的基础上进行优化的一款插件,简单好用,轻巧高性能,功能强大,API通俗易懂,是一款优秀的scroll插件,抛弃原生滚动条,从现在做起。

Demo:
效果预览地址: https://jawil.github.io/demo/... (PC端切换到移动模式)
Demo源码: https://github.com/jawil/webp...
注:在ustbhuangyi的源码下改进了一下,做成多页面,技术栈:webpack2+vue.js2+sass+axios

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






better-picker – 一款轻量级IOS风格的JavaScript选择器

github: https://github.com/ustbhuangy...

官方网站: http://ustbhuangyi.github.io/...

star:200+

Install:

npm: npm install better-picker --save-dev
import Picker from 'better-picker'

大小:46.5 KB

功能介绍:
  移动端最好用的的筛选器组件,高仿 ios 的 UIPickerView ,非常流畅的体验,原生 JS 实现,不依赖任何插件和第三方库。
Demo:
效果预览地址: http://ustbhuangyi.github.io/...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Sortable – 一款用于实现元素拖拽排序的功能的插件

github: https://github.com/RubaXa/Sor...

官方网站: http://rubaxa.github.io/Sorta...

star:9k+

Install:

Bower: bower install sortablejs --save
npm: npm install sortablejs 

大小:5 KB

功能介绍:
 Sortable:现代浏览器上用于实现元素拖拽排序的功能,支持 Meteor, AngularJS, React,不依赖 jQuery这玩意。

Demo:
效果预览地址: http://rubaxa.github.io/Sorta...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







slick – 功能异常强大的一个图片滑动切换效果库

github: https://github.com/kenwheeler...

官方网站: http://kenwheeler.github.io/s...

star:17k+

Install:

Bower: bower install slick-carousel --save
npm: npm install slick-carousel
CDNs:
https://cdnjs.com/libraries/slick-carousel
https://www.jsdelivr.com/projects/jquery.slick

大小:40 KB

功能介绍:
 slick 是一个功能异常强大的一个图片滑动切换效果库,接口丰富,支持各种动画和各种样式的切换滑动,唯一的缺点就是 基于jQuery,基本废了,现在没人喜欢用jQuery,该淘汰了。。。支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址: http://kenwheeler.github.io/s...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







swipe – 非常轻量级的一个图片滑动切换效果库

github: https://github.com/lyfeyaj/Sw...

官方网站: http://lyfeyaj.github.io/swip...

star:200+

Install:

Bower: bower install swipe-js  --save
npm: npm install swipe-js 

大小:5 KB

功能介绍:
 swipe:非常轻量级的一个图片滑动切换效果库, 性能良好, 尤其是对手机的支持, 压缩后的大小约 5kb。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址: http://lyfeyaj.github.io/swipe/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Slideout.js – 触摸滑出式 Web App 导航菜单

github: https://github.com/mango/slid...

官方网站: https://slideout.js.org/https://github.com/t4t5/sweet...

官方网站: http://t4t5.github.io/sweetal...

star:15k+

Install:

bower:bower install sweetalert
npm:npm install sweetalert<script src="dist/sweetalert.min.js"></script><link rel="stylesheet" type="text/css" href="dist/sweetalert.css">

大小:16 KB

功能介绍:
  Sweet Alert 是一个替代传统的 JavaScript Alert 的漂亮提示效果。SweetAlert 自动居中对齐在页面中央,不管您使用的是台式电脑,手机或平板电脑看起来效果都很棒。另外提供了丰富的自定义配置选择,可以灵活控制。

Demo:
效果预览地址: http://t4t5.github.io/sweetal...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

类似插件:limonte/sweetalert2,好像这个最近还在更新,这个感觉更漂亮,大同小异,这里不多做介绍。

github: https://github.com/limonte/sw...

官方网站: https://limonte.github.io/swe...






Awesomplete.js - 比datalist更强大更实用,零依赖的简单自动补全插件

github: https://github.com/leaverou/a...

官方网站: http://leaverou.github.io/awe...

star:5k+

Install:
`npm: npm install awesomplete
`

大小:5 KB

功能介绍:
 Awesomplete 是一款超轻量级的,可定制的,简单的自动完成插件,零依赖,使用现代化标准构建。你可以简单地添加 awesomplete 样式,让它自动处理(你仍然可以通过指定 HTML 属性配置更多选项),您可以用几行 JS 代码,提供更多的自定义。

Demo:
效果预览地址: http://leaverou.github.io/awe...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Cleave.js – 自动格式化表单输入框的文本内容

github: https://github.com/nosir/clea...

官方网站: http://nosir.github.io/cleave...

star:6k+

Install:

npm:npm install --save cleave.js
bower:bower install --save cleave.js

大小:11.1 KB

功能介绍:
  Cleave.js 有一个简单的目的:帮助你自动格式输入的文本内容。 这个想法是提供一个简单的方法来格式化您的输入数据以增加输入字段的可读性。通过使用这个库,您不需要编写任何正则表达式来控制输入文本的格式。然而,这并不意味着取代任何验证或掩码库,你仍应在后端验证数据。它支持信用卡号码、电话号码格式(支持各个国家)、日期格式、数字格式、自定义分隔符,前缀和块模式等,提供 CommonJS/AMD 模式以及ReactJS 组件端口。

Demo:
效果预览地址: http://nosir.github.io/cleave...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。
输入201748自动格式化成2017-04-08,是不是很方便





Immutable.js – JavaScript 不可变数据集合(Facebook出品)

github: https://github.com/facebook/i...

官方网站: http://facebook.github.io/imm...

star:18k+

Install:

npm install immutable --S-D

大小:60 KB

功能介绍:
  不可变数据是指一旦创建就不能被修改的数据,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估。Immutable JS 提供一个惰性 Sequence,允许高效的队列方法链,类似 map 和 filter ,不用创建中间代表。Immutable.js 提供持久化的列表、堆栈、Map, 以及 OrderedMap 等,最大限度地减少需要复制或缓存数据。

Demo:

<script src="immutable.min.js"></script><script>
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50</script>

更多信息和探讨请移步,这里不多做介绍: facebook immutable.js 意义何在,使用场景?






Popmotion.js – 小巧,灵活的 JavaScript 运动引擎

github: https://github.com/Popmotion/...

官方网站: https://popmotion.io/

star:3k+

Install:

npm install --save popmotion
import { tween } from 'popmotion';

大小:12 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址: http://codepen.io/popmotion/p...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







Dynamics.js - 创建逼真的物理动画的 JS 库

github: https://github.com/michaelvil...

官方网站: http://dynamicsjs.com/

star:6k+

Install:

npm: npm install dynamics.js
bower: bower install dynamics.js

大小:20 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址: http://dynamicsjs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Rainyday.js – 使用 JavaScript 实现雨滴效果

github: https://github.com/maroslaw/r...

官方网站: http://maroslaw.github.io/rai...

star:5k+

Install:

在github的dist目录下载rainyday.min.js

大小:10 KB

功能介绍:
 Rainyday.js 背后的想法是创建一个 JavaScript 库,利用 HTML5 Canvas 渲染一个雨滴落在玻璃表面的动画。Rainyday.js 有功能可扩展的 API,例如碰撞检测和易于扩展自己的不同的动画组件的实现。它是一个使用 HTML5 特性纯 JavaScript 库,支持大部分现代浏览器。

Demo:
效果预览地址: http://maroslaw.github.io/rai...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Swiper – 经典的移动触摸滑块轮播插件

github: https://github.com/nolimits4w...

官方网站: http://idangero.us/swiper/https://github.com/daniel-lun...

官方网站: http://daniel-lundin.github.i...

star:5k+

Install:

bower:bower install snabbt.js
npm:npm install snabbt.js
CDNs:
https://cdnjs.com/libraries/snabbt.js
http://www.jsdelivr.com/#!snabbt

大小:16 KB

功能介绍:
 Snabbt.js 是一个简约的 JavaScript 动画库。它会平移,旋转,缩放,倾斜和调整你的元素。通过矩阵乘法运算,变换等可以任何你想要的方式进行组合。最终的结果通过 CSS3 变换矩阵设置。

Demo:
效果预览地址: http://daniel-lundin.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






imagesLoaded – 检测网页中的图片是否加载完成

github: https://github.com/desandro/i...

官方网站: http://imagesloaded.desandro....

star:6k+

Install:

Bower: bower install imagesloaded --save
npm: npm install imagesloaded
CDNs:<script src="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.min.js"></script><script src="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.js"></script>

大小:5 KB

功能介绍:
 imagesLoaded 是一个用于来检测网页中的图片是否载入完成的 JavaScript 工具库。支持回调的获取图片加载的进度,还可以绑定自定义事件。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址: http://codepen.io/desandro/fu...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Fort.js – 时尚、现代的表单填写进度提示效果

github: https://github.com/idriskhenc...

官方网站: https://github.com/idriskhenc...

star:800+

Install:

CDN:
css:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.css
js:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.js

大小:6 KB

功能介绍:
  Fort.js 是一款用于时尚、现代的表单填写进度提示效果的 JavaScript 库,你需要做的就是添加表单,剩下的任务就交给 Fort.js 算法了,使用非常简单。提供了Default、Gradient、Sections 以及 Flash 四种效果,满足开发的各种场合需要。

Demo:
效果预览地址: http://idriskhenchil.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






MagicSuggest – Bootstrap 主题的多选组合框

github: https://github.com/nicolasbiz...

官方网站: http://nicolasbize.com/magics...

star:1k+

Install:

github自行进行下载

大小:21.8 KB

功能介绍:
  MagicSuggest 是专为 Bootstrap 主题开发的多选组合框。它支持自定义呈现,数据通过 Ajax 异步获取,使用组件自动过滤。它允许空间免费项目,也有动态加载固定的建议。

Demo:
效果预览地址: http://nicolasbize.com/magics...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Numeral.js – 格式化和操作数字的 JavaScript 库

github: https://github.com/adamwdrape...

官方网站: http://numeraljs.com/

star:4k+

Install:

npm: npm install numeral
CDNs:<script src="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js"></script>

大小:10 KB

功能介绍:
   Numeral.js 是一个用于格式化和操作数字的 JavaScript 库。数字可以格式化为货币,百分比,时间,甚至是小数,千位,和缩写格式,功能十分强大。支持包括中文在内的17种语言。

Demo:

var myNumeral = numeral(1000);

var value = myNumeral.value();
// 1000

var myNumeral2 = numeral('1,000');

var value2 = myNumeral2.value();
// 1000






Draggabilly – 轻松实现拖放功能(Drag & Drop)

github: https://github.com/desandro/d...

官方网站: http://draggabilly.desandro.c...

star:2k+

Install:

Bower: bower install draggabilly --save
npm: npm install draggabilly
CDNs:<script src="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.min.js"></script><script src="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.js"></script>

大小:5 KB

功能介绍:
 Draggabilly 是一个很小的 JavaScript 库,专注于拖放功能。只需要简单的设置参数就可以在你的网站用添加拖放功能。兼容 IE8+ 浏览器,支持多点触摸。可以灵活绑定事件,支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址: http://draggabilly.desandro.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Quill – 可以灵活自定义的开源的富文本编辑器

github: https://github.com/quilljs/qu...

官方网站: https://quilljs.com

star:12k+

Install:

npm: npm install quill
CDNs:<!-- Main Quill library --><script src="//cdn.quilljs.com/1.0.0/quill.js" type="text/javascript"></script><script src="//cdn.quilljs.com/1.0.0/quill.min.js" type="text/javascript"></script><!-- Theme included stylesheets --><link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet"><link href="//cdn.quilljs.com/1.0.0/quill.bubble.css" rel="stylesheet"><!-- Core build with no theme, formatting, non-essential modules --><link href="//cdn.quilljs.com/1.0.0/quill.core.css" rel="stylesheet"><script src="//cdn.quilljs.com/1.0.0/quill.core.js" type="text/javascript"></script>

大小:需求不同,大小不同

功能介绍:
  Quill 的建立是为了解决现有的所见即所得(WYSIWYG)的编辑器本身就是所见即所得(指不能再扩张)的问题。如果编辑器不正是你想要的方式,这是很难或不可能对其进行自定义以满足您的需求。

  Quill 旨在通过把自身组织成模块,并提供了强大的 API 来构建额外的模块来解决这个问题。它也并没有规定你用样式来定义编辑器皮肤。Quill 还提供了所有你希望富文本编辑器说用于的功能,包括轻量级封装,众多的格式化选项,以及广泛的跨平台支持。

Demo:
效果预览地址: https://quilljs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






basket.js – 基于 LocalStorage 的资源加载器

github: https://github.com/addyosmani...

官方网站: https://addyosmani.com/basket...

star:2k+

Install:

Bower: bower install basket.js --save
npm: npm install basket.js

大小:4 KB

功能介绍:
 basket.js是一款基于 LocalStorage 的资源加载器,可以用来缓存 script 和 css, 手机端使用速度快于浏览器直接缓存。
 
Demo:
效果预览地址: https://addyosmani.com/basket...
更多示例请查看官方文档






scrollReveal.js – 使元素以非常酷帅的方式进入画布 (Viewpoint)

github: https://github.com/jlmakes/sc...

官方网站: https://scrollrevealjs.org/https://github.com/moment/mom...

官方网站: http://momentjs.com/

star:30k+

Install:

bower install moment --save # bower
npm install moment --save   # npm
yarn add moment             # Yarn
Install-Package Moment.js   # NuGet
spm install moment --save   # spm
meteor add momentjs:moment  # meteor

大小:16.6 KB

功能介绍:
  moment.js是一个轻量级的JavaScript库日期解析、验证操作,格式化日期的库。

Demo:
效果预览地址: http://momentjs.com/

Demo效果预览:
这是一个GIF动图,不信,你看第一行的日期,时间在走。






infinite-scroll – 一款滚动加载按需加载的轻量级插件

github: https://github.com/infinite-s...

官方网站: http://www.infinite-scroll.co...

star:4k+

Install:

github自行下载

大小:20 KB

功能介绍:
 infinite-scroll是一款滚动加载,滚动到最下到自动加载的轻量级JavaScript插件,简单实用,按需加载提高用户体验,非常适合移动端使用,配合上面的图片懒加载如虎添翼。

Demo:
效果预览地址: http://www.dazeddigital.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






欢迎大家按照格式补充,持续更新,有什么好用的轮子赶紧滚起来吧!

推荐有福利,送1024论坛邀请码,嘿嘿嘿。


揭秘babel的魔法之class魔法处理

$
0
0

2017年,很多人已经开始接触ES6环境,并且早已经用在了生产当中。我们知道ES6在大部分浏览器还是跑不通的,因此我们使用了伟大的Babel来进行编译。很多人可能没有关心过,经过Babel编译之后,我们华丽的ES6代码究竟变成了什么样子?

这篇文章,针对Babel对ES6里面“类class”的编译进行分析,你可以在线 测试编译结果,毕竟纸上得来终觉浅,自己动手,才能真正体会其中的奥秘。

另外,如果你还不明白JS中原型链等OOP相关知识,建议出门左转找到经典的《JS高级程序设计》来补课;如果你对JS中,通过原型链来实现继承一直云里雾里,安利一下我的同事,前端著名网红 颜海镜大大早在2014年的文章

为什么使用选择Babel

Babel:The compiler for writing next generation JavaScript;
我们知道,现在大部分浏览器或者类似NodeJS的javascript引擎还不能直接支持ES6语法。但这并不构成障碍,比如Babel的出现,使得我们在生产环境中书写ES6代码成为了现实,它工作原理是编译ES6的新特性为老版本的ES5,从而得到宿主环境的支持。

Class例子

在这篇文章中,我会讲解Babel如何处理ES6新特性:Class,这其实是一系列语法糖的实现。

Old school方式实现继承

在探究ES6之前,我们先来回顾一下ES5环境下,我们如何实现类的继承:

// Person是一个构造器
function Person(name) {
    this.type = 'Person';
    this.name = name;
}

// 我们可以通过prototype的方式来加一条实例方法
Person.prototype.hello = function() {
    console.log('hello ' + this.name);
}

// 对于私有属性(Static method),我们当然不能放在原型链上了。我们可以直接放在构造函数上面
Person.fn = function() {
    console.log('static');
};

我们可以这么应用:

var julien = new Person('julien');
var darul = new Person('darul');
julien.hello(); // 'hello julien'
darul.hello(); // 'hello darul'
Person.fn(); // 'static'

// 这样会报错,因为fn是一个私有属性
julien.fn(); //Uncaught TypeError: julien.fn is not a function

New school方式(ES6)实现继承

在ES6环境下,我们当然迫不及待地试一试Class:

class Person {
    constructor(name) {
        this.name = name;
        this.type="person"
    }
    hello() {
        console.log('hello ' + this.name);
    }
    static fn() {
        console.log('static');
    };
}

这样写起来当然很cool,但是经过Babel编译,我们的代码是什么样呢?

Babel transformation

我们一步一步来看,

Step1: 定义
我们从最简单开始,试试不加任何方法和属性的情况下,

Class Person{}

被编译为:

function _classCallCheck(instance, Constructor) {
    // 检查是否成功创建了一个对象
    if (!(instance instanceof Constructor)) {  
        throw new TypeError("Cannot call a class as a function"); 
    } 
}

var Person = function Person() {
    _classCallCheck(this, Person);
};

你可能会一头雾水,_classCallCheck是什么?其实很简单,它是为了保证调用的安全性。
比如我们这么调用:

// ok
new p = new Person();

是没有问题的,但是直接调用:


// Uncaught TypeError: Cannot call a class as a function
Person();

就会报错,这就是_classCallCheck所起的作用。具体原理自己看代码就好了,很好理解。

我们发现,Class关键字会被编译成构造函数,于是我们便可以通过new来实现实例的生成。

Step2:Constructor探秘
我们这次尝试加入constructor,再来看看编译结果:

class Person() {
    constructor(name) {  
        this.name = name;
        this.type = 'person'
    }
}

编译结果:

var Person = function Person(name) {
    _classCallCheck(this, Person);
    this.type = 'person';
    this.name = name;
};

看上去棒极了,我们继续探索。

Step3:增加方法
我们尝试给Person类添加一个方法:hello:

class Person {
    constructor(name) {
        this.name = name;
        this.type = 'person'
    }

    hello() {
        console.log('hello ' + this.name);
    }
}

编译结果(已做适当省略):

// 如上,已经解释过
function _classCallCheck.... 

// MAIN FUNCTION
var _createClass = (function () { 
    function defineProperties(target, props) { 
        for (var i = 0; i < props.length; i++) { 
            var descriptor = props[i]; 
            descriptor.enumerable = descriptor.enumerable || false; 
            descriptor.configurable = true; 
            if ('value' in descriptor) 
            descriptor.writable = true; 
            Object.defineProperty(target, descriptor.key, descriptor); 
        } 
    } 
    return function (Constructor, protoProps, staticProps) { 
        if (protoProps) 
            defineProperties(Constructor.prototype, protoProps); 
        if (staticProps) 
            defineProperties(Constructor, staticProps); 
        return Constructor; 
    }; 
})();

var Person = (function () {
    function Person(name) {
        _classCallCheck(this, Person);

        this.name = name;
    }

    _createClass(Person, [{
        key: 'hello',
        value: function hello() {
            console.log('hello ' + this.name);
        }
    }]);

    return Person;
})();

Oh...no,看上去有很多需要消化!不要急,我尝试先把他精简一下,并加上注释,你就会明白核心思路:

var _createClass = (function () {   
    function defineProperties(target, props) { 
        // 对于每一个定义的属性props,都要完全拷贝它的descriptor,并扩展到target上
    }  
    return defineProperties(Constructor.prototype, protoProps);    
})();

var Person = (function () {
    function Person(name) { // 同之前... }

    _createClass(Person, [{
        key: 'hello',
        value: function hello() {
            console.log('hello ' + this.name);
        }
    }]);

    return Person;
})();

如果你不明白defineProperty方法, 请参考这里

现在,我们知道我们添加的方法:

hello() {
    console.log('hello ' + this.name);
}

被编译为:

_createClass(
    Person, [{
    key: 'hello',
    value: function hello() {
        console.log('hello ' + this.name);
    }
}]);

而_createClass接受2个-3个参数,分别表示:

参数1 => 我们要扩展属性的目标对象,这里其实就是我们的Person
参数2 => 需要在目标对象原型链上添加的属性,这是一个数组
参数3 => 需要在目标对象上添加的属性,这是一个数组

这样,Babel的魔法就一步一步被揭穿了。

总结

希望这篇文章能够让你了解到Babel是如何初步把我们ES6 Class语法编译成ES5的。下一篇文章我会继续介绍Babel如何处理子类的Super(), 并会通过一段函数桥梁,使得ES5环境下也能够继承ES6定义的Class。


Web性能优化

$
0
0

1 Web性能优化

Web网站的性能细线在几个方面:

  • 网站首页加载速度

  • 动画的流畅度

通过分析浏览器的渲染原理、资源对渲染的影响,得出优化网站性能的办法。

2 查看性能的工具

Chrome的 Timeline面板录制网页加载的过程,分析记录浏览器渲染过程中每个过程的耗时。

2.1 录制时注意事项

  1. 禁用浏览器缓存: Network Tab下的 disable cache

  2. 关闭Chrome扩展或者启用隐身模式

  3. 根据使用场景,模拟真实的网络加载情况: Network Tab下的 throttling下拉按钮

2.2 Timeline工具的各个组成

  • Main Thread中可以看到页面渲染的整个过程及耗时

图片描述

3 浏览器渲染原理

图片描述

3.1 DOM树构建

DOM树的构建过程

  1. 根据HTML文档的内容,根据标签进行分词 Token

  2. 根据 Token生产对应的节点 Node

  3. 将节点根据嵌套关系组合为一棵对象节点树 DOM

浏览器解析文档对象模型 DOM增量进行的,无需等待整个HTML文档加载完毕,便可以开始解析 DOM

CSSOM解析会阻塞 HTML Parser;JavaScript脚本文件 执行会阻塞HTML解析; CSS、JavaScript、Images和Font等静态资源的异步加载的,渲染页面与CSS解析与JavaScript执行会有相互的依赖

图片描述
图片描述

3.2 CSSOM树的构建

CSSOM的解析依赖于 选择器,选择器的匹配是从内到外的。所以选择器嵌套层次越深,匹配的时间会越长。

CSSOM只解析可视部分 body标签中的内容,将所有匹配的元素共同构建一个 CSSOM树, 从根节点一次向下,所有节点的属性向下继承

图片描述

3.3 RenderTree树的构建

利用DOM和CSSOM组合构建生成RenderTree,对应 Recaculate Style

RenderTree中包含所有渲染网页必须的节点

无需渲染的节点不会被添加到RenderTree中,如 headdisplay:none;的节点

visibility: hidden;的节点会添加到RenderTree中

图片描述

3.4 Layout

Layout利用渲染树的信息,计算渲染树中所有节点在页面上的 位置和大小

类似绘画中各个元素位置摆放及尺寸规划

会引起页面重新Layout的操作: 所有改变节点位置和大小的操作

  • 屏幕旋转

  • 浏览器视窗改变

  • 与大小、位置相关的CSS属性

  • 增加与删除DOM元素

Layout操作比较耗时,对于动画中频繁引起Layout的操作(元素位置移动), 最好使用transform代替,可以使用GPU进行动画处理(将Layout重绘在GPU完成)

图片描述

viewport

如果页面 body元素设置的宽度为 100%,并且根元素 html没有明确设置宽度绝对值, 此时 body元素的宽度等于 viewport的宽度 vw

  • 使用 meta标签可以设置浏览器 viewport的尺寸。 <meta name="viewport" content="width=device-width">

  • device-width为浏览器的理想视口(屏幕的物理分辨率)

  • 在移动端,如果不设置 device-width,默认 viewport宽度为980px, 导致文字很小,需要放大

viewport相当于可视内容布局的容器

3.5 Paint

填充Layout中的具体内容和样式,将Layout生成的区域填充为最终显示在屏幕上的像素

3.6 总结

  1. 浏览器通过 GET请求获取网页HTML,同时将增量解析HTML文档,生成 DOM

  2. 解析 DOM节点树时,对于需要加载的资源 全部执行异步加载,但是 CSS的解析、 JavaScript的执行与 font文件的下载会阻塞HTML Parser

  3. 局部 DOM树与 CSSOM树构建完成后, 立即组装 RenderTree进行渲染

图片描述

4 资源对渲染的影响

页面中加载的资源主要包括: cssjs脚本文件和 font字体与 images静态资源,不同资源类型对渲染的影响不同。

4.1 浏览器渲染页面的时机

增量解析解析 DOM树,并且完成相应 CSSOM解析后(RenderTree依赖于 DOM树, CSSOM树),开始直接渲染页面。

4.2 CSS加载会阻塞初次渲染

图片描述

4.3 非关键资源

对于首页无关的样式,需要使用适当的方式避免其阻塞初次渲染:

  • document.write()会阻塞页面初次渲染

  • 使用 media=print媒体查询,虽然加载样式表,但只针对打印时才应用该样式,不会阻塞初次渲染。

  • 通过 DOMAPI引入CSS,可以避免阻塞。

  • CSS中 <link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

图片描述

图片描述

图片描述

图片描述

4.4 JS文件

图片描述

  • 输出:先输出 Hello,10s之后再输出 World。JS脚本 执行会阻塞 HTML Parser,但是 HTML Parser是增量解析的, 并且CSS样式的解析会阻塞JS脚本执行,当解析完 Hello时,生成对应 DOM节点,并且完成其 CSSOM,直接开始渲染 Hello节点。

  • 脚本执行完成后再解析后续的 World

JS脚本执行会阻塞HTML Parser;

CSS解析会阻塞JS脚本执行:js可能会读、写CSSOM

虽然JS会阻塞HTML Parser解析; 但是浏览器的资源异步加载机制 Preload会异步加载 head标签内的资源

图片描述

图片描述

图片描述

4.5 非关键JS资源解析阻塞的优化方案

  • 将JS资源文件放在文档底部,延迟JS的执行(但是存在必须解析完HTML才能加载JS资源,相较于 head标签中加载会慢)

  • 使用 defer延迟脚本执行: scipt标签的 defer属性,脚本会在HTML文档解析完毕后再开始执行; defer的脚本在执行时严格按照HTML文档中出现的顺序执行---优势可以提早加载JS资源,但是解析完HTML再执行

  • 使用 async异步执行脚本:

    • script标签有 async属性时,脚本执行不会阻塞HTML Parser,只要脚本加载完毕便开始执行

    • async的脚本,不会严格按照在HTML文档中的顺序执行

    • async适用于无依赖的外部独立资源(注意不要错误操作状态)

图片描述
图片描述

4.6 font字体文件

  • font字体文件会阻塞内容渲染
    图片描述

4.7 图片资源

图片资源的加载不会阻塞渲染,但是最好在HTML标签中设置图片的高度和宽度,可以在 Layout时留出图片渲染的空间,避免页面的抖动

5 优化关键渲染路径

优化目标是将下列三个指标压缩到最低:

  • 关键资源数---初次渲染时依赖的资源

  • 关键资源的体积最小---压缩文件或图片

  • 关键资源网络来回数---网络传输资源消耗很多时间

图片描述
图片描述
图片描述
图片描述

6 其余优化过程

  • HTTP2可以在传输HTML页面后向客户端推送页面内包含的资源

  • 减少资源的大小:压缩

  • 减少请求的来回时间

图片描述
图片描述

微豆 - Vue 2.0 实现豆瓣 Web App 教程

$
0
0

微豆 Vdo

一个使用 Vue.js 与 Material Design 重构 豆瓣的项目。

项目网站 http://vdo.ralfz.com/

GitHub https://github.com/RalfZhang/Vdo

gif

快速使用

# 克隆项目到本地
git clone https://github.com/RalfZhang/Vdo.git

# 安装依赖
npm install

# 在 localhost:8080 启动项目
npm run dev

教程

安装 vue-cli 脚手架

运行如下命令,即可创建一个名为 my-project 的 vue 项目,并且通过本地 8080 端口启动服务

npm install -g vue-cli
vue init webpack my-project
cd my-project
npm install
npm run dev

在运行 vue init webpack my-project后,会依次要求输入以下配置内容

  • 项目名称

  • 项目描述

  • 作者

  • 选择 Vue 构建:运行+编译 或 仅运行时

  • 是否安装 vue-loader

  • 是否使用 ESLint

    • 如果是,请选择模式:标准模式、AirBNB 模式、自定义

  • 是否使用 Karma + Mocha 的单元测试

  • 是否使用 Nightwatch e2e 测试

图片描述

安装完成后,即可看到以下文件结构:

.
|-- build                            // 项目构建相关代码
|   |-- build.js                     // 生产环境构建代码
|   |-- check-version.js             // 检查 node、npm 等版本
|   |-- dev-client.js                // 热重载相关
|   |-- dev-server.js                // 构建本地服务器
|   |-- utils.js                     // 构建工具相关
|   |-- webpack.base.conf.js         // webpack 基础配置(出入口和 loader)
|   |-- webpack.dev.conf.js          // webpack 开发环境配置
|   |-- webpack.prod.conf.js         // webpack 生产环境配置
|-- config                           // 项目开发环境配置
|   |-- dev.env.js                   // 开发环境变量
|   |-- index.js                     // 项目一些配置变量(开发环境接口转发将在此配置)
|   |-- prod.env.js                  // 生产环境变量
|   |-- test.env.js                  // 测试环境变量
|-- src                              // 源码目录
|   |-- components                   // vue 公共组件
|   |-- store                        // vuex 的状态管理
|   |-- App.vue                      // 页面入口文件
|   |-- main.js                      // 程序入口文件,加载各种公共组件
|-- static                           // 静态文件,比如一些图片,json数据等
|-- test                             // 自动化测试相关文件
|-- .babelrc                         // ES6语法编译配置
|-- .editorconfig                    // 定义代码格式
|-- .eslintignore                    // ESLint 检查忽略的文件
|-- .eslistrc.js                     // ESLint 文件,如需更改规则则在此文件添加
|-- .gitignore                       // git 上传需要忽略的文件
|-- README.md                        // 项目说明
|-- index.html                       // 入口页面
|-- package.json                     // 项目基本信息
.

ESLint 配置

ESLint 配置在根目录的 .eslintrc.js里。
正常情况下,ESLint 报错是因为你的代码不符合现有的 ESLint 规范。
如果你的情况实在不想被 ESLint 报错,我举出两个解决方案,来处理 ESLint 报错问题。

注:本例使用 AirBNB ESLint 规则。
例:通过 npm run dev启动服务,打开 ./src/main.js,添加一句 console.log('abc'),结果如下:

import Vue from 'vue';
import App from './App';
import store from './vuex/store';
/* import router from './router';*/

// 添加此句
console.log('abc')

/* eslint-disable no-new */
new Vue({
  el: '#app',
  /* router,*/
  template: '<App/>',
  components: { App },
  store,
});

注:为做演示,句末未添加分号。

保存 main.js文件后,页面与终端均提示如下错误:

 ERROR  Failed to compile with 1 errors 
 error  in ./src/main.js⚠  http://eslint.org/docs/rules/no-console  Unexpected console statement
  C:\Users\Ralf\Documents\code\ralfz\vue\test\vue02\src\main.js:8:1
  console.log('abc')
   ^✘  http://eslint.org/docs/rules/semi        Missing semicolon
  C:\Users\Ralf\Documents\code\ralfz\vue\test\vue02\src\main.js:8:19
  console.log('abc')
                     ^
✘ 2 problems (1 error, 1 warning)
Errors:
  1  http://eslint.org/docs/rules/semi
Warnings:
  1  http://eslint.org/docs/rules/no-console
 @ multi ./build/dev-client ./src/main.js

以上输出表明出现两个问题:

  1. 警告:不允许 console 语句。

  2. 错误:句末未加分号。

解决问题 1

  • .eslintrc.js文件中的 rules键名下添加 'no-console': 'off',,即关闭 console 警告。

解决问题 2

  • 你可以选择继续在 .eslintrc.js文件中添加关闭句末分号判定的规则。

  • 或者,也可以把 package.json文件中的 script下的 lint命令改为
    "lint": "eslint --fix *.js *.vue src/* test/unit/specs/* test/e2e/specs/*"

即自动修复。值得注意的是,自动修复不能解决所有问题,有时也不甚完美,可以多试几次体会下 fix 的效果。

做完更改后,重新运行 npm run dev即可看到无问题报告,并且 console语句后已经自动加上了分号。

静态页面开发

此时,浏览器应该已经打开了 localhost:8080 页面。

在此情况下,请尝试更改 /src/App.vue/src/components/Hello.vue文件中 <template>标签内的内容,保存后即可立即看到浏览器页面已自动更新了你做出的改动。

接下来,你需要去阅读并学习 Vue.js 教程页面,务必熟悉 基础部分的内容,掌握 组件章节。

熟悉之后,便可以完成基础的静态页面(或者说是组件)设计工作。

本项目使用了基于 Vue 2.0 和 Material Desigin 的 UI 组件库 Muse-UI

提示: ./src/components文件夹多用于保存公用组件。至于页面组件,推荐在新建 ./src/view文件夹后存放于此。

vue-router 2 使用

当一个个静态组件完成后,需要按照路由组织这些组件文件。

请前往 vue-router 2 介绍阅读 基础部分教程,并可以边阅读边配置路由。

路由文件是 ./src/router.index.js

本项目中使用了 HTML5 History 模式,路由配置比较简单,可以参考。

API 请求转发配置

至此,你应该已经完成了所有的静态页面的工作,接下来我们准备搭建请求,为后面的 xhr 请求做好准备。

  1. 打开 http://api.douban.com/v2/movie/in_theaters查看接口数据,留意此地址。

  2. ./config/index.js中的 proxyTable配置代理:

    proxyTable: {'/api': {
            target: 'http://api.douban.com/v2',
            changeOrigin: true,
            pathRewrite: {'^/api': ''
            }
        }
    }
  3. 重新启动 npm run dev,打开 localhost:8080/api/movie/in_theaters查看结果是否与直接请求豆瓣 API 相同。

  4. 本应该使用了以下 API:

    • /v2/movie/search?q={text}电影搜索api;

    • /v2/movie/in_theaters正在上映的电影;

    • /v2/movie/coming_soon即将上映的电影;

    • /v2/movie/subject/:id单个电影条目信息。

更多请参考 豆瓣电影 API文档。

这样我们就可以在应用中调用 /api/movie/in_theaters来访问 http://api.douban.com/v2/movie/in_theaters,从而解决跨域的问题。

使用 axios

axios 库使用起来相当简单。

你可以在单个组件中尝试引入并调用:

import axios from 'axios';
axios.get('/v2/movie/in_theaters', { 'city': '广州' })
    .then((result) => {
        console.log(result);
    });

这里,可以用返回的 result去更新 data(){ }return的数据。

更多 axios 用法请参考 文档

使用 Vuex 并分离代码

为了试代码更加结构化,我们应当将数据请求和视图分离。

这一节中,我们有两个任务要做:

  1. 分离数据请求层逻辑。

  2. 使用 Vuex 管理状态。

将二者放到同一节中主要是因为二者再同一目录下,我们来查看 ./store文件夹的结构:

.
|-- store                          // 数据处理根目录
|   |-- movies                     // 单个电影模块文件夹
|   |   |-- api.js                 // 电影模块对外开放的接口
|   |   |-- module.js              // Vuex 模块
|   |   |-- type.js                // Vuex 操作 key
|   |-- base.js                    // 基础方法
|   |-- store.js                   // Vuex 入口
.

针对第一个任务:

  • base.js存放封装的基础请求函数

  • **/api.js存放该模块下公开的请求函数

针对第二个任务,我们需要先了解 Vuex。

请查看 Vuex 文档,了解其 核心概念

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

其实在我看来,Vuex 相当于某种意义上设置了读写权限的全局变量,将数据保存保存到该“全局变量”下,并通过一定的方法去读写数据。(希望这能帮助你理解 Vuex)

为了方便模块化管理:

  • 我将 store.js作为入口文件,去挂载各个模块;

  • /movies/文件夹下为电影相关的模块;

  • /movies/moudule.js为电影模块的主要 Vuex 文件;

  • /movies/type.js使用常量替代 Mutation 事件类型的实现。

到此便完成了所有开发上的基础问题。

发布

  1. 运行 npm run build,即可在生成的 /dist文件夹下看到所有文件。

  2. 将文件复制到你的服务器上某个目录(我的是 /var/www/Vdo/dist),按照下一节配置 Nginx 即可

提示:可以使用 scp命令将本地文件拷贝至服务器,例如 scp -P 20 -r dist user@host:/target/location

附:配置与开启 Nginx

注:以下以 CentOS 为例

  1. 安装 Nginx: yum install nginx

  2. 打开 /etc/nginx/conf.d/default.conf

  3. 替换全文为本项目 /doc/nginx.conf文件中的内容

  4. 运行 nginx

提示:

  1. 403 Forbidden错误可能是由于文件和文件夹权限引起的,请用 chmod把存放 index.html的所有路径上的文件夹权限设置为 755,并将 index.html文件权限设置成 644 即可。

  2. 更改 Nginx 配置文件后,可以使用 nginx -s reload命令刷新。

结语

至此,主体工作已经完成。

欢迎 Star 本项目。

https://github.com/RalfZhang/Vdo

感谢&参考

License

MIT

JavaScript 启动性能瓶颈分析与解决方案

$
0
0

JavaScript 启动性能瓶颈分析与解决方案翻译自 Addy Osmani 的 JavaScript Start-up Performance,从属于笔者的 Web 前端入门与工程实践。本文已获得原作者授权,为InfoQ中文站特供稿件,首发地址为 这里;如需转载,请与InfoQ中文站联系。随着现代 Web 技术的发展与用户交互复杂度的增加,我们的网站变得日益臃肿,也要求着我们不断地优化网站性能以保证友好的用户体验。本文作者则着眼于 JavaScript 启动阶段优化,首先以大量的数据分析阐述了语法分析、编译等步骤耗时占比过多是很多网站的性能瓶颈之一。然后作者提供了一系列用于在现代浏览器中进行性能评测的工具,还分别从开发者工程实践与 JavaScript 引擎内部实现的角度阐述了应当如何提高解析与编译速度。

在 Web 开发中,随着需求的增加与代码库的扩张,我们最终发布的 Web 页面也逐渐膨胀。不过这种膨胀远不止意味着占据更多的传输带宽,其还意味着用户浏览网页时可能更差劲的性能体验。浏览器在下载完某个页面依赖的脚本之后,其还需要经过语法分析、解释与运行这些步骤。而本文则会深入分析浏览器对于 JavaScript 的这些处理流程,挖掘出那些影响你应用启动时间的罪魁祸首,并且根据我个人的经验提出相对应的解决方案。回顾过去,我们还没有专门地考虑过如何去优化 JavaScript 解析/编译这些步骤;我们预想中的是解析器在发现 <script>标签后会瞬时完成解析操作,不过这很明显是痴人说梦。下图是对于 V8 引擎工作原理的概述:

下面我们深入其中的关键步骤进行分析。

到底是什么拖慢了我们应用的启动时间?

在启动阶段,语法分析,编译与脚本执行占据了 JavaScript 引擎运行的绝大部分时间。换言之,这些过程造成的延迟会真实地反应到用户可交互时延上;譬如用户已经看到了某个按钮,但是要好几秒之后才能真正地去点击操作,这一点会大大影响用户体验。

上图是我们使用 Chrome Canary 内置的 V8 RunTime Call Stats 对于某个网站的分析结果;需要注意的是桌面浏览器中语法解析与编译占用的时间还是蛮长的,而在移动端中占用的时间则更长。实际上,对于 Facebook, Wikipedia, Reddit 这些大型网站中语法解析与编译所占的时间也不容忽视:

上图中的粉色区域表示花费在 V8 与 Blink's C++ 中的时间,而橙色和黄色分别表示语法解析与编译的时间占比。Facebook 的 Sebastian Markbage 与 Google 的 Rob Wormald 也都在 Twitter 发文表示过 JavaScript 的语法解析时间过长已经成为了不可忽视的问题,后者还表示这也是 Angular 启动时主要的消耗之一。

随着移动端浪潮的涌来,我们不得不面对一个残酷的事实:移动端对于相同包体的解析与编译过程要花费相当于桌面浏览器2~5倍的时间。当然,对于高配的 iPhone 或者 Pixel 这样的手机相较于 Moto G4 这样的中配手机表现会好很多;这一点提醒我们在测试的时候不能仅用身边那些高配的手机,而应该中高低配兼顾:

上图是部分桌面浏览器与移动端浏览器对于 1MB 的 JavaScript 包体进行解析的时间对比,显而易见的可以发现不同配置的移动端手机之间的巨大差异。当我们应用包体已经非常巨大的时候,使用一些现代的打包技巧,譬如代码分割,TreeShaking,Service Workder 缓存等等会对启动时间有很大的影响。另一个角度来看,即使是小模块,你代码写的很糟或者使用了很糟的依赖库都会导致你的主线程花费大量的时间在编译或者冗余的函数调用中。我们必须要清醒地认识到全面评测以挖掘出真正性能瓶颈的重要性。

JavaScript 语法解析与编译是否成为了大部分网站的瓶颈?

我曾不止一次听到有人说,我又不是 Facebook,你说的 JavaScript 语法解析与编译到
底会对其他网站造成什么样的影响呢?对于这个问题我也很好奇,于是我花费了两个月的时间对于超过 6000 个网站进行分析;这些网站囊括了 React,Angular,Ember,Vue 这些流行的框架或者库。大部分的测试是基于 WebPageTest 进行的,因此你可以很方便地重现这些测试结果。 光纤接入的桌面浏览器大概需要 8 秒的时间才能允许用户交互,而 3G 环境下的 Moto G4 大概需要 16 秒 才能允许用户交互。

大部分应用在桌面浏览器中会耗费约 4 秒的时间进行 JavaScript 启动阶段(语法解析、编译、执行)

而在移动端浏览器中,大概要花费额外 36% 的时间来进行语法解析:

另外,统计显示并不是所有的网站都甩给用户一个庞大的 JS 包体,用户下载的经过 Gzip 压缩的平均包体大小是 410KB,这一点与 HTTPArchive 之前发布的 420KB 的数据基本一致。不过最差劲的网站则是直接甩了 10MB 的脚本给用户,简直可怕。

通过上面的统计我们可以发现,包体体积固然重要,但是其并非唯一因素,语法解析与编译的耗时也不一定随着包体体积的增长而线性增长。总体而言小的 JavaScript 包体是会加载地更快(忽略浏览器、设备与网络连接的差异),但是同样 200KB 的大小,不同开发者的包体在语法解析、编译上的时间却是天差地别,不可同日而语。

现代 JavaScript 语法解析 & 编译性能评测

Chrome DevTools

打开 Timeline( Performance panel ) > Bottom-Up/Call Tree/Event Log 就会显示出当前网站在语法解析/编译上的时间占比。如果你希望得到更完整的信息,那么可以打开 V8 的 Runtime Call Stats。在 Canary 中,其位于 Timeline 的 Experims > V8 Runtime Call Stats 下。

Chrome Tracing

打开 about:tracing 页面,Chrome 提供的底层的追踪工具允许我们使用 disabled-by-default-v8.runtime_stats来深度了解 V8 的时间消耗情况。V8 也提供了 详细的指南来介绍如何使用这个功能。

WebPageTest


WebPageTest 中 Processing Breakdown 页面在我们启用 Chrome > Capture Dev Tools Timeline 时会自动记录 V8 编译、EvaluateScript 以及 FunctionCall 的时间。我们同样可以通过指明 disabled-by-default-v8.runtime_stats的方式来启用 Runtime Call Stats。

更多使用说明参考我的 gist

User Timing

我们还可以使用 Nolan Lawson 推荐的 User Timing API来评估语法解析的时间。不过这种方式可能会受 V8 预解析过程的影响,我们可以借鉴 Nolan 在 optimize-js 评测中的方式,在脚本的尾部添加随机字符串来解决这个问题。我基于 Google Analytics 使用相似的方式来评估真实用户与设备访问网站时候的解析时间:

DeviceTiming

Etsy 的 DeviceTiming工具能够模拟某些受限环境来评估页面的语法解析与执行时间。其将本地脚本包裹在了某个仪表工具代码内从而使我们的页面能够模拟从不同的设备中访问。可以阅读 Daniel Espeset 的 Benchmarking JS Parsing and Execution on Mobile Devices一文来了解更详细的使用方式。

我们可以做些什么以降低 JavaScript 的解析时间?

  • 减少 JavaScript 包体体积。我们在上文中也提及,更小的包体往往意味着更少的解析工作量,也就能降低浏览器在解析与编译阶段的时间消耗。

  • 使用代码分割工具来按需传递代码与懒加载剩余模块。这可能是最佳的方式了,类似于 PRPL这样的模式鼓励基于路由的分组,目前被 Flipkart, Housing.com 与 Twitter 广泛使用。

  • Script streaming: 过去 V8 鼓励开发者使用 async/defer来基于 script streaming实现 10-20% 的性能提升。这个技术会允许 HTML 解析器将相应的脚本加载任务分配给专门的 script streaming 线程,从而避免阻塞文档解析。V8 推荐尽早加载较大的模块,毕竟我们只有一个 streamer 线程。

  • 评估我们依赖的解析消耗。我们应该尽可能地选择具有相同功能但是加载地更快的依赖,譬如使用 Preact 或者 Inferno 来代替 React,二者相较于 React 体积更小具有更少的语法解析与编译时间。Paul Lewis 在最近的 一篇文章中也讨论了框架启动的代价,与 Sebastian Markbage 的 说法不谋而合:最好地评测某个框架启动消耗的方式就是先渲染一个界面,然后删除,最后进行重新渲染。第一次渲染的过程会包含了分析与编译,通过对比就能发现该框架的启动消耗。

如果你的 JavaScript 框架支持 AOT(ahead-of-time)编译模式,那么能够有效地减少解析与编译的时间。Angular 应用就受益于这种模式:

现代浏览器是如何提高解析与编译速度的?

不用灰心,你并不是唯一纠结于如何提升启动时间的人,我们 V8 团队也一直在努力。我们发现之前的某个评测工具 Octane 是个不错的对于真实场景的模拟,它在微型框架与冷启动方面很符合真实的用户习惯。而基于这些工具,V8 团队在过去的工作中也实现了大约 25% 的启动性能提升:

本部分我们就会对过去几年中我们使用的提升语法解析与编译时间的技巧进行阐述。

代码缓存

Chrome 42 开始引入了所谓的 代码缓存的概念,为我们提供了一种存放编译后的代码副本的机制,从而当用户二次访问该页面时可以避免脚本抓取、解析与编译这些步骤。除以之外,我们还发现在重复访问的时候这种机制还能避免 40% 左右的编译时间,这里我会深入介绍一些内容:

  • 代码缓存会对于那些在 72 小时之内重复执行的脚本起作用。

  • 对于 Service Worker 中的脚本,代码缓存同样对 72 小时之内的脚本起作用。

  • 对于利用 Service Worker 缓存在 Cache Storage 中的脚本,代码缓存能在脚本首次执行的时候起作用。

总而言之,对于主动缓存的 JavaScript 代码,最多在第三次调用的时候其能够跳过语法分析与编译的步骤。我们可以通过 chrome://flags/#v8-cache-strategies-for-cache-storage来查看其中的差异,也可以设置  js-flags=profile-deserialization运行 Chrome 来查看代码是否加载自代码缓存。不过需要注意的是,代码缓存机制仅会缓存那些经过编译的代码,主要是指那些顶层的往往用于设置全局变量的代码。而对于类似于函数定义这样懒编译的代码并不会被缓存,不过 IIFE 同样被包含在了 V8 中,因此这些函数也是可以被缓存的。

Script Streaming

Script Streaming允许在后台线程中对异步脚本执行解析操作,可以对于页面加载时间有大概 10% 的提升。上文也提到过,这个机制同样会对同步脚本起作用。

这个特性倒是第一次提及,因此 V8 会允许所有的脚本,即使阻塞型的 <script src=''>脚本也可以由后台线程进行解析。不过缺陷就是目前仅有一个 streaming 后台线程存在,因此我们建议首先解析大的、关键性的脚本。在实践中,我们建议将 <script defer>添加到 <head>块内,这样浏览器引擎就能够尽早地发现需要解析的脚本,然后将其分配给后台线程进行处理。我们也可以查看 DevTools Timeline 来确定脚本是否被后台解析,特别是当你存在某个关键性脚本需要解析的时候,更需要确定该脚本是由 streaming 线程解析的。

语法解析 & 编译优化

我们同样致力于打造更轻量级、更快的解析器,目前 V8 主线程中最大的瓶颈在于所谓的非线性解析消耗。譬如我们有如下的代码片:

(function (global, module) { … })(this, function module() { my functions })

V8 并不知道我们编译主脚本的时候是否需要 module这个模块,因此我们会暂时放弃编译它。而当我们打算编译 module时,我们需要重分析所有的内部函数。这也就是所谓的 V8 解析时间非线性的原因,任何一个处于 N 层深度的函数都有可能被重新分析 N 次。V8 已经能够在首次编译的时候搜集所有内部函数的信息,因此在未来的编译过程中 V8 会忽略所有的内部函数。对于上面这种 module形式的函数会是很大的性能提升,建议阅读 The V8 Parser(s) — Design, Challenges, and Parsing JavaScript Better来获取更多内容。V8 同样在寻找合适的分流机制以保证启动时能在后台线程中执行 JavaScript 编译过程。

预编译 JavaScript?

每隔几年就有人提出引擎应该提供一些处理预编译脚本的机制,换言之,开发者可以使用构建工具或者其他服务端工具将脚本转化为字节码,然后浏览器直接运行这些字节码即可。从我个人观点来看,直接传送字节码意味着更大的包体,势必会增加加载时间;并且我们需要去对代码进行签名以保证能够安全运行。目前我们对于 V8 的定位是尽可能地避免上文所说的内部重分析以提高启动时间,而预编译则会带来额外的风险。不过我们欢迎大家一起来讨论这个问题,虽然 V8 目前专注于提升编译效率以及推广利用 Service Worker 缓存脚本代码来提升启动效率。我们在 BlinkOn7 上与 Facebook 以及 Akamai 也讨论过 预编译相关内容

Optimize JS 优化

类似于 V8 这样的 JavaScript 引擎在进行完整的解析之前会对脚本中的大部分函数进行预解析,这主要是考虑到大部分页面中包含的 JavaScript 函数并不会立刻被执行。

预编译能够通过只处理那些浏览器运行所需要的最小函数集合来提升启动时间,不过这种机制在 IIFE 面前却反而降低了效率。尽管引擎希望避免对这些函数进行预处理,但是远不如 optimize-js这样的库有作用。optimize-js 会在引擎之前对于脚本进行处理,对于那些立即执行的函数插入圆括号从而保证更快速地执行。这种预处理对于 Browserify, Webpack 生成包体这样包含了大量即刻执行的小模块起到了非常不错的优化效果。尽管这种小技巧并非 V8 所希望使用的,但是在当前阶段不得不引入相应的优化机制。

总结

启动阶段的性能至关重要,缓慢的解析、编译与执行时间可能成为你网页性能的瓶颈所在。我们应该评估页面在这个阶段的时间占比并且选择合适的方式来优化。我们也会继续致力于提升 V8 的启动性能,尽我所能!

延伸阅读

H5与Native交互之JSBridge技术

$
0
0

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过 UIWebView组件的 stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了 window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在 window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用 shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用 addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了 AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的 loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法 doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作

vue快速入门的三个小实例

$
0
0

1.前言

用vue做项目也有一段时间了,之前也是写过关于vue和webpack构建项目的相关文章,大家有兴趣可以去看下 webpack+vue项目实战(一,搭建运行环境和相关配置)(这个系列一共有5篇文章,这是第一篇,其它几篇文章链接就不贴了)。但是关于vue入门基础的文章,我还没有写过,那么今天就写vue入门的三个小实例,这三个小实例是我刚接触vue的时候的练手作品,难度从很简单到简单,都是入门级的。希望能帮到大家更好的学习和了解vue,也是让自己能够复习一下vue。如果发现文章写得有什么不好,写错了,或者有什么建议!欢迎大家指点迷津!

1.本篇文章使用的vue版本是 2.4.2,大家要注意版本问题
2.现在我也是假设您有基础的html,css,javascript的知识,也已经看过了 官网的基本介绍,对vue有了一个大概的认识了,了解了常用的vue指令(v-model,v-show,v-if,v-for,v-on,v-bind等)!如果刚接触前端的话,你看着文章可能会蒙圈,建议先学习基础,掌握了基础知识再来看!
3.下面的实例,建议大家边看文章边动手做!这样思路会非常清晰,不易混乱!也不会觉得文章长!如果只看文章,你可能未必会看完,因为文章我讲得比较细,比较长!

2.什么是vue

vue是现在很火的一个前端MVVM框架,它以数据驱动和组件化的思想构建,与angular和react并称前端三大框架。相比angular和react,vue更加轻巧、高性能、也很容易上手。大家也可以移步,看一下vue的介绍和核心功能 官网介绍。简单粗暴的理解就是:用vue开发的时候,就是操作数据,然后vue就会处理,以数据驱动去改变DOM(不知道有没有理解错,理解错了指点下)。
下面就是一个最简单的说明例子

代码如下

html

<div id="app"><p>{{ message }}</p><input v-model="message"></div>

js

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

栗子

相信也不难理解,就是 input绑定了 message这个值,然后在 input修改的时候, message就改了,由于双向绑定,同时页面的html( {{ message }})进行了修改!
好,下面进入例子学习!

3.选项卡

运行效果

clipboard.png

原理分析和实现

这个很简单,无非就是一个点击切换显示而已。但是大家也要实现。如果这个看明白了,再看下面两个!这个实例应该只是一个热身和熟悉的作用!

这个的步骤只有一步,原理也没什么。我直接在代码打注释,看了注释,大家就明白了!

完整代码

<!DOCTYPE html>
<html lang="en">
<head>

<meta charset="UTF-8"><title>Title</title>

</head>
<style>

body{
    font-family:"Microsoft YaHei";
}
#tab{
    width: 600px;
    margin: 0 auto;
}
.tab-tit{
    font-size: 0;
    width: 600px;
}
.tab-tit a{
    display: inline-block;
    height: 40px;
    line-height: 40px;
    font-size: 16px;
    width: 25%;
    text-align: center;
    background: #ccc;
    color: #333;
    text-decoration: none;
}
.tab-tit .cur{
    background: #09f;
    color: #fff;
}
.tab-con div{
    border: 1px solid #ccc;
    height: 400px;
    padding-top: 20px;
}

</style>
<body>
<div id="tab">

<div class="tab-tit"><!--点击设置curId的值  如果curId等于0,第一个a添加cur类名,如果curId等于1,第二个a添加cur类名,以此类推。添加了cur类名,a就会改变样式 @click,:class ,v-show这三个是vue常用的指令或添加事件的方式--><a href="javascript:;" @click="curId=0" :class="{'cur':curId===0}">html</a><a href="javascript:;" @click="curId=1" :class="{'cur':curId===1}">css</a><a href="javascript:;" @click="curId=2" :class="{'cur':curId===2}">javascript</a><a href="javascript:;" @click="curId=3" :class="{'cur':curId===3}">vue</a></div><div class="tab-con"><!--根据curId的值显示div,如果curId等于0,第一个div显示,其它三个div不显示。如果curId等于1,第二个div显示,其它三个div不显示。以此类推--><div v-show="curId===0">
        html<br/></div><div v-show="curId===1">
        css</div><div v-show="curId===2">
        javascript</div><div v-show="curId===3">
        vue</div></div>

</div>
</body>
<script src="vue.min.js"></script>
<script>

new Vue({
    el: '#tab',
    data: {
        curId: 0
    },
    computed: {},
    methods: {},
    mounted: function () {
    }
})

</script>
</html>

4.统计总价

运行效果

clipboard.png

原理分析和实现

首先,还是先把布局写好,和引入vue,准备vue实例,这个不多说,代码如下

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>
        .fl{
            float: left;
        }
        .fr{
            float: right;
        }
       blockquote, body, dd, div, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, img, input, li, ol, p, table, td, textarea, th, ul {
            margin: 0;
            padding: 0;
        }
       .clearfix{
          zoom: 1;
       }
        .clearfix:after {
            clear: both;
        }
        .clearfix:after {
            content: '.';
            display: block;
            overflow: hidden;
            visibility: hidden;
            font-size: 0;
            line-height: 0;
            width: 0;
            height: 0;
        }
        a{
            text-decoration: none;
            color: #333;
        }
        img{vertical-align: middle;}
        .page-shopping-cart {
            width: 1200px;
            margin: 50px auto;
            font-size: 14px;
            border: 1px solid #e3e3e3;
            border-top: 2px solid #317ee7; }
        .page-shopping-cart .cart-title {
            color: #317ee7;
            font-size: 16px;
            text-align: left;
            padding-left: 20px;
            line-height: 68px; }
        .page-shopping-cart .red-text {
            color: #e94826; }
        .page-shopping-cart .check-span {
            display: block;
            width: 24px;
            height: 20px;
            background: url("shopping_cart.png") no-repeat 0 0; }
        .page-shopping-cart .check-span.check-true {
            background: url("shopping_cart.png") no-repeat 0 -22px; }
        .page-shopping-cart .td-check {
            width: 70px; }
        .page-shopping-cart .td-product {
            width: 460px; }
        .page-shopping-cart .td-num, .page-shopping-cart .td-price, .page-shopping-cart .td-total {
            width: 160px; }
        .page-shopping-cart .td-do {
            width: 150px; }
        .page-shopping-cart .cart-product-title {
            text-align: center;
            height: 38px;
            line-height: 38px;
            padding: 0 20px;
            background: #f7f7f7;
            border-top: 1px solid #e3e3e3;
            border-bottom: 1px solid #e3e3e3; }
        .page-shopping-cart .cart-product-title .td-product {
            text-align: center;
            font-size: 14px; }
        .page-shopping-cart .cart-product-title .td-check {
            text-align: left; }
        .page-shopping-cart .cart-product-title .td-check .check-span {
            margin: 9px 6px 0 0; }
        .page-shopping-cart .cart-product {
            padding: 0 20px;
            text-align: center; }
        .page-shopping-cart .cart-product table {
            width: 100%;
            text-align: center;
            font-size: 14px; }
        .page-shopping-cart .cart-product table td {
            padding: 20px 0; }
        .page-shopping-cart .cart-product table tr {
            border-bottom: 1px dashed #e3e3e3; }
        .page-shopping-cart .cart-product table tr:last-child {
            border-bottom: none; }
        .page-shopping-cart .cart-product table .product-num {
            border: 1px solid #e3e3e3;
            display: inline-block;
            text-align: center; }
        .page-shopping-cart .cart-product table .product-num .num-do {
            width: 24px;
            height: 28px;
            display: block;
            background: #f7f7f7; }
        .page-shopping-cart .cart-product table .product-num .num-reduce span {
            background: url("shopping_cart.png") no-repeat -40px -22px;
            display: block;
            width: 6px;
            height: 2px;
            margin: 13px auto 0 auto; }
        .page-shopping-cart .cart-product table .product-num .num-add span {
            background: url("shopping_cart.png") no-repeat -60px -22px;
            display: block;
            width: 8px;
            height: 8px;
            margin: 10px auto 0 auto; }
        .page-shopping-cart .cart-product table .product-num .num-input {
            width: 42px;
            height: 28px;
            line-height: 28px;
            border: none;
            text-align: center; }
        .page-shopping-cart .cart-product table .td-product {
            text-align: left;
            font-size: 12px;
            line-height: 20px; }
        .page-shopping-cart .cart-product table .td-product img {
            border: 1px solid #e3e3e3;
            margin-right: 10px; }
        .page-shopping-cart .cart-product table .td-product .product-info {
            display: inline-block;
            vertical-align: middle; }
        .page-shopping-cart .cart-product table .td-do {
            font-size: 12px; }
        .page-shopping-cart .cart-product-info {
            height: 50px;
            line-height: 50px;
            background: #f7f7f7;
            padding-left: 20px; }
        .page-shopping-cart .cart-product-info .delect-product {
            color: #666; }
        .page-shopping-cart .cart-product-info .delect-product span {
            display: inline-block;
            vertical-align: top;
            margin: 18px 8px 0 0;
            width: 13px;
            height: 15px;
            background: url("shopping_cart.png") no-repeat -60px 0; }
        .page-shopping-cart .cart-product-info .product-total {
            font-size: 14px;
            color: #e94826; }
        .page-shopping-cart .cart-product-info .product-total span {
            font-size: 20px; }
        .page-shopping-cart .cart-product-info .check-num {
            color: #333; }
        .page-shopping-cart .cart-product-info .check-num span {
            color: #e94826; }
        .page-shopping-cart .cart-product-info .keep-shopping {
            color: #666;
            margin-left: 40px; }
        .page-shopping-cart .cart-product-info .keep-shopping span {
            display: inline-block;
            vertical-align: top;
            margin: 18px 8px 0 0;
            width: 15px;
            height: 15px;
            background: url("shopping_cart.png") no-repeat -40px 0; }
        .page-shopping-cart .cart-product-info .btn-buy {
            height: 50px;
            color: #fff;
            font-size: 20px;
            display: block;
            width: 110px;
            background: #ff7700;
            text-align: center;
            margin-left: 30px; }
        .page-shopping-cart .cart-worder {
            padding: 20px; }
        .page-shopping-cart .cart-worder .choose-worder {
            color: #fff;
            display: block;
            background: #39e;
            width: 140px;
            height: 40px;
            line-height: 40px;
            border-radius: 4px;
            text-align: center;
            margin-right: 20px; }
        .page-shopping-cart .cart-worder .choose-worder span {
            display: inline-block;
            vertical-align: top;
            margin: 9px 10px 0 0;
            width: 22px;
            height: 22px;
            background: url("shopping_cart.png") no-repeat -92px 0; }
        .page-shopping-cart .cart-worder .worker-info {
            color: #666; }
        .page-shopping-cart .cart-worder .worker-info img {
            border-radius: 100%;
            margin-right: 10px; }
        .page-shopping-cart .cart-worder .worker-info span {
            color: #000; }

        .choose-worker-box {
            width: 620px;
            background: #fff; }
        .choose-worker-box .box-title {
            height: 40px;
            line-height: 40px;
            background: #F7F7F7;
            text-align: center;
            position: relative;
            font-size: 14px; }
        .choose-worker-box .box-title a {
            display: block;
            position: absolute;
            top: 15px;
            right: 16px;
            width: 10px;
            height: 10px;
            background: url("shopping_cart.png") no-repeat -80px 0; }
        .choose-worker-box .box-title a:hover {
            background: url("shopping_cart.png") no-repeat -80px -22px; }
        .choose-worker-box .worker-list {
            padding-top: 30px;
            height: 134px;
            overflow-y: auto; }
        .choose-worker-box .worker-list li {
            float: left;
            width: 25%;
            text-align: center;
            margin-bottom: 30px; }
        .choose-worker-box .worker-list li p {
            margin-top: 8px; }
        .choose-worker-box .worker-list li.cur a {
            color: #f70; }
        .choose-worker-box .worker-list li.cur a img {
            border: 1px solid #f70; }
        .choose-worker-box .worker-list li a:hover {
            color: #f70; }
        .choose-worker-box .worker-list li a:hover img {
            border: 1px solid #f70; }
        .choose-worker-box .worker-list li img {
            border: 1px solid #fff;
            border-radius: 100%; }</style></head><body><div class="page-shopping-cart" id="shopping-cart"><h4 class="cart-title">购物清单</h4><div class="cart-product-title clearfix"><div class="td-check fl"><span class="check-span fl check-all"></span>全选</div><div class="td-product fl">商品</div><div class="td-num fl">数量</div><div class="td-price fl">单价(元)</div><div class="td-total fl">金额(元)</div><div class="td-do fl">操作</div></div><div class="cart-product clearfix"><table><tbody><tr><td class="td-check"><span class="check-span"></span></td><td class="td-product"><img src="testimg.jpg" width="98" height="98"><div class="product-info"><h6>【斯文】甘油&nbsp;|&nbsp;丙三醇</h6><p>品牌:韩国skc&nbsp;&nbsp;产地:韩国</p><p>规格/纯度:99.7%&nbsp;&nbsp;起定量:215千克</p><p>配送仓储:上海仓海仓储</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl"><span></span></a><input type="text" class="num-input" value="3"><a href="javascript:;" class="num-add num-do fr"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">800</span>.00</p></td><td class="td-total"><p class="red-text">¥<span class="total-text">800</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr><tr><td class="td-check"><span class="check-span check-true"></span></td><td class="td-product"><img src="testimg.jpg" width="98" height="98"><div class="product-info"><h6>【斯文】甘油&nbsp;|&nbsp;丙三醇</h6><p>品牌:韩国skc&nbsp;&nbsp;产地:韩国</p><p>规格/纯度:99.7%&nbsp;&nbsp;起定量:215千克</p><p>配送仓储:上海仓海仓储</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl"><span></span></a><input type="text" class="num-input" value="1"><a href="javascript:;" class="num-add num-do fr"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">800</span>.00</p></td><td class="td-total"><p class="red-text">¥<span class="total-text">800</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr></tbody></table></div><div class="cart-product-info"><a class="delect-product" href="javascript:;"><span></span>删除所选商品</a><a class="keep-shopping" href="#"><span></span>继续购物</a><a class="btn-buy fr" href="javascript:;">去结算</a><p class="fr product-total">¥<span>1600</span></p><p class="fr check-num"><span>2</span>件商品总计(不含运费):</p></div><div class="cart-worder clearfix"><a href="javascript:;" class="choose-worder fl"><span></span>绑定跟单员</a><div class="worker-info fl"></div></div></div></body><script src="vue.min.js"></script><script>
    new Vue({
        el:'#shopping-cart',
        data:{

        },
        computed: {},
        methods:{
            
        }
    })
</script></html>

然后准备下列表数据,根据下面表格的箭头

clipboard.png

所以大家就知道吗,下面的数据大概是涨这样

productList:[
    {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
        'pro_brand': 'skc',//品牌名称
        'pro_place': '韩国',//产地
        'pro_purity': '99.7%',//规格
        'pro_min': "215千克",//最小起订量
        'pro_depot': '上海仓海仓储',//所在仓库
        'pro_num': 3,//数量
        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
        'pro_price': 800//单价
    }
]

准备了这么多,大家可能想到,还缺少一个,就是记录产品是否有选中,但是这个字段,虽然可以在上面那里加,但是意义不大,比如在平常项目那里!后台的数据不会这样返回,数据库也不会有这个字段,这个字段应该是自己添加的。代码如下

new Vue({
    el:'#shopping-cart',
    data:{
        productList:[
            {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                'pro_brand': 'skc',//品牌名称
                'pro_place': '韩国',//产地
                'pro_purity': '99.7%',//规格
                'pro_min': "215千克",//最小起订量
                'pro_depot': '上海仓海仓储',//所在仓库
                'pro_num': 3,//数量
                'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                'pro_price': 800//单价
            }
        ]
    },
    computed: {},
    methods:{

    },
    mounted:function () {
        //为productList添加select(是否选中)字段,初始值为true
        this.productList.map(function(item){item.select=true;console.log(item)})
    }
})
          

步骤1

为了着重表示我修改了什么地方,代码我现在只贴出修改的部分,大家对着上面的布局,就很容易知道我改的是什么地方了!下面也是这样操作!

点击增加和减少按钮(箭头指向地方),所属列的金额改变(红框地方)
clipboard.png

执行步骤1之前,要先把列表的数据给铺出来。利用v-for指令。代码如下

<tr v-for="item in productList"><td class="td-check"><span class="check-span"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr>

这样,列表的数据就有了!

clipboard.png

也可以发现, clipboard.png这两个按钮的功能已经实现了,后面的金额也会发生变化!是不是感到很惊喜!其实这里没什么特别的,就是因为输入框利用v-model绑定了数量( pro_num),然后两个按钮分别添加了事件 @click="item.pro_num--"和@ click="item.pro_num++"。比如刚开始pro_num是3,点击 clipboard.pngpro_num就变成2,点击 clipboard.png
pro_num就变成4,然后后面的金额会改改,是因为 {{item.pro_price*item.pro_num}}。只要pro_price或者pro_num的值改变了,整一块也会改变,视图就会刷新,我们就能看到变化(这些事情是vue做的,这就是MVVM的魅力,数据驱动视图改变)。

步骤2

点击所属列选择按钮(箭头指向地方),总计的金额(红框地方)和已选产品的列数(蓝框地方)和全选(黄框地方)会改变(如果已经全选了,全选按钮自动变成全选,如果没有全选,全选按钮,自动取消全选)!

clipboard.png

首先,选择与取消选择,在这里只有两个操作(其实只有一个:改变这条记录的 select字段)。

clipboard.png

然后改变 clipboard.png,如果这条记录 selectfalse,就显示 clipboard.png,否则就显示 clipboard.png
代码如下

<td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td>

其实就是等于添加了 @click="item.select=!item.select" :class="{'check-true':item.select}"这里。点击这个,这条数据的 select字段就取反(true->false或者false->true)。然后 :class="{'check-true':item.select}",就会根据这条数据的 select字段进行判断,是否添加 check-true类名,如果 select字段为true,就添加类名,显示 clipboard.png。否则不添加类名,显示
clipboard.png

然后, clipboard.png全选按钮,是否变成 clipboard.png。这里用一个computed(计算属性)就好。代码如下

html

<div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}"></span>全选</div>

js

computed: {
    isSelectAll:function(){
        //如果productList中每一条数据的select都为true,返回true,否则返回false;
        return this.productList.every(function (val) { return val.select});
    }
}

代码我解释下,就是计算属性中,定义的isSelectAll依赖productList。只要productList改变,isSelectAll的返回值就会改变,然后 :class="{'check-true':isSelectAll}"根绝isSelectAll返回值是否添加 'check-true'类名,显示对应的样式!
最后, clipboard.png,这里的多少件产品和总价,也是使用计算属性,有了上一步的基础,给出代码,大家一看就明白了!
html

<p class="fr product-total">¥<span>{{getTotal.totalPrice}}</span></p><p class="fr check-num"><span>{{getTotal.totalNum}}</span>件商品总计(不含运费):</p>

js

computed: {
    //检测是否全选
    isSelectAll:function(){
        //如果productList中每一条数据的select都为true,返回true,否则返回false;
        return this.productList.every(function (val) { return val.select});
    },
    //获取总价和产品总件数
    getTotal:function(){
        //获取productList中select为true的数据。
        var _proList=this.productList.filter(function (val) { return val.select}),totalPrice=0;
        for(var i=0,len=_proList.length;i<len;i++){
            //总价累加
            totalPrice+=_proList[i].pro_num*_proList[i].pro_price;
        }
        //选择产品的件数就是_proList.length,总价就是totalPrice
        return {totalNum:_proList.length,totalPrice:totalPrice}
    }
},

代码很简单,html根据getTotal返回值显示数据,getTotal依赖productList的数据,只要productList改变,返回值会改变,视图也会改变!

clipboard.png

步骤3

点击全选按钮(箭头指向部分),会自动的对产品进行全选或者取消全选,下面的总计也会发生改变

clipboard.png
做到这一步,大家应该知道,全选或者取消全选,就是改变记录的 select。但是怎么知道现在的列表有没有全选呢?这个很贱,不需要在操作函数(全选与取消全选函数)里面遍历,大家应该还记得第二步的计算属性 isSelectAll(为true就是全选,否则不是全选),把这个传进操作函数就好,然后操作函数,根据参数,决定执行全选,还是取消全选操作。代码如下!
html

<div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}" @click="selectProduct(isSelectAll)"></span>全选</div>

js

 methods: {
    //全选与取消全选
    selectProduct:function(_isSelect){
        //遍历productList,全部取反
        for (var i = 0, len = this.productList.length; i < len; i++) {
            this.productList[i].select = !_isSelect;
        }
    }
},

clipboard.png

步骤4

点击删除产品,会删除已经选中的,全选按钮和下面的总计,都会变化!点击每条记录后面的删除,会删除当前的这条记录。全选按钮和下面的总计,也都会变化!

clipboard.png

首先,点击删除产品,删除已经选中。这个大家知道了怎么做了!就是遍历productList,如果哪条记录的select为true,就删除。
然后,点击每条记录后面的删除,删除当前的这条记录。这个在html遍历productList的时候。顺便带上索引,然后把索引当成参数,传进操作函数,然后根据索引参数,删除productList的哪一条记录。即可实现!代码如下!
html

<!--遍历的时候带上索引--><tr v-for="(item,index) in productList"><td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect" @click="deleteOneProduct(index)">删除</a></td></tr>
...<a class="delect-product" href="javascript:;" @click="deleteProduct"><span></span>删除所选商品</a>

js

//删除已经选中(select=true)的产品
deleteProduct:function () {
    this.productList=this.productList.filter(function (item) {return !item.select})
},
//删除单条产品
deleteOneProduct:function (index) {
    //根据索引删除productList的记录
    this.productList.splice(index,1);
},

完整代码

样式图片

clipboard.png

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>
            .fl {
                float: left;
            } 
            .fr {
                float: right;
            }
            blockquote, body, dd, div, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, img, input, li, ol, p, table, td, textarea, th, ul {
                margin: 0;
                padding: 0;
            }
            .clearfix {
                zoom: 1;
            }
            .clearfix:after {
                clear: both;
            }
            .clearfix:after {
                content: '.';
                display: block;
                overflow: hidden;
                visibility: hidden;
                font-size: 0;
                line-height: 0;
                width: 0;
                height: 0;
            }
            a {
                text-decoration: none;
                color: #333;
            }
            img {
                vertical-align: middle;
            }
            .page-shopping-cart {
                width: 1200px;
                margin: 50px auto;
                font-size: 14px;
                border: 1px solid #e3e3e3;
                border-top: 2px solid #317ee7;
            }
            .page-shopping-cart .cart-title {
                color: #317ee7;
                font-size: 16px;
                text-align: left;
                padding-left: 20px;
                line-height: 68px;
            }
            .page-shopping-cart .red-text {
                color: #e94826;
            }
            .page-shopping-cart .check-span {
                display: block;
                width: 24px;
                height: 20px;
                background: url("shopping_cart.png") no-repeat 0 0;
            }
            .page-shopping-cart .check-span.check-true {
                background: url("shopping_cart.png") no-repeat 0 -22px;
            }
            .page-shopping-cart .td-check {
                width: 70px;
            }
            .page-shopping-cart .td-product {
                width: 460px;
            }
            .page-shopping-cart .td-num, .page-shopping-cart .td-price, .page-shopping-cart .td-total {
                width: 160px;
            }
            .page-shopping-cart .td-do {
                width: 150px;
            }
            .page-shopping-cart .cart-product-title {
                text-align: center;
                height: 38px;
                line-height: 38px;
                padding: 0 20px;
                background: #f7f7f7;
                border-top: 1px solid #e3e3e3;
                border-bottom: 1px solid #e3e3e3;
            }
            .page-shopping-cart .cart-product-title .td-product {
                text-align: center;
                font-size: 14px;
            }
            .page-shopping-cart .cart-product-title .td-check {
                text-align: left;
            }
            .page-shopping-cart .cart-product-title .td-check .check-span {
                margin: 9px 6px 0 0;
            }
            .page-shopping-cart .cart-product {
                padding: 0 20px;
                text-align: center;
            }
            .page-shopping-cart .cart-product table {
                width: 100%;
                text-align: center;
                font-size: 14px;
            }
            .page-shopping-cart .cart-product table td {
                padding: 20px 0;
            }
            .page-shopping-cart .cart-product table tr {
                border-bottom: 1px dashed #e3e3e3;
            }
            .page-shopping-cart .cart-product table tr:last-child {
                border-bottom: none;
            }
            .page-shopping-cart .cart-product table .product-num {
                border: 1px solid #e3e3e3;
                display: inline-block;
                text-align: center;
            }
            .page-shopping-cart .cart-product table .product-num .num-do {
                width: 24px;
                height: 28px;
                display: block;
                background: #f7f7f7;
            }
            .page-shopping-cart .cart-product table .product-num .num-reduce span {
                background: url("shopping_cart.png") no-repeat -40px -22px;
                display: block;
                width: 6px;
                height: 2px;
                margin: 13px auto 0 auto;
            }
            .page-shopping-cart .cart-product table .product-num .num-add span {
                background: url("shopping_cart.png") no-repeat -60px -22px;
                display: block;
                width: 8px;
                height: 8px;
                margin: 10px auto 0 auto;
            }
            .page-shopping-cart .cart-product table .product-num .num-input {
                width: 42px;
                height: 28px;
                line-height: 28px;
                border: none;
                text-align: center;
            }
            .page-shopping-cart .cart-product table .td-product {
                text-align: left;
                font-size: 12px;
                line-height: 20px;
            }
            .page-shopping-cart .cart-product table .td-product img {
                border: 1px solid #e3e3e3;
                margin-right: 10px;
            }
            .page-shopping-cart .cart-product table .td-product .product-info {
                display: inline-block;
                vertical-align: middle;
            }
            .page-shopping-cart .cart-product table .td-do {
                font-size: 12px;
            }
            .page-shopping-cart .cart-product-info {
                height: 50px;
                line-height: 50px;
                background: #f7f7f7;
                padding-left: 20px;
            }
            .page-shopping-cart .cart-product-info .delect-product {
                color: #666;
            }
            .page-shopping-cart .cart-product-info .delect-product span {
                display: inline-block;
                vertical-align: top;
                margin: 18px 8px 0 0;
                width: 13px;
                height: 15px;
                background: url("shopping_cart.png") no-repeat -60px 0;
            }
            .page-shopping-cart .cart-product-info .product-total {
                font-size: 14px;
                color: #e94826;
            }
            .page-shopping-cart .cart-product-info .product-total span {
                font-size: 20px;
            }
            .page-shopping-cart .cart-product-info .check-num {
                color: #333;
            }
            .page-shopping-cart .cart-product-info .check-num span {
                color: #e94826;
            }
            .page-shopping-cart .cart-product-info .keep-shopping {
                color: #666;
                margin-left: 40px;
            }
            .page-shopping-cart .cart-product-info .keep-shopping span {
                display: inline-block;
                vertical-align: top;
                margin: 18px 8px 0 0;
                width: 15px;
                height: 15px;
                background: url("shopping_cart.png") no-repeat -40px 0;
            }
            .page-shopping-cart .cart-product-info .btn-buy {
                height: 50px;
                color: #fff;
                font-size: 20px;
                display: block;
                width: 110px;
                background: #ff7700;
                text-align: center;
                margin-left: 30px;
            }
            .page-shopping-cart .cart-worder {
                padding: 20px;
            }
            .page-shopping-cart .cart-worder .choose-worder {
                color: #fff;
                display: block;
                background: #39e;
                width: 140px;
                height: 40px;
                line-height: 40px;
                border-radius: 4px;
                text-align: center;
                margin-right: 20px;
            }
            .page-shopping-cart .cart-worder .choose-worder span {
                display: inline-block;
                vertical-align: top;
                margin: 9px 10px 0 0;
                width: 22px;
                height: 22px;
                background: url("shopping_cart.png") no-repeat -92px 0;
            }
            .page-shopping-cart .cart-worder .worker-info {
                color: #666;
            }
            .page-shopping-cart .cart-worder .worker-info img {
                border-radius: 100%;
                margin-right: 10px;
            }
            .page-shopping-cart .cart-worder .worker-info span {
                color: #000;
            }
            .choose-worker-box {
                width: 620px;
                background: #fff;
            }
            .choose-worker-box .box-title {
                height: 40px;
                line-height: 40px;
                background: #F7F7F7;
                text-align: center;
                position: relative;
                font-size: 14px;
            }
            .choose-worker-box .box-title a {
                display: block;
                position: absolute;
                top: 15px;
                right: 16px;
                width: 10px;
                height: 10px;
                background: url("shopping_cart.png") no-repeat -80px 0;
            }
            .choose-worker-box .box-title a:hover {
                background: url("shopping_cart.png") no-repeat -80px -22px;
            }
            .choose-worker-box .worker-list {
                padding-top: 30px;
                height: 134px;
                overflow-y: auto;
            }
            .choose-worker-box .worker-list li {
                float: left;
                width: 25%;
                text-align: center;
                margin-bottom: 30px;
            }
            .choose-worker-box .worker-list li p {
                margin-top: 8px;
            }
            .choose-worker-box .worker-list li.cur a {
                color: #f70;
            }
            .choose-worker-box .worker-list li.cur a img {
                border: 1px solid #f70;
            }
            .choose-worker-box .worker-list li a:hover {
                color: #f70;
            }
            .choose-worker-box .worker-list li a:hover img {
                border: 1px solid #f70;
            }
            .choose-worker-box .worker-list li img {
                border: 1px solid #fff;
                border-radius: 100%;
            }</style></head><body><div class="page-shopping-cart" id="shopping-cart"><h4 class="cart-title">购物清单</h4><div class="cart-product-title clearfix"><div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}" @click="selectProduct(isSelectAll)"></span>全选</div><div class="td-product fl">商品</div><div class="td-num fl">数量</div><div class="td-price fl">单价(元)</div><div class="td-total fl">金额(元)</div><div class="td-do fl">操作</div></div><div class="cart-product clearfix"><table><tbody><!--遍历的时候带上索引--><tr v-for="(item,index) in productList"><td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect" @click="deleteOneProduct(index)">删除</a></td></tr></tbody></table></div><div class="cart-product-info"><a class="delect-product" href="javascript:;" @click="deleteProduct"><span></span>删除所选商品</a><a class="keep-shopping" href="#"><span></span>继续购物</a><a class="btn-buy fr" href="javascript:;">去结算</a><p class="fr product-total">¥<span>{{getTotal.totalPrice}}</span></p><p class="fr check-num"><span>{{getTotal.totalNum}}</span>件商品总计(不含运费):</p></div></div></body><script src="vue.min.js"></script><script>
        new Vue({
            el: '#shopping-cart',
            data: {
                productList: [
                    {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    },
                    {
                        'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    },
                    {
                        'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    }
                ]
            },
            computed: {
                //检测是否全选
                isSelectAll:function(){
                    //如果productList中每一条数据的select都为true,返回true,否则返回false;
                    return this.productList.every(function (val) { return val.select});
                },
                //获取总价和产品总件数
                getTotal:function(){
                    //获取productList中select为true的数据。
                    var _proList=this.productList.filter(function (val) { return val.select}),totalPrice=0;
                    for(var i=0,len=_proList.length;i<len;i++){
                        //总价累加
                        totalPrice+=_proList[i].pro_num*_proList[i].pro_price;
                    }
                    //选择产品的件数就是_proList.length,总价就是totalPrice
                    return {totalNum:_proList.length,totalPrice:totalPrice}
                }
            },
            methods: {
                //全选与取消全选
                selectProduct:function(_isSelect){
                    //遍历productList,全部取反
                    for (var i = 0, len = this.productList.length; i < len; i++) {
                        this.productList[i].select = !_isSelect;
                    }
                },
                //删除已经选中(select=true)的产品
                deleteProduct:function () {
                    this.productList=this.productList.filter(function (item) {return !item.select})
                },
                //删除单条产品
                deleteOneProduct:function (index) {
                    //根据索引删除productList的记录
                    this.productList.splice(index,1);
                },
            },
            mounted: function () {
                var _this=this;
                //为productList添加select(是否选中)字段,初始值为true
                this.productList.map(function (item) {
                    _this.$set(item, 'select', true);
                })
            }
        })</script></html>  

5.todoList

运行效果

clipboard.png

原理分析和实现

首先,还是先把布局写好,和引入vue,准备vue实例,这个不多说,代码如下

<!DOCTYPE html><html><head><meta charset="UTF-8"><title></title><style>
            body{font-family: "微软雅黑";font-size: 14px;}
            input{font-size: 14px;}
            body,ul,div,html{padding: 0;margin: 0;}
            .hidden{display: none;}
            .main{width: 800px;margin: 0 auto;}
            li{list-style-type: none;line-height: 40px;position: relative;border: 1px solid transparent;padding: 0 20px;}
            li .type-span{display: block;width: 10px;height: 10px;background: #ccc;margin: 14px 10px 0 0 ;float: left;}
            li .close{position: absolute;color: #f00;font-size: 20px;line-height: 40px;height: 40px;right: 20px;cursor: pointer;display: none;top: 0;}
            li:hover{border: 1px solid #09f;}
            li:hover .close{display: block;}
            li .text-keyword{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            .text-keyword{box-sizing: border-box;width: 100%;height: 40px;padding-left: 10px;outline: none;}</style></head><body><div id="app" class="main"><h2>小目标列表</h2><div class="list"><h3>添加小目标</h3><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认"/><p>共有N个目标</p><p><input type="radio" name="chooseType" checked="true"/><label>所有目标</label><input type="radio" name="chooseType"/><label>已完成目标</label><input type="radio" name="chooseType"/><label>未完成目标</label></p></div><ul><li class="li1"><div><span class="type-span"></span><span>html5</span><span class="close">X</span></div></li><li class="li1"><div><span class="type-span"></span><span>css3</span><span class="close">X</span></div></li></ul></div></body><script src="vue2.4.2.js"></script><script type="text/javascript">
    new Vue({
        el: "#app",
        data: {
        },
        computed:{
        },
        methods:{
        }
    });</script></html>

布局有了,相当于一个骨架就有了,下面实现功能,一个一个来

步骤1

输入并回车,多一条记录。下面的记录文字也会改变

clipboard.png

首先,大的输入框回车要添加纪录,那么输入框必须绑定一个值和一个添加纪录的方法。
代码如下:
然后,下面的记录也要改变,所以,下面的记录也要帮一个值,因为这个记录可能会有多个,这个值就是一个数组,也可以看到,记录除了名称,还有记录是否完成的状态,所以,绑定记录的这个值肯定是一个对象数组!代码如下
最后,记录文字 clipboard.png要改变。这个只是一个当前记录的长度即可!

为了着重表示我修改了什么地方,代码我现在只贴出修改的部分,大家对着上面的布局,就很容易知道我改的是什么地方了!下面也是这样操作!

html代码

<!--利用v-model把addText绑定到input--><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认" @keyup.13='addList' v-model="addText"/><p>共有{{prolist.length}}个目标</p><!--v-for遍历prolist--><li class="li1" v-for="list in prolist"><div><span class="type-span"></span><span>{{list.name}}</span><span class="close">X</span></div></li>

js代码

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ]
    },
    computed:{
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        }
    }
});

测试一下,没问题

clipboard.png

步骤2

点击切换,下面记录会改变

clipboard.png

看到三个选项,也很简单,无非就是三个选择,一个是所有的目标,一个是所有已经完成的目标,一个是所有没完成的目标。
首先.新建一个新的变量(newList),储存prolist。遍历的时候不再遍历prolist,而是遍历newList。改变也是改变newList。
然后.选择所有目标的时候,显示全部prolist,把prolist赋值给newList。
然后.选择所有已经完成目标的时候,只显示prolist中,status为true的目标,把prolist中,status为true的项赋值给newList,
最后.选择所有未完成目标的时候,只显示status为false的目标,把prolist中,status为false的项赋值给newList。

代码如下

html

<ul><li class="li1" v-for="list in newList"><div><span class="status-span"></span><span>{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div></li></ul>

js

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ],
        newList:[]
    },
    computed:{
        noend:function(){
            return this.prolist.filter(function(item){
                return !item.status
            }).length;
        }
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            //type=1时,选择所有目标
            //type=2时,选择所有已完成目标
            //type=3时,选择所有未完成目标
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
    },
    mounted(){
        //初始化,把prolist赋值给newList。默认显示所有目标
        this.newList=this.prolist;
    }
});

运行结果

clipboard.png

步骤3

红色关闭标识,点击会删除该记录。前面按钮点击会切换该记录完成状态,颜色也改变,记录文字也跟着改变

clipboard.png

首先点击红色关闭标识,点击会删除该记录。这个应该没什么问题,就是删除prolist的一条记录!
然后前面按钮点击会切换该记录完成状态。这个也没什么,就是改变prolist的一条记录的status字段!
最后记录文字的改变,就是记录prolist中status为false的有多少条,prolist中status为true的有多少条而已

html代码

<!--如果noend等于0,就是全部完成了就显示‘全部完成了’,如果没有就是显示已完成多少条(prolist.length-noend)和未完成多少条(noend)--><p>共有{{prolist.length}}个目标,{{noend==0?"全部完成了":'已完成'+(prolist.length-noend)+',还有'+noend+'条未完成'}}</p>
<ul><li class="li1" v-for="(list,index) in newList"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span>{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div></li></ul>

js

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ],
        newList:[]
    },
    computed:{
        //计算属性,返回未完成目标的条数,就是数组里面status=false的条数
        noend:function(){
            return this.prolist.filter(function(item){
                return !item.status
            }).length;
        }
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
    },
    mounted(){
        this.newList=this.prolist;
    }
});

运行结果

clipboard.png

步骤4

文字双击会出现输入框,可输入文字,如果回车或者失去焦点,就改变文字,如果按下ESC就恢复原来的文字

clipboard.png

首先.双击出现输入框,就是双击文字后,给当前的li设置一个类名(‘ eidting’),然后写好样式。当li出现这个类名的时候,就出现输入框,并且隐藏其它内容。
然后.回车或者失去焦点,就改变文字这个只需要操作一个,就是把类名(‘ eidting’)清除掉。然后输入框就会隐藏,其它内容显示!
最后.按下ESC就恢复原来的文字,就是出现输入框的时候,用一个变量(‘ beforeEditText’)先保存当前的内容,然后按下了ESC,就把变量(‘ beforeEditText’)赋值给当前操作的值!

代码如下:

html

<ul><li class="li1" v-for="(list,index) in newList" :class="{'eidting':curIndex===index}"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span @dblclick="curIndex=index">{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div><input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited'/></li></ul>

css(加上)

li div{display: block;}
li.eidting div{display: none;}
li .text2{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
li.eidting .text2{display: block;}

js

methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            //type=1时,选择所有目标
            //type=2时,选择所有已完成目标
            //type=3时,选择所有未完成目标
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
        //修改前
        editBefore(name){
            //先记录当前项(比如这一项,{name:"HTML5",status:false})
            //beforeEditText="HTML5"
            this.beforeEditText=name;
        },
        //修改完成后
        edited(){
            //修改完了,设置curIndex="",这样输入框就隐藏,其它元素就会显示。因为在li元素 写了::class="{'eidting':curIndex===index}"  当curIndex不等于index时,eidting类名就清除了!
            //输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步只是清除eidting类名,隐藏输入框而已
            //还有一个要注意的就是虽然li遍历的是newList,比如改了newList的这一项({name:"HTML5",status:false}),比如改成这样({name:"HTML",status:true})。实际上prolist的这一项({name:"HTML5",status:false}),也会被改成({name:"HTML",status:true})。因为这里是一个对象,而且公用一个堆栈!修改其中一个,另一个会被影响到
            this.curIndex="";
        },
        //取消修改
        cancelEdit(val){
            //上面说了输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步就是把之前保存的beforeEditText赋值给当前项的name属性,起到一个恢复原来值得作用!
            val.name=this.beforeEditText;
            this.curIndex="";
        }
 },

运行结果

clipboard.png

还有一个小细节,大家可能注意到了,就是双击文字,出来输入框的时候,还要自己手动点击一下,才能获得焦点,我们想双击了,输入框出来的时候,自动获取焦点,怎么办?自定义指令就行了!

computed:{...},
methods:{...},
mounted(){...},
directives:{"focus":{
        update(el){
            el.focus();
        }
    }
}

然后html 调用指令

<input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited' v-focus/>

完整代码

<!DOCTYPE html><html><head><meta charset="UTF-8"><title></title><style>
            body{font-family: "微软雅黑";font-size: 14px;}
            input{font-size: 14px;}
            body,ul,div,html{padding: 0;margin: 0;}
            .hidden{display: none;}
            .main{width: 800px;margin: 0 auto;}
            li{list-style-type: none;line-height: 40px;position: relative;border: 1px solid transparent;padding: 0 20px;}
            li .status-span{display: block;width: 10px;height: 10px;background: #ccc;margin: 14px 10px 0 0 ;float: left;}
            li .status-span.status-end{
                background: #09f;
            }
            li .close{position: absolute;color: #f00;font-size: 20px;line-height: 40px;height: 40px;right: 20px;cursor: pointer;display: none;top: 0;}
            li:hover{border: 1px solid #09f;}
            li:hover .close{display: block;}
            li div{display: block;}
            li.eidting div{display: none;}
            li .text2{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            li.eidting .text2{display: block;}
            li .text-keyword{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            .text-keyword{box-sizing: border-box;width: 100%;height: 40px;padding-left: 10px;outline: none;}</style></head><body><div id="app" class="main"><h2>小目标列表</h2><div class="list"><h3>添加小目标</h3><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认" @keyup.13='addList' v-model="addText"/><!--如果noend等于0,就是全部完成了就显示‘全部完成了’,如果没有就是显示已完成多少条(prolist.length-noend)和未完成多少条(noend)--><p>共有{{prolist.length}}个目标,{{noend==0?"全部完成了":'已完成'+(prolist.length-noend)+',还有'+noend+'条未完成'}}</p><p><input type="radio" name="chooseType" checked="true" @click='chooseList(1)'/><label>所有目标</label><input type="radio" name="chooseType" @click='chooseList(2)'/><label>已完成目标</label><input type="radio" name="chooseType" @click='chooseList(3)'/><label>未完成目标</label></p></div><ul><li class="li1" v-for="(list,index) in newList" :class="{'eidting':curIndex===index}"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span @dblclick="curIndex=index">{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div><input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited' v-focus/></li></ul></div></body><script src="vue2.4.2.js"></script><script type="text/javascript">
    new Vue({
        el: "#app",
        data: {
            addText:'',
            //name-名称,status-完成状态
           prolist:[
                   {name:"HTML5",status:false},
                   {name:"CSS3",status:false},
                   {name:"vue",status:false},
                   {name:"react",status:false}
            ],
            newList:[],
            curIndex:'',
               beforeEditText:""
        },
        computed:{
            //计算属性,返回未完成目标的条数,就是数组里面status=false的条数
            noend:function(){
                return this.prolist.filter(function(item){
                    return !item.status
                }).length;
            }
        },
        methods:{
            addList(){
                //添加进来默认status=false,就是未完成状态
                this.prolist.push({
                    name:this.addText,
                    status:false
                });
                //添加后,清空addText
                this.addText="";
            },
            chooseList(type){
                //type=1时,选择所有目标
                //type=2时,选择所有已完成目标
                //type=3时,选择所有未完成目标
                switch(type){
                    case 1:this.newList=this.prolist;break;
                    case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                    case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
                }
            },
            delectList(index){
                //根据索引,删除数组某一项
                this.prolist.splice(index,1);
                //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
                this.newList=this.prolist;
            },
            //修改前
            editBefore(name){
                //先记录当前项(比如这一项,{name:"HTML5",status:false})
                //beforeEditText="HTML5"
                this.beforeEditText=name;
            },
            //修改完成后
            edited(){
                //修改完了,设置curIndex="",这样输入框就隐藏,其它元素就会显示。因为在li元素 写了::class="{'eidting':curIndex===index}"  当curIndex不等于index时,eidting类名就清除了!
                //输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步只是清除eidting类名,隐藏输入框而已
                //还有一个要注意的就是虽然li遍历的是newList,比如改了newList的这一项({name:"HTML5",status:false}),比如改成这样({name:"HTML",status:true})。实际上prolist的这一项({name:"HTML5",status:false}),也会被改成({name:"HTML",status:true})。因为这里是一个对象,而且公用一个堆栈!修改其中一个,另一个会被影响到
                this.curIndex="";
            },
            //取消修改
            cancelEdit(val){
                //上面说了输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步就是把之前保存的beforeEditText赋值给当前项的name属性,起到一个恢复原来值得作用!
                val.name=this.beforeEditText;
                this.curIndex="";
            }
        },
        mounted(){
            //初始化,把prolist赋值给newList。默认显示所有目标
            this.newList=this.prolist;
        },
        directives:{
        "focus":{
            update(el){
                el.focus();
            }
        }
    }
    });</script></html>

6.小结

好了,三个小实例在这里就说完了!别看文章这么长,其实都是基础,可能是我比较啰嗦而已!如果大家能熟透这几个小实例,相信用vue做项目也是信手拈来。基础的语法在这里了,有了基础,高级的写法也不会很难学习!如果以后,我有什么要分享的,我会继续分享。最后一句老话,如果觉得我哪里写错了,写得不好,欢迎指点!

Viewing all 148 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>