Liby


  • 首页

  • 分类

  • 归档

  • 标签

  • 站点地图

JS之封装cookie操作

发表于 2017-08-30

原生js中,我们通过document.cookie可以获取cookie,这样我们可以不用考虑兼容性,但是增,删,查我们最好通过封装的形式把他们区分开来,使得我们能够像操作对象一样去操作cookie,这样使用起来也直观方便,底层还是基于document.cookie来操作。

下面直接上代码加注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//用闭包将代码包起来,避免引入时污染全局作用域
(function(global){
//获取cookie对象,格式化成对象形式
function getCookiesObj(){
var cookies = {};
if(document.cookie){
var objs = document.cookie.split(';');
for(var i in objs){
var index = objs[i].indexOf('=');
var key = objs[i].substr(0,index);
var value = objs[i].substr(index+1,objs[i].length);
cookies[key] = value;
}
}
return cookie;
}
//获取cookie
function get(key){
return decodeURIComponent(getCookiesObj()[key]) || null;
}
//设置cookie
function set(name,value,opts){
//opts: maxAge,path,domain,secure
if(name && value){
var cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
if(opts){
if(opts.maxAge){
cookie += ';max-age=' + opts.maxAge;
}
if(opts.path){
cookie += ';max-age=' + opts.path;
}
if(opts.domain){
cookie += ';max-age=' + opts.doamin;
}
if(opts.secure){
cookie += ';max-age=' + opts.secure;
}
}
document.cookie = cookie;
return cookie;
}else{
return '';
}
}
function remove(key){
if(getCookiesObj()[key]){
document.cookie = key + '=;max-age=0';
}
}
function clear(){
var cookies = getCookiesObj();
for(var i in cookies){
document.cookie = cookies[i] + '=;max-age=0';
}
}
global['cookie'] = {
'getCookiesObj': getCookiesObj,
'set': set,
'get': get,
'remove': remove,
'clear': clear
}
})(window)

封装好后,我们只要导入这个文件,就可以用全局变量的方式去操作cookie了!

JS之封装AJAX

发表于 2017-08-30

前端中对于ajax的使用越来越多,原生的ajax需要考虑兼容性,写法也较为复杂,所以打算自己封装一个。

原生ajax

原生ajax的使用可以分为4个步骤(3的位置不是固定的,只要在1后面即可):

  1. 新建一个XHR对象
  2. 用open()方法指定请求方法,请求资源和是否异步
  3. 为XHR对象绑定一个onreadystatechange函数
  4. 用send()发送请求

下面一一介绍:

新建XHR对象

创建XHR对象很简单,一行代码搞定:

1
var xhr = new XMLHttpRequest();

考虑要兼容IE浏览器,得像下面这样写:

1
2
3
4
5
if(window.XMLHttpRequest){
var xhr = new XMLHttpRequest();
}else{
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
}

指定open()方法

当我们要跟服务器交互的时候,就需要像正常的http请求那样指定请求方式和请求的资源,这里XHR提供了一个open()方法,它可以接受3个参数:

1
2
3
4
open(method,url,async)
method:请求的类型,常见的有GET和POST
url:请求资源的路径
async:true(异步)或者false(同步)

绑定回调函数

当我们获取到资源时,我们希望能够通过回调的方式来处理它,这时就需要指定onreadystatechange方法了。在这之前,我们需要先了解两个东西,readyState和status。

首先是redayState,它一共有5个可能的值,代表的是请求的建立到成功接受响应的一系列过程:

1
2
3
4
5
0:请求未初始化
1:与服务器连接已建立
2:请求被接受
3:请求处理中
4:请求完成,且返回响应信息

每当xhr对象的readyState改变时,onreadystatechange事件就会被触发,从单词也很明显能看出了把!这样xhr请求的过程中,onreadystatechange事件就会被触发5次,一般我们只会判断readyState是否为4,因为这时候我们已经拿到了服务器的返回的信息(可能是资源也可能是其他)。

但是拿到了响应信息并不代表我们的请求就成功了,与服务器交互的结果有可能是404页面未找到,也有可能是500服务器内部错误等其他情况,这时候就需要通过另一个判断条件–status来判断了
常见的状态码有这些:

  • 200 请求成功
  • 301 永久重定向
  • 302 临时重定向
  • 304 使用缓存(条件请求If-Modified-Since)
  • 400 请求出现语法错误
  • 401 用户未认证(不能靠这个状态码来确定用户是否认证)
  • 403 资源不可用
  • 404 页面未找到
  • 500 服务器内部错误

当readyState为4且status为200时,我们就可以提取数据了,提取数据可以通过2个属性:

1
2
responseText:获得字符串形式的响应数据
responseXML: 获得XML格式的响应数据

调用send()发送请求

当我们使用post请求时,要发给服务端的数据要通过’&’连接放在send()方法里

1
xhr.send(data)

ajax封装

整合以上代码并添加一些判断以及处理函数,我们自己封装的ajax就成了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
* 调用形式:
* $.ajax({
* url: '.',
* type: 'POST',
* data: {
* name: 'liby',
* height: '172'
* }
* success: function(res){
* //回调代码
* },
* fail: function(status){
* //失败的回调代码
* }
* })
*/
var $ = {
ajax: function(obj){
//处理传递过来的数据,拼接成'a=1&b=2'的形式
function formatData(obj){
var result = '';
for (var key in obj) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]) + '&';
}
return result.slice(0,-1);
}
//如果传递进来的不是对象或者为空,直接return
if(obj == null || tpeof obj != 'object'){
return false
}
//获取传递的参数
var type = obj.type;
var url = obj.url;
var async = (obj.async == undefined || obj.async)?true:false;
var data = formatData(obj.data);
var success = obj.success || function(){};
var fail = obj.fail || function(){};
//新建xhr对象,兼容IE
if(window.XMLHttpRequest){
var xhr = new XMLHttpRequest();
}else{
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
//绑定回调函数
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
var result = null;
var resultType = xhr.getResponseHeader('Content-Type');
if(resultType.indexOf("json") != -1){
result = JSON.parse(xhr.responseText);
}else if(resultType.indexOf("xml") != -1){
result = xhr.responseXML;
}else{
result = xhr.responseText;
}
success(result);
}else{
fail(xhr.status);
}
}
// 根据请求方式设置open方法和send方法,发送请求
if(type == 'get'){
url = url + '?' + data;
xhr.open(type, url, async);
xhr.send(null);
}else{
xhr.open(type, url, async);
//设置提交表单的内容类型
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);
}
}
}

JS事件的那些事

发表于 2017-08-06

之前已经有博客介绍了js的事件,下面的作为补充。

为何移动端点击会有0.3s的延迟

移动端设备会有这样的现象,就是移动端页面对于触摸事件会有300ms的延迟,据研究表明,当延迟超过100ms时,用户就能感受到明显的卡顿,当延迟达到300ms,用户更能感受到界面响应速度的缓慢。

其实这应该追溯到手机开始支持双击缩放的时候了,ios上的safari为了能将pc端的大网页较好的展示在手机端上,开始使用双击缩放的方案。通过双击缩放,我们能够看清原本因为尺寸太小而看不清楚的字体,图片等。虽然这为我们带来了便利,但在另一方面却造成了困扰,要支持这个功能,浏览器就必须判断用户是否在极短的事件内双击,就算你只是想点击一个按钮,浏览器也会按照惯例等待300ms的时间来确定用户到底是不是要缩放页面,这也就是延迟的由来。

解决方法

使用fastclick.js

fastclick的使用方法简单,在window load事件后,在body上调用fastClick.attach()即可

1
2
3
window.addEventListener(function(){
FastClick.attach(document.body);
})

当 FastClick 检测到当前页面使用meta设置了user-scalable=no或者 touch-action 属性的解决方案时,会静默退出.

jQuery和zepto.js则使用tap事件取代click事件

