Myer's Diff algorithm

前言

本文主要参考自Myers’ Diff Algorithm(1)(算法原理)和

Myers’ Diff Algorithm(2)(优化后的算法)这两篇文章,

根据自己的理解对算法步骤进行说明。原论文(http://www.grantjenks.com/wiki/_media/ideas:diffalgorithmlcs.pdf)。
在本文中出现的名词如:

  • snake
  • diagonal k(对角线k)
  • edit graph(编辑图)
  • furthest reaching point(最远到达点,实则为编辑图的横坐标)
  • V[k](记录对角线k的最远到达点)
  • edit script(编辑路径)
    可在上述的参考文章中找到说明。
    算法的思想建基于:
  1. 最短编辑距离的路径会包含一条有最远到达点的对角线
  2. 编辑路径D(D表示编辑长度)与对角线k的关系为
    注意编辑图的(0, 0)点不表示字符串A和B各自的第一个字符, 而是表示没有任何字符编辑的出发点

algorithm1

初步的算法(在文章1中)就是从0开始到最大D循环,从每个D中拿到对角线k的序列,进行INSERT(B向前一步)或DELETE(A向前一步)的编辑。

看看后面的字符是否相同,如果相同则A和B的各向前一步;

如果不同则把当前的横坐标记回到V[k]中。如当前坐标到达边界,则当前的D就为最短编辑路径。

algorithm2

  1. 在文章2中介绍了1中的算法的优化,它包含了分治的思想。
    概括地说是

    1. 记A B长度的差delta为N - M
    2. 从起始点正向执行1中的算法和从终点反向执行,它们的V[k]分别记为VForward[k]和VBackward[k]
    3. 当VForward[k] > VBackward[k]时停止迭代,同时需要满足当delta为奇数时,,迭代要在Forward处结束;
      当delta为偶数时,迭代要在Backward时结束。
    4. 结束3中的最后一次迭代的路径称为middle snake,在原论文中证明middle snake为最短编辑路径的子路径
    5. 根据middle snake(左上角和上一次的出发点)和(右下角和上一次的终点)把编辑图分成左上和右下两个子图,这两个子问题。

    再重复执行这个这个算法,直到子问题的D不大于1,或者任意子字符串的长度等于0为止。

  2. 下面是应用算法2以求出从A到B字符串编辑操作的代码。

    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
    // 操作类型
    const RETAIN = 'retain' // 保留A和B的字符
    const INSERT = 'insert' // 插入B字符,在图中表示为垂直移动
    const DELETE = 'delete' // 删除A字符,在途中表示为水平移动

    function calcMiddleSnake(A, a0, N, B, b0, M, VForward, VBackward) {
    const delta = N - M
    const maxD = Math.floor((N + M + 1) / 2)
    //初始化V序列
    VForward[1] = 0
    //初始的时候从delta - 1的对角线开始
    VBackward[N - M - 1] = N
    const isOdd = Boolean(delta & 1)
    for (let d = 0; d <= maxD; d++) {
    for (let k = -d; k <= d; k += 2) {
    // k为-d时必为向下,k为d时必为向右,其余情况选择V序列较大的方向,这里的方向表示从Kprev到K
    const down = k === -d || (k !== d && VForward[k - 1] < VForward[k + 1])
    const kPrev = down ? k + 1 : k - 1
    const startX = VForward[kPrev]
    const startY = startX - kPrev
    const midX = down ? startX : startX + 1
    const midY = midX - k
    let endX = midX
    let endY = midY
    while (endX < N && endY < M && A[a0 + endX] === B[b0 + endY]) {
    endX++
    endY++
    }
    VForward[k] = endX
    // 当delta为奇数时,运行到这里才有反向d = 正向d - 1,才需要返回middleSnake,
    // k < delta - (d - 1) || k > delta + (d - 1)是保证正向k没有超出上一轮反向k的范围
    if (!isOdd || k < delta - (d - 1) || k > delta + (d - 1)) continue
    if (VForward[k] < VBackward[k]) continue
    return [2 * d - 1, down, startX, startY, midX, midY, endX, endY]
    }
    // 正向是以k = 0为中心,反向是以k = delta为中心
    for (let k = -d + delta; k <= d + delta; k += 2) {
    // 方向选择逻辑与正向相近
    const up = k === d + delta || (k !== -d + delta && VBackward[k - 1] < VBackward[k + 1])
    const kPrev = up ? k - 1 : k + 1
    const startX = VBackward[kPrev]
    const startY = startX - kPrev
    const midX = up ? startX : startX - 1
    const midY = midX - k
    let endX = midX
    let endY = midY
    while (endX > 0 && endY > 0 && A[a0 + endX - 1] === B[b0 + endY - 1]) {
    endX--
    endY--
    }
    VBackward[k] = endX
    // 当delta为偶数时,运行到这里才有反向d = 正向,才需要返回middleSnake,
    // k < -d || k > d是保证反向k没有超出这一轮正向k的范围
    if (isOdd || k < -d || k > d) continue
    if (VBackward[k] > VForward[k]) continue
    return [2 * d, up, endX, endY, midX, midY, startX, startY]
    }
    }
    }

    function innerDiff(A, a0, N, B, b0, M, VForward, VBackward) {
    let newOps = []
    if (M === 0 && N > 0) newOps = [[DELETE, N]]
    if (N === 0 && M > 0) newOps = [[INSERT, M]]
    if (M === 0 || N === 0) return newOps
    const middleSnake = calcMiddleSnake(A, a0, N, B, b0, M, VForward, VBackward)
    const startX = middleSnake[2]
    const startY = middleSnake[3]
    const midX = middleSnake[4]
    const midY = middleSnake[5]
    const endX = middleSnake[6]
    const endY = middleSnake[7]
    const D = middleSnake[0]
    const isVertical = middleSnake[1]
    const editOp = isVertical ? INSERT : DELETE
    const middleEditOps = []
    //确保点坐标的位置符合x:[0, N],y:[0, M],才去添加middle snake的edit script
    if (midY <= M && startX >= 0 && midX <= N && startY >= 0) {
    let editLength1 = isVertical ? midY - startY : midX - startX
    if (editLength1) middleEditOps.push([(midY - startY === midX - startX) ? RETAIN : editOp, editLength1])
    }
    if (midY >= 0 && endX <= N && midX >= 0 && endY <= M) {
    let editLength2 = isVertical ? endY - midY : endX - midX
    if (editLength2) middleEditOps.push([(endY - midY === endX - midX) ? RETAIN : editOp, editLength2])
    }
    if (D > 1) {
    // diff左上角的编辑图
    newOps = innerDiff(A, a0, startX, B, b0, startY, VForward, VBackward)
    newOps.push(...middleEditOps)
    // diff右下角角的编辑图
    newOps.push(...innerDiff(A, a0 + endX, N - endX, B, b0 + endY, M - endY, VForward, VBackward))
    }
    else if (D === 0) newOps.push(...middleEditOps)
    else {
    if (startX) newOps.push([RETAIN, startX])
    newOps.push(...middleEditOps)
    }
    return newOps
    }

    function diff(A, B) {
    return innerDiff(A, 0, A.length, B, 0, B.length, [], [])
    }

    // test
    diff('react is the best framework', 'preact is the best library')
    /*
    output:
    0: ["insert", 1] insert 'p'
    1: ["retain", 18] retain 'react is the best '
    2: ["insert", 1] insert 'l'
    3: ["insert", 1] insert 'i'
    4: ["insert", 1] insert 'b'
    5: ["delete", 1] delete 'f'
    6: ["retain", 2] retain 'ra'
    7: ["delete", 1] delete 'm'
    8: ["delete", 2] delete 'ew'
    9: ["delete", 1] delete 'o'
    10: ["retain", 1] retain 'r'
    11: ["insert", 1] insert 'y'
    12: ["delete", 1] delete 'k'
    */

像redux这样的状态管理库是用来做什么的

前言

在这的很长一段之前,我一直都没有理解到为什么大型项目会需要redux这样状态管理库。因为我以前一直在觉得,redux做的无非只是把祖先的state无需通过一层层parent to child的props传递提供便利(这个功能react的context就可以做到了)和让改变数据的动作更加清晰而已。加上redux那一堆麻烦的定义文件,所以造成一直以来都不太愿意把数据放到store里面去。但是我的认识在这周发生了转变。

正文

这个转变源自于我需要做一个在同一个父组件下跟随子组件B进行view更新的子组件A。不使用store的情况就是父组件得到子组件B的变化,然后通过props传给子组件A,让子组件进行更新。可是,问题出现在,父组件也有因为涉及不当的原因,其render函数里包含了许许多多的子组件(许许多多是一个抽象的数字)。假如通过props传给子组件A,那么必然触发父组件的render方法,那么就需要一个个地去diff其下的这些子组件。这无疑是会损耗大量的性能。那么怎么解决呢。

一个直觉的类比,我们可以像直接控制DOM一样直接让子组件A更新
具体的做法,可以是通过ref获取到子组件A的实例,直接通过子组件A.setState的方式更新。虽然这种方法简单高效,但是这在react文档中是不推荐的,因为他让组件的关系变得复杂且难以预测,用某个学到的词来概括(反模式的),其实我的感觉就是这样子不够一般化,因为需要修改组件内部(这个跟react不提倡使用extends的方法创建组件的理由可能是一致的),添加ref。正确的模式其实还是应该通过传递props来引起更新。但是就会出现上面说的性能问题。这时候就要redux(准确上来讲是react-redux)的出场了。

connect组件
connect组件是react-redux提供的一个wrapper,包裹我们设计的组件形成一个高阶组件。这个wrapper主要的工作是监听store的变化,当store变化时,触发onStateUpate方法,通过用户定义好的mapStateToProps和mapDispatchToProps的方法从store中提取需要的数据设置到wrapper的state上,并在render时与非从store推导的props进行merge作为最终的props传到我们设计的组件中(如上面的子组件A)

另一个启示
上面的(暂且叫做)精准更新的需要的一个原因是避免有大量子Element的父组件,进行重新渲染。注意到大量这个词,也就是说,我们也可以通过减少父组件下子Element的数量,来提高性能。一个立刻能想到的可行性方案是,当父组件render的vdom间,所依赖的数据项有明显分解时,例如有分别依赖state.mails和state.friends产生列表项的,这时我们可以封装这两个列表项为两个组件并作为pureComponent,这样例如我们更新mails的时候就可以不进入friends列表项的render和HTMLElement diff阶段了了。

webpack.DllPlugin简单介绍配合部分源码

前言

最近深感由于公司项目过于庞大,在开发调试时,改动某处代码,常常会让 devServer 崩溃,需要重新启动打包,打包又要等待至少 5 分钟时间,严重影响开发效率这一弊病。于是乎,周末的时候看看有没有优化打包速度的方法,然后就来到这篇文章的主题了。

正文

所谓的 DLL 其实是一个预编译好的 JS 文件。在使用时除了打包 app 文件的 webpack config 外,需要有一个用于打包 dll 的 webpack cofig 文件。打包 dll 端需要加入 webpack.DllPlugin,app 端需要加入 webpack.DllReferencePlugin。
假如不加入这个 DllPlugin,就只会生成普通的打包好的 JS 文件,加入以后就会多产出一个 manifest.json 文件,表明这个 library 的包信息。
manifest.json 的作用在于在 app 端引入时,配合 webpack.DllReferencePlugin,生成相应的 externals 配置和把 require dll 文件里的模块的路径转成先 require dll 的父模块然后再去 require 子模块的形式。e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(require("../dll/alpha"));
// 这行app端的require语句会在webpack编译后的包中变成以下形式
__webpack_require__("dll-reference alpha_21c1490edb92ec8e9390")("./alpha.js")
// 前面的dll-reference alpha_21c1490edb92ec8e9390实际上是dll-reference前缀加上
// alpha_21c1490edb92ec8e9390这个包名
__webpack_require__("dll-reference alpha_21c1490edb92ec8e9390")
// 上面的这句话实际上是下面这样返回alpha_21c1490edb92ec8e9390这个全局变量
function(module, exports) {
eval("module.exports = alpha_21c1490edb92ec8e9390;\n\n");})
// 而alpha_21c1490edb92ec8e9390这个变量的定义可以简单理解为
// 一个可以require alpha_21c1490edb92ec8e9390这个包内模块的__webpack_require__函数
var alpha_21c1490edb92ec8e9390 = (function (modules) {
function __webpack_require__(moduleId) {
...
return module.exports
}
return __webpack_require__
})({'./alpha': ..., ...})

