0%

什么是作用域

作用域可以视为一套规则,引用《你不知道的 JavaScript》(上卷)中的解释,如下

这套规则可以用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找

词法作用域

什么是词法作用域

词法作用域是由你写代码时将变量和块作用域写在哪里决定的

思考以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;

function fn1() {
var a = 2;

function fn2() {
console.log(a);
}

fn2();
}

fn1();

它的作用域如图所示

scope

  • 1包含整个全局作用域,其中有afn1

  • 2包含fn1创建的作用域,其中有afn2

  • 3包含fn2创建的作用域

fn2执行的时候,会先找3作用域,没找到,然后向上找到2作用域里面的a,所以这里会输出2

再思考以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;

function fn2() {
console.log(a);
}

function fn1() {
var a = 2;

fn2();
}

fn1();

结果会输出1

没有任何函数的作用域可以同时出现在两个外部作用域中

所以,这里的作用域,如图所示

scope

fn2执行的时候,会先找2作用域,没找到,然后向上找到1作用域里面的a,所以这里会输出1

欺骗词法作用域

词法作用域完全由写代码时函数声明的位置来定义的,那怎么来动态修改或者欺骗词法作用域呢?

eval

eval可以动态插入 JS 代码,在执行eval之后的代码时,引擎就不知道前面代码的是否是动态插入的,是否会修改作用域,引擎只会按照既定的规则进行作用域查找

思考以下代码

1
2
3
4
5
6
7
8
var a = 1;

function fn1(a) {
eval(a);
console.log(a);
}

fn1('var a = 2;');

eval(a)会在fn1创建的作用域中添加一个变量a,所以会输出2

with

with 会根据你传递给它的对象,新建一个全新的作用域

思考以下代码

1
2
3
4
5
6
7
8
9
10
var a = 1;

function fn1(obj) {
with (obj) {
a = 2;
}
console.log(a);
}

fn1({});

当把空对象{}传递给with时,新建作用域其实就是这个对象,然而对象中没有a标志符,就会向上查找,所以会修改全局变量的a,最后会输出2

Promise/A+

下面是一个简单的Promise的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise1 = new Promise((resolve, reject) => {
console.log(1);
resolve(2);
console.log(3);
reject(4);
});

const promise2 = new Promise((resolve, reject) => {
reject(5);
});

console.log(6);

promise1.then(console.log, console.log);
promise2.then(console.log, console.log);

它对应的输出结果为

1
2
3
4
5
1
3
6
2
5

结合Promise/A+规范,可以总结出Promise基本特征如下所示(promise表示Promise的实例)

  1. new Promise时,需要传入一个立即执行函数executor
  2. executor接受两个异步执行的回调函数,分别是成功后的回调resolve和失败后的回调reject
  3. promise只有三个状态,pendingfulfilledrejected
  4. promise初始状态为pending
  5. promise只能从pending修改为fulfilled或者rejected,修改以后不可逆,无法再次修改
  6. promise有一个value属性,用来保存成功后返回的值
  7. promise有一个reason属性,用来保存失败后返回的值
  8. promise有一个then方法,接收 Promise成功的回调onFulfilledPromise失败的回调onRejected

Promise 基本结构

下面是一个 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
const PromiseStatusEnum = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
};

class MyPromise {
constructor(executor) {
this.status = PromiseStatusEnum.PENDING;
this.value = null;
this.reason = null;
this.resolveQueue = [];
this.rejectQueue = [];
}

then = (onFulfilled, onRejected) => {};

catch = (onRejected) => {};

finally = (callback) => {};

static resolve = (value) => {};

static reject = (error) => {};

static all = (promiseArr) => {};

static race = (promiseArr) => {};
}

executor

executor接受两个回调函数resolvereject,通过箭头函数绑定当前实例的this,通过setTimeout异步执行

1
2
3
4
5
6
7
8
9
const resolve = (value) => {
const run = () => {};
setTimeout(run);
};

const reject = (error) => {
const run = () => {};
setTimeout(run);
};

run主要负责

  1. 判断当前的状态是否为pending,并对其修改
  2. 记录成功或者失败的返回值

