Chiwent


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • works

  • 搜索

未命名

发表于 2018-05-17 | 更新于: 2018-05-17

异步操作

JavaScript的执行环境是“单线程”的,一次只能执行单个任务,如果有多个任务要处理,那么后续任务需要等待前一个任务完成才能执行。

这样的运行模式运行起来简单,但是如果前面的任务假死了,那么后面的任务就长时间无法执行了。所以我们引入了”同步模式“和”异步模式”的执行模式。

异步操作的一个典型例子就是ajax,当发起ajax请求并获取到服务端的数据后,再通过返回的结果做出相应的处理。

对比“同步模式”和“异步模式”,其实简要的概括就是代码顺序和任务执行顺序是否一致

  • 同步模式:后面的任务等待前面的任务执行完毕,然后再执行,执行顺序和所有任务的排列顺序一致。
  • 异步模式:每个任务都有一个或多个回调函数,前面的任务运行结束后不会马上执行后面的任务,而是先执行前面的那个任务的回调,后面的任务则不等前面的任务结束就执行。这样看起来,所有的任务执行顺序和排列顺序是不一致的。

同步任务在主线程上排队执行的任务。

异步任务不马上进入主线程,进入任务队列。只有等主线程任务执行完毕,任务队列通知主线程某个异步任务可以执行了,该异步任务才会进入到主线程执行。


异步任务通常可以分为两大类:I/O 函数(AJAX、readFile等)和计时函数(setTimeout、setInterval)

其实都是带回调

传统模式下的异步编程,主要是靠回调函数、事件监听来完成,后来引入了Promise,我们可以避免了传统回调函数的回调地狱,再后来引入了async和await,我们就可以避免Promise带来的多个then方法。

回调函数

基本的样子:

1
2
3
4
5
6
7
step1(function(result1){
step2(function(result2){
step3(function(result3){
//...
})
})
})

如果回调多了就看起来很乱。

setTimeout

一个demo:

1
2
3
4
5
6
for (var i=1;i<=2;i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
console.log(i);

结果是先输出一个2,等待1s后输出两个2,原因:

  • i作用域在setTimeout回调函数内部,一旦脱离该作用域即失效,所以只保留最后的结果
  • 根据前面已知,循环结束后,i等于2,输出也就是2
  • JavaScript事件处理器在现场空闲之前不会运行,setTimeout里的回调是异步任务,它加入到任务队列中,当执行完外层的输出(也就是主线程执行完毕),任务队列里面的任务才会执行,所以这才紧接着输出两个2


另一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var aa;
var test1 = () => {
setTimeout(() => {
aa = 1
console.log(aa)
aa += 1
},0)
// console.log('y') // 它不会加到任务队列
}
var test2 = () => {
if (aa !== undefined)
console.log(aa);
else
console.log('oops')
}
test1();
test2();

上述结果先输出oops,然后输出1,也就是先输出了test2内部的内容,然后在输出test1的,由于变量aa是在test1中赋值的,所以test2在没有取得aa的准确值前都是undefined

这段代码的运行流程是这样的:首先test1的setTimeout内的回调投入到任务队列中,主线程发现还有个test2任务要处理,那么先处理test2,test2没有获取到变量值,输出了oops,然后主线程没有什么任务要处理了,任务队列就通知它那就把我的事情解决了吧,此时就调用test1搁置在队列中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var aa;
var test1 = (fn) => {
setTimeout(() => {
aa = 1
console.log(aa)
aa += 1
fn()
},0)
// console.log('y')
}
var test2 = () => {
if (aa !== undefined)
console.log(aa);
else
console.log('oops')
}
test1(test2);

这样,分别输出1和2

test1含有setTimeout,那么setTimeout内部的回调任务会加到任务队列中,然后在运行的时候发现除了test1之外也没有其他占用主线程的任务了(test2已经作为了test1的回调了,当然也是投入到任务队列中),任务队列就通知主线程请求执行任务。

事件监听/订阅、发布

同样的,如果是事件监听,其内部的回调也会被加到任务队列里面,所以要等到主线程的其他任务都处理完了才会处理其中的回调。


注意:CSS动画不算在主线程任务里面,就像下面的例子那样,在变色还没结束前点击目标dom还是会执行事件监听内部的回调的

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
// html部分
<body>
<div id="div1"></div>
<div id="div2"></div>
</body>

// css部分
#div1 {
width: 100px;
height: 100px;
background-color: #ff0000;
}
#div2 {
width: 200px;
height:200px;
background-color: #32442d;
animation: change 5s linear;
}
@keyframes change {
0%{
background-color: #435523;
}
50%{
background-color: #ff0000;
}
100%{
background-color: #000;
}
}