通过 plugin 的配置项进行进一步的讲解
这个 demo 来自于 webpack 官方的 example
dll 目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Downloads/dll
│ a.js
│ alpha.js
│ b.js
│ beta.js
│ build.js
│ c.jsx
│ README.md
│ template.md
│ webpack.config.js

└───dist
alpha-manifest.json
beta-manifest.json
MyDll.alpha.js
MyDll.beta.js

/dll/webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var path = require('path');
var webpack = require('webpack');
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js', '.jsx'],
},
entry: {
alpha: ['./alpha', './a', 'module'],
beta: ['./beta', './b', './c'],
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'MyDll.[name].js',
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
name: '[name]_[hash]',
}),
],
};

上面的 output.libray 和 DllPlugin 的 options.name 需要一致,假如 output.libray 为'[name]',dll 端生成的是var alpha = ...而 app 端生成的是module.exports = alpha_21c1490edb92ec8e9390,会对应不上。
DllPlugin 的 options.path:manifest.json 的输出路径
options 里还有一个属性是 context:是一个文件路径,主要作用是 manifest.json 的 content 的 key 会转化为 js 文件路径相对于这个 context 的相对路径。
e.g.假如 alpha.js 的绝对路径是 C:\Users\Logicarlme\Downloads\dll\alpha.js,context 为 C:\Users\Logicarlme\Downloads\dll,那么 key 就等于’./alpha’