基本的executor如下

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
const resolve = (value) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.FULFILLED;
this.value = value;
}
};
setTimeout(run);
};

const reject = (error) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.REJECTED;
this.reason = error;
}
};
setTimeout(run);
};

//兼容executor的异常情况
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}

then

then接受两个回调函数onFulfilledonRejected,并返回一个Promise对象

1
2
3
4
5
6
7
then = (onFulfilled, onRejected) => {
// onFulfilled或者onRejected不是函数时,返回当前的值
typeof onFulfilled !== 'function' ? (onFulfilled = (value) => value) : null;
typeof onRejected !== 'function' ? (onRejected = (error) => error) : null;

return new MyPromise((resolve, reject) => {});
};

再根据promise的状态进行不同的操作

  1. state === 'fulfilled'时,执行 onFulfilled
  2. state === 'rejected'时,执行 onRejected
  3. 如果在执行回调函数时抛出了异常,那么就会把这个异常作为参数,直接reject
  4. 回调函数返回的值不是Promise,直接resolve
  5. 回调函数返回的值是Promise,调用then方法,保证Promise会被全部执行
  6. state === 'pending'时,需要将回调函数放入队列中,等待执行
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
then = (onFulfilled, onRejected) => {
return new MyPromise((resolve, reject) => {
const resolveFn = (value) => {
try {
const x = onFulfilled(value);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};

const rejectFn = (error) => {
try {
const x = onRejected(error);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};

switch (this.status) {
case PromiseStatusEnum.PENDING:
this.resolveQueue.push(resolveFn);
this.rejectQueue.push(rejectFn);
break;
case PromiseStatusEnum.FULFILLED:
resolveFn(this.value);
break;
case PromiseStatusEnum.REJECTED:
rejectFn(this.reason);
break;
}
});
};

resolveQueuerejectQueue收集的回调,需要在executor中的resolvereject执行

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
const resolve = (value) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.FULFILLED;
this.value = value;

while (this.resolveQueue.length) {
const callback = this.resolveQueue.shift();
callback(value);
}
}
};

setTimeout(run);
};
const reject = (error) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.REJECTED;
this.reason = error;

while (this.rejectQueue.length) {
const callback = this.rejectQueue.shift();
callback(error);
}
}
};

setTimeout(run);
};

catch

实现了then以后,catch就比较简单了,直接调用thenonFulfilled传空就行了

1
catch = (onRejected) => this.then(undefined, onRejected);

finally

finally接受一个回调函数,在promise结束时,无论结果是fulfilled或者是rejected,都会执行该回调函数

1
2
3
4
5
finally = (callback) =>
this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(error) => MyPromise.resolve(callback()).then(() => MyPromise.reject(error))
);

完整代码

还有一些静态方法,resolverejectallreace等,基本实现了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
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
const PromiseStatusEnum = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
};

class MyPromise {
constructor(executor) {
this.status = PromiseStatusEnum.PENDING;
this.value = null;
this.reason = null;
this.resolveQueue = [];
this.rejectQueue = [];

const resolve = (value) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.FULFILLED;
this.value = value;

while (this.resolveQueue.length) {
const callback = this.resolveQueue.shift();
callback(value);
}
}
};

setTimeout(run);
};
const reject = (error) => {
const run = () => {
if (this.status === PromiseStatusEnum.PENDING) {
this.status = PromiseStatusEnum.REJECTED;
this.reason = error;

while (this.rejectQueue.length) {
const callback = this.rejectQueue.shift();
callback(error);
}
}
};

setTimeout(run);
};

try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}

