renderjs 是一个运行在视图层的 js。它比 WXS 更加强大。它只支持 app-vue 和 web。

主要作用

  1. 大幅降低逻辑层和视图层的通讯损耗,提供高性能视图交互能力

    uni-app 的 app 端逻辑层和视图层是分离的,这种机制有很多好处,但也有一个副作用是在造成了两层之间通信阻塞。尤其是 App 的 Android 端阻塞问题影响了高性能应用的制作。

    renderjs 运行在视图层,可以直接操作视图层的元素,避免通信折损。

  2. 在视图层操作 dom,运行 for web 的 js 库

    官方不建议在 uni-app 里操作 dom,但如果你不开发小程序,想使用一些操作了 dom、window 的库,其实可以使用 renderjs 来解决。

    在 app-vue 环境下,视图层由 webview 渲染,而 renderjs 运行在视图层,自然可以操作 dom 和 window。

注意事项

  1. 目前仅支持内联使用。
  2. 不要直接引用大型类库,推荐通过动态创建 script 方式引用。
  3. 可以使用 vue 组件的生命周期(不支持 beforeDestroydestroyedbeforeUnmountunmounted),不可以使用 App、Page 的生命周期
  4. 视图层和逻辑层通讯方式与 WXS 一致,另外可以通过 this.$ownerInstance 获取当前组件的 ComponentDescriptor 实例,使用 callMethod 方法,去抛出方法、传值,类似于 vue 组件间 emit
  5. 注意逻辑层给数据时最好一次性给到渲染层,而不是不停从逻辑层向渲染层发消息,那样还是会产生逻辑层和视图层的多次通信,还是会卡
  6. 观测更新的数据在视图层可以直接访问到。
  7. APP 端视图层的页面引用资源的路径相对于根目录计算,例如:./static/test.js。
  8. APP 端可以使用 dom、bom API,不可直接访问逻辑层数据,不可以使用 uni 相关接口(如:uni.request)
  9. H5 端逻辑层和视图层实际运行在同一个环境中,相当于使用 mixin 方式,可以直接访问逻辑层数据。

演示代码

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
<template>
<view>
<view :msg="msg" :change:msg="renderScript.receiveMsg" class="renderjs" id="renderjs-view"> {{msg}} </view>
<button @click="renderScript.emitData">直接调用renderjs中的emitData的方法</button>
<button @click="changeMsg" class="app-view">改变msg的值,直接调用renderjs中receiveMsg的值</button>
<button @click="renderScript.renferMsg">通过renderjs改变msg的值,同时调用renderjs中的emitData的方法</button>
</view>
</template>

<script>
export default {
data() {
return {
msg: "我是service层原来的msg"
};
},
methods: {
// 触发逻辑层出入renderjs数据改变
changeMsg() {
this.msg = "msg值改变了";
},
// 接收renderjs发回的数据
receiveRenderData(val) {
console.log("renderjs返回的值-->", val);
},
//接收renderjs发回的数据,同时触发:change:msg,调用enderjs中的emitData的方法
serviceClick(e) {
this.msg = e;
}
}
};
</script>

<script module="renderScript" lang="renderjs">
export default {
data() {
return {
name: "我是renderjs数据"
};
},
methods: {
renferMsg(event, ownerInstance) {
// 调用 service层的serviceClick方法,传值
ownerInstance.callMethod("serviceClick", {
test: "这是点击renderjs的区域,向service层传递变量"
});
},
// 接收逻辑层发送的数据
receiveMsg(newValue, oldValue, ownerVm, vm) {
console.log("msg变化了newValue", newValue);
console.log("msg变化了oldValue", oldValue);
console.log("msg变化了ownerVm", ownerVm);
console.log("msg变化了vm", vm);

this.$ownerInstance.callMethod("receiveRenderData", newValue); // 同 ownerVm.callMethod
},
// 发送数据到逻辑层
emitData(e, ownerVm) {
ownerVm.callMethod("receiveRenderData", this.name);
}
}
};
</script>

多表达式多 if 判断

我们可以在数组中存储多个值,并且可以使用数组 include 方法。

1
2
3
4
5
6
7
8
// 长
if (x === "abc" || x === "def" || x === "ghi" || x === "jkl") {
//logic
}
// 短
if (["abc", "def", "ghi", "jkl"].includes(x)) {
//logic
}

