对于社会主义的中国人来讲,经过初中、高中、大学的思想政治课的洗礼,理解矛盾并非难事,我们辩证唯物主义的内容要更丰富得多。

第二章 培养矛盾的思维框架

不要理性,要合理

这两个相似的词忽然让我很难揣摩这句只包含6个字的话。什么样的事情是合理但不理性的呢?我理解,理性是对事件真实客观的描述,是能用逻辑严格证明的,往往获取“理性”需要很大的成本,甚至在有意义的时间内无法获取;而合理则是一种模棱两可的适当妥协,在有些事情无法获取“理性”之时,委曲求全的权宜之计都可能是一种合理。合理是一种“反牛角尖的”行为,似乎更倾向于“糊弄”。

在作者涉猎的软件咨询行业,很难想象不求甚解的方案也能行得通。从技术上来讲,确实再隐晦的故障都是可以绝对定位的,但对于项目来说,往往最初的设计和真正的落地会有很大差距,如果没有绝对的经验积累,那么项目在循循渐进的过程中往往都会带着猜的成份。换句话说,没有理性,只要比上一版本又说改进,即为合理。从这角度上来说,一味得追求最佳既是不可能的任务,也不能称之为理性的抉择,毕竟世界是个矛盾体,在矛盾中寻求理性的过程收获利益,才是上上之选。

从这里可以衍生出一种经典的产品进化模型,比如想造一辆汽车,下面有两种方案:

  1. 第一种,先造轮子、发动机、车架,最终拼接成完善的汽车;
  2. 第二种,先造一辆自行车、摩托车、三轮车,最后一辆汽车

第二种方案看似费时费力,往往更容易被人接受,也更容易实现。

自以为无所不知的人最容易上当

过于自信就是自负,往往容易对客观事实视而不见。这和“撞车的都是老司机”、“淹死的都是会水的”是类似的道理。

生活太重要,所以不能太较真

这论题太大了,似乎已经上升到了哲学和三观的高度上来了。我想作者要表达的意思应该是,对于无法解释的事物不必过于在意,毕竟你的无知正是矛盾世界里的必不可少的成份。

不付出就什么也得不到

正是矛盾的合理体现,要想得到什么,不可避免地要失去什么,当然,反过来不一定成立,往往都不成立。

提升一方面,就要牺牲另一方面

同上

费舍基本定理——你越适应现状,就越难适应变化

显然,与现有系统耦合越大,与其它系统兼容越小。抽象与具象。

伴随着年纪的增长,我觉得自己就越来越难以适应变化。

顾问一般在解决你提出的第三个问题时最有成效

这也解释了为什么人在20多岁的时候最富创造性,因为年老后才更“善于”和这个“平庸”的世界打交道。

始终关注自己在什么时候拥有最高效的进步,在合适的时候,应当勇于舍弃舒适的温床。

我确实应该换个工作了🍟

我们能做,这是所需的费用

没有成本的改变是不现实的,懂得这个道理,避免在愚蠢的任务上浪费时间与口舌。

元素在文档流中的位置应与页面滚动量无关,指的是该元素左上角(包括border,但不包括margin)距离整个页面左上角的水平和垂直位置。

获取此位置有两种方法,观察 jQuery 2.2 源码

1
2
3
4
5
6
var rect = elem.getBoundingClientRect();
var win = elem.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};

getBoundingClientRect 是一个十分高效的方法,用来获取元素相对于可见的视口的位置,而这个位置是与滚动量有关的,只有这个位置加上滚动量,即是与文档左上角的距离。

如果 getBoundingClientRect 不存在,我们依然可以通过遍历计算出结果。首先需要了解 DOM 元素的几个属性:

  1. offsetParent 向上祖先中第一个定位元素;
  2. offsetLeft 距离 offsetParent 左边界的水平位置,与滚动量无关
  3. offsetTop 距离 offsetParent 上边界的垂直位置,与滚动量无关
  4. clientLeft 一般为左边框宽度
  5. clientTop 一般为上边框宽度
  6. scrollLeft 水平滚动距离
  7. scrollTop 垂直滚动距离