tap事件的大致思路:

  • 在touchstart/touchend时记录时间,手指位置
  • 在touchend时进行比较,确认是否为同一位置(或允许较小的位移)
  • 判断时间间隔(一般为200ms)
  • 过程未曾触发touchmove,即认为触发移动端的’click’,成为’tap’

meta viewport

meta viewport中指定页面不可缩放,则click时不存在300ms延迟

mouseenter/mouseleave 和 mouseover/mouseout

区别:

  • mouseover,mouseout可冒泡,mouseenter,mouseleave不可冒泡
  • mouseenter只作用于目标元素,进入子元素返回不能再触发(进入只触发一次)
  • mouseover作用于目标元素及后代元素,进入子元素返回可再触发父元素的事件(mouseout)(可多次触发)
  • mouseleave与mouseenter类似,各自只触发自己的事件
  • mouseout与mouseover类似,进入子元素可触发父元素的事件(mouseover)

在mouseover/mouseout看来,它的事件都与后代元素有关,进入子元素和从子元素移出会触发父元素相应的mouseout和mouseover,且子元素事件会冒泡;而在mouseenter/mouseleave中,一旦进入了元素,无论是子元素或者父元素,都看作单独的个体,各自触发事件且不会冒泡,从父元素进入子元素和从子元素移出到父元素都不会触发父元素的事件。

总结:这两组事件的区别总结起来就是:是否冒泡和子元素的移入移出是否会触发父元素的事件。牢记这两点就ok了!

clientX/clientY 和 pageX/pageY

当我们通过鼠标触发页面的鼠标事件时,就会产生一个事件对象e并传给我们绑定的事件函数,里面包含很多的内容。

通常鼠标操作的话,主要是要获取鼠标当前的坐标值,并利用坐标值经过一系列的计算来完成我们的目的,例如:音乐播放器的进度条的拉拽,页面上元素的拖拽以及边界的判断等等。

而事件对象e中就提供了很多有用的信息:最常用的还是clientX/clientY 和 pageX/pageY这两组属性。

client表示的是页面的可视区域(即当前浏览器窗口),而page表示的则是整个页面(包括超出窗口宽高的部分)。

这样就很明了了,在没发生滚动的情况下,两组属性随便你选,但是一旦发生滚动,还是需要选择page开头的属性,才能获得正确的数据。

兼容性:IE不支持,需要用event.y和event.y获取

除了上面两组属性外,通过鼠标触发的事件e中还有screenX和screenY,不过不是很常用,就不解释了。

DOM0和DOM2级事件的区别

Dom0级事件

dom0级有两种形式:

  • 在标签内绑定事件,如‘onclick=fn()’
  • 在js中用‘onclick=function(){}’的形式绑定

看看下面的例子:

1
2
3
4
5
6
7
8
9
10
<a id='test' href='javascript:void(0);' onclick='alert(click)'></a>
//js
var oTest = document.getElementById('oTest');
oTest.onclick = function(){
alert('testing...')
}
oTest.onclick = function(){
alert('finished!')
}

上面的例子执行后,你会发现只会弹出finished字样的提示框,这是因为同一类型的事件只能绑定一个,后面会覆盖前面的。

Dom2级事件

Dom2级事件有两个方法:addEventListener()和removeEventListener(),

都可传入3个参数:

  • 事件名,不需要加on
  • 事件函数
  • capture标识,true或false,默认false(冒泡)

我们将上面例子用Dom2级改写后就能顺序输出testing和finished字样了:

1
2
3
4
5
6
7
8
9
10
<a id='test' href='javascript:void(0);'></a>
//js
var oTest = document.getElementById('oTest');
oTest.addEventListener('click', function(){
alert('testing...')
})
oTest.addEventListener('click', function(){
alert('finished!')
})

移除事件时需要传入和绑定事件时完全相同的参数,所以匿名函数绑定的事件不能被移除

IE 通过attachEvent()和detachEvent()来支持,只传入事件名(要加on前缀)和处理函数,只支持冒泡。

JS之深浅拷贝

发表于 2017-08-04

浅拷贝导致的问题

js中我们有时候需要对数组或者对象这些复杂类型进行拷贝,当我们把他们赋予其他变量时就以为万事大吉了,可实际上当我们对改变这些变量时,原来的数组或者对象也会跟着改变,如下面的例子:

1
2
3
4
5
6
7
8
var a = [1,2,3]
var arr = a;
arr[1] = 1;
console.log(a[1]);//1
console.log(arr[1]);//1
console.log(a=arr);//true

是的,最后打印出来的都会是相同的结果,这也证明了我们的拷贝与原来的对象并不是完全分离的,这其实是浅拷贝导致的问题。

但是有时候我们想要获得跟原对象完全不相干的拷贝,那应该怎么办呢,这时候就需要深拷贝了

深浅拷贝的原理及区别

基本类型和引用类型

ECMAScript中类型变量分为两类:

  • 基本数据类型:number/string/boolean/undefined/null/symbol(ES6新增)
  • 引用数据类型:object/Array/Date/Function等

平时我们总听说堆栈,其实他们就是用来储存我们的变量和值的。

栈内存

对于基本数据类型而言,它们是保存在栈内存里面的,包含变量名及对应的值。
结构大致如下:

变量 值
a 55

堆内存

引用类型则是保存在堆内存中,栈内存中会保存有变量名以及引用类型在堆内存中的地址。当我们访问引用类型时,会先通过变量名在栈内存中找到对象在堆内存中的地址指针,然后再到相应的堆内存中查找数据。
现在栈内存的结构是这样的:

栈内存

变量 值
a 堆地址1

堆内存

栈内存中的堆地址1 —> 对内存中的obj1

值
obj1

栈内存中存放的必须时大小固定的数据,而引用类型大小不固定,只能存放在堆中。基本数据类型是按值访问,而引用类型是按地址访问。

基本类型的复制

对于基本类型来说,当拷贝时,会将值也一同复制给新变量,当我们修改它时,是下面这样的:

1
2
3
4
var a = 1;
var b = a;
b = 2;
console.log(a);//1

从上面也可以看出,修改b后,并不会影响到a的值,因为它们是相互独立的。

栈内存中对于相同的基本数据类型值只会保存一份,并不会存放两个1,如果两个变量都指向1,那么他们就是相等的,指向不同值,则是不等(变量相互独立)的。函数参数的按值传递跟上面的例子原理一样,所以函数内就算重新修改了新变量也不会改变原来的变量。

引用类型的复制

1
2
3
4
5
var a = [1,2,3];
var b = a;
b.push(4);
console.log(a);//[1,2,3,4]
console.log(b);//[1,2,3,4]

虽然我们的本意是只想对拷贝后的b进行修改,但是由于我们拷贝的只是a的引用类型地址,所以其实他们指向的是同一个对象,这时候无论对谁进行修改都会导致两者都发生变化。

###深浅拷贝
聪明的你一定会想到下面这种方法:

1
2
3
4
5
6
7
var a = [1,2,3];
var b = [];
for(var i = 0;i<a.length;i++){
b[i] = a[i];
}
b.pop();
console.log(a);//[1,2,3]

很好,你会发现现在就算我们修改了新对象,原对象也并不会发生改动(浅复制),但如果你以为这样就成功了,那还有我们深拷贝什么事呢!

仔细看,你会发现我们的引用类型中的数据都是基本数据类型,那么换成引用类型又会发生什么呢?

1
2
3
4
5
6
7
8
9
var a = [[1,2,3],2,3];
var b = [];
for(var i = 0;i<a.length;i++){
b[i] = a[i];
}
b[0].pop();
console.log(a);//[[1,2],2,3]
b.shift();
console.log(a);//[[1,2],2,3]

现在,你会无奈的发现,新建了对象后并没有什么卵用,其实还是引用类型在作怪,谁让它是通过栈内存中地址来指向的呢。

很明显,外层的修改并不会有什么问题,但是一旦涉及到引用类型,那么又会回到前面的老问题上面!