简写 if else

如果 if-else 的逻辑比较降低,可以使用下面这种方式镜像简写,当然也可以使用三元运算符来实现。

1
2
3
4
5
6
7
8
9
10
11
// 长
let test: boolean;
if (x > 100) {
test = true;
} else {
test = false;
}
// 短
let test = x > 10 ? true : false;
// 也可以直接这样
let test = x > 10;

合并变量声明

当我们声明多个同类型的变量时,可以像下面这样简写。

1
2
3
4
5
6
// 长
let test1;
let test2 = 1;
// 短
let test1,
test2 = 1;

合并变量赋值

当我们处理多个变量并将不同的值分配给不同的变量时,这种方式非常有用。

1
2
3
4
5
6
7
// 长
let test1, test2, test3;
test1 = 1;
test2 = 2;
test3 = 3;
// 短
let [test1, test2, test3] = [1, 2, 3];

&& 运算符

如果仅在变量值为 true 的情况下才调用函数,则可以使用 && 运算符。

1
2
3
4
5
6
// 长
if (test1) {
callMethod();
}
// 短
test1 && callMethod();

箭头函数

1
2
3
4
5
6
// 长
function add(a, b) {
return a + b;
}
// 短
const add = (a, b) => a + b;

短函数调用

可以使用三元运算符来实现这些功能。

1
2
3
4
5
6
7
8
9
10
11
const fun1 = () => console.log("fun1");
const fun2 = () => console.log("fun2");
// 长
let test = 1;
if (test == 1) {
fun1();
} else {
fun2();
}
// 短
(test === 1 ? fun1 : fun2)();

Switch 简记法

我们可以将条件保存在键值对象中,并可以根据条件使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 长
switch (data) {
case 1:
test1();
break;

case 2:
test2();
break;

case 3:
test();
break;
// And so on...
}

// 短
const data = {
1: test1,
2: test2,
3: test
};

data[something] && data[something]();

默认参数值

1
2
3
4
5
6
7
8
// 长
function add(test1, test2) {
if (test1 === undefined) test1 = 1;
if (test2 === undefined) test2 = 2;
return test1 + test2;
}
// 短
const add = (test1 = 1, test2 = 2) => test1 + test2;

扩展运算符

1
2
3
4
5
6
// 长-合并数组
const data = [1, 2, 3];
const test = [4, 5, 6].concat(data);
// 短-合并数组
const data = [1, 2, 3];
const test = [4, 5, 6, ...data];
1
2
3
4
5
6
// 长-拷贝数组
const test1 = [1, 2, 3];
const test2 = test1.slice();
// 短-拷贝数组
const test1 = [1, 2, 3];
const test2 = [...test1];

模版字符串

1
2
3
4
// 长
const welcome = "Hi " + test1 + " " + test2 + ".";
// 短
const welcome = `Hi ${test1} ${test2}`;

简写对象

1
2
3
4
5
6
let test1 = "a";
let test2 = "b";
// 长
let obj = { test1: test1, test2: test2 };
// 短
let obj = { test1, test2 };

在数组中查找最大值和最小值

1
2
3
const arr = [1, 2, 3];
Math.max(…arr); // 3
Math.min(…arr); // 1

队列是数据结构中的一种,它与实际生活中的排队相似:在一条队伍中,先来的人总是能够先得到服务,后来的人只能排在队伍末尾等候。队列也是一样,它符合先进先出 FIFO(First Input First Out)的顺序。

队列的类型

队列有两种类型:一种是和日常排队类似的队列,叫做普通队列;另一种叫做环形队列

对于一个队列来说,有队头和队尾,以及容量。

普通队列

普通队列的队头和队尾是分开的,当队头的元素离开队列后,下一个元素就会成为队头,而新加入的元素会跟随在原本的队尾之后,成为新的队尾。

环形队列

当只有一个元素时,队头队尾是同一个元素;当队列容量已满时,队头和队尾是连接在一起的;当队头的元素离开后,下一个元素会成为队头,而新的元素则会插入原本队头的位置成为新的队尾。

在容量确定的情况下,普通队列前面的元素离开后,对应的内存就会被空置,而在环形队列中,前面的元素离开,新的元素就会占据原来的内存。相比之下,就容量而言,环形队列具有更高的内存利用率,可以减小内存的开支消耗。