// js部分
document.getElementById('div1').addEventListener('click',() => {alert('div1')});
document.getElementById('div2').className = 'test';

Promise

Promise对象可以传入一个函数作为参数,这个函数又可以传入两个参数,分别是resolve和reject,分别代表异步操作执行成功后的回调函数和失败后的回调函数

Promise在实例化之后会马上运行,可以将实例化部分代码封装在一个函数中,比如:

1
2
3
4
5
6
7
8
9
10
function test() {
return new Promise((resolve, reject) => {
// do something
if(true) { // 事件处理成功
resolve(data);
} else { // 处理失败
reject(data);
}
})
}

then方法

1
2
3
test().then((data) => {
console.log(data); // 可以使用前面异步操作的数据
})

当test执行完毕后调用then方法,then就相当于回调函数

Promise 有三种状态:pending(进行中)、fulfilled(成功)和 rejected(已失败)


3种状态

其中,pending是不定态,它可以转变为其他两个状态中的任意一个,fulfilled和rejected都是最终的确定态,一旦pending转变为确定态,那么就不会再发生状态转变。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test() {
return new Promise((resolve, reject) => {
let n = Math.ceil(Math.random() * 10);
if (n < 5) {
resolve(n);
} else {
reject('out of range');
}
})
}
test().then((n) => {
console.log(n);
}, (reason) => {
console.log(reason);
})

如果随机数小于5,那么将输出这个随机数;如果大于5,那么输出out of range

catch方法

catch方法实际上就等同于promise.then(undefined, onRejected),也就是用来处理失败回调函数失败后的回调函数,并且,还能即使捕获resolve回调中的错误,一旦出错,不会阻塞,立马执行catch中的回调。

catch捕获机制

比如前面的方法我们可以改写一下:

1
2
3
4
5
6
7
test().then((n) => {
console.log(n);
}, (reason) => {
console.log(reason);
}).catch((err) => {
console.log(err);
})

then本身也可以传入失败的回调,那么和用catch有什么区别?

  • 使用promise.then(onFulfilled, onRejected)的话,在 onFulfilled中发生异常的话,在onRejected中是捕获不到这个异常的。
  • 在promise.then(onFulfilled).catch(onRejected)的情况下
    then中产生的异常能在.catch中捕获
  • .then和.catch在本质上是没有区别的,需要分场合使用。

all方法

它接收一个数组参数,数组每一项返回的都是promise对象,只有数组内所有元素都执行完才会进入then回调(如果有其中一项出现了失败,那么最终的结果就是失败的),然后每一项返回的数据都会以一个数组的形式传递到then回调中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 1000);
})
.then(result => result)
.catch(err => err);

const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 1000);
})
.then(result => result)
.catch(err => err);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(err => console.log(err));

上述代码在3s后输出['p1','p2']

一个更加明显的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function timeDelay(delay) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(delay);
}, delay);
});
}
var start = Date.now();
Promise.all([
timeDelay(1000),
timeDelay(2000),
timeDelay(3000),
timeDelay(4000)
]).then((val) => {
console.log(Date.now() - start + 'ms');
console.log(val);
});

最后在4s后输出[1000,2000,3000,4000]。由此可以看出,传递给 Promise.all 的promise并不是一个个的顺序执行的,而是同时开始、并行执行的。如果是串行处理,那么等待时间就是总和,即为10s。

race方法

它和all方法很像,不同之处在于all方法需要每一项都返回了成功结果才会执行then,而race则是只要其中任意一项返回成功即执行then回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1');
}, 1000);
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2');
}, 3000);
})
.then(result => result);

Promise.race([p1, p2])
.then(result => console.log(result));
// 等待1秒后输出 'p1'

这样看起来,如果只要并行任务中一旦有其中一项运行完成,不等待其他任务结束就直接调用了then

Generator函数

使用generator函数可以避免promise带来的一大堆then方法