then = (onFulfilled, onRejected) => {
return new MyPromise((resolve, reject) => {
const resolveFn = (value) => {
try {
const x = onFulfilled(value);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};

const rejectFn = (error) => {
try {
const x = onRejected(error);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};

switch (this.status) {
case PromiseStatusEnum.PENDING:
this.resolveQueue.push(resolveFn);
this.rejectQueue.push(rejectFn);
break;
case PromiseStatusEnum.FULFILLED:
resolveFn(this.value);
break;
case PromiseStatusEnum.REJECTED:
rejectFn(this.reason);
break;
}
});
};

catch = (onRejected) => this.then(undefined, onRejected);

finally = (callback) =>
this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(error) => MyPromise.resolve(callback()).then(() => MyPromise.reject(error))
);

static resolve = (value) =>
new MyPromise((resolve, reject) =>
value instanceof MyPromise ? value : new MyPromise((resolve, reject) => resolve(value))
);

static reject = (error) => new MyPromise((resolve, reject) => reject(error));

// 静态all方法
static all = (promiseArr) => {
let count = 0;
let result = [];
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result);
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(
(value) => {
count++;
result[i] = value;
if (count === promiseArr.length) {
resolve(result);
}
},
(error) => {
reject(error);
}
);
});
});
};

// 静态race方法
static race = (promiseArr) =>
new MyPromise((resolve, reject) => {
promiseArr.forEach((p) => {
MyPromise.resolve(p).then(
(value) => {
resolve(value);
},
(error) => {
reject(error);
}
);
});
});
}

const promise1 = new MyPromise((resolve, reject) => {
console.log(1);
resolve(2);
console.log(3);
reject(4);
});

const promise2 = new MyPromise((resolve, reject) => {
reject(5);
});

console.log(6);

promise1.then(console.log, console.log);
promise2.then(console.log, console.log);

它对应的输出结果与之前一致

1
2
3
4
5
1
3
6
2
5

背景

项目里有用到inline-manifest-webpack-plugin,当把html-webpack-plugin升级到v4时,插件就报错了。原本想等插件适配,结果发现不维护了。所以自己动手实现一个吧!

webpack 构建流程

webpack的基本构建流程如下:

  1. webpack 初始化: 包括校验配置文件,初始化默认插件,创建Compiler实例等
  2. run: 执行 run 方法
  3. compilation:创建Compilation实例,回调compilation相关钩子
  4. emit:文件内容已经解析完毕,准备生成文件
  5. afterEmit:文件已经写入磁盘
  6. done:完成编译

webpack 插件实例

webpack-plugin需要实现一个apply方法,参数是Compiler实例,下面是一个简单的实例

1
2
3
4
5
6
7
8
9
class MyWebpackPlugin {
constructor(options) {}
apply(compiler) {
compiler.hooks.emit.tap("MyWebpackPlugin", (compilation) => {
console.log("MyWebpackPlugin");
});
}
}
module.exports = MyWebpackPlugin;

只需要在webpack的配置文件中,添加插件就能在控制台看见输出了

1
2
3
4
5
module.exports = {
plugins:[
new MyWebpackPlugin(),
]
};

要实现inline-manifest-webpack-plugin的功能,只需要在合适的hook里面实现我们的逻辑就行了

更多 hook

inline-manifest-webpack-plugin 的作用

将我们选择的chunk文件直接插入到html中,减少请求次数,方便做缓存等,一般用来抽离 webpack 的运行时文件(runtime.js)。它大概实现了一下三个功能:

  1. 删除index.html里的script标签
  2. 将删除标签所对应的内容直接插入index.html
  3. 删除原来的js文件

删除 index.html 里的 script 标签

inline-manifest-webpack-plugin是基于html-webpack-plugin上的插件,html-webpack-plugin为我们提供的 hook 如图所示

Events

在生成标签之前(beforeAssetTagGeneration)去掉指定 chunk 文件

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
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

// 缓存第二步里插入的内容和位子
this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);
});

将删除标签所对应的内容直接插入 index.html

生成标签的时候把第一步缓存的标签插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

删除原来的 js 文件

在 webpack 生成所以文件之后,删除原本的文件

1
2
3
4
5
// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});

此时插件的功能基本上实现完成,一下是完整代码

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
const sourceMappingURL = require("source-map-url");
const HtmlWebpackPlugin = require("html-webpack-plugin");