属性

  1. 队列长度的属性,因为队列的实际长度可能并不会达到队列容量的大小
  2. 队列中用来存放元素的数组(为什么是?如果说队列与 JS 中的哪一种数据类型最相似的话,那数组肯定是最好的答案)

方法

  1. 将元素插入队尾的方法
  2. 将队头移出队列的方法
  3. 清空队列的方法
  4. 判断队列是否已满(如果已满,则不能再插入元素)
  5. 判断队列是否为空(如果为空,则不能移除元素)
  6. 遍历所有元素的方法
  7. ……(你还可以根据你的实际需要增加方法,如定时从队列中执行任务、增加任务等)

代码实现

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**
* 生成随机字符串
* @param {number} [len] 字符串长度
*/
export function randomStr(len = 6) {
let str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let l = str.length;
let pwd = "";
for (let i = 0; i < len; i++) pwd += str.charAt(Math.floor(Math.random() * l));
return pwd;
}

/**
* 函数处理队列
*/
export class Queue {
RUN_TIMER = {};
DATA_ALL = [];
DATA_ALL_ID = [];
DATA_RUNING = {};
DATA_ERROR = [];
$EVENT = (type, data) => {};
constructor(options = {}, event = () => {}) {
const { NAME = "", RUN_MAX = 1, RUN_TIMEOUT = 20000, ERROR_DELAY = true, ERROR_DELAY_TIME = 20000, ERROR_TRYTIMES = 0, RUN_NOW = true } = options;
this.NAME = NAME; //队列名称
this.RUN_NOW = RUN_NOW; //是否立即执行
this.RUN_MAX = RUN_MAX; //同时进行的数量
this.RUN_TIMEOUT = RUN_TIMEOUT; //单个超时时间
this.ERROR_DELAY = ERROR_DELAY; // 是否开启运行出错后延迟处理
this.ERROR_DELAY_TIME = ERROR_DELAY_TIME; // 运行出错后重试延迟时间
this.ERROR_TRYTIMES = ERROR_TRYTIMES; // 运行出错后重试次数,0表示无上限,达到次数后移除队列
this.$EVENT = event; // 状态
}
getId() {
return Date.now().toString().slice(7) + randomStr();
}
log(...args) {
// console.log(this.NAME, ...args);
}
push(id, fn) {
if (typeof id === "function") {
fn = id;
id = this.getId();
} else {
if (this.DATA_ALL_ID.includes(id) || this.DATA_RUNING[id]) {
return;
}
}
this.DATA_ALL_ID.push(id);
this.DATA_ALL.push({
id,
entryTime: Date.now(),
fn,
startTime: 0,
tryTimes: 1
});
this.log("■■■■■■push■■■■■■", id, this.DATA_ALL_ID, this.DATA_RUNING);
this.check();
return {
id,
length: this.DATA_ALL.length,
runing: Object.keys(this.DATA_RUNING).length
};
}
unshift(id, fn) {
if (typeof id === "function") {
fn = id;
id = this.getId();
} else {
if (this.DATA_ALL_ID.includes(id) || this.DATA_RUNING[id]) {
return;
}
}
this.DATA_ALL_ID.unshift(id);
this.DATA_ALL.unshift({
id,
entryTime: Date.now(),
fn,
startTime: 0,
tryTimes: 1
});
this.log("■■■■■■unshift■■■■■■", id, this.DATA_ALL_ID, this.DATA_RUNING);
this.check();
return {
id,
length: this.DATA_ALL.length,
runing: Object.keys(this.DATA_RUNING).length
};
}
try(item) {
if (item.tryTimes < this.ERROR_TRYTIMES || !this.ERROR_TRYTIMES) {
item.tryTimes++;
item.tryEntryTime = Date.now();
this.DATA_ALL.push(item);
this.DATA_ALL_ID.push(item.id);
this.check();
return { code: 0, isEnd: false, data: item };
} else {
this.DATA_ERROR.push(item);
return { code: 1, isEnd: true, data: item };
}
}
start() {
this.RUN_NOW = true;
this.check();
}
check() {
if (!this.RUN_NOW) {
return;
}
const nowTime = Date.now();
const runing = Object.values(this.DATA_RUNING);
if (runing.length === 0 && this.DATA_ALL.length === 0) {
this.finish();
return;
}
runing.forEach((item) => {
const startTime = item.tryStartTime || item.startTime;
if (nowTime - startTime > this.RUN_TIMEOUT) {
this.error(item.id, "checktimeout");
}
});
setTimeout(() => {
const runingNum = Object.keys(this.DATA_RUNING).length;
this.next(this.RUN_MAX - runingNum);
});
}
next(len = 1) {
if (this.DATA_ALL.length === 0 || len < 1) {
return;
}
const runData = this.DATA_ALL.splice(0, len);
this.DATA_ALL_ID.splice(0, len);
runData.forEach((item) => {
item.startTime = Date.now();
this.DATA_RUNING[item.id] = item;
((item) => {
const t = item.id;
setTimeout(
() => {
item.fn(
() => {
this.done(t);
},
() => {
return this.error(t);
}
);
this.RUN_TIMER[t] = setTimeout(() => {
this.error(t, "timeout");
}, this.RUN_TIMEOUT);
},
item.tryTimes > 1 ? this.ERROR_DELAY_TIME : 0
);
})(item);
});
}
done(id) {
const item = this.DATA_RUNING[id];
this.RUN_TIMER[id] && clearTimeout(this.RUN_TIMER[id]);
if (item) {
delete this.DATA_RUNING[id];
this.check();
}
}
finish() {
this.$EVENT("finish", this.DATA_ERROR);
}
error(id, type = "fn") {
this.RUN_TIMER[id] && clearTimeout(this.RUN_TIMER[id]);
delete this.RUN_TIMER[id];
const item = this.DATA_RUNING[id];
console.warn(this.NAME, "error", id, type);
if (item) {
this.done(item.id);
return this.try(item);
}
return { code: 1, isEnd: true, data: item };
}
clear() {
for (const key in this.RUN_TIMER) {
this.RUN_TIMER[key] && clearTimeout(this.RUN_TIMER[key]);
}
this.DATA_ALL = [];
this.DATA_ALL_ID = [];
this.DATA_RUNING = {};
this.DATA_ERROR = [];
}
}