基本的格式:

  • function关键字和函数名之间有一个*
  • 函数内部用yield,定义不同的内部状态

demo:

1
2
3
4
5
6
function* g() {
yield 'a';
yield 'b';
return 'end';
}
var gen = g();

每次调用gen.next()的时候,分别输出a、b、end;
后面再调用就输出undefined

循环输出20以内斐波拉切数:

1
2
3
4
5
6
7
8
9
10
11
function* fib() {
let [prev, curr] = [0, 1];
for(;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fib()) {
if (n > 20) break;
console.log(n);
}

逐行读取文本文件:

1
2
3
4
5
6
7
8
9
10
11
12
function* reader() {
let file = new FileReader("text.txt");
try {
while(!file.eof) {
yield parseInt(file.readline(), 10);
}
} catch(e) {
console.log(e);
} finally {
file.close()
}
}

这部分内容不细说,更详细内容可以看:

阮一峰 Generator 函数的语法

async/await

async/await就是generator函数的语法糖

如何体现?

比如这个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var plugin = require('xxx');
var doSomethings = ((args) => {
return new Promise((resolve, reject) => {
plugin.Func(args, (data, error) => {
if (error)
reject(error);
resolve(data);
});
});
});
var gen = function* (){
var f1 = yield doSomethings('a');
var f2 = yield doSomethings('b');
}

如果是写成async:

1
2
3
4
5
var plugin = require('xxx');
var asyncFunc = async function() {
var f1 = await plugin.Func('a');
var f2 = await plugin.Func('b');
}

对比一下前后的区别,async就是把generator函数的*替换为async,将yield替换为await

它的实现,就是将generator函数和自执行器放在一个函数内

1
2
3
4
5
6
7
8
9
async function func(args){ 
//...
}
// 等同于
function func(args) {
return fn(function* (){
//...
});
}

这里的fn函数就是执行器
,其实其内部也是返回了一个Promise对象

你可以这样测试:

1
2
3
4
5
async function test() {
return 'test'
}
var res = test();
console.log(res);

结果将打印一个Promise对象。

如果在async函数中返回一个直接量,那么async会将其通过Promise.resolve()封装为Promise对象

所以,事实上,async/await也可以用类似Promise那样的then方法:

1
2
3
4
5
6
7
async function test(arg) {
console.log('result is', arg)
}

test(2).then(() => {
console.log('end')
})

如果async没有返回值,那么将返回Promise.resolve(undefined)

因为其内部就封装了Promise,所以有着和Promise一样的特点–无等待,在没有await的情况下执行async,它会被立即执行,返回一个Promise对象,并且不会阻塞后面的代码。

说完了async,再说说await。await在使用时要封装在一个async函数内。前面提到async返回一个promise,await就在等待一个表达式,注意,这个表达式不一定是async返回的promise,也可以是其他值:

1
2
3
4
5
6
7
8
9
10
11
12
13
function test1() {
return "test1"
}
async function test2() {
return "test2"
}
async function test3(){
const t1 = await test1();
const t2 = await test2();
console.log(t1,t2)
}
test3();
// test1 test2

从上可以看出,假设await等到的不是一个promise对象,那么它的运算结果就是它接收到的返回值;如果它等到的是一个promise对象,那么它就阻塞后面代码,等着promise的resolve方法,然后获取到resolve处理的返回值后再做处理

一个实际点的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test');
}, 2000);
});
};
var test = async () => {
var t = await fn();
console.log(t);
};
test();
console.log('out');

在上述代码中,先是打印出out,等待2s后输出test

在test函数中,t取得了fn中的值,并且通过await阻塞了后面代码(console.log(t))的运行,直到fn这个异步函数的执行完毕,才执行了后面的代码

看看它相比较promise做了什么优化:

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

function takeTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n+200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeTime(n);
}

// Use Promise
function doit(){
console.log('begin');
var time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.log('end');
});
}
doit();

// Use async/await
async function doit2() {
console.log('begin');
var time1 = 300;
var time2 = await step1(time1);
var time3 = await step2(time2);
var result = await step3(time3);
console.log(`result is ${result}`);
console.log('end');
}
doit2();

可以看见,如果用async/await就可以避免promise那一段很长的then,看起来就像是同步的编程方式