因为我们要得到的距离是当时的绝对距离,与该元素的各个祖先元素的滚动量是有关的,因此我们不能简单地通过加和 offsetLeftoffsetTop 来得到最后的值,必须减去每一级祖先的滚动量。

具体逻辑大概就是,向该元素的上面遍历,减去每个元素的滚动量,一旦遇到是 offsetParent,则加上 offsetLeftoffsetTop。具体可参考 jQuery 1.4.4 源码。简单来讲就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function offset(ele) {
var top = ele.offsetTop;
var left = ele.offsetLeft;

var offsetParent = ele.offsetParent;

while((ele = ele.parentNode) && ele !== document) {
left -= (ele.scrollLeft);
top -= (ele.scrollTop);
if (offsetParent === ele) {
top += ele.offsetTop + ele.clientTop;
left += ele.offsetLeft + ele.clientLeft;
offsetParent = offsetParent.offsetParent;
}
}

return {top,left};
}

当然 clientTop 与 clientLeft 并非永远代表的是上边框和左边框的宽度。

上一篇我们熟悉了一下 Inferno.js 的 DOM Diff 算法。今天我们来看 Vue.js 框架的 DOM Diff 算法。

Vue.js 的作者没有编写自己的算法,而是使用了 Snabbdom,并做了适当的修改。

DOM Diff 的关键算法在 https://github.com/snabbdom/snabbdom/blob/v0.7.1/src/snabbdom.ts#L179

同样,我们依然假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

创建4个指针 oldStartIdxoldEndIdxnewStartIdxnewEndIdx,初始分别指向A的起始点、结束点和B的起始点、结束点。显然:

1
2
3
4
oldStartIdx=0
oldEndIdx=5
newStartIdx=0
newEndIdx=5

第一步,比较 A[oldStartIdx] 和 B[newStartIdx],如果相同,则 oldStartIdx++、newStartIdx++。
第二步,比较 A[oldEndIdx] 和 B[newEndIdx],如果相同,则 oldEndIdx++、newEndIdx++。

在本例中,以上两步全部不满足,跳过。

第三步,比较 A[oldStartVnode] 和 B[newEndVnode],如果相同,则节点发生了右移。
第四步,比较 A[oldEndIdx] 和 B[newStartIdx],如果相同,则节点发生了左移。

在本例中,以上两步全部不满足,跳过。

第五步,在A中搜索 B[newStartIdx],即 i,找到则把A中的 i 移到 A[oldStartVnode] 前面并 newStartIdx++、oldStartVnode++,否则则创建它。

在本例中,A变成 idfbge

返回第一步。


可以看到这个算法类似于优化后的插入排序。按照此算法,A的变换路径为:

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
A:dfibge
B:igfheb

=>

A:idfbge
B:igfheb
^

=>

A:igdfbe
B:igfheb
^

=>

A:igfdbe
B:igfheb
^

=>

A:igfhdbe
B:igfheb
^

=>

A:igfhedb
B:igfheb
^

=>

A:igfhebd
B:igfheb
^
=>

A:igfheb
B:igfheb
^

用一句话概括此算法的核心就是:依次遍历B集合,在A集合中找到对应项,放到与在B集合中相同的位置上。只不过 Snabbdom 使用了双向同时遍历来进行优化。

事实上,React 的 DOM Diff 算法与此也是非常类似的,只不过受限于 Fiber,只进行了单向搜索。但是即便如此, React 也引入了优化策略,尽量使得更多的元素不必移动。从本质上来看,Inferno 和 React 都利用递增子序列来进行了优化,但是 Inferno 使用算法来保证是最大递增子序列,而 React 的子序列是一定从第一个元素开始的,因此不一定是最大子序列。这在尾部元素移动到首部的时候,差异表现得更明显。

已经关注 Inferno.js 有两年的时间,终于在刚刚不久的过去建立起了官网和文档。

值得特别关注的是,Inferno.js 提供了类 React API 的同时,在 DOM Diff 算法上借鉴了 ivijs,取得了更高的效率,从而有较强的性能表现。