class InlineManifestWebpackPlugin {
constructor(chunkName) {
this.chunkName = chunkName || "runtime";
this.chunkScript = null;
this.chunkIndex = -1;
}

getAssetByName = (chunks, chunkName) => {
return (chunks.find((chunk) => chunk.name === chunkName) || { files: [] }).files[0];
};

apply(compiler) {
// 注入 html-webpack-plugin 的处理过程
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);

// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});
}
}

module.exports = InlineManifestWebpackPlugin;

背景

现在绝大部分异步请求都有如下类似的套路代码

1
2
3
4
loading = true;
ajax().finally(() => {
loading = false;
});

都 0202 年了,高速的网络会导致 loading 出现闪烁情况

Promise.all 的解决方案

假设我有两个 ajax 请求时间分别是50ms150ms,我现在希望不管是50ms还是150ms,loading 动画都有一个比较完整的展示时间

这种情况只需要用一个延迟的 Promise.resolve(),通过 Promise.all 方法去拉长响应时间

1
2
3
4
5
6
7
8
9
const delay = ms => new Promise((resolve, _) => setTimeout(resolve, ms));

loading = true;
Promise.all([ajaxPromise, delay(300)])
.then(handleSuccess)
.catch(handleError)
.finally(() => {
loading = false;
});

这种解决方案对于响应快的情况有点本末倒置的感觉

Promise.race 的解决方案

现在我希望响应时间超过100ms的情况才展示 loading 动画

这种情况只需要用一个延迟的 Promise.reject(),通过 Promise.race 方法去和 ajax 竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const timeout = ms =>
new Promise((_, reject) => setTimeout(() => reject(Symbol.for('timeout')), ms));

Promise.race([ajaxPromise, timeout(100)])
.then(handleSuccess)
.catch(err => {
if (Symbol.for('timeout') === err) {
loading = true;
return ajaxPromise
.then(handleSuccess)
.catch(handleError)
.finally(() => {
loading = false;
});
}
return handleError(err);
});

当我的响应时间为101ms的时候,闪烁还是无法避免的

Promise.all 和 Promise.race 的解决方案

现在我希望响应时间小于100ms时不展示 loading 动画,大于100ms时展示300ms的 loading 动画时间

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
const timeout = ms =>
new Promise((_, reject) => setTimeout(() => reject(Symbol.for('timeout')), ms));

const delay = ms => new Promise((resolve, _) => setTimeout(resolve, ms));

const request = ({ config, target, timeoutTime = 100, delayTime = 300 }) => {
// 返回promise的ajax请求
const promise = axios(config);

const startLoading = target => {
if (!target) {
return;
}
// startLoading
};

const endLoading = () => {
// endLoading
};

const handleSuccess = data => {
// 兼容Promise.all和Promise.race不同的返回值
const response = Array.isArray(data) ? data[0] : data;
// 处理成功的情况
return Promise.resolve(response.data);
};

const handleError = ({ response }) => {
// 处理失败的情况
return Promise.reject(response);
};

return Promise.race([promise, timeout(timeoutTime)])
.then(handleSuccess)
.catch(err => {
if (Symbol.for('timeout') === err) {
startLoading(target);
return Promise.all([promise, delay(delayTime)])
.then(handleSuccess)
.catch(handleError)
.finally(() => {
endLoading();
});
}
return handleError(err);
});
};

timeoutTime 和 delayTime 可以根据自己的网站调整

什么是hook

hook可以让你在不编写class组件的情况下使用state以及其他的React特性

具体的使用方法可见官网

实现useState

我们都知道useState会返回一个状态变量和修改它的函数,就像这样

1
const [state, setState] = useState();

单一状态

那么针对一个变量的情况就简单很多了,只需要用一个全局变量就能简单的实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const state = null;

const useState = defaultState => {
if (!state) {
state = defaultState;
}
const setState = newState => {
state = newState;
};
return [state, setState()];
};

const singleState = () => {
const [count, setCount] = useState(0);
console.log(count);
setCount(count + 1);
};

多个状态

而对于多个状态可能就需要一个格外的变量num来标记,我们用的哪一 state

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
const states = {};
let num = 0;