优点

  • 1)内置执行器

    async不信generator函数那样,它自带执行器,可以像普通函数那样使用
  • 2)语义化好,比起generator函数的yield和*,ayanc/await编程更像是同步操作
  • 3)适用性更广。yield后面只能是thunk函数或者Promise对象,而async后面可以跟Promise对象和原始类型值(数值、字符串和布尔值)



参考:

浏览器事件循环机制(event loop)

Javascript异步编程的4种方法

谈一谈几种处理 JavaScript 异步操作的办法

JavaScript 异步编程学习笔记

js中的同步和异步的个人理解

Promise迷你书

Promise使用手册

Promise的个人理解及实践

ES6 Promise介绍

阮一峰 Generator 函数的语法

[译] 如何在 JavaScript 中使用 Generator?

AlloyTeam ES6 generator介绍

理解 JavaScript 的 async/await

async 函数的含义和用法

未命名

发表于 2018-05-17 | 更新于: 2018-05-16

数组的拓展

Array.from

Array.from方法用于将两类对象转换为真正的数组:类似数组的对象和可遍历对象(包括ES6新增的Set和Map)。所谓的类似数组的对象,本质特征只有一点,即必须具有length属性,任何有length属性的对象,都可以通过该方法转换为数组。只要部署了iterator接口的数据结构,都可以用该方法转换为数组

类数组对象

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arrLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
}

// ES5
var arr1 = [].slice.call(arrLike);
// result: ['a','b','c']

// ES6
let arr2 = Array.from(arrLike);
// result: ['a','b','c']

实际上的应用例子:

通过getElementsByClassName等方法获取的NodeList集合,以及函数内部的arguments对象,都可以用该方法将其转换为真正的数组

可遍历对象

demo:

1
2
3
4
5
6
Array.from('hello');
// ['h','e','l','l','o']

let newSet = new Set(['a','b'])
Array.from(newSet)
// ['a','b']

回调

Array.from可接受第二个参数,它是一个回调函数,作用类似map方法,用来处理每个元素,然后将处理后的值放入返回的数组:

1
2
3
4
Array.from(arrLike, x => x+=1);

// 类似于
Array.from(arrLike).map(x => x+=1);

如果回调函数内用到了this关键字,那么还可以传入第三个参数,用来绑定this。

另外一个技巧

通过扩展运算符也可以将某些带遍历器接口的对象转换为数组:

1
2
3
4
5
function func() {
var args = [...arguments];
}

[...document.getElementsByClassName('test')]

polyfill

1
2
3
const toArray = (() => {
Array.from ? Array.from : obj => [].slice.call(obj)
})();



Array.of()

该方法可以将一组数据转换为数组

demo:

1
Array.of(1,2,3) // [1,2,3]

Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同导致的重载,它总是能返回参数值组成的数组,如果没有参数,就返回一个空数组

polyfill

1
2
3
const arrayOf = (() => {
return [].slice.call(arguments);
})



copyWithin()

数组实例的copyWithin方法,可以在当前数组内部将指定位置的成员赋值到其他位置(会覆盖原有成员),然后返回当前数组。

参数:

  • target(必须):从该位置开始替换数据
  • start(可选):从该位置开始读取数据,默认为0,负数表示为倒数
  • end(可选):结尾,默认等于数组长度,负数表示倒数

上述参数应该为数值,如果不是,会自动转换

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[1,2,3,4,5].copyWithin(0,3)
// [4,5,3,4,5]
// 从3好位置知道数组结束的成员(4,5)复制到从0号位置开始的位置,覆写了原有的1,2

[1,2,3,4].copyWithin(0,2,3)
// [3,2,3,4]

[1,2,3,4].copyWithin(1,2,3)
// [1,3,3,4]

[1,2,3,4].copyWithin(0,-1,-2)
// [3,2,3,4]

// 兼容写法
[].copyWithin.call(new Int32Array([1,2,3,4,5]), 0,3,4);
// Int32Array [4,2,3,4,5]


find()和findIndex()

数组实例的find方法找出第一个符合条件的数组成员,它的参数是一个回调函数,所有数组成员一次执行该回调,直到找出第一个返回值为true的成员,然后将其返回。如果没有符合条件的成员,则返回undefined。

1
2
[1,2,3,4].find((n) => n>2)
// 3

数组实例的findIndex方法返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1.