优秀的 DOM Diff 算法的核心都是在将一个 DOM 集合转换成另一个 DOM 集合的同时,尽可能地复用已有 DOM 并具有较少的 DOM 移动操作,这是由于 DOM 操作(甚至访问)的成本较高。

Inferno.js 在两个 VDom 之间进行比较,从而避免了频繁访问 DOM 的性能开销。下面我们看一下它实现的 DOM Diff 算法。

我们假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

首先得到集合B中元素在A中的原始位置,如果在A中不存在则为-1,得到:

1
2
3
var A       = [d, f, i,  b, g, e];
var B = [i, g, f, h, e, b];
var sources = [2, 4, 1, -1, 5, 3];

现在我们获取该数组的最大递增子序列的序列位置,为:

1
var seq = [0, 1, 4];// [2, 4, 5]

这个数组的意义在于:“新数组中第0、1、4位置的元素在原始数组中是无需移动的”,这是我们能获取最小移动步数的关键。

一共6个成员,3个无需移动,那么一定有3个需要操作

现在我们从后往前遍历集合B,观察每个成员的位置是否存在于 seq 中,存在则不必操作,不存在则需移动。

首先我们需要先删除B中存在,但A中不存在的元素 d,此时:

1
A=[f, i, b, g, e]

观察B中最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=5

位置是5,不存在于 seq 中,需要把集合A中的 b 移到最后,此时:

1
A=[f, i, g, e, b] --- (1)

观察B中倒数第二个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=4

位置是4,存在于 seq 中,无需操作。

观察B中倒数第三个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=3

在A中不存在,我们需要创建 h,并放到 e 的前面,此时:

1
A=[f, i, g, h, e, b] --- (2)

观察B中倒数第四个元素:

1
2
3
A=[f, i, g, h, e, b]
B=[i, g, f, h, e, b]
^ pos=2

位置是2,不存在于 seq 中,需要把集合A中的 f 移到 h 的前面,此时:

1
A=[i, g, f, h, e, b] --- (3)

观察B中倒数第五个元素:

1
2
3
A=[i, g, f, h, e, b]
B=[i, g, f, h, e, b]
^ pos=1

位置是1,存在于 seq 中,无需任何操作。

观察B中倒数第六个元素,也是最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=0

位置是0,存在于seq中,也无需任何操作。

此时,所有操作都已结束,仅需3步,集合A已经转换成了集合B。其中,对 d 的移除和对 h 的创建是不可避免的,除此之外,仅进行了两次DOM移动,也印证了上面提到的 6-3=3 的操作步骤。

我们在移动 DOM 的时候都是执行的“插在XXX之前”,是因为 DOM 中的 insertBefore 方法,如果存在 insertAfter,那么从前往后操作也是等效的。

我们都知道 JavaScript 是通过原型链继承的,在不支持 class、extends 语法的环境里,继承又该如何实现呢?

首先我们来整理实现继承后的效果表征,假设存在两个类(函数)A 和 B,我们要实现 B 继承于 A,那么需要满足:

  1. new B() instanceof A 为真;
  2. B.prototype.constructor === B 为真;
  3. 如果 A.foo = 1,则 B.foo = 1 为真,即静态变量可继承;
  4. 在普通方法 foo 中可以调用 super.foo() 调用基类方法;
  5. 在构造方法中可以调用 super() 调用基类构造方法

在很多年前,jQuery 作者 John Resig 曾写过一个继承的实现,在那个还在 ES3 语法的时代已经非常难得。在今天看来,这种实现方式的语义已经远远落后,同时也不能实现静态变量。


下面我们来一步一步地去理解 babel 的实现方式。

首先定义两个类(函数),以及子类的实例:

1
2
3
4
function A() {}
function B() {}

var b = new B();

想实现 B 继承于 A,那边必然 b instanceof B 为真,根据 instanceof 的意义,一定有:

1
b.__proto__.__proto__... === A.prototype

我们知道:

1
b.__proto__ === B.prototype

那么我们只需要:

1
B.prototype.__proto__... === A.prototype

于是我们覆写 B.prototype

1
B.prototype = Object.create(A.prototype)

这样一定有 A.prototype.isPrototypeOf(B.prototype) 为真。

现在我们已经实现了:

1
b instanceof A === true

也就是说第一条我们已经实现了,下面看第二条,只需要:

1
B.prototype.constructor = B;

或者更优雅一些:

1
2
3
4
5
6
7
8
B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
}
});

现在我们要实现静态成员的继承:

1
Object.setPrototypeOf(B, A);

目前为止,我们定义的是两个空类,现在我们为他们增加 name 属性:

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
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => B

现在我们要在 B 的 name 中获取父类的 name 属性,那么我们就需要找到 B.prototype 中的 name。

在 B 环境中,灵活的做法是通过 B.prototype 找到 A.prototype,根据 *B.prototype = Object.create(A.prototype)*,我们知道:

1
A.prototype === Object.getPrototypeOf(B.prototype)

于是:

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
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = Object.getPrototypeOf(B.prototype);
var desc = Object.getOwnPropertyDescriptor(sup, 'name');
return desc.get.call(this) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这就实现了 super.name 的效果。当然,我们假设 B 的父类是定义了 name 的,如果没有找到 name,那应该沿着原型链继续向上寻找。因此更健壮的写法是:

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
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这里没有考虑 name 不是 getter 的情形,不过原理类似,不再冗述。

最后我们来实现在构造方法中调用 super()。显然,实质是在 B 函数中找到函数 A,根据 *Object.setPrototypeOf(B, A)*,我们有:

1
2
3
function B() {
Object.getPrototypeOf(B).call(this)
}

我们为 A 添加一个 age 构造参数:

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
function A(age) {
this.age = age;
}
function B(age) {
Object.getPrototypeOf(B).call(this, age);
}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new B(18).age); // => 18

于是,一个基本的继承的手写版本就实现了,当然,babel 还考虑了更多细节,比如构造方法有返回值的情况等等,但基本的继承原理就是这个样子。

下图很好地反应了诸多对象的关系。值得一提的是, b 也可能成为下一级类的 prototype,你可以清晰地看到通过 __proto__ 搜索原型链的轨迹。

js-inherit


在现代浏览器的不断迭代下,已经有大部分版本都实现了对 class 关键字和继承的原生支持,相信在不久的将来,就无需在这么麻烦地实现继承了。无论如何,JavaScript 继承的原理仍然是不变的,上面这些内容有助于理解原型链是怎么样工作的。

开头一个问题是:什么是 Plain Object

并没有看到有官方去专门定义它,更可能它只不过是业界的一种通俗叫法,因此也没有严格的定义。但我们在汉语环境里通常叫它“纯对象”。

业界解释:https://www.quora.com/What-is-a-plainObject-in-JavaScript

下面我们来看一下常见 Library 对 isPlainObject 函数的实现。

jQuery

jQuery 3.3 版本中的 isPlainObject 定义在这里