既然一层的复制不行,那么我们就用递归的方式对下面嵌套的所有引用类型一一进行浅复制,直到最后都转换为基本数据类型的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//深拷贝函数
function deepClone(obj){
var newObj = {};
if((typeof obj) !== 'object'){
return obj
}else{
for(var attr in obj){
newObj[attr] = arguments.callee(obj[attr])
}
}
return newObj;
}
var a = {
name:'liby',
skills:{
language:'CET6',
computer:['js','python','node'],
others:['skating']
}
}
var b = deepClone(a);
b.skills.others.push('Balisong');
console.log(a);//nothing changed

现在,深拷贝完成,a和b已经不会再产生交集了,随便我们怎么折腾,哈哈!

数组和对象原生方法实现深浅拷贝

上面我们用自己写的方法递归实现了深复制,但是其实用对象提供的一些原生方法完全能够做到深浅拷贝,一起来记录一下吧!

数组

数组有4种方法可以实现浅拷贝:concat()和slice(),Array.from(),扩展运算符

slice()

slice方法可用来在原数组上面分割形成新数组,第一个参数为0时,即切割全部,返回一个新数组

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = arr.slice(0)
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

concat()

concat方法用来合并两个数组,不传入参数则深拷贝此数组

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = arr.concat();
newArr[0] = 6;
console.log(arr);//[1,2,3,4,5]

Array.from()

这时ES6中的方法,用来将类数组转换为真正的数组,当然也可以用来深拷贝

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = Array.from(arr);
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

…(扩展运算符)

同样也是ES6的东西,能够将一个数组轻易的解构并按照同样的模式赋值给新对象,其实内部是用的迭代器遍历复制,跟我们之前遍历复制很像,不过代码量少得多,哈哈,这也是浅拷贝!

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = [...arr];
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

对象的拷贝

object.assign()(浅拷贝)

object.assign()用来将任意uoge对象自身的可枚举属性拷贝给目标对象,不过拷贝的只是对象的引用,而不是对象本身,即浅拷贝。

JSON.parse()

将元对象转成JSON字符串再转回来,最简单粗暴的一种深拷贝方法!!!

1
2
var obj = {'name':'liby','course':{'english':80}};
var newObj = JSON.parse(JSON.stringify(obj));

jQuery.extend()

jQuery.extend这个扩展对象的方法也可以用来进行深拷贝,需要传入true参数。

深浅拷贝的原理及方法已介绍完,如有遗漏,后续会补充上来!

事件委托的优缺点

发表于 2017-08-02

前面有篇博客已经大致介绍了JS的事件流机制,包含了捕获/冒泡/事件委托,接下来就委托的优缺点进行分析记录。

事件委托(代理)出现背景

一般情况下,我们如果想给元素绑定事件处理函数的话,都会采用DOM0级或者DOM2级提供的方法,如onclick或者addEventListener等。但是当我们在一个ul下面有很多个li元素,我们如果还用老方法一个个的进行绑定注册,这样不仅会增加我们的代码量,而且当我们要移除某个li元素时还得一个个地去解除元素和事件处理函数的绑定(关乎内存),正是这些问题的出现,才有了基于冒泡机制的事件委托(代理)。

在js中,当我们移除某个元素但没有将元素和监听函数进行解绑时,事件处理函数依旧会留在内存中,无法被当成垃圾回收。

优缺点总结

优点:

1.减少事件注册,节省内存,如:

  • table可以代理所有td的click事件
  • ul代理所有li的click事件

2.减少了dom节点更新的操作,处理逻辑只需在委托元素上进行,如:

  • 新添加的li不用绑定事件
  • 删除li时,不需要进行元素与处理函数的解绑

缺点:

1.事件委托基于冒泡,对于onfoucs和onblur等事件不支持
2.层级过多,冒泡过程中,可能会被某层阻止掉(建议就近委托)

总之一切都是基于冒泡的,只要事件不支持冒泡或者中途有event.stopPropagation()等,那么委托就会失败,所以并不适用于直接在document上进行委托。

性能优化之重绘回流

发表于 2017-08-01

上一篇博客在介绍浏览器请求资源的过程中提及了重绘和回流,也对重绘回流的概念进行了介绍。根据概念,至少会有一次回流和重绘发生在第一次页面加载时,且回流一定会导致重绘,重绘不一定引起回流.

要进行性能优化,我们就必须搞清楚什么情况会导致回流和重绘,其中回流是重点,因为它的开销会比重绘高出很多。

回流何时发生

当页面布局和集合属性发生变化时就需要回流,常见的有以下情况:

  • 添加或者删除可见DOM元素
  • 元素位置改变
  • 元素尺寸改变
  • 文本改变
  • 图片(没有固定高度)加载src
  • 页面初始化渲染
  • 浏览器窗口尺寸改变(resize)
  • 操作class属性
  • 脚本操作DOM
  • 计算offsetWidth和offsetHeight属性
  • 设置style属性的值

浏览器队列

当我们在js中操作dom的一些样式是,通常都会引起页面的重绘和回流,如果每次一引起回流重绘浏览器就重新去渲染的话,这样会耗费大量的时间。所以很多浏览器会维护1个队列,里面存放着回流和重绘的操作,等队列满了或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理,所有的重绘回流就变成了一次。

但有时我们的代码会引起浏览器提前flush队列,比如,当我们向浏览器请求以下style信息时,就会提前让浏览器flush队列:

  • offsetTop,offsetLeft,offsetWidth,offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • width,height
  • 请求了getComputedStyle()或者IE的currentStyle

原因:
请求以上这些值时,浏览器需要清空队列,计算出最新的元素尺寸和位置样式信息(重绘回流),因为浏览器认为队列中的某些操作会造成我们获取的值并不是最精确的!

优化方法

在没有维护队列的浏览器中,减少重绘回流就需要我们合并样式的修改,尽量一次渲染到位,而有优化策略的浏览器,我们就要好好的利用这一点,减少会提前flush队列的操作。

1.将多个样式修改放到一个class中或者通过classText一次性修改:

1
2
3
4
5
6
7
8
9
//bad
var left = 1;
var top = 1;
el.style.left = left + 'px';
el.style.top = top + 'px';
//good
el.className += 'className1';
//good
el.style.cssText += 'left:'+ left + 'px;top:' + top + 'px;'

2.避免访问会引起flush队列的属性,如要访问,利用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//bad
for(){
el.style.left = el.offsetLeft + 5 + 'px';
el.style.top = el.offsetTop + 5 + 'px';
}
//good
var left = el.offsetLeft,top=el.offsetTop;
s = el.style;
for(){
left += 10;
top +=10;
s.left = left + 'px';
s.top = top + 'px';
}

3.动画效果应用到脱离文档流的元素上
元素脱离了文档流之后,不会影响其他元素的布局,所以只会导致重绘,可以减少开销

4.避免使用css表达式

5.让要操作的元素进行”离线处理”,处理完后一起更新

a) 使用DocumentFragment进行缓存操作,引发一次回流和重绘;
b) 使用display:none技术,只引发两次回流和重绘;
c) 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;

浏览器请求资源的过程发生了什么

发表于 2017-07-30

当我们在浏览器地址栏输入像’www.google.com’这样的网址时,按下Enter键后一段时间后就能看到网页呈现在我们面前,其实中间发生了很多事情,大致的流程是这样的:

  • DNS域名解析
  • 建立TCP连接(三次握手)
  • 发起HTTP请求
  • 获取响应结果
  • 浏览器解析HTML,获取其他静态资源
  • 浏览器页面渲染
  • 断开链接(四次挥手)

DNS域名解析

DNS解析过程其实就是查询域名与IP映射的过程,在网络上,IP才是计算机的唯一(通讯)标识,之所以会出现域名,是因为它对于用户来说方便记忆并且有比IP更高的可用性。既然底层的通信是通过ip来进行的,那么我们输入url后,势必就需要一个中间人来帮我们进行域名到ip之间的转换,这样我们才可能进行后续的步骤,这个中间人就是DNS解析。

域名解析过程

在分析解析过程之前,我们需要域名的组成方式:

组成部分 说明 例子
根域 位于域名的末尾,用句号(.)表示,表明最高级别的层次结构 .
顶级域 用来指示某个国家/地区或组织使用的名称的类型名称 .com
二级域 个人或组织在Internet上使用的注册名称 google.com
子域 已注册的二级域名的派生域名,即网站名 www.google.com

域名服务器分类

这里有几种不同的域名服务器分类:

  • 根域名服务器:管理顶级域名,会告诉本地服务器顶级域名服务器的ip
  • 顶级域名服务器:管理二级域名,与根服务器类似
  • 权限域名服务器:负责一个区的域名服务器
  • 本地域名服务器:主机发出的dns请求会先被发送到本地域名服务器

查询方式

1 主机向本地域名服务器的查询一般都是递归式查询。递归查询:主机向本地服务器询问要查询域名对应的ip时,如果本地服务器不知道,便代替用户向根域名服务器发起请求。

2 本地域名服务器向根域名服务器的查询是迭代查询。迭代查询:当根服务器收到本地服务器的请求时,要么给出所要查询的ip地址,要么告诉本地服务器向下一级的域名服务器去查询。本地域名服务器再向顶级域名服务器发起请求,结果类似,查询不到的情况下继续向权限服务器发起请求,最终将结果返回给主机

综上所述:域名的解析过程类似下面:

.->.com->google.com->www.google.com

DNS负载均衡

现实生活中,淘宝双十一每分钟的请求量是无比巨大的,如果用户请求的都是同一台服务器的话,那么这对服务器的性能要求是非常之高的。但实际上,用户并不关心获取数据的来源,他们只关注更好的服务和更快更流畅的体验,这时我们就可以根据每台机器的负载量,该机器离用户地理位置的距离来给用户分配一个合适的服务器IP,这就叫做DNS负载均衡,又称DNS重定向,开发中经常涉及到的CDN就是利用了这个原理。

反向代理的原理和DNS负载均衡很像,都是为了解决负载均衡。

建立TCP连接

由于HTTP是一个无状态要求可靠传输的协议,所以我们需要建立TCP而不是UDP链接。
TCP在建立链接的时候,需要经过三次握手:

  • client先发送一个(SYN=1,seq=client_seq_num)标志的数据包给接收方
  • server接收后,回传一个(SYN=1,ack=client_seq+1,seq=server_seq_num)标志的数据包进行确认
  • client再回传一个(SYN=0,ack=server_seq+1)标志的数据包来表示握手成功

TCP对于由于各种问题而丢失的数据包会进行重传,这让用户能够接收到完整且正确的信息,这也是选用TCP而不是UDP的原因!

浏览器发送http请求

三次握手建立tcp连接完毕后,就是给服务器发送http请求了。
http请求报文包括三个部分:

  • 起始行
  • 首部
  • 主体

起始行

起始行中包括了请求方式,资源路径,http协议版本三部分
EXP: GET index.html HTTP/1.1

常见的方法有:GET,POST,PUT,DELETE

首部

首部在请求报文中又称请求报头,里面包含客户端自身的信息和向服务器发送的附加信息。

常见的请求报头有: Accept, Accept-Charset, Accept-Encoding, Accept-Language, Content-Type, Authorization, Cookie, User-Agent等。
Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。

主体

主体在请求报文中又称请求正文,对于GET方法来说他是空的,对于POST,PUT等方法来说,里面是要向服务器发送的数据,请求包头中有些字段与它有关,例如当主体中的数据格式为json时,这是要设置content-type为application/json。

返回响应报文

与请求报文类似,响应报文也由三部分组成:

  • 状态码
  • 响应报头
  • 响应正文

状态码

状态码都是3位的数字,百位上的数字代表了响应的类别,有5中可能的取值:

  • 1**:信息性状态码
  • 2**:成功状态码
    • 200:OK 请求正常处理
    • 204:No Content请求处理成功,但没有资源可返回
    • 206:Partial Content对资源的某一部分的请求
  • 3**:重定向状态码
    -301:Moved Permanently 永久重定向
    -302:Found 临时性重定向
    -304:Not Modified 缓存中读取
  • 4**:客户端错误状态码
    • 400:Bad Request 请求报文中存在语法错误
    • 401:Unauthorized需要有通过Http认证的认证信息
    • 403:Forbidden访问被拒绝
    • 404:Not Found无法找到请求资源
  • 5**:服务器错误状态码
    • 500:Internal Server Error 服务器端在执行时发生错误
    • 503:Service Unavailable 服务器处于超负载或者正在进行停机维护

响应报头

常见字段:server,connection

响应正文

请求返回的文本信息(资源),如html,css,js,图片等文件就存放在里面。

浏览器解析HTML,获取其他静态资源

浏览器对于页面的解析时至上而下的,通过解析html来构建DOM树,当解析到<link>标签或@import时,就会请求服务器获取css文件,在下载的同时浏览器还是会继续向下解析的,但当下载js文件和执行它时,解析器便会停止手头的工作,等待js的操作完成后再向下解析,这便是js的阻塞问题,也是为什么<link>标签可以放在<head>中,而引入的js文件最好放在</body>前面的原因,这样可以避免js阻塞了html的解析而导致页面短时间内无法呈现在用户面前的尴尬情况。

html5中提供了defer和async来实现js外联的无阻塞加载

<link>和@imoprt的区别:

  • link是XHTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持
  • ink支持使用Javascript控制DOM去改变样式;而@import不支持

浏览器渲染页面

渲染树(render树)

前面已经说过,解析html的时候会生成DOM树,而解析css则会生成CSSOM树,前者描述内容,后者描述应用与内容的样式规则。

DOM树和CSSOM结合在一起会构成一棵渲染树,渲染树既包含了页面上所有的可视DOM节点,又包含了CSSOM中每个节点的样式信息。

渲染树的构建步骤:

  • 从DOM树的根节点开始,遍历所有的可视节点,不可视节点有:

    • 脚本标签,元数据标签
    • 应用display:none的元素
  • 对于可视节点,从CSSOM中找到对应的样式规则,附加在节点上

  • 输出可视节点以及每个节点计算出来的样式

布局

通过渲染树,浏览器已经能知道可视内容的样式信息了,但是真正要渲染时,我们还需要获取节点的位置和尺寸,这是布局阶段要做的工作,也成为“回流”(reflow).

布局阶段的输出结果成为“盒模型”(box model),盒模型精确表达了窗口中元素的位置和大小,所有相对的度量单位都会被转化为屏幕上的绝对像素位置。

当以上步骤都完成后,浏览器就能把节点绘制成屏幕上每个真实的像素点了,此阶段为“绘制”或者“重绘”(resterizing)

其实从这里也能引出一个概念了:

回流必定导致重绘,重绘不一定导致回流^_^!

页面渲染的过程中至少会发生一次reflow和repaint,reflow的开销相对与repaint要高得多。一般来说如果一个元素的尺寸发生了改变,会对后面的已渲染的页面造成影响,那么就需要重新计算布局,即回流,而如果只是改变了外观的话,那么只需要进行重绘即可。

举个通用的例子来说明一下重绘与回流:

  1. 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件;
  2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
  4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
  5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
  7. 浏览器发现了一个包含一行JavaScript代码的<script>标签,赶快运行它;
  8. javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。杯具啊,突然就少了这么一个元素,浏览器不得不重新渲染这部分代码;
  9. 终于等到了</html>的到来,浏览器泪流满面……
  10. 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径;
  11. 浏览器召集了在座的各位<div><span><ul><li>们,“大伙儿收拾收拾行李,咱得重新来过……”,浏览器向服务器请求了新的CSS文件,重新渲染页面

对于重绘回流的优化以及js阻塞的解决会在另一篇博客提及。

断开链接(四次挥手)

当浏览器获取到了所有想要的资源并且用户没有发起新的请求之前,为了不让tcp空耗着,我们会选择断开这个tcp链接,断开的主动方可以时服务器也可以是客户端。