数据处理队列

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/**
* 数据处理队列
*/
export class dataQueue {
RUN_TIMER = {}; // 超时监听
DATA_ORIGIN = []; //原始单条数据
DATA_GROUP = []; //切割后的数据 Array<{id,time,data}>
DATA_RUNING = {}; //正在运行的数据
/**
*
* @param {object} options 配置参数
* @param {function} run 执行函数(data, done, err)
* @param {function} callback 状态回调函数
*/
constructor(options = {}, run = (data, done, err) => {}, callback = (code, data, msg) => {}) {
const { NAME = this.getId(), RUN_TIMEOUT = 20000, RUN_DELAY = 5000, RUN_NUM = 10, RUN_MAX = 1, RUN_NOW = true, ERROR_TRYTIMES = 3 } = options;
this.NAME = "$$UNI__DATA:" + NAME; //队列名称(用于本地存储)
this.RUN_NOW = RUN_NOW; //是否立即执行
this.RUN_TIMEOUT = RUN_TIMEOUT; //单个超时时间
this.RUN_NUM = RUN_NUM; // 执行条数
this.RUN_DELAY = RUN_DELAY; //push后延迟执行时间
this.ERROR_TRYTIMES = ERROR_TRYTIMES; //重试次数
this.RUN_MAX = RUN_MAX; // 同时执行任务
this._RUN = run; // 状态
this._CALLBACK = callback; // 状态
this.DATA_ORIGIN = this.getLocalData();
if (this.DATA_ORIGIN.length > 0) {
console.log(this.DATA_ORIGIN);
this.addAfter(true);
}
}
getId() {
return Date.now().toString().slice(7) + randomStr();
}
getLocalData() {
return JSON.parse(uni.getStorageSync(this.NAME) || "[]");
}
setLocalData() {
const all = [...this.DATA_ORIGIN];
this.DATA_GROUP.forEach((item) => {
all.push(...item.data);
});
for (const id in this.DATA_RUNING) {
const item = this.DATA_RUNING[id];
all.push(...item.data);
}
uni.setStorageSync(this.NAME, JSON.stringify(all));
}
push(item, immediate = false) {
this.DATA_ORIGIN.push(item);
this.log("■■■■■■push", item);
this.addAfter(immediate);
}
unshift(item, immediate = true) {
this.DATA_ORIGIN.unshift(item);
this.log("■■■■■■unshift", item);
this.addAfter(immediate);
}
runAll() {
this.addAfter(true);
}
/**
* 切割数据
* @param {boolean} all 是否全部切割
* @returns {boolean} 是否切割数据
*/
groupData(all = false) {
// 数据长度够RUN_NUM 或者 切割所有数据
if (this.DATA_ORIGIN.length >= this.RUN_NUM || (this.DATA_ORIGIN.length > 0 && all)) {
const data = this.DATA_ORIGIN.splice(0, this.RUN_NUM);
this.DATA_GROUP.push({ data });
this.groupData();
return true;
}
// 是否有完整数据
return false;
}
addAfter(immediate) {
this.setLocalData();
// 清除定时器PUSH防抖定时器
this.RUN_TIMER["PUSH"] && clearTimeout(this.RUN_TIMER["PUSH"]);
if (immediate) {
this.groupData(true);
this.next(10);
} else {
this.groupData() && this.check();
this.RUN_TIMER["PUSH"] = setTimeout(() => {
delete this.RUN_TIMER["PUSH"];
console.log(this);
this.groupData(true);
this.check();
}, this.RUN_DELAY);
}
}
start() {
this.RUN_NOW = true;
this.check();
}
check() {
if (!this.RUN_NOW) {
return;
}
const nowTime = Date.now();
for (const id in this.DATA_RUNING) {
const item = this.DATA_RUNING[id];
const time = item.time;
if (nowTime - time > this.RUN_TIMEOUT) {
this.try(item);
}
}
const runingNum = Object.keys(this.DATA_RUNING).length;
this.next(this.RUN_MAX - runingNum);
}
next(len = 1) {
if (len < 1 || (!this.DATA_ORIGIN.length && !this.DATA_GROUP.length)) {
return;
}
if (this.DATA_GROUP.length < len) {
this.groupData(true);
}
const runData = this.DATA_GROUP.splice(0, len);
runData.forEach((item) => {
this.runItem(item);
});
}
runItem(item) {
this.log("run", item);
const data = item.data;
const id = (item.id = item.id || this.getId());
item.time = Date.now();
item.errTimes = item.errTimes || 0;
this.DATA_RUNING[id] = item;
// 超时后执行check
this.RUN_TIMER[id] = setTimeout(() => {
this.err(id);
}, this.RUN_TIMEOUT);
// 执行新的一组数据
this._RUN(
data,
() => {
this.done(id);
},
() => {
this.err(id);
}
);
}
done(id) {
const item = this.clearItem(id);
this.log("done", item);
this.setLocalData();
this.check();
}
err(id) {
const item = this.clearItem(id);
this.log("err", item);
this.check();
if (item) {
this.try(item);
}
}
try(item) {
if (item.errTimes < this.ERROR_TRYTIMES) {
item.errTimes++;
this.DATA_GROUP.push(item);
this.check();
} else {
uni.getNetworkType({
success: (res) => {
if (res.networkType === "none") {
this.DATA_GROUP.push(item);
this.log("无网状态", item);
} else {
this.setLocalData();
this._CALLBACK(50, item.data, `重试${item.errTimes}次失败`);
}
}
});
}
}
clearItem(id) {
let item = this.DATA_RUNING[id];
this.RUN_TIMER[id] && clearTimeout(this.RUN_TIMER[id]);
if (!item) {
// 要删除的id正在重试
const index = this.DATA_GROUP.findIndex((a) => id === a.id);
item = this.DATA_GROUP[index];
this.DATA_GROUP.splice(index, 1);
console.warn("超时后成功", item);
}
delete this.DATA_RUNING[id];
delete this.RUN_TIMER[id];
return item;
}
clear() {
for (const key in this.RUN_TIMER) {
this.RUN_TIMER[key] && clearTimeout(this.RUN_TIMER[key]);
}
this.DATA_ORIGIN = [];
this.DATA_GROUP = [];
this.DATA_RUNING = {};
}
log(...args) {
console.warn(...args);
}
}