app 端的 webpack.config.js
目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
Downloads/dll-user/webpack.config.js
│ build.js
│ example.html
│ example.js
│ math.js
│ README.md
│ template.md
│ webpack.config.js

├───dist
└───js
output.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
25
26
27
28
29
30
// /dll-user/webpack.config.js
var path = require('path');
var webpack = require('webpack');
module.exports = {
mode: 'development',
entry: path.join(__dirname, 'example.js'),
output: {
path: path.join(__dirname, 'js'),
filename: 'output.js',
},
plugins: [
new webpack.DllReferencePlugin({
context: path.join(__dirname, '..', 'dll', 'dist'),
manifest: require('../dll/dist/alpha-manifest.json'), // eslint-disable-line
}),
new webpack.DllReferencePlugin({
scope: 'beta',
manifest: require('../dll/dist/beta-manifest.json'), // eslint-disable-line
extensions: ['.js', '.jsx'],
}),
],
};

// /dll-user/example.js
console.log(require('../dll/alpha'));
console.log(require('../dll/a'));

console.log(require('beta/beta'));
console.log(require('beta/b'));
console.log(require('beta/c'));

上面require的路径,一种是相对路径../dll/ 一种是scope类路径 beta/,对于路径解析下面会有进一步的说明。

plugins 里有两个 webpack.DllReferencePlugin,分别对应两个打包好的 dll 文件。
第一个 DllReferencePlugin 的 context 属性的意思是,当一个 require 解析后的 request 路径是以这个 context 开头时,那 webpack 就不会去把这个文件的内容打包进去,
而是把它作为 externals 处理,代理到 dll 包,从它里面去取。
源码部分:

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
compiler.hooks.compile.tap('DllReferencePlugin', params => {
let name = this.options.name
let sourceType = this.options.sourceType
let content = 'content' in this.options ? this.options.content : undefined
if ('manifest' in this.options) {
let manifestParameter = this.options.manifest
let manifest
if (typeof manifestParameter === 'string') {
// If there was an error parsing the manifest
// file, exit now because the error will be added
// as a compilation error in the "compilation" hook.
if (params['dll reference parse error ' + manifestParameter]) {
return
}
manifest =
/** @type {DllReferencePluginOptionsManifest} */ (params[
'dll reference ' + manifestParameter
])
} else {
manifest = manifestParameter
}
if (manifest) {
if (!name) name = manifest.name
if (!sourceType) sourceType = manifest.type
if (!content) content = manifest.content
}
}
const externals = {}
const source = 'dll-reference ' + name
externals[source] = name
const normalModuleFactory = params.normalModuleFactory
new ExternalModuleFactoryPlugin(sourceType || 'var', externals).apply(
normalModuleFactory
) /* 这里把"dll-reference " + name作为externals的字段,
对应上面说的dll-reference alpha_21c1490edb92ec8e9390,
而externals的variable的变量名就是包名alpha_21c1490edb92ec8e9390,
对应上面提到的module.exports = alpha_21c1490edb92ec8e9390
*/
new DelegatedModuleFactoryPlugin({
source: source,
type: this.options.type,
scope: this.options.scope,
context: this.options.context || compiler.options.context,
content,
extensions: this.options.extensions
}).apply(normalModuleFactory)
// 这里的DelegatedModuleFactoryPlugin的作用,
// 实际上是把提到的console.log(require("../dll/alpha"));的require
// 变成__webpack_require__("dll-reference alpha_21c1490edb92ec8e9390")("./alpha.js"),
// 也就是说代理到dll-reference alpha_21c1490edb92ec8e9390上

})

需要注意的是假如是相对路径的require,那么对应的文件必须真实存在于该路径。
这是由于当使用scope类型的request时,DelegatedModuleFactoryPlugin会在normalModuleFactoryfactory的钩子调用时
就已经创建了一个DelegatedModule,如果是相对路径的情况,则要等到module钩子的时候才创建。factorymodule两个周期之间,还有resolver钩子,假如resolver阶段特定不到对应路径的文件,则会报错。

2020.09.04更新
scope类型的request的合法条件经过下面的步骤替换掉scope后的innerRequest需要在manifest.json中存在。
相对路径类型的request,除了要满足可在文件系统中找到这个条件外,同样也需要替换掉context后的requestkey在在manifest.json中存在。

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
// 下面是DelegatedModuleFactoryPlugin的执行流程
apply(normalModuleFactory) {
const scope = this.options.scope;
if (scope) { //这里可以看一下上面beta.dll的scope
normalModuleFactory.hooks.factory.tap(
"DelegatedModuleFactoryPlugin",
factory => (data, callback) => {
const dependency = data.dependencies[0];
const request = dependency.request;
if (request && request.indexOf(scope + "/") === 0) {
//可以看出它会先把scope去掉,让"." + 剩下的部分作为实际的require请求
const innerRequest = "." + request.substr(scope.length);
let resolved;
if (innerRequest in this.options.content) {
//会在manifest.json的content中找 实际的require请求 的对应字段
resolved = this.options.content[innerRequest];
return callback(
null,
new DelegatedModule(
this.options.source,
resolved,
this.options.type,
innerRequest,
request
)
);
}
for (let i = 0; i < this.options.extensions.length; i++) {
const extension = this.options.extensions[i];
const requestPlusExt = innerRequest + extension;
if (requestPlusExt in this.options.content) {
resolved = this.options.content[requestPlusExt];
return callback(
null,
new DelegatedModule(
this.options.source,
resolved,
this.options.type,
requestPlusExt,
request + extension
)
);
}
}
}
return factory(data, callback);
}
);
} else {
normalModuleFactory.hooks.module.tap(
"DelegatedModuleFactoryPlugin",
module => {
if (module.libIdent) {
// 这里其实跟上面去除scope的作用是类似的,把前面的context去掉,留下实际的request
const request = module.libIdent(this.options);
if (request && request in this.options.content) {
const resolved = this.options.content[request];
return new DelegatedModule(
this.options.source,
resolved,
this.options.type,
request,
module
);
}
}
return module;
}
);
}
}
//无论是否使用scope最后生成的都是一个DelegatedModule