假设有client发起终端请求,则过程是这样的:

  • client端发起FIN报文,告诉server端如果数据没发送完,可以不急着关闭socket,继续发送
  • server端收到FIN报文后,回传一个确认包并让client等待(因为自己要确认是否已经发送完数据了),client进入FIN_WAIT状态
  • server确认自己已经发送完所有数据了,可以真正关闭链接了,向client发送FIN
  • client收到FIN后回传一个ACK并进入TIME_WAIT状态
  • server接收到ACK后关闭链接
  • client在2MSL(报文最大生存时间,4分钟)后如果没有收到server发过来的FIN包,证明server端已经关闭成功,那么自己也可以关闭了

需要四次挥手的原因:

当一方主动发起FIN请求是,可能另一方的数据还没发送完,故此只能返回一个ACK让他先等待,只有当自己确认已经发送完所有的数据后才会发起一个FIN来并当接收到一个回传的ACK时结束这个连接,俗称四次挥手。

H5之canvas标签(一)

发表于 2017-06-21

HTML5中添加了很多新的标签,canvas就是其中之一,它被用来进行图形的绘制,关于图形的绘制我们需要通过javascript来完成,canvas标签仅是一个图形的容器。

IE9及其他现代浏览器基本都支持这个标签。

canvas由几组API组成,除了绘制基本图形的2D上下文,还有一个名为WebGL的3D上下文,不过浏览器支持还不够好,

基本用法

使用\时,我们需要设置其宽高,用来指定绘图的区域。出现在开始和结束标签之间的内容作为后备信息,当浏览器不支持该标签时,里面的内容就会显示出来,HTML5中其他标签也是这么干的。
一个简单的canvas实例如下:

1
<canvas width="299" height="199" id="myCanvas">sorry,the canvas isn't supported.</canvas>

这是我们的画布,也是我们绘制的图像的一个载体,要在画布上绘图,我们需要先通过getContext()方法取得绘图上下文:

1
2
3
4
5
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
}

使用toDataUrl()方法可以导出在canvas上绘制的图像。该方法接受一个MIME类型参数,如果我们需要取得画布中的一幅png图像,可以这样做:

1
2
3
4
5
6
7
8
var drawing = document.getElementById("myCanvas");
if(drawing.getContext){
var imageUrl = drawing.toDataUrl("image/png");
//显示图像
var image = document.createElement("img");
image.src = imageUrl;
document.body.append(image);
}

默认情况下,MIME类型为PNG格式。

canvas坐标系

canvas以左上角为原点(0,0),坐标值都以原点为参照进行计算,与background背景图片的位置计算方法相同。第一个值代表离左边框的距离,第二个值代表离上边框的距离。

填充和描边

通过2D绘图上下文提供的方法,我们可以绘制矩形,弧线等2D图形。对于这些图形,我们可以选择绘制的方式。这里有两个属性,来决定是进行填充还是进行描边:fillStyle和strokeStyle

这两个属性的值可以是字符串、渐变对象、或模式对象,默认值都是“#000000”。我们可以用任何的颜色格式(rgb、rgba、hsl、hsla)来定义样式:

1
2
3
4
5
6
7
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.strokeStyle = "green";
context.fillStyle = "#f6c";
}

这样设置后,下面涉及到填充和描边的操作都会使用这两个样式,绘制过程中样式可更改。

绘制矩形

与绘制矩形相关的方法有:fillRect()、strokeRect()、clearRect(),这些参数都接受4个参数:矩形x坐标、矩形y坐标、矩形宽度、矩形高度,单位都为像素。

绘制图形前都应先指定填充或者描边的样式:

1
2
3
4
5
6
7
8
9
10
11
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.strokeStyle = "green";
context.strokeRect(10,10,50,50);
context.fillStyle = "rgba(0,0,0,0.6)";
context.fillRect(100,20,30,30);
}

此外,还有以下属性能够控制线条和线条末端的形状:

  • lineWidth:线条宽度
  • lineCap:用于控制线条末端的形状,取值有butt(平头),round(圆头),square(方头)
  • lineJoin:控制线条相交的方式,取值有round(圆交),bevel(斜交),miter(斜接)

绘制路径

2D绘图上下文提供了很多用于绘制路径的方法。在绘制之前,需要先调用beginPath(),表示要开始绘制新路径。然后再调用下面的方法绘制实际路径。

绘制圆弧

arc(x,y,radius,startAngle,endAngle,conterclockwise):以(x,y)为圆心,radius为半径,起始和结束弧度分别为startAngle,endAngle来画圆弧,counterclockwise为false代表按逆时针计算角度。

我们用这个方法来绘制一个圆形:

1
2
3
4
5
6
7
8
9
10
11
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.beginPath();
context.arc(100,100,40,0,Math.PI*2,true)'
//注意,这里不关闭路径的话会从上个路径继续绘制下个图形
context.closePath();
context.fillStyle = 'rgba(0,0,0,0.6)';
context.fill();
}

canvas1

绘制完一个图形要关闭路径,并且最后需要调用fill()和stroke()才能将图形绘制到画布上。

画圆弧还有另外一个方法,arcTo(x1,y1,x2,y2,radius):传入弧起点和终点的坐标以及弧的半径。

moveTo()和lineTo()

  • moveTo(x,y):将绘图游标移动到(x,y),不画线
  • lineTo(x,y):在上一点和(x,y)之间绘制一条直线

注意:

每次画线都是从moveTo的点到lineTo的点

如果没有moveTo,第一次lineTo的效果和moveTo相同

每次lineTo后如果没有moveTo,下次会从上次lineTo的终点开始画线

结合上面和圆弧来写一个时钟表盘的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.beginPath();
//绘制外圆
context.arc(100,100,80,0,Math.PI*2,false);
//绘制内圆
context.arc(100,100,75,0,Math.PI*2,false);
//绘制分针
context.moveTo(100,100);
context.lineTo(100,35);
//绘制时针
context.moveTo(100,100);
context.lineTo(47,100);
//描边
context.stroke();
}

效果:

cavas2

贝塞尔曲线(bezier)

我们可以通过bezierCurveTo()方法来画贝塞尔曲线,该方法有6个参数:clx,cly,c2x,c2y,x,y。该方法会从上一点到(x,y)之间绘制一条曲线,并以(c1x,c1y)和(c2x,c2y)为控制点。

还有一个跟这个方法类似的方法–quadraticCurveTo(c1x,c2y,x,y),用法相同,区别在于只有一个控制点(c1x,c1y),这是一个二次曲线的绘制方法。

如下例:

1
2
3
4
5
6
7
8
9
10
11
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.beginPath();
context.moveTo(50,50);
context.bezierCurveTo(50, 50,150, 50, 150, 100);
context.quadraticCurveTo(150, 150, 250, 200);
context.stroke();
}

效果:

canvas3

绘制文本

绘制文本主要有两个方法:fillText()和strokeText()。这两个方法都接收4个参数:要绘制的字符串,x坐标,y坐标,最大像素宽度。

同时,这两个方法都以下面3个属性为基础:

  • font:表示文本样式、大小和字体,如”bold 14px Arial“
  • textAlign:表示文本对齐方式,取值有start、end、left、right、center。建议使用start和end,而不是left、right
  • textBaseline:表示文本的基线(垂直对齐方式),取值有top、hanging、middle、alphabetic、ideographic、bottom

这几个属性值都有默认值,fillText()会使用fillStyle来填充文字,strokeText()则使用strokeStyle为文字描边。

绘制文本比较复杂,特别是但我们需要将文本控制在某个区域中时,为此,2d上下文为我们提供了一个确定文本你大小的方法measureText(),返回的是个包含width属性的对象。

现在,如果我们想在一个150px宽度的矩形中绘制“hello canvas!”,我们可以这样做:

1
2
3
4
5
6
7
8
var fontSize = 30;
context.font = fontSize+"px Arial";
while(context.measureText("hello canvas!")>150){
fontSize--;
context.font = fontSize + "px Arial";
}
context.fillText("hello canvas!",10,10);

上面代码会从30像素开始递减,直到文本的宽度小于150px,即找到合适的字体大小。

