一道赋值面试题引发的思考3【并发数控制】

记一道控制并发数的前端面试题【手动维护 HTTP 请求排队】

23行代码实现一个带并发数限制的fetch请求函数

Promise面试题1

有这样一道关于promise的面试题,描述如下:

页面上有一个输入框,两个按钮,A按钮和B按钮,点击A或者B分别会发送一个异步请求,请求完成后,结果会显示在输入框中。

题目要求,用户随机点击A和B多次,要求输入框显示结果时,按照用户点击的顺序显示,举例:

用户点击了一次A,然后点击一次B,又点击一次A,输入框显示结果的顺序为先显示A异步请求结果,再次显示B的请求结果,最后再次显示A的请求结果。

UI界面如图:

这个需求该如何用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
//dom元素
var a = document.querySelector("#a")
var b = document.querySelector("#b")
var i = document.querySelector("#ipt");
//全局变量p保存promie实例
var P = Promise.resolve();
a.onclick = function(){
//将事件过程包装成一个promise并通过then链连接到
//全局的Promise实例上,并更新全局变量,这样其他点击
//就可以拿到最新的Promies执行链
P = P.then(function(){
//then链里面的函数返回一个新的promise实例
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
i.value = "a";
},1000)
})
})
}
b.onclick = function(){
P = P.then(function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
console.log("b")
i.value = "b"
},2000)
})
})
}

我们用定时器来模拟异步请求,仔细于阅读代码我们发现,在全局我们定义了一个全局P,P保存了一个promise的实例。

然后再观察点击事件的代码,用户每次点击按钮时,我们在事件中访问全局Promise实例,将异步操作包装到成新的Promise实例,然后通过全局Promise实例的then方法来连接这些行为。

连接的时候需要注意,then链的函数中必须将新的promise实例进行返回,不然就会执行顺序就不正确了。

需要注意的是,then链连接完成后,我们需要更新全局的P变量,只有这样,其它点击事件才能得到最新的Promise的执行链。

这样每次用户点击按钮就不需要关心回调执行时机了,因为promise的then链会按照其连接顺序依次执行。

这样就能保证用户的点击顺序和promise的执行顺序一致了。

Promise面试题2

按照要求:

实现 mergePromise 函数,把传进去的函数数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

代码如下:

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
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});

const ajax1 = () => timeout(2000).then(() => {
console.log('1');
return 1;
});

const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});

const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});

const mergePromise = ajaxArray => {
// 在这里实现你的代码

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

分析:

timeout是一个函数,这个函数执行后返回一个promise实例。

ajax1 、ajax2、ajax3 都是函数,不过这些函数有一些特点,执行后都会会返回一个 新的promise实例。

按题目的要求我们只要顺序执行这三个函数就好了,然后把结果放到 data 中,但是这些函数里都是异步操作,想要按顺序执行,然后输出 1,2,3并没有那么简单,看个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function A() {
setTimeout(function () {
console.log('a');
}, 3000);
}

function B() {
setTimeout(function () {
console.log('b');
}, 1000);
}

A();
B();

// b
// a

例子中我们是按顺序执行的 A,B 但是输出的结果却是 b,a 对于这些异步函数来说,并不会按顺序执行完一个,再执行后一个。

这道题主要考察的是Promise 控制异步流程,我们要想办法,让这些函数,一个执行完之后,再执行下一个,代码如何实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 保存数组中的函数执行后的结果
var data = [];

// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。
var sequence = Promise.resolve();

ajaxArray.forEach(function (item) {
// 第一次的 then 方法用来执行数组中的每个函数,
// 第二次的 then 方法接受数组中的函数执行后返回的结果,
// 并把结果添加到 data 中,然后把 data 返回。
sequence = sequence.then(item).then(function (res) {
data.push(res);
return data;
});
})

// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。
return sequence;

大概思路如下:全局定义一个promise实例sequence,循环遍历函数数组,每次循环更新sequence,将要执行的函数item通过sequence的then方法进行串联,并且将执行结果推入data数组,最后将更新的data返回,这样保证后面sequence调用then方法,如何后面的函数需要使用data只需要将函数改为带参数的函数。

ES6参考如下答案

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
76
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});