//DelegatedModule的source方法可以印证上面DelegatedModuleFactoryPlugin的作用,以demo为例
source(depTemplates, runtime) {
const dep = /** @type {DelegatedSourceDependency} */ (this.dependencies[0]);
const sourceModule = dep.module;
let str;

if (!sourceModule) {
str = WebpackMissingModule.moduleCode(this.sourceRequest);
} else {
str = `module.exports = (${runtime.moduleExports({
module: sourceModule,
request: dep.request
})})`;
// 这一部分对应webpack/lib/RuntimeTemplate.js的moduleExports方法,
// 生成module.exports = __webpack_require__("dll-reference alpha_21c1490edb92ec8e9390")部分
switch (this.type) {
case "require":
str += `(${JSON.stringify(this.request)})`; //这里再在str后加上("./alpha.js")
break;
case "object":
str += `[${JSON.stringify(this.request)}]`;
break;
}

str += ";";
}
}

ExternalModule

说了 dll,其实也要顺带说一下 ExternalModule 的原理。概括来说就是把 require 模块的内容不直接写到 bundle 中,而是把他的引用作为 module 的 exports
具体可以看下下面的源码:
lib/ExternalModule.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
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
   //external模块为global变量
getSourceForGlobalVariableExternal(variableName, type) {
if (!Array.isArray(variableName)) {
// make it an array as the look up works the same basically
variableName = [variableName];
}

// needed for e.g. window["some"]["thing"]
const objectLookup = variableName
.map(r => `[${JSON.stringify(r)}]`)
.join("");
return `(function() { module.exports = ${type}${objectLookup}; }());`;
}
//external模块为commonjs模块
getSourceForCommonJsExternal(moduleAndSpecifiers) {
if (!Array.isArray(moduleAndSpecifiers)) {
return `module.exports = require(${JSON.stringify(
moduleAndSpecifiers
)});`;
}

const moduleName = moduleAndSpecifiers[0];
const objectLookup = moduleAndSpecifiers
.slice(1)
.map(r => `[${JSON.stringify(r)}]`)
.join("");
//e.g. 输出格式require("some")["thing"]
return `module.exports = require(${JSON.stringify(
moduleName
)})${objectLookup};`;
}

//external模块为amd或umd模块
getSourceForAmdOrUmdExternal(id, optional, request) {
const externalVariable = `__WEBPACK_EXTERNAL_MODULE_${Template.toIdentifier(
`${id}`
)}__`;
const missingModuleError = optional
? this.checkExternalVariable(externalVariable, request)
: "";
return `${missingModuleError}module.exports = ${externalVariable};`;
}

//external模块为一个普通的全局变量
getSourceForDefaultCase(optional, request) {
if (!Array.isArray(request)) {
// make it an array as the look up works the same basically
request = [request];
}

const variableName = request[0];
const missingModuleError = optional
? this.checkExternalVariable(variableName, request.join("."))
: "";
const objectLookup = request
.slice(1)
.map(r => `[${JSON.stringify(r)}]`)
.join("");
//e.g.输出格式module.exports = some["thing"];
return `${missingModuleError}module.exports = ${variableName}${objectLookup};`;
}

说一下 webpack.config.externals 的配置项,下面是官方示例

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
module.exports = {
//...
externals: [
{
// String
react: 'react',
// Object
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // indicates global variable
},
// Array
subtract: ['./math', 'subtract'],
},
// Function
function(context, request, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
},
// Regex
/^(jquery|\$)$/i,
],
};

之前我一直都不明白这些配置是怎么用的,尤其是以 function 使用时,callback 的第一个参数用的是 null,这到底指代什么。还有 umd,cmd,root 这些,他们是什么情况夏才会起效的。带着上面这些疑问,我阅读了下源码:lib/ExternalModuleFactoryPlugin.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
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
const handleExternal = (value, type, callback) => {
if (typeof type === "function") {
callback = type;
type = undefined;
}
if (value === false) return factory(data, callback);
if (value === true) value = dependency.request;
if (type === undefined && /^[a-z0-9]+ /.test(value)) {
const idx = value.indexOf(" ");
type = value.substr(0, idx);
value = value.substr(idx + 1);
}
callback(
null,
new ExternalModule(value, type || globalType, dependency.request)
);
return true;
};
...
if (typeof externals === "string") {
if (externals === dependency.request) {
return handleExternal(dependency.request, callback);
}
} else if (Array.isArray(externals)) {
let i = 0;
const next = () => {
let asyncFlag;
const handleExternalsAndCallback = (err, module) => {
if (err) return callback(err);
if (!module) {
if (asyncFlag) {
asyncFlag = false;
return;
}
return next();
}
callback(null, module);
};

do {
asyncFlag = true;
if (i >= externals.length) return callback();
handleExternals(externals[i++], handleExternalsAndCallback);
} while (!asyncFlag);
asyncFlag = false;
};

next();
return;
} else if (externals instanceof RegExp) {
if (externals.test(dependency.request)) {
return handleExternal(dependency.request, callback);
}
} else if (typeof externals === "function") {
externals.call(
null,
context,
dependency.request,
(err, value, type) => {
if (err) return callback(err);
if (value !== undefined) {
handleExternal(value, type, callback);
} else {
callback();
}
}
);
return;
} else if (
typeof externals === "object" &&
Object.prototype.hasOwnProperty.call(externals, dependency.request)
) {
return handleExternal(externals[dependency.request], callback);
}
callback();
};
...

上面的各个条件语句分别对应 externals 里各种形式的配置。
分别举例 1.externals: 'react' 会转成

1
callback(null, new ExternalModule('react', undefined || globalType, 'react'));

tips:from lib/WebpackOptionsApply.js

1
2
3
4
5
6
7
8
9
if (options.externals) {
ExternalsPlugin = require('./ExternalsPlugin');
new ExternalsPlugin(
options.output.libraryTarget,
// 当设置了externals后,会添加一个ExternalsPlugin,而它的type默认为output.libraryTarget,
// 而libraryTarget默认为'var',这里是一个伏笔,后面会提到
options.externals
).apply(compiler);
}

2.externals: ['react', 'jquery'] 只不过是单个 externals 推广为多个,把里面的每一个配置按除 2 以外的规则进行处理,数组里面可以是 string, regExp 和 Object 3.externals: /^(jquery|\$)$/i 当 require 的 request 符合正则的形式时,会把这个 request 与 1 一样处理