渐变

canvas中的渐变我们通过canvasGradient实例来实现,生成渐变对象的方法有两个:

  • createLinearGradient(x1,y1,x2,y2):接收渐变的起点(x1,y1)和终点和(x2,y2)
  • createRadialGradient(x1,y1,r1,x2,y2,r2):跟上面方法类似,前三个参数定义一个以(x1,y1)为圆心,r1为半径的圆,后三个参数定义一个以(x2,y2)为圆心,r2为半径的圆

通过上述方法创建好实例后,需要用addColorStop方法来上色,
addColorStop()有两个参数:色标位置以及css颜色值,色标位置介于0到1之间。

可以根据需要添加多个色标。

来看2个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
//创建一个线性渐变实例
var gradient = context.createLinearGradient(50,50,150,150);
gradient.addColorStop(0,'#33cccc');
gradient.addColorStop(1,'#ffccff');
context.fillStyle = gradient;
context.fillRect(50,50,100,100);
//创建径向渐变实例
var radial = context.createRadialGradient(250,100,20,300,150,50);
radial.addColorStop(0,'white');
radial.addColorStop(1****,'black');
context.fillRect(200,50,100,100);
}

效果:

canvas6

阴影

2D上下文提供了几个绘制阴影的属性,这些属性会自动为图形或者路径添加阴影。

  • shadowColor:css颜色值形式的阴影颜色
  • shadowOffsetX:沿x轴的偏移量
  • shadowOffsetY:沿y轴的偏移量
  • shadowBlur:模糊的像素数,默认为0

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
//创建一个线性渐变实例
context.shadowOffsetX = 5;
context.shadowOffsetY = 4;
context.shadowBlur = 5;
context.shadowColor = '#cff8de';
context.fillStyle="#3399ff";
context.fillRect(40,40,100,100);
}

效果:

canvas7

变形

2D绘制上下文支持我们对图像进行绘制变换,变换时我们需要用到变换矩阵,下面的方法会改变变换矩阵,从而导致不同的效果。

save()和restore()

了解变形之前需要先知道两个在绘制图形时必不可少的方法–save()和restore(),他们是用来保存和回复canvas状态的,不需要传入参数。

canvas状态存储在栈中,当save()方法被调用时,当前的状态就会被保存到栈中。

canvas状态包括的内容有:

  • 当前应用的变形(scale,translate,rotate)
  • strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值
  • 当前的裁剪路径(clipping path)

而restore()方法被调用时,则会从栈中弹出上一个保存的状态,恢复所有设定。

translate()

translate(x,y)方法接收新位置的坐标,x是左右偏移量,y是上下偏移量,表示将坐标原点移到(x,y)。

这里需要牢记偏移的是坐标原点,实际上所有变形操作的都是坐标原点。

在变形之前用save()保存状态是一个好的习惯,因为在一个循环中做位移但没有保存和回复canvas状态,最后有些东西会不见,因为它很可能超出了canvas范围之外了。

rotate()

rotate()方法接收一个angle参数,通过这个方法我们可以围绕原点旋转图像。

通过旋转,我们可以绘制有趣的图形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.translate(100,100);
context.translate(100,100);
for(var i=1;i<6;i++){
context.save();
context.fillStyle='rgb('+(51*i)+',255,'+(255-51*i)+')';
for(var j=0;j<i*6;j++){
context.rotate(Math.PI*2/(i*6));
context.beginPath();
context.arc(0,i*12.5,5,0,Math.PI*2,true);
context.fill();
}
context.restore();
}
}

效果:

canvas4

scale()

变形的另外一个方法是scale(x,y),即缩放。它接收两个参数,分别为x轴和y轴的缩放因子,默认都是1.

transform

变形的最后一个方法是transform(m11,m12,m21,m22,dx,dy),这个方法会讲当前的变形矩阵乘上一个基于自身参数的矩阵,各个参数的意义如下:

  • m11:水平方向的缩放
  • m12:水平方向的偏移
  • m21:垂直方向的偏移
  • m22:垂直方向的缩放
  • dx:水平方向的移动
  • dy:垂直方向的移动

setTransform(m11,m12,m21,m22,dx,dy)会将变化矩阵重置为单位矩阵然后再调用transform()。

resetTransform(m11,m12,m21,m22,dx,dy)将当前变形为单位矩阵。

tranlate()、scale()、rotate()三个方法调用的先后顺序不同,最后出现的效果也不同,但是只要记住这三个方法都是操作的坐标轴,就会好理解得多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var drawing = document.getElementById("myCanvas");
//检测浏览器是否支持canvas
if(drawing.getContext){
var context = drawing.getContext("2d");
context.save(); //保存了当前context的状态
context.fillStyle = "#EEEEFF";
context.fillRect(0, 0, 400, 300);
context.fillStyle = "red";
//平移 缩放 旋转
context.translate(100, 100);
context.scale(0.5, 0.5);
context.rotate(Math.PI / 4);
context.fillRect(0, 0, 100, 100);
context.restore(); //恢复到刚刚保存的状态
context.save(); //保存了当前context的状态
context.fillStyle = "green";
//缩放 平移 旋转 2 1 3
context.scale(0.5, 0.5);
context.translate(100, 100);
context.rotate(Math.PI / 4);
context.fillRect(0, 0, 100, 100);
context.restore(); //恢复到刚刚保存的状态
context.save(); //保存了当前context的状态
context.fillStyle = "blue";
//缩放 旋转 平移 2 3 1
context.scale(0.5, 0.5);
context.rotate(Math.PI / 4);
context.translate(100, 100);
context.fillRect(0, 0, 100, 100);
context.restore(); //恢复到刚刚保存的状态
context.save(); //保存了当前context的状态
context.fillStyle = "pink";
//旋转 平移 缩放 3 1 2
context.rotate(Math.PI / 4);
context.translate(100, 100);
context.scale(0.5, 0.5);
context.fillRect(0, 0, 100, 100);
}

效果:

canvas5

(平移,缩放,旋转)和(平移,旋转,缩放)效果一样,(缩放,旋转,平移)和(旋转,缩放,平移)一样

JS之事件(二)

发表于 2017-06-20

键盘与文本事件

对键盘事件的支持主要遵循的是DOM0级,DOM3中制定了新的规范。

键盘事件有三个:

  • keydown:按下任意键时触发,按住不放会重复触发
  • keypress:用户按下键盘上的字符键时触发,按住不放会重复触发
  • keyup:用户释放按键时触发

虽然所有元素都支持这些事件,但一般在文本框输入时才会用到。

文本事件只有一个:textInput,此事件是对keypress的补充,在文本出入文本框之前会触发这事件

按下字符键时,键盘事件的触发顺序如下:


  1. keydown事件

  2. keypress事件

  3. keyup事件

keydown和keypress都是文本框内容发生变化前触发,而keyup则是文本框发生变化后触发的。

如果用户按下的是字符键,那么会先触发keydown事件,然后就是keyup事件。

键盘事件也支持相同的修改键,所以键盘事件的事件对象中也有shiftKey、ctrlKey、altKey和metaKey属性。

键码

在发生keydown和keyup事件时,event对象的keyCode属性中会包含一个键码,该键码的值与数字字母字符键对应的ASCII码相同,如A对应的keyCode值就是65,详细的对应表大家可以百度。

字符编码

IE9+及现代浏览器(除Opera)都支持一个charCode属性,只有在发生keyPress事件时才会包含此属性,值为字符键所对对应的ASCII码或者为0(非字符键)。IE8之前版本和Opera并不支持这个属性,所以我们需要实现跨浏览器的方式来获取字符编码。

1
2
3
4
5
6
7
8
9
var eventUtil = {
'getCharCode':function(event){
if(typeof event.charCode == "number"){
return event.charCode;
}else{
return event.keyCode;
}
}
}

取得字符编码后,就可以用fromCharCode()将其转化为实际的字符。

DOM3级键盘事件