const useState = defaultState => {
if (!states[num]) {
states[num] = defaultState;
}
const setState = newState => {
states[num] = newState;
};
const result = [states[num], setState];
num += 1;
return result;
};

const withHook = renderFunc => {
return (...args) => {
// 重置
num = 0;
return renderFunc(...args);
};
};

withHook(function multipleState() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

console.log(count1, count2);
setCount1(count1 + 1);
setCount2(count2 + 2);
});

注意每次调用函数组件的时候应该把num重置

多个函数

对于多个函数组件的话,函数相互调用会打乱我们num的顺序,那应该怎么保持有序执行呢?这里就要用一个stack,像我们平时调试程序的时候会在浏览器控制台里面看到一个call stack调用栈,每次运行一个函数就把它入栈,执行完毕就出栈,这样就保证了顺序

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
const contextStack = [];

const useState = defaultState => {
const context = contextStack[contextStack.length - 1];
const { num, states } = context;

if (!states[num]) {
states[num] = defaultState;
}
const setState = newState => {
states[num] = newState;
};
const result = [states[num], setState];
context.num += 1;
return result;
};

const withHook = renderFunc => {
return (...args) => {
contextStack.push({ num: 0, states: [] });
const result = renderFunc(...args);
contextStack.pop();
return result;
};
};

withHook(function state1() {
const [count1, setCount1] = useState(0);

console.log(count1);
setCount1(count1 + 1);
});

withHook(function state2() {
const [count2, setCount2] = useState(0);

console.log(count2);
setCount2(count2 + 2);
});

React结合起来

React是基于Fiber来实现,对于16.6以下的版本,我们用类组件来实现

根据上面的代码,我们只需要在setState里面去更新函数就可以了,代码如下

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
import { Component } from 'react';

const contextStack = [];

const useState = defaultState => {
const context = contextStack[contextStack.length - 1];
const { component } = context;
const states = Object.values(component.state || {});

if (component.firstRender) {
states[context.num] = defaultState;
}

const setState = num => val => {
component.setState({ [num]: val });
};

const result = [states[context.num], setState(context.num, component)];
context.num += 1;
return result;
};

const withHook = renderFunc => {
const HooksComponent = class extends Component {
constructor(props) {
super(props);
// 标记组件是否是第一次渲染,对于useState初始值的优化
this.firstRender = true;
}

componentDidMount() {
this.firstRender = false;
}

render() {
contextStack.push({ num: 0, component: this });
const result = renderFunc(this.props);
contextStack.pop();
return result;
}
};
HooksComponent.displayName = renderFunc.name;
return HooksComponent;
};

export { withHook, useState };

总结

整个实现过程还是比较容易,先从最简单的一个函数一个状态到多个函数多个状态循序渐进

实现过程参考这篇文章

什么是 gRPC

gRPC中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,可以更轻松地创建分布式应用程序和服务。gRPC基于定义服务的思想,指定可以使用其参数和返回类型远程调用的方法。在服务器端,服务器实现此接口并运行gRPC服务器来处理客户端调用。在客户端,它提供与服务器相同的方法

什么是 Protocol Buffers

Protocol Buffers是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据

Web 上的实践

准备工作

  1. 需要安装protobuf

mac 可用用 homebrew 安装,我装的版本为 3.7.1

1
brew install protobuf
  1. 安装protoc-gen-grpc-web
1
2
$ sudo mv ~/Downloads/protoc-gen-grpc-web-1.0.6-darwin-x86_64 /usr/local/bin/protoc-gen-grpc-web
$ chmod +x /usr/local/bin/protoc-gen-grpc-web
  1. 安装grpc-webgoogle-protobuf
1
2
yarn add grpc-web
yarn add google-protobuf

开始实践

  1. 定义 proto 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// proto 版本
syntax = "proto3";

// 包名
package grpc;

// 定义请求参数结构
message RequestDTO {
string id = 1;
}

// 定义返回结构
message ResponseBO {
}

// 定义请求方法
service DemoService {
// 获取用户普通话主页
rpc GetInfo (RequestDTO) returns (ResponseBO) {
}
}