4.

1
2
3
4
5
6
externals: function(context, request, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request)
}
callback()
},

如果是 function 时就直接执行这个 function,而 callback 参数为

1
2
3
4
5
6
7
8
(err, value, type) => {
if (err) return callback(err);
if (value !== undefined) {
handleExternal(value, type, callback);
} else {
callback();
}
};

这里可以回答上面的问题,callback 的第一个参数 null 到底指的是什么,指的是否出现 err。
第二个参数’commonjs ‘ + request,为包导出方式 + 空格 + 模块名的形式,它会在 handleExternal 中以第一个空格分成两个字符串,字符串 1 表示 ExternalModule 的导出形式,字符串 2 为模块名 5.

1
2
3
4
5
6
7
8
9
10
11
12
externals: {
// String
react: 'react',
// Object
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_' // indicates global variable
},
// Array
subtract: ['./math', 'subtract']
},

上面 Object 的情况有三种形式:
第一种 value 是 string,实际上是 value 是 Object 的特殊情况。表示不论是以 commonjs, amd, root 等哪种方式导出,他的变量名都是 react
第二种 value 是 Object, 表示根据导出的方式,返回对应的变量名
e.g. commonjs 时是 module.exports = require(‘loadsh’) root 时是 module.exports = _
那这个导出方式是以什么决定的呢?如果是普通的 externals,那么就跟 1 中的 tips 代码注释写的一样,跟打包时 output.libraryTarget 一致的。如果是 dll 的情况就取
DllReferencePlugin 的 options.sourceType 或者 manifest.json 的 type 字段,如果都没有默认就是 var。这里就是 1 中的 tips 里说的伏笔,因为我之前没有在官方的例子中,找到有用过 var 字段的,以为默认是 root 或者 global 因为一般只看到 externals 中用到这两种表示全局的。但事实上他们不是划为同一种进行处理的,
看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//lib/ExternalModule.js
switch (this.externalType) {
case "this":
case "window":
case "self":
...
case "global":
...
case "commonjs":
case "commonjs2":
...
case "amd":
case "amd-require":
case "umd":
case "umd2":
...
default:
...
}

并且在分情况返回变量名的处理方法是this.request[this.externalType],也就是说以’var’和上面的 lodash 为例子的话,那就相当于({commonjs: ‘lodash’,amd:’lodash’,root: ‘_‘ })[‘var’]。那这样的话自然在编译时就变成了module.exports=undefined
_第三种 value 是 Array,它的处理方式以当包导出方式是以 var 为例说明,根据上面的 switch condition 可知当为 var 时,按 defaultCase 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getSourceForDefaultCase(optional, request) {
...
// request参数代入我们的['./math', 'subtract'],
// 可得最后return的值为module.exports = ./math["subtract"],
// 可以看出数组的第一个参数是作为模块名或者变量名,后面的参数作为对象的属性,一层层获取的。
// 然后就是var的情况./math["subtract"],显然是不合法的。
// 如果是commonjs的话,就是require('./math')["subtract"],这个代入getSourceForCommonJsExternal可以知道。
const variableName = request[0];
const objectLookup = request
.slice(1)
.map(r => `[${JSON.stringify(r)}]`)
.join("");
//e.g.输出格式module.exports = some["thing"];
return `${missingModuleError}module.exports = ${variableName}${objectLookup};`;
}

总结

上面的解析写的比较乱,而且有很多文章内的引用,下次可以考虑使用锚点进行页内跳转。
dll 的工作流程大概是,通过 DllPlugin 打包 library 获得 js 和 manifest 文件,使用时通过 DllReferencePlugin 读取 manifest 文件,解析 dll 中包含的子模块名等信息。
DllReferencePlugin 内部,创建 ExternalModule,把 dll 加入到 externals 中,然后通过 DelegatedModule,把对实际文件的 require 请求,代理到 dll 包中。
p.s.:使用HTMLWebpackPlugin的默认配置并不会在使用DllReferencePlugin后把加载dllscript标签加入到打包后的html里。

使用命令行编译C++

最近这几个月都有在看C++的知识,由于之前一直都在用VSCode写代码,所以也想在编写C++代码时也尽量不需要换IDE。但是本身VSCode在辅助编译上没有等提供UI的支持,所以需要知道一些原始的命令行编译方面的配置。
VSCode的改作C++IDE,可以按照这个网页的说明进行配置[VSCode的C++配置]https://www.zhihu.com/question/30315894/answer/154979413
1.上面的配置适用于单文件的无第三方库的编译,从里面的参数来看, 命令行只需要

1
clang++ $fileName -o $fileNameWithoutExt.exe就可以生成可执行文件

2.如果需要编译多个.cpp文件则需要,注意头文件不能放在命令行中直接引入
1
clang++ $fileName1 $fileName2 ...(表示省略第二文件以后的文件名) -o $fileNameWithoutExt.exe

3.如果我在文件中用了第三方库e.g.Boost的话,下面是Boost的入门demo
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <boost/lambda/lambda.hpp>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
using namespace boost::lambda;
typedef std::istream_iterator<int> in;

std::for_each(
in(std::cin), in(), std::cout << (_1 * 3) << " " );
}

如果我们直接进行编译,编译器会显示fatal error: ‘boost/lambda/lambda.hpp’ file not found错误,说编译器找不到头文件,如果是我们编写的头文件在include的时候会使用double quote(双引),编译器默认会在文件的当前目录下进行查找这个头文件。但是如果是尖括号的<boost/lambda/lambda.hpp>,则需要把库文件的目录加入到命令行中,供编译器进行查找。则需要用到-I<dir>参数,如:
1
clang++ -I "C:/Program Files/boost_1_69_0" $fileName -o $fileNameWithoutExt.exe

4.虽然上面这个简单的用例可以不报错了,但是对于以下这个例子不适用
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
#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