1
2
3
4
[1,2,3,4].findIndex((value,index,arr) => {
return value > 2;
})
// 2

对indexOf的补充

这两个方法都可以发现NaN:

1
2
3
4
5
[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0


fill()

fill方法使用给定值填充数组

1
2
3
4
5
[1,2,3].fill(5)
// [5,5,5]

new Array(3).fill(1)
// [1,1,1]

由上可见,fill方法用在空数组的初始化非常方便。填充的时候会覆写原有的元素。该方法还可以接收第二个和第三个参数,用于指定填充位置的始末。

1
2
3
4
5
[1,2,3].fill(5,1,2)
// [1,5,3]

[1,2,3].fill(5,3,5)
// [1,2,3] 理解填充的意思,越界了就不是填充


keys()、values()、entries()

他们都是用来遍历数组,返回的都是一个遍历器对象。keys()是对键名的遍历,values()是对键值的遍历,entries()是对键值对的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for(let index of ['a','b'].keys()) {
console.log(index)
}
// 0
// 1

for(let elem of ['a','b'].values()) {
console.log(elem)
}
// 'a'
// 'b'

for(let [index,elem] of ['a','b'].entries()) {
console.log(index,elem)
}
// 0 'a'
// 1 'b'

let iter = ['a','b','c'];
let entries = iter.entries();
console.log(entries.next().value); //[0,'a']


includes()

返回一个布尔值,表示某个数组是否含有指定值,它支持判断NaN,类似字符串的includes方法

可以设置第二个和第三个参数,分别代表起始

1
2
3
[1,2,3].includes[2]; // ture
[1,2,NaN].includes[NaN]; // true
[1,2,3].includes[3,-1]; // true

polyfill

1
2
3
4
5
6
const contains = (() => {
Array.prototype.includes
? (arr,value) => arr.includes(value)
: (arr,value) => arr.some(el => el === value)
})();
contains(['a','b'],'test'); // false


原生js实现ajax

发表于 2018-04-24 | 更新于: 2018-05-10

原生js实现ajax

ajax创建的步骤


  1. 实例化XMLHttpRequest(或ActiveXObject)对象

  2. 连接服务器

  3. 发送请求

  4. 接收服务器响应数据

  5. 根据响应的状态码进行做不同的处理

其中,在第4个步骤中需要引入一个回调函数,在接收到服务器响应的状态码时进行判断和处理

1.创建ajax对象

1
2
3
4
5
6
var xhr = null;
if(window.XMLHttpRequest){
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP'); //兼容IE6
}

2.连接服务器

主要用到了open函数,它的三个参数分别是:

请求方式、请求地址、是否设置异步请求(true表示异步,是默认值,false表示同步,同步请求的情况很少)

1
2
3
xhr.open("GET",url + ? + params,true);  //GET方式

xhr.open("POST",url,true); //POST方式

3.发送请求

请求的方式:

GET请求是通过将数据拼接到URL实现的,POST则是将数据作为发送请求的参数提交到服务器

POST在发送请求(send)之前要先设置表单提交的内容类型

GET提交信息时将请求内容拼接在URL中,所以send传入的参数为空(或者可以是null),POST就必须要把请求参数作为send的参数

1
2
3
4
xhr.send();  //GET方式

xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); //POST方式
xhr.send(params);

完整点的demo:

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
function ajax(url, method, data) {
var xhr = null;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
alert("你的浏览器不支持ajax");
}
}
}
xhr.onerror = function(e) {
console.log(e);
}
xhr.open(method, url);
try {
setTimeout(function() {
xhr.send(data);
});
} catch(e) {
console.log('error:',e);
}
return xhr;
}

关于params

params是提交到服务器的参数,它必须经过encodeURIComponent()进行编码,每次发送请求时都会在参数列表中拼接类似v=xx的字符串,这样是为了拒绝生成缓存,每次都直接将请求发送到服务器

具体实现:

1
2
3
4
5
6
7
8
9
10
function formatParams(data){
var arr = [];
for(var name in data){
arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
}
arr.push(("v=" + Math.random()).replace("."));
return arr.join("&");
}

var params = formaxtParams(data);