proto 文件可以找后端的同事要。这里有个坑,import 路径不能像服务端的包名一样,我就只有放在一个目录里面,直接引入文件名import 'xxx.proto'

  1. 编译 js 和 client 文件
1
2
3
4
# js
protoc -I=$DIR *.proto --js_out=import_style=commonjs:$OUT_DIR
# grpc-web
protoc -I=$OUT_DIR *.proto --grpc-web_out=import_style=commonjs,mode=grpcweb:$OUT_DIR

$DIR为你 proto 文件所在路径,$OUT_DIR为输出路径,我用的当前文件. import_style采用的前端比较常用的commonjs mode支持文本和二进制,我用的grpcweb,也可以用grpctext

  1. 编写 js 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 引入路径根据你生成文件的路径做相应的调整
const { DemoServiceClient } = require('./proto/grpc/service_grpc_web_pb');
const { RequestDTO } = require('./proto/grpc/demo.js');

const client = new DemoServiceClient('https://local.hongkazhijia.com:3000');

const main = () => {
const requestDTO = new RequestDTO();
requestDTO.setId('test');

const getInfo = client.getInfo(requestDTO, {}, (err, response) => {
if (err) {
console.log(err);
} else {
console.log(response);
}
});
getInfo.on('status', status => console.log('status', status));
getInfo.on('data', data => console.log('data', data));
getInfo.on('end', end => console.log('end', end));
getInfo.on('error', error => console.log('error', error));
};

export { main };
  1. 浏览器跨域问题

开发时调用的服务端的接口和本地地址肯定会有跨域问题平时开发的时候用的 webpack-devServer 做反向代理,因为gRPC基于http2,所以在配置文件里面设置http2 = true(注意http2只支持node < 10)

DemoServiceClient 的地址指向本地,然后添加一个 proxy

1
2
3
4
5
// grcp.DemoService 为你的package名 + service名称,不知道的话先直接请求一次直接在console里面看就行了
'/grcp.DemoService': {
target: 'http://xxx.xx.xx.xx:xxxx',
changeOrigin: true,
},

至此整个流程就跑通啦,你就能在控制台看见后端返回的结果了

剩下的就是组织代码,怎么更好的集成在自己的项目中了

维基百科中的定义

在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)

而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在

1
2
3
4
5
╭─────────────╮                 ╭───────────────╮   Fire Event   ╭──────────────╮
│ │ Publish Event │ │───────────────>│ │
│ Publisher │────────────────>│ Event Channel │ │ Subscriber │
│ │ │ │<───────────────│ │
╰─────────────╯ ╰───────────────╯ Subscribe ╰──────────────╯

实现思路

  1. 作为发布者提供 subscribe 方法给订阅者

  2. 发布者提供了订阅的方法后应该将这些订阅者都存起来,记录以便日后给他们推送消息

  3. 作为发布者要有 publish 方法推送消息

  4. 订阅者如何订阅消息或者事件,订阅者还可以通过 unsubscribe 取消订阅

代码实现

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
class PubSub {
topics = {};

subUid = -1;

subscribe = (topic, func) => {
const token = (this.subUid += 1).toString();
// 如果没有订阅过此类消息,创建一个缓存列表
if (!this.topics[topic]) {
this.topics[topic] = [];
}
this.topics[topic].push({
token,
func,
});
return token;
};

unsubscribe = token => {
Object.values(this.topics).forEach(value => {
value.forEach((item, idx) => {
if (item.token === token) {
value.splice(idx, 1);
console.log(`unsubscribe: ${token}`);
}
});
});
};

publish = (topic, ...args) =>
this.topics[topic]?.forEach(({ func }) => {
func(args);
});
}

// 一个简单的消息记录器,记录通过我们收到的任何主题和数据
const messageLogger = msg => {
console.log(`Logging: ${msg}`);
};

const pubSub = new PubSub();

const subscription1 = pubSub.subscribe("friend1", messageLogger);
const subscription2 = pubSub.subscribe("friend2", messageLogger);

pubSub.publish("friend1", "hello, friend1!");
pubSub.publish("friend2", "hello, friend2!");

pubSub.unsubscribe(subscription1);