int main(int argc, char* argv[])
{
try
{
if (argc != 2)
{
std::cerr << "Usage: client <host>" << std::endl;
return 1;
}

boost::asio::io_context io_context;

tcp::resolver resolver(io_context);
tcp::resolver::results_type endpoints = resolver.resolve(argv[1], "daytime");
tcp::socket socket(io_context);
boost::asio::connect(socket, endpoints);

for (;;)
{
boost::array<char, 128> buf;
boost::system::error_code error;
size_t len = socket.read_some(boost::asio::buffer(buf), error);

if (error == boost::asio::error::eof)
break;
else if (error)
throw boost::system::system_error(error);
std::cout.write(buf.data(), len);
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}

因为boost::asio会需要用到windows的动态库wsock32等,直接编译会报undefined reference to `__imp_WSAStartup'等错误,
这时候需要加上-lwsock32 -lWs2_32这个option(-l指定某个文件),导入wsock32和Ws2_32这两个dll才能成功编译。其实dll也有类似头文件的搜索路径,
windows默认在C:\Windows\System32,如果要添加搜索路径可以通过-L<dir>添加。

在写这篇记录的时候知道两个小知识,第一个是如果cpp中用到了标准库,则需要指定–target=x86_64-w64-mingw这个option,默认在windows的target是x86_64-pc-windows-msvc,由于本机没有安装visual studio,如果不改target的话会找不到头文件。
另外获取编译时编译器查找library的search path的方法为在编译的命令行语句后加上-v这个flag

待续
总结有些仓促,如果有新的发现,会进行更新。

在Vue的template里面使用临时变量

前言
实际上很久以前就发现了这样的问题了,在 vue 的 template 中不能使用临时变量,而使用 render 函数就不存在这样的问题了。这周五因为在重构项目里某个方法,而在 UI 方面使用的是 vue,里面就有需要使用临时变量的情况。
某个部分的 template 大概是这样的

1
<div v-if="hasNextParams"><div v-for="getNextParams"></div></div>

他用到的两个方法其实逻辑是差不多的,实际上可以都用 getNextParams 的结果作为 v-if 的判断。但问题是由于需要在两个地方需要引用到这个临时的结果,导致没修改前需要重复调用一个方法,正常来说,我们应该只做一次运算,缓存这个结果就好了。于是,我下班后就查了一下,在 stackOverflow 里找到了 scope-slot 的方案。(其实这个假如是熟悉 vue 的同学应该一下就想出来了,惭愧)
这个方案结合我自己的实际情况,修改后做了一个这样的组件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div><slot v-bind="passedProps" /></div>
</template>
<script>
export default {
name: 'Pass',
props: {
passedProps: Object,
default() {
return {}
}
}
}
</script>
<!--组件调用-->
<template>
<Pass style="color:#c00;" :passedProps="{params: [1, 2, 3]}">
<!--这里的slot-scope拿到的是上面bind到slot上的值,这里还用了解构取到里面的params参数-->
<template slot-scope="{params}">
<div v-if="params.length >= 0"><div>{{ params }}</div></div>
</template>
</Pass>
</template>

后记
在我实践过程中发现实际上<template><div/></template>组件跟React里面{[<div/>]}代表的意思是一样的,他返回的是一个数组,实际他不是一个根,由于是一个数组,会报错跟你说需要提供一个单根而不能是multi-root,所以也就能理解为什么Vue文件里的template下不能直接加template标签作为他的根了。

学习Needleman-Wunsch-algorithm

_前言_
最近一段时间,有位同事被分配开发一个基于对象树的类 git 的功能。其中需要做对象列表的插入删除的 diff 实现,在她分享中她提到是通过改编过的 Needleman-Wunsch 算法实现的,能够返回插入或删除的具体对象。感觉这个算法是挺有用的,于是乎这个周末就找资料了解了一下。
具体算法说明
一开始这个算法是为了比较两个蛋白质的氨基酸序列的相似性而提出的。后来也推广应用到文本比对。
具体来讲,设我们有两个字符串。

1
2
3
4
const a = 'nowyouseeme'
const b = 'cuzletitbe'
const lenA = a.length
const lenB = b.length

1.首先需要构造一个(lenA + 1) * (lenB + 1)大小的矩阵

1
2
3
const matrix = Array.from({
length: lenB + 1
}).map(() => new Array(lenA + 1).fill(0))

2.给第一行和第一列设置初始值

1
2
3
4
5
6
7
8
let i
for (i = 1; i < lenB + 1; i++) {
matrix[i][0] = -1 * i
}
let j
for (j = 1; j < lenA + 1; j++) {
matrix[0][j] = -1 * j
}

一开始我就有一个疑问,为什么初始值是-1 * 序号呢?-1 是什么含义?
其实这里的-1 表示的是当前格的分数是从左一格或者从上一格的求出的时候的得分。
然后这里就要说明一下这个矩阵的格子除了初始的行列以外其余格的数值是怎么算出来的了。
这些格子的取是左上格,上格,左格这三个前面的格子的值分别加上当前格得分后的最大值。
当前格的得分定义

1
2
3
4
5
6
7
8
9
10
11
12
13
let match = 1 // 当前行对应的字符等于当前列对应的字符时的得分,这个值只能与左上格数值相加
let dismatch = -3 // 当前行对应的字符不等于当前列对应的字符时的得分, 这个值只能与左上格数值相加
let gap = -1 // 不管相等还是不相等,默认都加在上格或者左格进行最大值判定
/*
例如比较上面两个字符串的第一个字符
| | | n |
-------------
| | 0 |-1 |
-------------
| c |-1 |-1 |
因为第一个字符不相等所以
matrix[1][1] = Math.max(matrix[0][0] - 3, matrix[0][1] -1, matrix[1][0] -1) // 1
*/

计算格子值代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (i = 1; i < lenB + 1; i++) {
for (j = 1; j < lenA + 1; j++) {
const scoreFromTopLeft = a[j - 1] === b[i - 1] ? match : dismatch
matrix[i][j] = Math.max(
matrix[i - 1][j - 1] + scoreFromTopLeft,
matrix[i - 1][j] + gap,
matrix[i][j - 1] - gap
)
//下面这个链接的会把这个最大值是取自哪个这个信息给存到traceback_type_status这个二维数组里面
//方便回溯的时候知道上一步来自哪个方向,当然也可以不存,在回溯的时候再判断
// const intermediate_scores = [matrix[i - 1][j - 1] + scoreFromTopLeft, matrix[i - 1][j] + gap,matrix[i][j - 1] - gap]
// const tracebackTypeStatus = intermediate_scores.map((e, i) => e === score);
}
}

得分计算有人做了一个动态计算表格生动展示了其机制,地址:[needle-wunsch]https://blievrouw.github.io/needleman-wunsch/

回溯的方法:
1.像上面提到的,根据当前格子的值是取自哪里来确定回溯方向的
2.上面这个在线 demo 他取目标字符串为列, 原字符串为行,删除添加字符操作的记录方法为:
如果是添加(值取自左格),给原字符串的 alignment 添加’-‘字符,目标字符串的 alignment 添加目标字符串的对应字符
如果是删除(值取自上格),给目标字符串的 alignment 添加’-‘字符,原字符串的 alignment 添加原字符串的对应字符
如果是一致或者不一致(值取自左上格),则同时添加对应字符 3.重复 1,2 的步骤没有可回溯的方向
但是上面这个方法,稍稍一想可能会注意到,假如是 dismatch(不一致)的情况,那么按照上面的方法就不会添加’-‘字符表示操作了,
当然也可以在这种情况下默认是添加新字符,删除旧字符两个操作,但是这样就比较生硬。所以为了排除当 dismatch 时,会取左上格的情况,
我们需要让 dismatch 的 penalty 小于 gap_penalty * 2,这样最大值就不会取自左上了。

最后要提一点的是demo里的回溯代码,他没有用递归实现,而是把下一步要进入的路径,保存到上一条路径的next里,当上一条路径走完,
会把next变成current,因为这个我不会太会用这种方法,同时这种方法效率也会比较高,所以觉得比较有趣就提一下吧。

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
while (current) {
pos = current.pos
alignment = current.alignment
// Get children alignments
children = this.alignmentChildren(current.pos)
// Store completed alignments
if (!children.length) {
final_alignments.push(alignment)
}
current = current.next
for (t = 0, len = children.length; t < len; t++) {
child = children[t]
child.alignment = {
seq1: alignment.seq1.concat(
child.tracebackType === 0 ? '-' : this.seq1[pos[0] - 1]
), // -1 refers to offset between scoring matrix and the sequence
seq2: alignment.seq2.concat(
child.tracebackType === 2 ? '-' : this.seq2[pos[1] - 1]
)
}
// Move down a layer
child.next = current
current = child
}
}

通过简化版的Observer的实现来说明vue的watch的工作原理

前言
其实关于vue实例watch项的设置是怎么观测到数据变化的,这个问题很久之前我就很有兴趣去了解了。之前也有通过一些文章去了解过,由于懒,了解完以后就自己亲自做深入的探索或者说自己去阅读源码,所以很长的一段时间来说,我对这个机制还是有点模糊的。但是,这周周五,工作上碰到了一个问题——怎么脱离vue(或者说不去创建vue的实例设置watch项)然后去检测vuex store的变化呢。
其实这个问题最简单的,后来发现原来vuex有一个api是Vuex.store.watch能够直接监听state的变化(说明地址:https://vuex.vuejs.org/api/#watch),但是怎么能因为有现成的api而放弃这样一个看源码了解其中原理的机会呢,于是,有了下面这一段简化版的源码以及用例
简化版源码
其实下面的代码来自几个文件,这里为了阅读清晰就把他们放在一起了。

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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
(function() {
const Dep = (function() {
let uid = 0
class Dep {
constructor() {
this.id = uid++
this.subs = new Set()
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

removeSub(sub) {
this.subs.delete(sub)
}

addSub(sub) {
this.subs.add(sub)
}

notify() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
Dep.target = null
return Dep
}
)()

function parsePath(exp) {
const path = exp.split('.')
return function(vm) {
return path.reduce(function(result, property) {
return result[property]
}, vm)
}
}

const Watcher = (function() {
let uid = 0
class Watcher {
constructor(vm, expOrFn, cb) {
this.id = uid++
this.vm = vm
this.cb = cb
this.newDeps = new Map()
this.deps = new Map()
this.getter = parsePath(expOrFn)
this.value = this.get()
}

get() {
Dep.target = this
let value
const vm = this.vm
value = this.getter.call(vm, vm)
Dep.target = null
this.cleanupDeps()
}

addDep(dep) {
console.log(this.newDeps)
const id = dep.id
if (!this.newDeps.has(id)) {
this.newDeps.set(id, dep)
if (!this.deps.has(id)) {
dep.addSub(this)
}
}
}

cleanupDeps() {
this.deps.forEach((dep)=>{
if (!this.newDeps.has(dep.id)) {
dep.removeSub(this)
}
}
);
let nextDeps = this.newDeps
this.newDeps = this.deps
this.deps = nextDeps
this.newDeps.clear()
}

update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
return Watcher
}
)()
const Observer = (function() {
const arrayProto = Array.prototype
const methodToPatch = ['push', 'pop', 'splice', 'shift', 'unshift']
const arrayMethods = Object.create(arrayProto)
methodToPatch.forEach(function(method) {
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function() {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted)
ob.observeArray(inserted)
ob.dep.notify()
return result
},
enumerable: false
})
})
function observe(value) {
if (typeof value !== 'object')
return
let ob
if (value.hasOwnProperty('__ob__')) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false
})
if (Array.isArray(value)) {
copyAugment(value, arrayMethods, methodToPatch)
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items)
}
}
}
function copyAugment(target, src, keys) {
keys.forEach(function(key) {
Object.defineProperty(target, key, {
value: src[k],
enumerable: false
})
})
}
function defineReactive(obj, key) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property) {
const {getter, setter} = property
let val
if (!getter || setter) {
val = obj[key]
}
let childOb = observe(val)
Object.defineProperty(obj, key, {
get: function() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function(newVal) {
const value = getter ? getter.call(obj) : val
if (value === newVal) {
return
}
if (getter && !setter)
return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}
}
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
return Observer
}
)()

let a = {
b: 2
}
new Observer(a)
console.log(a)
new Watcher(a,'b',function(newVal) {
console.log('观测到更新')
}
)
new Watcher(a,'b',function(newVal) {
console.log('我也观测到更新')
}
)
a.b = 3
}
)()

机制总结
上面的代码可以简单地总结为,为obj创建一个Observer,对他的每个属性都去添加getter/setter,setter是为了知道属性被设置的这一信息,同时当被设置的属性为Object的时候重新observe这个value,这样reactive的机制才不会断。
一般来说以前我一般只会用到setter这一点,而不会去使用getter,因为好像getter只是获取这个值而已,而跟观测数据变化没有什么关系的样子。但是在这里的作用是,当设置了property的getter,然后watcher创建时,获取property的值时,getter就会执行,由于此时Dep这个class的target属性被设置为当前的这个watcher,所以getter就有机会在执行的时候把这个watcher作为自己的订阅者(当setter执行时,一一去notify自己的订阅者,通知数据变化)
watcher里自身也会去维护自己的publisher(订阅的对象),应该是为了当自己不再watch的时候,能够让publisher取消通知自己,所以说是publisher维护subscriber,subscriber维护自己的publisher,一个双向的维护(也是值得一提的点)
当observe的对象是数组时,数组首层element的变化会通过Observer的dep去发布,这一点与Object的情况略有不同。
而通过重写数组操作的一些prototype上的方法,而监听数据变化的做法,也是比较好理解的,这里就不赘述了。

探索抽象语法树

源起:
前段时间通过一篇关于开发者他介绍自己开发的 web 应用的文章知道了这名开发者。觉得他很厉害,要向他学习。所以呢,就学习一下他之前做过的项目。虽然是想认真研究完的,一来是代码没有什么注释,二来我认真看过的部分某些其实可参考意义不是很大。所以这个计划的执行就暂停了。不过呢,在其中我也找到了一个我想了解的方向,就是 AST(抽象语法树),这个其实会挺有用的。
原因我暂时想到的有两点:
一、通过解析代码,能够拿到代码结构化的信息,这样可以应用于一些代码自动化生成,解析模块依赖等(这种可以自动化的东西就是我辈所向往的)
二、单纯自己通过正则去实现这样的解析还是一个比较麻烦的工程,暂时非自己能力所及。
实践:
JS 的 AST 的解析主要会用到 Babel 这个库。其中需要了解到的主要有这三个个包@babel/parser(babylon)、@babel/traverse、@babel/type。
下面以官方的一个插件说明 AST 中的一些概念和使用流程:

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
/* 以下为babel-plugin-transform-remove-console的部分源码
插件export的是一个接收当前babel对象的函数,这里他取这个对象的types属性,这个types属性出自于@babel/types,
在这里用于创建AST节点替换原来的节点。
函数返回一个对象,对象比较重要的属性是visitor。用来定义当遍历访问到某类节点时,需要进行的用户自定义的操作。
节点的类型可以很简单地在AST Explorer中书写代码,通过显示的解析后的AST树中获知。
*/
module.exports = function({ types: t }) {
return {
name: "transform-remove-console",
visitor: {
// CallExpression表示函数调用,这些vistor的函数接收两个参数第一个是NodePath的对象
// 另一个是用于缓存遍历数据的参数state
// 便于用户在访问节点时存储自己需要的信息
CallExpression(path, state) {
const callee = path.get("callee");
// NodePath通过get方法,获得对应的子Path,注意返回的也是一个NodePath的对象,并非是节点本身
// Node可以通过NodePath.node获取,NodePath除了存储当前的Node以外,还保存节点的层级结构
// 如通过Path.getSibling(index) 获取nth的当前层级的NodePath;
// path.parentPath 获父NodePath
// 另外所有替换删除的操作都是在NodePath上进行

// NodePath有一系列is开头的方法,用于确认Path是否为某种类型
if (!callee.isMemberExpression()) return;

if (isIncludedConsole(callee, state.opts.exclude)) {
// console.log()
if (path.parentPath.isExpressionStatement()) {
path.remove(); // 删除Path
} else {
path.replaceWith(createVoid0());
// 用一个新的NodePath代替原来的NodePath
// NodePath一般用上面的types参数以types.UnaryExpression等
// types后接NodePath type的构造方法创建
// 如这里的createVoid0就是以types.UnaryExpression('void', [0])构造的
}
} else if (isIncludedConsoleBind(callee, state.opts.exclude)) {
// console.log.bind()
path.replaceWith(createNoop());
}
},
MemberExpression: {
// 访问节点有两个阶段一个是enter、一个exit,默认是enter
// 访问结束发生该节点下的子节点遍历完返回时
exit(path, state) {
if (
isIncludedConsole(path, state.opts.exclude) &&
!path.parentPath.isMemberExpression()
) {
if (
path.parentPath.isAssignmentExpression() &&
path.parentKey === "left"
) {
path.parentPath.get("right").replaceWith(createNoop());
} else {
path.replaceWith(createNoop());
}
}
}
}
}
};
// 值得一提的是在这个插件加了一个验证console是否为全局的操作
function isGlobalConsoleId(id) {
const name = "console";
// 首先通过scope.getBinding确认console是否是为用户定义的,其次再验证在全局作用域中是否有console定义
return (
id.isIdentifier({ name }) &&
!id.scope.getBinding(name) &&
id.scope.hasGlobal(name)
);
}
//ps: 一个小的发现在visitor函数中this等于参数state

结语:
我这里写的只是 AST 的冰山一粒,上述内容其实在 babel handbook 上都有,把自己心得体会记录下来,一是要是以后忘记了,看自己的文字更加容易唤醒自己的记忆,二是为自己的学习留下痕迹,再者有新的发现再到新文章中再叙吧。

使用不用终端维护同一个hexo博客

初步不合理的尝试

这个问题来自于今天在公司我尝试使用 hexo 去生成个人博客,hexo 的 deploy 只会生成页面 build 好的文件并推送到远程仓库,
这样远程仓库就没有 hexo 项目的 source 了。因此,回家以后就没有项目的文件进行下一步的维护。只好重新创建一次项目,并推送
远程仓库分支上,这样不同在不同终端上都能获取到这些文件了。
于是我凭着直觉和对 git 微薄的认知,进行了一下两次不同的错误尝试。

尝试一:

1.

1
git clone git@github.com:Sociosarbis/study-memo.git

2.

1
git checkout -b hexo

3.

1
hexo init

初始化 hexo 项目,然后问题来了,hexo 提示需要当前文件夹是一个空文件夹,只能把所有文件夹的都删,包括.git 文件夹。
这样一来就相当于重头来过

尝试二:

1. 既然需要一个空文件夹,那么就从刚才的空文件夹开始

1
hexo init

2.

1
git checkout -b hexo

3.

1
git remote add origin git@github.com:Sociosarbis/study-memo.git

自觉这一步是会关联所有的本地分支和远程分支的,
但其实只是相当于给仓库地址创建了一个 shortname,例如上面的 origin 代表了 git@github.com:Sociosarbis/study-memo.git 的地址

4. 直接git push origin/hexo然后提示远程并没有这个仓库

5. 好吧,既然这样就手动在 github 上创建 hexo 的分支,但是这个分支完完全全是 master 的复制
6. 创建完以后再尝试 push,然后提示需要 pull 一次
7. 然后直接git pull,错误提示没有指定需要从哪个远程分支 pull

8. 那我就用这个命令把两个分支关联起来

1
git branch --set-upstream-to=origin/hexo hexo

然后再git pull一次再次错误提示refusing to merge unrelated histories

9. 网上 search 过后在git pull后面加上--allow-unrelated-histories,终于 pull 成功了

10. 接下来就是我比较熟悉的过程了,删除 pull 下来的生成的页面文件,commit 以后再 push 上去,成功解决

正确做法

虽然最后成功完成了任务,但是这样明显是不够优雅的。
再去网上查找,找到了两个能很好地解决这次问题的或者说关于本地与远程仓库关联的两个命令。

分别是:

1.

1
git push --set-upstream origin branch_name

在远程创建一个与本地 branch_name 分支同名的分支并跟踪

2.

1
git checkout --track orgin/branch_name

在本地创建一个与 branch_name 同名分支跟踪远程分支