4.接收响应

  • responseText:获取字符串形式的响应数据,只读
  • responseXML:获取XML形式的响应数据,只读
  • status和statusText:以数字和文本形式返回HTTP状态码,只读
  • responseType:响应类型,缺省为空字符串,可以取arraybuffer、blob、document、json、text
  • responseURL:返回ajax请求最终的url,如果有请求过程有发生重定向,则返回重定向后的url
  • getAllResponseHeader():获取所有响应报头
  • getResponseHeader():查询响应中某个字段的值

readyState属性:

1
2
3
4
5
0:请求未初始化,open还未调用
1:服务器连接已经建立,open已经调用
2:请求已经接收,接收到头信息
3:请求处理中,接收到响应主体了
4:请求完成,响应就绪

status状态码:

1
2
3
4
5
1xx: 信息响应类,接收到请求并继续处理
2xx:处理成功响应类,动作被成功接收、理解和接受
3xx:重定向响应类,为了完成指定的动作,必须接受下一步处理
4xx:客户端错误,客户请求包含雨大错误或者是不能正确执行
5xx:服务端错误,服务器不能正确执行一个正确的请求

响应事件

onreadystatechange事件


该事件的回调方法在readyState状态改变时触发,在一个收到响应的ajax请求周期中,onreadystatechange方法会被触发4次。因此可以像下面那样处理:

1
2
3
4
5
6
7
xhr.onreadystatechange = function(e) {
if (xhr.readyState == 4) {
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.responseText);
}
}
}

较为完整的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var xhr = false;
if (XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
};
if (xhr) {
xhr.open("GET","./data.json",true);
xhr.send();

xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText).name);
// 解析取到的json数据
}
}
}



onloadstart事件:

该事件回调在ajax请求发送前触发,触发的节点在readyState==1之后,readyState==2之前

它还默认传入一个ProgressEvent事件进度对象,包含了3个只读属性:

  • lengthComputable: boolean,默认false,表示长度是否可计算
  • loaded:表示已加载资源大小。若是http下载资源,不包括headers信息
  • total:表示资源总大小。若是http下载资源,不包括headers信息



onprogress事件:

该事件在reaadyState==3时触发,默认传入ProgressEvent进度对象,主要是用来获取资源下载进度。

注意,本事件只适用于IE10+


onload事件:

该事件在ajax请求成功后触发,也就是readyState==4。

onloadend事件:

该事件在ajax请求完成后触发,也就是readyState==4后或者readyState==2后。也是默认传入ProgressEvent事件进度对象


ontimeout事件:

请求超时时触发

1
2
3
4
xhr.timeout = 5;
xhr.ontimeout = function(e){
console.error('请求超时')
}




请求二进制文件

处理二进制文件主要使用的是html5的FileReader

相关属性:

  • error:表示读取文件器件发送的错误
  • readyState:表示读物文件的状态。0:文件未加载;1:文件正在读取;2:文件读取完成
  • result:读取的文件内容

相关方法:

  • readAsArrayBuffer:读取文件(或blob对象)为类型化数组(ArrayBuffer), 类型化数组允许开发者以数组下标的方式, 直接操作内存, 由于数据以二进制形式传递, 效率非常高
  • readAsDataURL:读取文件(或blob对象)为base64编码的URL字符串, 与window.URL.createObjectURL方法效果类似
  • readAsText:读取文件(或blob对象)为文本字符串
  • onload:文件读取完成时的事件回调, 默认传入event事件对象. 该回调内, 可通过this.result 或 event.target.result获取读取的文件内容

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
var xhr = new XMLHttpRequest(),
url = '';
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = function(){
if(this.status == 200) {
var blob = this.response;
var img = document.createElement("img");
//方案1
img.src = window.URL.createObjectURL(blob); //这里blob依然占据内存
img.onload = function() {
window.URL.revokeObjectURL(img.src); // 释放内存
};
// 方案2
var reader = new FileReader();
reader.readAsDataURL(blob);//FileReader将返回base64编码的data-uri对象
reader.onload = function(){
img.src = this.result;
}
// 方案3
img.src = url;

document.body.appendChild(img);
}
}
xhr.send();

xhr对象

xhr level1

  • 仅支持文本数据传输,无法传输二进制数据
  • 传输数据时,无进度信息提示,只提示是否完成
  • 同源策略,无法请求跨域资源
  • 无超时机制