单一循环队列

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
let singleQueueFlag = 0;

/**
* 单一循环队列
* @param {Function} fn 返回成功、失败、完成三个状态
*/
export function singleQueue(fn) {
let times = 0;
const check = () => {
return fn()
.then((isEnd = false) => {
if (isEnd === true) {
return Promise.resolve({ times, isEnd });
} else {
if (singleQueueFlag) {
return Promise.reject({ times, isEnd, singleQueueFlag, from: 1 });
} else {
times++;
return check();
}
}
})
.catch((res = "") => {
return Promise.reject({ times, res, from: 2 });
});
};
return check();
}

/**
* 单一循环队列状态标记
*/
export function singleQueueSign(flag) {
singleQueueFlag = flag;
}

等待队列

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
77
78
79
80
81
82
83
84
85
86
/**
* 等待队列,间隔最小时间后执行下一个队列
*/
export class WaitingQueue {
// 传递等待时间 默认一秒
constructor(waitingTime = 1) {
this.isBusy = false;
this.isFirst = true;
this.waitingTime = waitingTime;
this.box = [];
}
// 初始化
init() {
this.isBusy = false;
this.isFirst = true;
this.box = [];
}
get size() {
return this.box.length;
}

get isEmpty() {
return !this.box.length;
}

// 清空队列
clear() {
this.init();
}

lazyRun(func, time = 0) {
setTimeout(func.bind(this), time);
}
// 进入队列方法,
entry(func) {
// 无论如何,队列方法先放入数组中
this.box.unshift(func);
// 判断是否第一次执行,如果是,自执行启动
if (this.isFirst) {
this.isFirst = false;
this.next();
return;
}
}
// 执行下一个队列方法
next() {
// 每次执行下一个队列时,先检查能否执行
if (!this.check()) {
return;
}
// 取出队列中的方法
let func = this.box.pop();
// done 函数,外部调用,time:强制等待时间,cb:调用后通知
func(
(time) =>
new Promise((resolve) => {
// 不忙
this.isBusy = false;
// 等待时间 可能传0不等待,或者默认
let waitTime = (time ?? this.waitingTime) * 1000;
// 延迟执行
this.lazyRun(() => {
// 下一步延迟执行,确保resolve先执行
this.lazyRun(this.next);
resolve();
}, waitTime);
})
);
}
// 检查能否执行下一个队列的方法
check() {
// 如果队列都空了,不执行
if (this.isEmpty) {
this.isFirst = true;
this.isBusy = false;
return false;
}
// 如果队列正在执行,不执行
if (this.isBusy) {
return false;
}
// 其他的都继续执行
this.isBusy = true;
return true;
}
}