pubSub.publish("friend1", "goodbye, friend1!");
pubSub.publish("friend2", "goodbye, friend2!");

// console
// Logging: hello, friend1!
// Logging: hello, friend2!
// unsubscribe: 0
// Logging: goodbye, friend2!

背景

公司最近要做一个 web 网页(只设计了 pc 端)移动端的适配,我也是第一次做涉及到移动端的开发,前期在网上查了一些资料,总结方案如下:

  1. 固定高度,宽度自适应

  2. 固定宽度,viewport 缩放

  3. rem 做宽度,viewport 缩放

用 rem 来页面,会根据不同的设备宽度在根元素上设置不同的字体大小。然后使用px替换rem。这样做以后,字体大小,内容尺寸,对随着屏幕宽度的变大而变大。

第一种方案太笨,后面两种都可以做到更好的适配,第三种比较灵活一点。所以采用第三种方案来做适配

涉及到的知识点

  1. viewport

    在电脑浏览器中,viewport 就是浏览器窗口的宽度高度。在移动端设备上就很复杂,简单点来说就是你看到的屏幕大小

  2. 物理像素

    物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件

  3. 设备独立像素(density-independent pixel)

    设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说 CSS 像素),然后由相关系统转换为物理像素

  4. CSS 像素

    CSS 像素是一个抽像的单位,主要使用在浏览器上,用来精确度量 Web 页面上的内容。一般情况之下,CSS 像素称为与设备无关的像素(device-independent pixel),简称 DIPs

  5. 屏幕密度

    屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)

  6. 设备像素比(device pixel ratio)

    简称 dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按设备像素比 = 物理像素 / 设备独立像素计算得到

  7. rem

    就是根元素<html>font-size大小

实践

使用lib-flexible,它会帮我们计算好 rem。接下来把视觉稿中的px转换成rem就行了,但是每次转换会很费时间,所以我们可以借助插件来帮我们完成,这里我采用的是 px2rem-loader(webpack)。最后你就可以放心的按照设计稿开发了。


参考使用 Flexible 实现手淘 H5 页面的终端适配

实现原理

vuex 或者 mobx 都是通过 Object.defineProperty 来实现数据劫持。当我们访问或设置对象的属性的时候,都会触发相对应的函数,通过劫持对象属性的 setter 和 getter 操作,来进行双向绑定。其主要由下面三个部分组成:

  • Observer 负责数据劫持,把所有的属性定义成被观察者,达到对数据进行观测的目的

  • Watcher 数据的观察者,在数据发生变化之后执行的相应的回调函数

  • Dep 每一个 observer 会创建一个 Dep 实例,实例在 get 数据的时候为数据收集 watcher,在 set 的时候执行 watcher 内的回调方法

代码实现

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
// Dep
class Dep {
sub = [];
addSub = watcher => this.sub.push(watcher);
notify = () => this.sub.forEach(watcher => watcher.fn());
}

// Observer
class Observer {
constructor(obj, key, value) {
const dep = new Dep();
let backup = value;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
// 存储依赖
dep.addSub(Dep.target);
}
return backup;
},
set(newVal) {
if (backup === newVal) return;
backup = newVal;
// 执行依赖
dep.notify();
},
});
}
}

// Watcher
class Watcher {
constructor(data, k, fn) {
this.fn = fn;
Dep.target = this;
// 触发get方法
data[k];
Dep.target = null;
}
}

const observable = obj => {
if (Object.prototype.toString.call(obj) === "[object Object]") {
// 递归
Object.entries(obj).forEach(([key, value]) => {
if (Object.prototype.toString.call(value) === "[object Object]") {
observable(value);
}
new Observer(obj, key, value);
new Watcher(obj, key, (v, key) => {
console.log("你修改了数据");
// ...你想要的操作
});
});
}
};

下载

Pre-built

集成

将下载好的包放你项目的里,nginx 配置

1
2
3
4
location / {
# 没找到对应html返回index.html
try_files $uri /index.html =404;
}

访问

根据你的路径访问 web/viewer.html?file=${yourpdf}