xhr level2

  • 支持二进制数据,可以上传文件,可以用FormData对象管理表单
  • 有进度提示,可以用xhr.upload.onprogress事件回调方法获取传输进度
  • 还是有同源策略限制,但是用户可以将Access-Control-Allow-Origin等headers设置为*表示允许任何域名请求
  • 可以设置timeout和ontimeout

xhr level2支持IE10及以上,IE8和IE9可以使用XDomainRequest来解决

CORS

全称:跨域资源共享(Cross-origin resource sharing)


它允许浏览器向跨域服务器发出异步http请求, 从而克服了ajax受同源策略的限制

在实际通信过程中,浏览器不会拦截不合法的跨域请求,而是拦截服务器返回的响应,服务器实际上是接收到客户端发出的请求的,只是服务端向客户端发送的响应被浏览器拦截下来了

相关的headers信息

HTTP Response Header(服务器提供)

  • Access-Control-Allow-Origin: 指定允许哪些源的网页发送请求.

  • Access-Control-Allow-Credentials: 指定是否允许cookie发送.

  • Access-Control-Allow-Methods: 指定允许哪些请求方法.

  • Access-Control-Allow-Headers: 指定允许哪些常规的头域字段, 比如说 Content-Type.

  • Access-Control-Expose-Headers: 指定允许哪些额外的头域字段, 比如说 X-Custom-Header.

该字段可省略. CORS请求时, xhr.getResponseHeader() 方法默认只能获取6个基本字段: Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma . 如果需要获取其他字段, 就需要在Access-Control-Expose-Headers 中指定. 如上, 这样xhr.getResponseHeader(‘X-Custom-Header’) 才能返回X-Custom-Header字段的值.(该部分摘自阮一峰老师博客)

  • Access-Control-Max-Age: 指定preflight OPTIONS请求的有效期, 单位为秒.

HTTP Request Header(浏览器OPTIONS请求默认自带)

  • Access-Control-Request-Method: 告知服务器,浏览器将发送哪种请求, 比如说POST.
  • Access-Control-Request-Headers: 告知服务器, 浏览器将包含哪些额外的头域字段.

CORS请求

可分为简单请求和非简单请求,满足下列两个条件的就是简单请求

1)请求是以下3种之一:

  • HEAD
  • GET
  • POST

2)http头不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type字段限三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器将发送一次http请求,同时在Request头域中增加Origin字段,用来表示请求发起源,服务器根据该源采取不同的响应策略,如果服务器认为该请求合法,那么就会在返回的HTTP Response中加入Access-Control-*等字段

对于非简单请求,浏览器会发送两次http请求来验证源是否合法,其中第二次才是真正的http请求



一些demo

原生js上传文件并绑定事件

1
2
3
4
5
6
7
8
9
10
var xhr = ajax(url, method, formDate);
xhr.upload.onprogress = function(e) {
console.log("upload progress:",e.loaded/e.total*100 + "%");
};
xhr.upload.onload = function(){
console.log("upload onload");
};
xhr.onload = function(){
console.log("onload");
}

fetch上传

fetch只要发送一个POST请求,并将body属性设置为formData即可,但是它无法跟踪上传的进度信息

1
2
3
4
5
6
7
8
fetch(url, {
method: method,
body: formData
}).then(function(res){
console.log(res);
}).catch(function(e){
console.log(e);
})

jquery文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$.ajax({
type: method,
url: url,
data: formData,
progressData: false,
contentType: false, //必须是false才会自动加上正确的Content-Type
xhr: function() {
var xhr = $.ajaxSettings.xhr(); //实际就是return new window.XMLHttpRequest()对象
if(xhr.upload) {
xhr.upload.addEventListener("progress", function(e){
console.log("jq upload progress:", e.loaded/e.total*100 + "%");
}, false);
xhr.upload.addEventListener("load", function(){
console.log("jq upload onload.");});
xhr.addEventListener("load", function(){
console.log("jq onload.");
});
return xhr;
}
}
});





参考:

Ajax知识体系大梳理

Ajax关于readyState(状态值)和status(状态码)的研究

HTML5新特性之文件和二进制数据的操作

Hello World

发表于 2018-04-20 | 更新于: 2018-04-20

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

Chiwent

Chiwent

但愿不要辜负自己

4 日志
1 标签
GitHub
© 2018 Chiwent
由 Hexo 强力驱动