应用

通过一个专门用来存放请求的队列,实现请求发起的前后顺序(先进入的先发起)及当前页面中同时发起请求的数量(进入队列的队列在发起的同时移出,请求结束后向队列中添加下一个请求),甚至可以通过队列实现请求的自动发起(这就需要对 demo 中的代码进行修改从而实现想要的功能)。

前言

每一个网页都离不开 css,但是很多人又认为,css 主要是用来完成页面布局的,像一些细节或者优化,就不需要怎么考虑,实际上这种想法是不正确的

作为页面渲染和内容展现的重要环节,css 影响着用户对整个网站的第一体验

因此,在整个产品研发过程中,css 性能优化同样需要贯穿全程

实现方式

实现方式有很多种,主要有如下:

  • 内联首屏关键 CSS
  • 异步加载 CSS
  • 资源压缩
  • 合理使用选择器
  • 减少使用昂贵的属性
  • 不要使用@import

内联首屏关键 CSS

在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联 css 关键代码能够使浏览器在下载完 html 后就能立刻渲染

而如果外部引用 css 代码,在解析 html 结构过程中遇到外部 css 文件,才会开始下载 css 代码,再渲染

所以,CSS 内联使用使渲染时间提前

注意:但是较大的 css 代码并不合适内联(初始拥塞窗口、没有缓存),而其余代码则采取外部引用方式

异步加载 CSS

在 CSS 文件请求、下载、解析完成之前,CSS 会阻塞渲染,浏览器将不会渲染任何已处理的内容

前面加载内联代码后,后面的外部引用 css 则没必要阻塞浏览器渲染。这时候就可以采取异步加载的方案,主要有如下:

使用 javascript 将 link 标签插到 head 标签最后

1
2
3
4
5
6
7
8
9
// 创建 link 标签
const myCSS = document.createElement("link");
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到 header 的最后位置
document.head.insertBefore(
myCSS,
document.head.childNodes[document.head.childNodes.length - 1].nextSibling
);

设置 link 标签 media 属性为 noexis,浏览器会认为当前样式表不适用当前类型,会在不阻塞页面渲染的情况下再进行下载。加载完成后,将 media 的值设为 screen 或 all,从而让浏览器开始解析 CSS

1
2
3
4
5
6
<link
rel="stylesheet"
href="mystyles.css"
media="noexist"
onload="this.media='all'"
/>

通过 rel 属性将 link 元素标记为 alternate 可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将 rel 设回 stylesheet

1
2
3
4
5
<link
rel="alternate stylesheet"
href="mystyles.css"
onload="this.rel='stylesheet'"
/>

资源压缩

利用 webpackgulp/gruntrollup 等模块化工具,将 css 代码进行压缩,使文件变小,大大降低了浏览器的加载时间

合理使用选择器

css 匹配的规则是从右往左开始匹配,例如 #markdown .content h3 匹配规则如下:

  • 先找到 h3 标签元素
  • 然后去除祖先不是 .content 的元素
  • 最后去除祖先不是 #markdown 的元素

如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高

所以我们在编写选择器的时候,可以遵循以下规则:

  • 不要嵌套使用过多复杂选择器,最好不要三层以上
  • 使用 id 选择器就没必要再进行嵌套
  • 通配符和属性选择器效率最低,避免使用

减少使用昂贵的属性

在页面发生重绘的时候,昂贵属性如 box-shadow/border-radius/filter/opacity/:nth-child 等,会降低浏览器的渲染性能

不要使用@import

css 样式文件有两种引入方式,一种是 link 元素,另一种是@import

@import 会影响浏览器的并行下载,使得页面在加载时增加额外的延迟,增添了额外的往返耗时

而且多个@import 可能会导致下载顺序紊乱

比如一个 css 文件 index.css 包含了以下内容:@import url("reset.css")

那么浏览器就必须先把 index.css 下载、解析和执行后,才下载、解析和执行第二个文件 reset.css

其他

  • 减少重排操作,以及减少不必要的重绘
  • 了解哪些属性可以继承而来,避免对这些属性重复编写
  • cssSprite,合成所有 icon 图片,用宽高加上 backgroud-position 的背景图方式显现出我们要的 icon 图,减少了 http 请求
  • 把小的 icon 图片转成 base64 编码
  • CSS3 动画或者过渡尽量使用 transform 和 opacity 来实现动画,不要使用 left 和 top 属性

总结

css 实现性能的方式可以从选择器嵌套、属性特性、减少 http 这三面考虑,同时还要注意 css 代码的加载顺序。

在 HTML 中会遇到以下三类 script:

1
2
3
<script src="xxx"></script>
<script src="xxx" async></script>
<script src="xxx" defer></script>

script 标签用于加载脚本与执行脚本,直接使用 script 脚本时,html 会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的 DOM 渲染。

比如现在大家习惯于在页面中引用各种第三方脚本,但如果第三方服务商出现了一些小问题,比如延迟之类的,就会使得页面白屏。

针对上述情况,script 标签提供了两种方式来解决问题,就是加入属性 async 以及 defer,这两个属性使得 script 标签加载都不会阻塞 DOM 的渲染。

  • defer:此布尔属性被设置为向浏览器指示脚本在文档被解析后执行。
  • async:设置此布尔属性,以指示浏览器如果可能的话,应异步执行脚本。

defer

如果 script 标签设置了 defer 属性,则浏览器会异步下载该文件并且不会影响后续 DOM 的渲染。

如果有多个设置了 defer 属性的 script 标签存在,则会按照顺序执行所有的 script,defer 脚本会在文档渲染完毕后,DOMContentLoaded 事件调用前执行。

async

async 属性会使得 script 脚本异步的加载并在允许的情况下执行,而 async 的执行并不会按照 script 标签在页面中的顺序来执行,而是谁先加载完谁先执行。

思考:该处理是否必须同步完成?数据是否必须按顺序完成?