const ajax1 = () => timeout(5000).then(() => {
console.log('1');
return 1;
});

const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});

const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});

const mergePromise = ajaxArray => {
// 在这里实现你的代码
// 保存数组中的函数执行后的结果
var data = [];

// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。
var sequence = Promise.resolve();

ajaxArray.forEach(function (item) {

console.log(item);

// 第一次的 then 方法用来执行数组中的每个函数,
// 第二次的 then 方法接受数组中的函数执行后返回的结果,
// 并把结果添加到 data 中,然后把 data 返回。
sequence = sequence.then(item).then(function (res) {
data.push(res);
return data;
});
})

// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。
return sequence;

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
</script>
</head>

<body>

</body>

</html>

async 并发执行和继发执行

并发执行, 按次序输出

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});

const ajax1 = () => timeout(5000).then(() => {
console.log('1');
return 1;
});

const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});

const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});

const mergePromise = async (urls) => {
const arr = [];

// 并发执行
const textPromises = urls.map(url => {
return url();
});

// 按次序输出
for (const textPromise of textPromises) {
arr.push(await textPromise);
}

return arr
};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
</script>
</head>

<body>

</body>

</html>

Promise面试题3


题目是这样的:

有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = [‘http://example.com/1.jpg', …., ‘http://example.com/8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。

但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个

请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

已有代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var urls = [
'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',
'https://www.kkkk1000.com/images/getImgData/gray.gif',
'https://www.kkkk1000.com/images/getImgData/Particle.gif',
'https://www.kkkk1000.com/images/getImgData/arithmetic.png',
'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',
'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',
'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',
'https://www.kkkk1000.com/images/wxQrCode2.png'
];

function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
console.log('一张图片加载完成');
resolve();
}
img.onerror = reject
img.src = url
})
};

看到这个题目的时候,脑袋里瞬间想到了高效率排队买地铁票的情景,那个情景类似下图:

上图这样的排队和并发请求的场景基本类似,窗口只有三个,人超过三个之后,后面的人只能排队了。

首先想到的便是利用递归来做,就如这篇文章采取的措施一样,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//省略代码

var count = 0;
//对加载图片的函数做处理,计数器叠加计数
function bao(){
count++;
console.log("并发数:",count)
//条件判断,urls长度大于0继续,小于等于零说明图片加载完成
if(urls.length>0&&count<=3){
//shift从数组中取出连接
loadImg(urls.shift()).then(()=>{
//计数器递减
count--
//递归调用
}).then(bao)
}
}
function async1(){
//循环开启三次
for(var i=0;i<3;i++){
bao();
}
}
async1()

以上是最常规的思路,我将加载图片的函数loadImg封装在bao函数内,根据条件判断,是否发送请求,请求完成后继续递归调用。

以上代码所有逻辑都写在了同一个函数中然后递归调用,可以优化一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var count = 0;   //当前正在进行数

// 封装请求的异步函数,增加计数器功能
function request(){
count++;
loadImg(urls.shift()).then(()=>{
count--
}).then(diaodu)


}
// 负责调度的函数
function diaodu(){
if(urls.length>0&&count<=3){
request();
}
}

function async1(){
for(var i=0;i<3;i++){
request();
}
}
async1()

上面代码将一个递归函数拆分成两个,一个函数只负责计数和发送请求,另外一个负责调度。

这里的请求既然已经被封装成了Promise,那么我们用Promise和saync、await来完成一下,代码如下:

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
//省略代码

// 计数器
var count = 0;
// 全局锁
var lock = [];
var l = urls.length;
async function bao(){
if(count>=3){
//超过限制利用await和promise进行阻塞;
let _resolve;
await new Promise((resolve,reject)=>{
_resolve=resolve;
// resolve不执行,将其推入lock数组;
lock.push(_resolve);
});
}
if(urls.length>0){
console.log(count);
count++
await loadImg(urls.shift());
count--;
lock.length&&lock.shift()()
}
}
for (let i = 0; i < l; i++) {
bao();
}