为便于阅读,核心代码经过整理后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isPlainObject(obj) {
var proto, Ctor;

// (1) null 肯定不是 Plain Object
// (2) 使用 Object.property.toString 排除部分宿主对象,比如 window、navigator、global
if (!obj || ({}).toString.call(obj) !== "[object Object]") {
return false;
}

proto = Object.getPrototypeOf(obj);

// 只有从用 {} 字面量和 new Object 构造的对象,它的原型链才是 null
if (!proto) {
return true;
}

// (1) 如果 constructor 是对象的一个自有属性,则 Ctor 为 true,函数最后返回 false
// (2) Function.prototype.toString 无法自定义,以此来判断是同一个内置函数
Ctor = ({}).hasOwnProperty.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

lodash

lodash 4.0.6 版本中的 isPlainObject 定义在这里

基本与 jQuery 版本相同,多了一个 Ctor instanceof Ctor 的条件,满足此条件的仅有 FunctionObject 两个函数。

1
2
3
4
5
6
7
8
9
10
11
function isPlainObject(value) {
if (!value || typeof value !== 'object' || ({}).toString.call(value) != '[object Object]' ) {
return false;
}
var proto = Object.getPrototypeOf(value);
if (proto === null) {
return true;
}
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor == 'function' && Ctor instanceof Ctor && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

redux

redux 从 4.0.0 开始在测试中使用了 isPlainObject,代码在这里

它的实现比较简单。

1
2
3
4
5
6
7
8
9
10
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false

let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// proto = null
return Object.getPrototypeOf(obj) === proto
}

我们并没有一个能判断 Plain Object 的清晰逻辑,大概能理出来的思路是:

  1. 先判断 obj 本身是否满足我们熟悉的合法对象概念;
  2. 判断 obj 的构造函数是不是 Object

至于判断 prototype 是不是 null,无非是一种 shortcut 罢了。

已经不关注社区有大半年了,或者起码有一年了,毕竟被持续增长的业务缠了也有一年了,虽然很“充实”,但毫无疑问地说,也是一无所获。

因为这一年来始终在 React Naitve 体系上工作,传统 Web 的知识也忘得大半了。还记得去年专门写过一篇文章来总结2016年前端社区的现状,一年过去了,如果我不是特别孤陋寡闻的话,那么,我认为,现状并没有改变多少。

框架

React

React 经历了一次许可证的“惊心动魄”,虽然新版本的许可证已经是安全的,但有一定研发实力的公司都已经在造自己的轮子。当然,从开发者的角度,肯定是使用成熟的方案更舒服,显然近在咫尺的理想也不见得能容易实现。

React 16 启用了新的引擎,这些细节是值得玩味的。

除此之外,React 依然拥有大量的开发者用户,就像去年一样。

Angular

未曾想到的是,Angular 2 还立足未稳,Angular 4 就出现了。刚刚看到一篇文章,作者抱怨自己的项目使用了 Angular 2 的 beta 开发之后,暂且不表 TypeScript 语法的绑定,光是要快速 follow 短时间内几十个版本的升级就已经相当心累了。

当然你可以说这位开发者在一开始就不应该使用 beta 版本,但是要等到正式版本发布的话,究竟是什么时候呢?我还记得 Angular 2 的核心包去掉了 beta 后缀的时候,其官方维护的一众功能包,比如动画等等,依旧是在 alpha/beta 状态。好吧,即使等到所有包都达到了起码的 stable 状态,Angular 4 的出现又是几个意思呢?

可以预见,Angular 1 时代的用户,相当一部分会放弃继续使用 Angular 的升级版本。曾经也一味推崇 Angular 2 的我,也渐渐失去了耐心。说到底,还是 Angular 的开发团队自己玩死的。

Vue

其实去年的这个时候,我是比较看衰 Vue 的,语法不喜欢、个人维护难以为继,和 React、Angular 甚至 Ember 完全没法比。

不过 Vue.js 的流行程度经过这一年来,毫无减弱的趋势,我想至少有一下几方面原因:

  1. 稳定,用户不必在业务迭代的同时,额外烦恼于持续升级基础框架;
  2. 简单,上手容易,具有普通开发经验的人,也可以在十分钟内写出一个较为复杂的应用;
  3. 生态工具较为丰富,特别是重要工具由作者维护,质量有保证且唯一,不用担心选择恐惧症

说白了,对于真正的业务开发环境,稳定和简单是特别重要的两个特性,不求能有多激进的特性,但求省心。

另外,Vue 的 ssr 方面也有足够的能力和性能支持,这在今天的 web 应用中,还是比较有吸引力的。React 生态也仅有一个简单的 next.js 还算勉强能用,而 Angular 和 Ember 基本都属于残次品。

Ember

Ember 属于比较小众的框架,我已经关注它有两年了,毕竟推广不多,很多人不知道,社区发展也有限。过去这一年依旧表现平平。

React-Native

如果说去年上半年我起码还在 React Native 上积累了点经验的话,那么下半年基本就达到了瓶颈了。许可证是一方面,公司环境是另一方面,但这都不重要。重要的是,我依旧不看好这个东西。

“行百里者半九十”,我们已经写了成千上万行代码,过亿的用户,以及良好的开发上线流程,但依旧避免不了要时刻面对框架底层缺陷带给我们的额外工作量与业务损失。作为前端开发者,过去面对浏览器兼容性起码还有前辈多年的经验积累,但 React Native 带给我们的就是大大提升了自身的技术钻研能力。

工程化

Webpack

Webpack 也从 2 升级到了3,但变化俨然远远不如从 1 到 2。使用配置难度依旧居高不下,想我这种上了年纪的人,没得商量,每次使用都要再看一遍 doc。

Rollup

Rollup 的定位十分明确,与 Webpack 不构成竞争关系。写 library 用 Rollup,写 app 用 Webpack。

npm

jspmbower 几乎已成骨灰,yarn 依旧不支持 scope。

新方向

webassembly

在下一代人上小学之前能看到定稿的 webassembly 我就已经满足了。

webcomponents

在下一代人上小学之前能看到普及的 webcomponents 我就已经满足了。

数据

断断续续学习 D3,但明显精力不如从前。

微信小程序

如我所料,半温不火。令人鄙视的是,支付宝和百度都还在模仿,简直了。

我的工作

去年我还在研究一种解决方案,能够兼顾的开发效率、运行时性能这两个看似简单却互为矛盾的指标。最后确实有了一个雏形。今年,我已经不再感兴趣,没有别的,就一个原因,想的太多,跟本没有那么复杂的使用场景。

总体来看过去一年,对于个人来讲是毫无建树的,今年上半年,我可能需要拓展一下视野,找一找我不会做的事情做一做。

Unicode 是字符集(charset),UTF-8、UTF-16 只是 Unicode 的编码规则(encoding)。

标准的 UTF-8 对 Unicode 分为4种变长的编码格式。

区间 字节数 编码格式
0~0x7F 1 0xxxxxxx
0x80~0x7FF 2 110xxxxx 10xxxxxx
0x800~0xFFFF 3 1110xxxx 10xxxxxx 10xxxxxx
0x10000~0x10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

也就是说,对于英文字母这种ASCII字符,UTF-8与ASCII是兼容的,都采用1字节存储。

对于希伯来语字母 א,UTF-8采用两个字节存储,方法是把它的 Unicode 码 0x05d0 的二进制形式拆成两部分,后6位前补10,再前数5位前补110,拼到一起(余下全为0),即为 0xd790。

0x05D0 = b0000010111   010000
            11010111 10010000 = 0xD790

同样道理,汉字“我”采用3字节,Unicode 码拆成3部分:

0x6211 = b0110   001000   010001
      11100110 10001000 10010001 = 0xE68891

总结来看,字节起始为0则为单字节字符,起始为110、1110、11110分别代表有2个、3个、4个字节组合代表一个字符。其它字节起始必然全为10,代表不是字符起始位置。

断断续续用了半年时间才读完了《咨询的奥秘》(今天一口气读了好几章),深感作为大牛咨询师,作者深谙计算机职场的人性,其中众多“法则”的总结无不精辟入骨,虽然鄙人不懂咨询,但从咨询的眼光和角度去看职场,不免有种“不识庐山真面目,只缘身在此山中”的彻悟。

草草读过一遍甚感粗糙,因此决定第二遍细细研读。下面我努力每一章都抽出时间写下读后感。

忽然发现,此书英文原版第一版竟出版于1986年,而内容时至今日依旧不过时。

第一章 咨询为什么这么难

咨询第一定律——不管客户和你说什么,问题总会有

是的,任何系统和设计总会有或多或少的问题,可能是无足轻重的功能补充,也可能是深藏不露的致命缺陷,或大或小,但总会有。

因为没有事物是完美的,所以缺陷的存在是必然的。每当有人向我滔滔不绝讲了一大堆关于他的想法,我都习惯性的点头表示“同意”,并很少打断。如果如此结束谈话,自然相安无事,我最担心的是他补上一句——“有没有什么问题?”。

问题总会有,一旦我一时提不出来任何问题,我知道,我一定是没有理解他的观点,他的话被我当做了耳旁风。

在面试新员工的时候,我也必然地在最后问对方有没有什么问题问我,往往我得到的答案是没有。在这个角度上,我并不喜欢没人任何问题的求职者。

在会议的末尾,主持人经常询问大家有没有问题,绝大多数情况下,会议室是沉默的。

显然问题的提出有助于消除歧义,增进理解,提高沟通效率;没有问题可能是不负责任、拖延症、懒惰的表现。

虽然别人的问题会一直存在,但不必指出得过于强烈,要照顾自尊,因此:

永远不要承诺百分之十以上的改进

这就相当于指出对方存在“很大”的问题,人非圣贤,这样指出问题无疑会招致对方强烈的反感。

如果出现意外:

如果不小心让改进超过百分之十,要确保没人注意到它

处心积虑,为对方留足面子,处世之道。

咨询第二定律——不管一开始看起来什么样,它永远是人的问题

诸多系统的异常在开始往往都被归咎于异常的客观事实,这种偏见非常地常见,人们在潜意识里往往不相信自己出了错。

另一种理解是,无论是哪个环节出了问题,都可以归咎于人的管理失职,毕竟只有人才有主观能动性。一般来讲,人们对于难以预防的灾害,最多能够原谅一次,从第二次开始,都可以归咎于负责人对于经验总结的失察,于是:

马文定律——不管客户在做什么,都要建议他们做些别的

抛开咨询师和客户的关系不谈,由于当局者迷,当事人往往会深陷思维定势,不妨换个角度思考,很多时候只是睡一觉、喝杯茶,问题就迎刃而解。

咨询第三定律——永远别忘了客户是按小时付费,而不是按照解决方案付费的

这一句很容易被误解,它实际上是对人性的极其深刻的讽刺——我只要向老板表态我已经在解决问题了,这是最重要的,至于最终有没有解决——原本就不可能被解决啊,否则依我的能力,早就解决了!

这也导出了另一个推论:

你要是在意功劳记在谁头上,那就啥事也干不成了

人们需要保留一种底线的面子,不能做出证明他一无是处的事情,谁都不想承认自己犯有严重的错误。

咨询第四定律——要是他们没聘用你,不要帮他们解决问题

很容易理解,没主动问你,你就别瞎BB,招人烦。

树莓酱定律——铺的越广,摊得越薄

精力有限,战线不能拉得太长,顾此失彼。这也是信息守恒的一种体现。

温伯格双胞胎定律——大部分时间,在世界上大多数地方,不管人们有多努力,都不会发生什么大事

这印证了一句话,“努力了不一定成功,不努力一定不成功”,似乎,即使努力,成功的几率也是相当低的,很令人沮丧。但这是自然规律,熵增的趋势是极其平缓的。

书中有一句话很经典:对于世界上大多数系统而言,对于其下一时刻行为的最佳预测,就是和上一时刻做同样的事情

在互联网这种信息高速传播的时代,常常会有新事物出现,往往冠以“革命性”的名头。然经过详细了解后就可以发现,大多数都只是旧瓶装新酒,仅仅对旧事物做了一定的改进,暂且不关心改进是否会带来其它的副作用,微小改进就难以称之为任何“革命”。遇到这种新事物的时候,先不要盲目追宠,相信温伯格双胞胎定律。

鲁迪黄萝卜理论——一旦你干掉了头号问题,二号问题就升级了

问题是没完没了的,队列不空,多次出栈后仍有队头。


所以,“咨询为什么这么难”?世界上的问题是无穷无尽的,而人们又不愿意被指出自己有问题,为了照顾人家的情绪,既不能改进过多,又不能包揽全部功劳。即便如此,纵然你再努力,能够做出实质性改变的机会仍然不大。作者在最后抛出了做咨询的三条困难定律:

  • 要是不能接受失败,做顾问就永远不会成功
  • 一旦消灭了头号问题,二号问题就升级了
  • 帮助自己要比帮助别人更难

自从去年11月中旬在公司内部转岗后,我就一直从事着React-Native业务研发工作,算起来也有大半年了,在这项技术的应用、运维上也算是有了点经验。

往往在产品上应用一项新技术的KickOff上,都必然要涉及该项技术的明显领先优势,React-Native 给人的一贯明显优势无非是:

  1. 开发效率提升
  2. 与Native一致的用户体验
  3. 无需发版,随时上线

那么如果工程做得好的话,也要必然提到它的劣势——不够稳定

我以前向来是个React-Native黑,我不认为它的稳定性足以支撑一项C端产品,更何况是亿级用户产品。

因此,当初做React-Native算是有些被迫的,毕竟万事俱备只欠开发,毕竟要用事实说话,毕竟挣钱吃饭重要。


初期的开发工作可谓极其艰苦,不但没有开发经验,而且基础调试设施不完善,效率很低,更何况连实体测试机都不够。但最终总算是上线了。

另我十分惊讶的是,不稳定性带来的客户端Crash比预想中的要低得多得多。在平台的横向比较上,Android要比iOS的奔溃率高,但还不算很离奇,勉强可以忍受。

我从来不会想到这么一个残缺不堪的东西竟然真的没有掉链子,由此我反思了一下,想起了我党在40年前的那句话:

实践是检验真理的唯一标准

实践是检验真理的唯一标准

是的,不能因为外界的评论甚至是自己的无知,就对新事物抱有成见。除非自己亲自实践,否则无权瞎BB。

经过这么一来,我对手头上的这个项目信心大增,业务的迭代速度即将迎来一次飞跃。

然,下面才是冷静之后的现实,更说明了海量事实的总体分布就像那宇宙微波背景辐射一样的均匀各项同性。

宇宙微波背景辐射

线上追踪问题困难

React 异步渲染的性质,导致了在渲染视图中出现crash,崩溃堆栈是不会追溯到你的业务代码中的。

crash stack

在这种情形下,代码被压缩根本就不是问题,问题是你完全不知道哪里出了故障,只能靠猜。

年初我就强行猜了一次,幸运地解决了一个bug。

然后如果说线上问题毕竟不在现场,不好追踪的话,那么如果同样的问题发生在了调试阶段,你能看到的只是样式错乱的页面,也不会报给你任何错误信息,就很难让人接受了。

因此,【】是开发React-Native的一项重要技能。

石头剪子布

Android 无头问题居多

这依然算是固有的稳定性问题。此类问题有两个特点:

  1. 偶发不可复现;
  2. 逻辑上不可能

不可复现的原因很繁杂,可能与设备型号、操作系统版本、软件版本、当前运存容量、用户操作路径都有关系。还有就是一些原则上根本不可能发生的错误。

这些问题由于迟迟无法解决,长时间耽误了我们全量的进程。最后属于不了了之,算是向bug妥协了。

性能差

相信我,KickOff 中“与Native具有相同或相近的性能”的说辞是不成立的,特别是在Android上。

为了提升性能,我们不得不把能耗高的部分交给Native去实现,但这样显然又减弱了效率的优势。

兼容客户端版本

如果说Web可以不考虑浏览器环境,做到始终如一的版本发布,没人会有异议,但React-Native不行。

React-Native是运行在客户端上的,它的行为和格调都必须与当前客户端的环境相匹配。

这意味着,一套React-Native代码必须考虑到多个客户端环境。特别是在视觉大改版的情况下,React-Native融入两套视觉代码,是什么感觉你造吗?

根据版本号的if-else逻辑目前已充斥相当一部分逻辑代码中,代码越来越不够优雅。

文本渲染不准确

我们已经有充分的证据证明React-Native在渲染文本特别是多行文本时存在明显的bug。这导致我们不能按照设计的预期严格的排版。

此问题很深奥,而且以目前的能力来看,基本无解。

坑多多

这些坑不一定是不可解决的,其实大部分我们都可以通过规避来解决,但问题是如果不进行全网搜索,你很难提前预知风险,也就严重影响了效率,往往一个奇葩的问题能耽误你一天的时间。

https://github.com/super-fe/superfe-rn-troubleshooting/issues


总结来看,应用React-Native技术带来的收益并非是特别明显的,在我看来,它最大的贡献是把一拨做业务的人悄无声息地分成了两拨,显然迭代效率就上来了。

客观上,你不能无视上面提到的各种问题,但这些问题说严重就严重,说不严重也不严重,总体看来是可勉强接受的,相对于传统的客户端开发方式,依然是各向同性的

从最初的腹黑到今天的淡然面对,深深感到世界系统的复杂性以及熵增的强大扭矩,革命性没那么容易,起码React-Native不是。

0%