解决方案

  1. 将数据分页,利用分页的原理,每次服务器端只返回一定数目的数据,浏览器每次只对一部分进行加载。

  2. 使用懒加载的方法,每次加载一部分数据,其余数据当需要使用时再去加载。

  3. 使用数组分块技术,基本思路是为要处理的项目创建一个队列,然后设置定时器每过一段时间取出一部分数据,然后再使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。

在使用 Git 作为版本控制的时候,我们可能会由于各种各样的原因提交了许多临时的 commit,而这些 commit 拼接起来才是完整的任务。那么我们为了避免太多的 commit 而造成版本控制的混乱,通常我们推荐将这些 commit 合并成一个。

查看提交历史

1
git log

首先你要知道自己想合并的是哪几个提交,可以使用 git log 命令来查看提交历史,假如最近 4 条历史如下:

1
2
3
4
5
6
7
commit 3ca6ec340edc66df13423f36f52919dfa3......

commit 1b4056686d1b494a5c86757f9eaed844......

commit 53f244ac8730d33b353bee3b24210b07......

commit 3a4226b4a0b6fa68783b07f1cee7b688.......

历史记录是按照时间排序的,时间近的排在前面。

git rebase

想要合并 1-3 条,有两个方法:

从 HEAD 版本开始往过去数 3 个版本

1
git rebase -i HEAD~3

指名要合并的版本之前的版本号

1
git rebase -i 3a4226b

请注意 3a4226b 这个版本是不参与合并的,可以把它当做一个坐标

选取要合并的提交

  1. 执行了 rebase 命令之后,会弹出一个窗口,头几行如下:
1
2
3
4
5
pick 3ca6ec3   '注释**********'

pick 1b40566 '注释*********'

pick 53f244a '注释**********'
  1. 将 pick 改为 squash 或者 s,之后保存并关闭文本编辑窗口即可。改完之后文本内容如下:
1
2
3
4
5
pick 3ca6ec3   '注释**********'

s 1b40566 '注释*********'

s 53f244a '注释**********'
  1. 然后保存退出,Git 会压缩提交历史,如果有冲突,需要修改,修改的时候要注意,保留最新的历史,不然我们的修改就丢弃了。修改以后要记得敲下面的命令:
1
2
3
git add .

git rebase --continue

如果你想放弃这次压缩的话,执行以下命令:

1
git rebase --abort
  1. 如果没有冲突,或者冲突已经解决,则会出现如下的编辑窗口:
1
2
3
4
5
6
7
8
# This is a combination of 4 commits.
#The first commit’s message is:
注释......
# The 2nd commit’s message is:
注释......
# The 3rd commit’s message is:
注释......
# Please enter the commit message for your changes. Lines starting # with ‘#’ will be ignored, and an empty message aborts the commit.
  1. 输入 wq 保存并推出, 再次输入 git log 查看 commit 历史信息,你会发现这两个 commit 已经合并了。

  1. git push origin --delete [branchName] 删除远程分支;

  2. git branch -a 查看远程和本地所有分支,发现还会显示已删除的分支;

  3. git fetchgit pull 后依然如此;

  4. git remote show origin 查看远程库,看到远程分支和本地分支的对应关系;

  5. git remote prune origin 删除远程没有,本地有的分支;

  6. git branch -a 再次查看,即与远程分支同步。

  • 小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口;

  • 架构是数据驱动的架构模式,UI 和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现;

  • 从技术讲和现有的前端开发差不多,采用 JavaScript、WXML、WXSS 三种技术进行开发;

  • 功能可分为 webview 和 appService 两个部分;

  • webview 用来展现 UI,appService 有来处理业务逻辑、数据及接口调用;

  • 两个部分在两个进程中运行,通过系统层 JSBridge 实现通信,实现 UI 的渲染、事件的处理等。

vue

1
2
let page = this.$mp.page.$getAppWebview();
page.setStyle({ popGesture: "none" });

注意:

  • 此方法在普通 vue 页面使用如下代码禁用页面得侧滑返回
  • 在 nvue 中因未获取到正确的页面实例不会生效

nvue

1
2
3
let pages = getCurrentPages();
let currentPages = pages[pages.length - 1].$getAppWebview();
currentPages.setStyle({ popGesture: "none" });