DOM3级事件中作出了一些修改:

  • key:用来取代keyCode,值是一个字符串,在按下字符键时,值是对应的文本字符(区分大小写),按下非字符键时,值是对应键名(如“shift”)
  • char:取代charCode,与key类似,不过按下非字符键时,值为null

IE9支持key但不支持char属性。Safari 5 和Chrome支持KeyIdentifier的属性,于key属性类似,不过当按下字符键时,keyIdentifier的值是一个Unicode值,不再是ASCII码。由于存在跨浏览器的兼容问题,不推荐使用key,char,和keyIdentifier。

HTML5事件

DOM规范并没有包含所有浏览器支持的所有事件,很多浏览器都实现了自定义的事件,为此,html5列出了浏览器应该支持的所有事件。

contextmenu事件

网页中,我们通过右键可以调出上下文菜单,有时我们需要屏蔽这个默认事件,转而使用我们自定义的菜单,这时就需要使用contextmenu事件。此事件是冒泡的,所以我们可以为document指定一个事件处理程序,用以处理页面中触发的所有此类事件。

实际应用中,我们需要结合事件对象的clientX和clietY来定位菜单出现的位置,同时当用户点击菜单时应该隐藏菜单。

beforeunload事件

beforeunload让我们有可能在页面卸载前阻止这一操作,继续使用原有页面。但是我们不能让用户无法离开当前页面,而是应该将决定权交给用户,只是在离开前提示一些信息,询问用户是否真的要离开。

如果要在用户关闭时弹出一个信息框,需要这样做:

1
2
3
4
5
6
EventUtil.addEventHander(window,"beforeunload",function(event){
event = eventUtil.getEvent(event);
var message = '你真的要离开此页面吗?'
event.returnValue = message;//IE及FF做法
return message;//safari及Chrome做法
})

pageshow事件和pagehide事件

在FF和Opera中有一个特性,往返缓存(back-foward cache 或bfcache),能够使用户在后退或者前进时加快页面加载速度,其实他们是把整个网页保存在了缓存中。但是这里会有一个问题,如果我们的网页在bfcache中,那么再次打开着页面是不会触发load事件的,这对于那些需要在页面加载完执行一些事件的页面来说,就有可能会造成页面显示不正确。

而pageshow事件在页面显示时会触发,无论该页面是不是存在于bfcache中。虽然事件的目标是document,但是必须要将其监听程序绑定在window上。

pageshow事件对象还包含一个persisted属性,如果值为true,则表示页面存放在bfcache中。

pagehide事件会在unload事件之前触发,如果页面在卸载之后是存放在bfcache中,那么persisted值为true。

IE9+及现代浏览器都支持这两个事件。

触摸与手势事件

现如今,移动端已然成为了除PC端外另一个网络流量的汇聚地,其中ios和Adroid最为耀眼,但是这些设备没有鼠标也没有键盘,那么我们要怎么监听用户的操作呢?移动端其实主要还是通过用户的手指触摸(touch)来触发事件,为此,html5提供了touch系列的事件来实现我们的目的。

  • touchstart:当手指触摸屏幕时触发,即时已经有手指放在屏幕上也会触发
  • touchmove:当手指在屏幕滑动时连续触发
  • touchend:手指从屏幕移开时触发
  • touchcancel:系统停止跟踪触摸是触发

这些事件都是会冒泡,且event对象中也都包含常见的属性:clientX,clientY,bubbles,cancelable,detial,screenX,screenY等

除此之外,触摸事件还支持三个用于跟踪触摸的属性:

  • touches:表示当前跟踪的触摸操作的touch对象的数组
  • targetTouchs:特定于事件目标的touch对象的数组
  • changeTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组

触摸事件和鼠标事件的触发顺序如下:


  1. touchstart

  2. mouseover

  3. mousemove

  4. mousedown

  5. mouseup

  6. click

  7. touchend

关于js事件较常用的都已经介绍完了,如果想更加详尽的学习,可以看javascript高级程序设计这本书,后面可能会通过一些例子或者项目将这些知识结合起来,敬请期待!

JS之事件(一)

发表于 2017-06-19

事件,像click,load,mouseover都是用户或者浏览器执行的某种动作,都会触发某些事件。在触发事件的时候,我们需要有一个事件监听函数(事件处理程序)来响应事件。事件监听函数的名字都是“on”+事件名组成的,像click的事件监听函数就是onclick。

HTML事件监听程序

对于元素支持的每种事件,都可以在元素中指定一个与事件监听函数同名的特性。特性的值是要执行的js代码,比如下面的例子:

1
<input type="button" value="clicked me" onclick="console.log('clicked')"/>

如果函数体内容多的话,需要把js代码放到javascript标签内,如下:

1
2
3
function showMessage(){
console.log(‘clicked’);
}

1
<input type="button" value="clicked me" onclick="showMessage()"/>

但是这样指定事件监听函数让得HTML与Javascript紧密耦合,如果需要更换事件监听函数,那么HTML和javascripot代码都需要修改,所以为什么不干脆全部使用js来绑定和声明事件监听函数呢!

js事件监听程序

DOM0级事件处理程序

每个元素都有自己的事件处理程序属性,通常事件名都为小写,如:onclick,onmouseover,在绑定处理函数时需要先获得元素的引用,直接看一个简单的例子吧:

1
2
3
4
5
var btn = doucment.getElementById('button1');
btn.onclick = function(){
console.log(this.id);//button1
}

程序中的this指向的是当前元素,即btn,通过点取的方式,我们可以访问元素的任何属性和方法。

DOM2级事件处理程序

“DOM2级事件”定义了两个用于绑定事件和移除事件的方法:addEventListener()和removeEevntListener(),这两个方法在之前冒泡和捕获那一节已经介绍过了,就不过多说明了。

这两个方法都接收三个参数:

  • 事件名(不带on)
  • 事件处理函数
  • 是否捕获

addEventListener()的优势在于可以为同一元素绑定多个方法,执行顺序由绑定先后决定。

在移除时传入的参数要与绑定时相同,这也意味着不能移除匿名函数

IE事件处理程序

IE实现两个类似的方法:attachEvent()和detachEvent(),之前同样也介绍过,下面就说一下特别的地方。

这两个方法都接收两个参数:

  • 事件名
  • 事件处理函数

DOM0级方法中事件处理程序会在元素的作用域内进行,而使用这两个方法时程序会在全局作用域中运行,因为this指向window。

和addEventListener()类似,attachEvent()可以为元素绑定多个方法,不过是后绑定的先运行。

跨浏览器的事件处理程序

为了让我们的代码可以在所有浏览器正常运行,我们可以自己编写一个兼容所有浏览器的事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var EventUtil = {
'addHandler':function(element,type,handler){
if(element.addEventListener){
element.addEventListener(type,handler,false);
}else if(element.attachEvent){
element.attachEvent("on"+type,handler);
}else{
element["on"+type] = handler;
}
}
'removeHandler':function(element,type,handler){
if(element.removeEventListener){
element.removeEventListener(type,handler,false);
}else if(element.detachEvent){
element.detachEvent("on"+type,handler);
}else{
element["on"+type] = null;
}
}
}

事件对象

这算一个比较重要的点了,我们触发某个事件时,会产生一个事件对象event,其中包含着所有与时间有关的信息,如事件的类型,事件的目标(事件委托应用到),以及与特定事件相关的信息。例如,鼠标事件中,有包含鼠标位置的信息,键盘事件中则包含于按键相关的信息。浏览器虽然都支持event对象,但支持方式不尽相同。

DOM中的事件对象

当触发了事件时,兼容DOM的浏览器都自动会给我们的事件处理程序传入一个event对象,我们要做的只是在声明监听函数的时候加上一个event参数以便浏览器传入就ok了,如下:

1
2
3
4
5
6
btn.onclick = function(event){
...
}
btn.addEventListener = function(event){
...
}