大致思路是,遍历执行urls.length长度的请求,但是当请求并发数大于限制时,超过的请求用await结合promise将其阻塞,并且将resolve填充到lock数组中,继续执行,并发过程中有图片加载完成后,从lock中推出一项resolve执行,lock相当于一个叫号机;

以上代码可以优化为:

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

//省略代码

// 计数器
var count = 0;
// 全局锁
var lock = [];
var l = urls.length;
// 阻塞函数
function block(){
let _resolve;
return new Promise((resolve,reject)=>{
_resolve=resolve;
// resolve不执行,将其推入lock数组;
lock.push(_resolve);
});
}
// 叫号机
function next(){
lock.length&&lock.shift()()
}
async function bao(){
if(count>=3){
//超过限制利用await和promise进行阻塞;
await block();
}
if(urls.length>0){
console.log(count);
count++
await loadImg(urls.shift());
count--;
next()
}
}
for (let i = 0; i < l; i++) {
bao();
}

最后一种方案,也是我十分喜欢的,思考好久才明白,大概思路如下:

用 Promise.race来实现,先并发请求3个图片资源,这样可以得到 3 个 Promise实例,组成一个数组promises ,然后不断的调用 Promise.race 来返回最快改变状态的 Promise,然后从数组(promises )中删掉这个 Promise 对象实例,再加入一个新的 Promise实例,直到全部的 url 被取完。

代码如下:

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 limitLoad(urls, handler, limit) {
// 对数组做一个拷贝
const sequence = [].concat(urls)
let promises = [];

//并发请求到最大数
promises = sequence.splice(0, limit).map((url, index) => {
// 这里返回的 index 是任务在 promises 的脚标,
//用于在 Promise.race 之后找到完成的任务脚标
return handler(url).then(() => {
return index
});
});

(async function loop() {
let p = Promise.race(promises);
for (let i = 0; i < sequence.length; i++) {
p = p.then((res) => {
promises[res] = handler(sequence[i]).then(() => {
return res
});
return Promise.race(promises)
})
}
})()
}
limitLoad(urls, loadImg, 3)

第三种方案的巧妙之处,在于使用了Promise.race。并且在循环时用then链串起了执行顺序。

15 行代码实现并发控制(javascript)

做过爬虫的都知道,要控制爬虫的请求并发量,其实也就是控制其爬取频率,以免被封IP,还有的就是以此来控制爬虫应用运行内存,否则一下子处理N个请求,内存分分钟会爆。

python爬虫一般用多线程来控制并发,

然而如果是node.js爬虫,由于其单线程无阻塞性质以及事件循环机制,一般不用多线程来控制并发(当然node.js也可以实现多线程,此处非重点不再多讲),而是更加简便地直接在代码层级上实现并发。

为图方便,开发者在开发node爬虫一般会找一个并发控制的npm包,然而第三方的模块有时候也并不能完全满足我们的特殊需求,这时候我们可能就需要一个自己定制版的并发控制函数。

下面我们用15行代码实现一个并发控制的函数。

首先,一个基本的并发控制函数,基本要有以下3个参数:

  • list {Array} - 要迭代的数组
  • limit {number} - 控制的并发数量
  • asyncHandle {function} - 对list的每一个项的处理函数

设计

以下以爬虫为实例进行讲解

设计思路其实很简单,假如并发量控制是 5

1.首先,瞬发 5 个异步请求,我们就得到了并发的 5 个异步请求

1
2
3
4
// limit = 5
while(limit--) {
handleFunction(list)
}
  1. 然后,这 5 个异步请求中无论哪一个先执行完,都会继续执行下一个list
1
2
3
4
5
6
7
let recursion = (arr) => {
return asyncHandle(arr.shift())
.then(()=>{
// 迭代数组长度不为0, 递归执行自身if (arr.length!==0) return recursion(arr)
// 迭代数组长度为0,结束 elsereturn'finish';
})
}
  1. list所有的项迭代完之后的回调
1
return Promise.all(allHandle)

代码