下面列举type的属性及方法:

  • type:事件类型
  • bubbles:表明事件是否可以冒泡
  • cancelable:表明是否可以阻止默认事件
  • currentTarget:事件处理程序当前正在处理的元素,通常用target较多
  • preventDefault():取消事件的默认行为,需要cancelable属性为True方可用,可用来定制自己的处理行为
  • stopPropagation():取消事件的进一步捕获或者冒泡,需要bubbles为True方可用
  • target:事件的真正目标
  • eventPhase:确定事件当前正处于事件流的哪个阶段,1代表处于捕获阶段,2代表处于目标对象上,3代表处于冒泡阶段

在事件处理程序内部,this始终等于currentTarget的值,如果不使用事件委托,那么this,currentTarget,target的值相同.

IE中的事件对象

访问IE中的event的方式有几种,取决于绑定监听函数的方式。

如果是使用DOM0级方法添加的事件监听函数,那么event对象会作为window对象的一个属性存在。

1
2
3
4
5
var btn = document.getElementById("button1");
btn.onclick = function(){
var event = window.event;
console.log(event.type);
}

如果通过attachEevent方法来绑定监听函数,那么event对象会自动传入我们我们的事件处理函数中。

1
2
3
4
var btn = document.getElementById("button1");
btn.attachEvent("onclick",function(event){
console.log(event.type);
})

同样,IE下的event对象也有一些属性和方法,大多都与DOM下的event属性方法对应:

  • cancelBubble:取消冒泡,默认为false,与stopPropagation()对应
  • returnValue:默认为true,置为false可阻止默认事件,与preventDefault()对应
  • srcElement:与Dom中的target属性对应
  • type:同DOM下的type

IE中事件处理程序中的this会随绑定方式的不同而变化,所以最好使用srcElement比较稳妥。

跨浏览器的事件对象

下面来实现事件对象的兼容写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var eventUtil = {
getEvent:function(event){
return event?event:window.event;
}
getTarget:function(event){
return event.target || event.srcElement;
}
preDefault:function(event){
if(event.preventDefault){
event.preventDefault();
}else{
event.returnValue = false;
}
}
stopPropagation:function(event){
if(event.stopPropagation){
event.stopPropagation();
}else{
event.cancelBubble = true;
}
}
}

事件类型

介绍完基本的事件监听函数和事件对象,接下来就应该介绍种类繁多的事件类型及应用了。

DOM3级事件规定了一下事件:

  • UI(user Interface)事件,用户与元素交互时触发
  • 焦点事件,元素获取或失去焦点时触发
  • 鼠标事件,通过鼠标执行操作时触发
  • 滚轮事件,使用滚轮时触发
  • 文本事件,在文档中输入文本时触发
  • 键盘事件,在键盘上执行操作触发
  • 合成事件,在IME(输入法编辑器)输入字符触发
  • 变动事件,底层DOM结构发生变化时触发

包括IE9在内的主流浏览器斗殴支持DOM2级事件,IE9也支持DOM3事件

UI事件

UI事件指的并不一定是与用户操作有关的事件,较常用的有:

  • load:页面完全加载后在window上触发,当所有框架加载完在框架集上触发,当图像加载完毕是在img元素上触发
  • unload:与上面类似,只不过是在写在的时候触发
  • select:用户选择文本框中的字符时触发。
  • resize:当窗口或框架大小发生改变时触发
  • scroll:用户滚动带滚动条的元素中的内容时触发

这些事件都可以用HTML事件处理程序的方式实现。

根据DOM级事件规范,我们应该在document上处理load事件,但实际上所有浏览器在window上都实现了该事件,确保向后兼容。

焦点事件

焦点事件与document.hasFocus()及document.activeElement属性配合,可以知道用户在页面上的行踪。

焦点事件有以下这些:

  • blur:元素失去焦点时触发,不会冒泡
  • focus:元素获取焦点是触发,不会冒泡
  • focusin:元素获取焦点时触发,冒泡,除了FF外基本都支持(DOM3)
  • focusout:元素失去焦点触发,冒泡,除了FF外基本都支持(DOM3)
  • DOMFocusOut:只有Opera支持,与focusout等价
  • DOMFocusIn:只有Opera支持,与focusin等价

在页面中当焦点从一个元素移到另一个焦点上时,事件的触发顺序是这样的:


  1. focusout在失去焦点的元素上触发

  2. focusin在获得焦点的元素上触发

  3. blur在失去焦点的元素上触发

  4. DOMFocusOut在失去焦点的元素上触发

  5. focus在获得焦点的元素上触发

  6. DOMFoucusIn在获得焦点的元素上触发

要确定浏览器是否支持dom3的焦点事件,可以用以下代码

1
var isSupported = document.implementation.hasFeature("focusEvent","3.0");

鼠标和滚轮事件

网页中用户大多的操作行为都是通过鼠标来实现的,DOM3级中实现了9个鼠标事件:

  • click:用户单击鼠标左键和enter键时触发
  • dblclick:用户双击鼠标左键时触发,DOM3将它纳入了标准
  • mousedown:用户按下鼠标按钮还没放开时触发
  • mouseup:用户释放鼠标按钮时触发
  • mouseenter:鼠标光标首次移进目标元素内触发,此事件不冒泡,移进后代元素不会触发,DOM3将它纳入了标准
  • mouseoutleave:鼠标光标移到元素范围之外时触发,事件不冒泡,移进后代元素不会触发,DOM3将它纳入了标准
  • onmouseover:鼠标光标首次移进目标元素内触发,移进后代会触发
  • onmouseover:鼠标光标移出目标元素内触发,移进后代会触发

总结:除mouseenter和mouseleave外,其他函数都会冒泡,也可以被取消默认行为。只有相继触发mousedown和onmouseup才会触发click事件。连续两次触发click才会引发dblclick事件。

这四个事件触发的先后顺序如下:


  1. mousedown

  2. mouseup

  3. click

  4. mousedown

  5. mouseup

  6. click

  7. dblclick


要确定浏览器是否支持DOM3和DOM3的鼠标事件,可以用以下代码

1
2
var isSupported2 = document.implementation.hasFeature("MouseEvents","2.0");
var isSupported3 = document.implementation.hasFeature("MouseEvent","2.0");

滚轮事件

鼠标能触发的还有一个滚轮事件,mousewheel,其实之前的博客中已经做了详细的介绍了。

鼠标事件中事件对象具有以下属性:

  • clientX:事件发生时鼠标在视口中的水平坐标
  • clientY:事件发生时鼠标在视口中的垂直坐标
  • pageX:事件发生时鼠标在页面中的水平坐标(包含了滚动的距离)
  • pageY:事件发生时鼠标在页面中的垂直坐标(包含了滚动的距离)

    以pageY为例,使用clientY和滚动信息就可以计算出pageY,这里我们需要通过document.body(混杂模式)和document.documentElement(标准模式)中的scrollLeft和scrollTop。

    pageY = event.clientY + document.body.scrollTop || document.documentElement.scrollTop

  • screenX:事件发生时鼠标距离屏幕左边的距离
  • screenY:事件发生时鼠标距离屏幕上方的距离
  • 修改键:

    • 有时我们会结合键盘和鼠标来修改鼠标事件,这时就要使用类似Shift、Ctrl、Alt和Meta之类的修改键了,有4个表示这些修改键状态的属性:
      • shiftKey
      • ctrlKey
      • altKey
      • metaKey

mousewheel事件触发后,会冒泡到document或者window,此外,该事件对应的事件对象中有一个wheelDelta属性,当滑轮向上滚是,该属性值是120的倍数;向下滚时,该属性值是-120的倍数.
FF(fireFox)还支持一个DOMMouseScroll事件,与mousewheel类似,只不过该事件的信息存放在event.detail(Opera也是)中,属性值为-3的倍数时,为滑轮向前滚;为3的倍数时,为滑轮向后滚。

为避免篇幅过长,其它事件在下一篇博客进行介绍。

123
Liby

Liby

22 日志
11 标签
github
  • CNZZ
  • 百度统计
  • leancloud
  • Google分析
  • Next主题配置文档
  • Hexo官方配置文档
© 2017 Liby
由 Hexo 强力驱动
主题 - NexT.Mist