上述步骤组合起来,就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @params list {Array} - 要迭代的数组
* @params limit {Number} - 并发数量控制数
* @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
* @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
*/

let mapLimit = (list, limit, asyncHandle) => {
let recursion = (arr) => {
return asyncHandle(arr.shift())
.then(()=>{
if (arr.length!==0) return recursion(arr) // 数组还未迭代完,递归继续进行迭代
else return 'finish';
})
};

let listCopy = [].concat(list);
let asyncList = []; // 正在进行的所有并发异步操作
while(limit--) {
asyncList.push( recursion(listCopy) );
}
return Promise.all(asyncList); // 所有并发异步操作都完成后,本次并发控制迭代完成
}

测试demo

模拟一下异步的并发情况

1
2
3
4
5
6
7
8
9
10
11
12
13
var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
return new Promise(resolve => {
count++
setTimeout(()=>{
console.log(curItem, '当前并发量:', count--)
resolve();
}, Math.random() * 5000)
});
}).then(response => {
console.log('finish', response)
})

完整Code

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
/** 
* @params list {Array} - 要迭代的数组
* @params limit {Number} - 并发数量控制数
* @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
* @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
*/

function mapLimit(list, limit, asyncHandle) {
function recursion(arr) {
return asyncHandle(arr.shift())
.then(() => {
if(arr.length !== 0) return recursion(arr) // 数组还未迭代完,递归继续进行迭代
else return 'finish';
})
};

let listCopy = [].concat(list);
let asyncList = []; // 正在进行的所有并发异步操作

while(limit--) {
asyncList.push(recursion(listCopy));
}
return Promise.all(asyncList); // 所有并发异步操作都完成后,本次并发控制迭代完成
}

//测试
var dataLists = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 100, 123];
var count = 0;

mapLimit(dataLists, 3, function(curItem){
return new Promise(resolve => {
count++
setTimeout(() => {
console.log(curItem, '当前并发量:', count--)
resolve();
}, Math.random() * 1000)
});
}).then(response => {
console.log('finish', response)
})

结果如下:

手动抛出异常中断并发函数测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
return new Promise((resolve, reject) => {
count++
setTimeout(()=>{
console.log(curItem, '当前并发量:', count--)
if(curItem > 4) reject('error happen')
resolve();
}, Math.random() * 5000)
});
}).then(response => {
console.log('finish', response)
})

并发控制情况下,迭代到5,6,7 手动抛出异常,停止后续迭代:

JS数组拍平[扁平化]的3种方法

在开发过程中有得时候总是碰一些共性的问题,比如将一个二维数组拍平成一维数组,或者三维数组拍平成一维数组。这些问题在遇到的时候总会重新思考,不如将其提炼出来,总结一下。

下面笔者将为大家演示一下,将一个多维数组拍平成一个一维数组的两种方法,算是抛砖引玉,大家有更好的方法可以在留言区发表。

首先是第一种方法,闭包+递归处理,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var arr = [1, 2, 3, [3, 3, 3, [5, 4, 5, 6, 6, 7, 8]],[333, 4444]];

function product() {
// 1、创建一个空数组,
var newarr = [];
///2、并且返回一个函数,函数参数为要拍平的数组
return function flatten(arr) {
// 3、循环数组,判断每一项,不为输的话将其塞入newarr
// 若为数组,递归调用 faltten,并将结果与newarr合并
for (var t of arr) {
if (!Array.isArray(t)) {
newarr.push(t);
} else {
newarr.concat(flatten(t))
}
}
return newarr
}
}

var flatten = product();
console.log(flatten(arr))

执行结果为:

1
[1, 2, 3, 3, 3, 3, 5, 4, 5, 6, 6, 7, 8, 333, 4444]

上面这这种方法比较中规中矩,代码详解见注释,下面这种方法运用到了javascript语言的一些新特性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
var arr = [1, 2, 3, [3, 3, 3, [5, 4, 5, 6, 6, 7, 8]], [333, 4444] ];
functionflatten(arr){
return arr.reduce(function(pre,cur){
if(!Array.isArray(cur)){
return [...pre,cur];
}else{
return [...pre,...flatten(cur)]
}
},[])
}

console.log(flatten(arr))

上面代码中用了ES6的一个新特性扩展云算法 “…”,“[…abc,…fff]”其作用相当于abc.concat(fff),这种用法更加直观明了,还有就是运用了reduce方法。reduce是javascript语言中数组的一个方法。

数组调用recduce方法时,可以传递两个参数,第一个参数为回调函数,第二个参数为一个初始值。回调函数中需要传递两个参数,第一个参数为每次执行函数的返回值,第二个参数为当前索引对应数组的值。reduce的第二个参数是可以省略的,省略的话,回调函数第一次调用的参数为数组的第一项和第二项的值,如果没有省略,回调函数的第一个参数就是这个初始值。上面的例子,reduce的第二个参数设置了一个空数组。

相比来说第一种比较好理解,第二种的难点在于对reduce函数的运用和理解。

第三种比较粗暴

1
2
3
4
5
var arr = [1, 2, 3, [3, 3, 3, [5, 4, 5, 6, 6, 7, 8]],[333, 4444]];

arr.join(",").split(",")

//["1", "2", "3", "3", "3", "3", "5", "4", "5", "6", "6", "7", "8", "333", "4444"]

页面返回要刷新

之前在项目中使用pageshow,发现页面返回的时候persisted依然为false,这时候只好找其他方案解决。
这时候发现有一个window.performance对象,performance.navigation.type是一个无符号短整型

  • TYPE_NAVIGATE (0):
    当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址,type值为0
  • TYPE_RELOAD (1)
    点击刷新页面按钮或者通过Location.reload()方法显示的页面,type值为1
  • TYPE_BACK_FORWARD (2)
    页面通过历史记录和前进后退访问时。type值为2
  • TYPE_RESERVED (255)
    任何其他方式,type值为255

这真是我们需要的部分,于是可以预见,解决方案如下:

1
2
3
4
5
6
window.addEventListener('pageshow', () => {
if (e.persisted || (window.performance &&
window.performance.navigation.type == 2)) {
location.reload()
}
}, false)

两个鲜有人知的 Vuex 技巧

当在我们 Vue.js 的组件中使用了 Vuex,除了映射功能的函数之外,我们好像忘记了它所暴露出来的其他有用的 API。

我们一起来看看可以利用它来干些什么。首先,还是先来创建一个基本的 store:

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
getCountPlusOne: state => state.count + 1
},
mutations: {
increment(state) {
state.count++;
}
}
});

Watch 方法

watch 是将 Vuex 与其他外部代码整合的最有用的方法,可以在你的 awesomeService 或者是在 catchAllAuthUtils 等等类似的服务中使用。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const unsubscribe = store.watch(
(state, getters) => {
return [state.count, getters.getCountPlusOne];
},
watched => {
console.log("Count is:", watched[0]);
console.log("Count plus one is:", watched[1]);
},
{}
);

// To unsubscribe:
unsubscribe();

我们所做的就是在调用 vuex 的实例方法 watch 时,传入两个函数作为实参,第一个函数实参返回我们想要在 state 与/或 getters 上监听的属性;第二个函数实参是当属性值 state.countgetters.getCountPlusOne 有改变时,调用的回调函数。

这是用来结合 Vuex 与 react 或者 angular 甚至是 JQuery 代码时,非常有用的技巧。

可以在这个 CodeSandbox 上查看例子。

SubscribeAction 方法

有时候,与其监听 store 中的一个属性改变,不如使用 subscribeAction 方法订阅一个特定的 action,比如像 loginlogout 之类的异步请求,这也是更有用的方案。

调用监听函数,在每一个 action 分发的时候调用指定的回调函数,并在其中调用自定义代码。

我们在每一个 action 的分发前以及完成后,来分别开始和停止全局的 spinner。

1
2
3
4
5
6
7
8
9
10
11
const unsubscribe = store.subscribeAction({
before: (action, state) => {
startBigLoadingSpinner();
},
after: (action, state) => {
stoptBigLoadingSpinner();
}
});

// To unsubscribe:
unsubscribe();