柔水刻刀


  • 首页

  • 归档

This Summary

发表于 2018-03-21
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
/**
* This Summary
* by Monar
*/

// This 指向当前函数正在执行的上下文环境

// 1. 函数调用
// 函数调用的this指向全局对象,在浏览器环境中是window对象,在node环境下为global对象
// 如果函数被定义为严格模式,那么内部所有的this全部为undefined

// example 1.1
function func1_1 () {
return this
}

console.log('this: ' + func1_1()) // this: [object global]

// example 1.2
function func1_2 () {
this.someNum = 2 // 为全局环境添加变量someNum
}

func1_2()
console.log('someNum: ' + someNum) // someNum: 2

// example 1.3
// 闭包的执行环境具有全局性

let obj1_3 = {
num1: 1,
num2: 2,
sum: function () {
console.log('this === obj1_3: ' + (this === obj1_3)) // true
// cal() 构成一个闭包,内部执行环境其实是全局global
function cal() {
console.log('this === obj1_3: ' + (this === obj1_3))
return this.num1 + this.num2
}
// return cal.call(this) // 正确打开方式
return cal() // 错误打开方式
}
}

console.log(obj1_3.sum()) // NaN

console.log('*******************************')

// 2. 方法调用
// 方法是作为一个对象存储的函数
// 当在一个对象里调用方法时,this代表对象本身

// example 2.1
let obj2_1 = {
foo: function () {
console.log(this)
}
}

let func2_1 = obj2_1.foo

obj2_1.foo() // obj2_1
func2_1() // global

// example 2.2
class Class2_1 {
constructor (attr) {
this.attr = attr
}

show () {
console.log(this === obj2_2)
}
}

let obj2_2 = new Class2_1()
obj2_2.show() // true

console.log('*******************************')

// 3. 构造函数调用
// 构造函数调用使用new关键字,构造函数调用的上下文环境是新构建的对象
// 当构造器函数失去new关键字的时候即为函数调用,this的指向会是全局环境

// example 3.1
function func3_1 () {
console.log(this instanceof func3_1)
this.attr = '3_1'
}

let obj3_1 = new func3_1 // true

console.log('*******************************')

// 4. 间接调用
// 间接调用一般为一个函数使用了.call()和.apply()方法
// .call()和.apply()函数用来配置调用函数的上下文环境
// 间接调用的上下文为传给方法的第一个参数

console.log('*******************************')

// 5. 绑定函数调用
// 绑定函数调用为将函数绑定一个执行对象,即调用.bind()方法
// .bind() 返回一个新方法


// example 5.1
function func5_1 (num) {
return this * num
}

let double = func5_1.bind(2) // 2 绑定到func5_1的this上
double(3) // 6
double(5) // 10


// .bind() 创建了一个永恒的上下文链并不可修改
// 一个绑定函数即使使用 .call() 或者 .apply()传入其他不同的上下文环境,
// 也不会更改它之前连接的上下文环境,重新绑定也不会起任何作用

// example 5.2
function func5_2 () {
return this
}

let one = func5_2.bind(1)

one() // 1
one.call(2) //1
one.apply(3) // 1
one.bind(4)() // 1
new one() // object

console.log('*******************************')

// 6. 箭头函数
// 箭头函数绑定定义时候的上下文环境
// 箭头函数并不创建它自身执行的上下文,使得 this 取决于它在定义时的外部函数

// example 6.1
let func6_1 = () => this

console.log(func6_1()) // {},浏览器环境返回window

// example 6.2
let obj6_1 = {
num: 1,
add() {
console.log(this)
let sum = () => {
return this.num += 1
}
return sum()
}
}

console.log(obj6_1.add()) // 2

你好,新战友!

发表于 2018-02-28

前年就想着入一台Macbook pro了,16年发布的Touch-bar还是很让我惊艳和心动的。两年来自己的人生经历了很多大事,并无过多结余,遂作罢。

也是今年幸运,到手年终奖之后略有结余,就一咬牙入了一台,一来奖励自己,二来真正想用Macbook做一些东西出来。打开屏幕的那一刹那,感觉自己做的决定真是没有错。

你好,新战友,愿你陪伴我乘风破浪!

纪念《英雄志》断更十年

发表于 2018-01-22

重读《英雄志》,像做了一个很长的梦。

两年的零碎时间,重新把高中大学时期看过的武侠小说看了一个遍,前前后后大概有二十部,从头到尾看下来,还是《英雄志》里的人物和故事最打动我。

要说文字功底,孙晓和金庸不可同日而语,再看意境刻画,孙晓又差了古龙几个档次。可现在,反而是孙晓笔下的诸多人物和画面深深刻在了脑海中,反复涌现。

很多小说描绘芸芸众生,大千世界不同的人物性格迥异,主角作为其中之一,纵使会有成长和历练,但是一般主角的个性相对变化不大。可是《英雄志》讲的却是变化,跨越十年的变化。印象很深刻的是杨肃观回答银川公主的一句话:那年臣屡遭变故,从此挥别轻狂,步入中年。“那年”是十年前,杨肃观二十六岁。

于是就想起自己,跟十年前相比,有没有变化。想来想去,中二的性格似乎一成不变,但是年少的热情与叛逆,却在与日俱减。

孙晓曾说,杨肃观与秦仲海这两位主角已经完全化,修罗一怒和怒王冲冠,都狰狞恐怖。卢云也将在第二十三卷完全化,最晚完全化的是伍定远,因为他太强,也太压抑。那么自己呢,没有十年水瀑洗礼,也没有煮酒论英雄的经历,自己还是个傻小子,什么时候完全化呢?

书中记忆特别深的一段剧情,也许现在我都还不理解。那就是卢云在布庄看到暌违十年的顾倩兮,他站在阴影中注视着自己的未婚妻,突然发现顾倩兮留着指甲尖,不知不觉中感到无比茫然,脑子里明明白白记着,银川有指甲尖,琼芳也有,甚至连艳婷他都记得有,可就是想不起来顾倩兮以前留不留指甲尖。什么都似曾相识,却什么都记不起来了,卢云泪流满面。正统十年元宵节深夜,杨夫人就在眼前,而顾小姐却远在天涯,再也找不到了。

这一章的名字叫做章台柳,是我看过的武侠小说里边写重逢写的最好的,吊打神雕的十六年后。

《英雄志》断更十年,好在结局呼之欲出,让自己有所期待。

闭上眼,眼前依旧是白水河的瀑布、金水桥畔的细雨、红螺寺的小雪以及仲海身穿白衣白甲手持“怒”字旗,帅军奔驰在苍茫的草原上……

闭包不完全指西

发表于 2017-10-30

理解闭包是前端开发工程师的基本。 ——鲁迅

1. 闭包是什么

要讲清楚闭包,首先要先从js的作用域说起。

js在运行之前会建立起一条作用域链:

1
2
3
4
5
6
7
8
var glob = 'Global';

function Outer() {
var outer = 'Outer';
function Inner() {
var inner = 'Inner';
}
}

上述代码的作用域链为Global => Outer => Inner,作用域链越靠近末端,可以访问的变量范围越大。任何一个函数可以访问其自己所在作用域中的变量以及父作用域中的变量,比如在Inner函数内部可以访问到glob、outer、inner三个变量。

函数无法访问自己子作用域中的任何变量,除非定义为全局的(不推荐):

1
2
3
4
5
6
7
8
9
10
11
var glob = 'Global';

function Outer() {
var outer = 'Outer';
// console.log(inner); 无法访问
function Inner() {
var inner = 'Inner';
}
}

// console.log(outer); 无法访问

js中函数是一等公民,也就是说函数可以作为任何值被传递,所以看上边的例子,虽然在全局环境无法访问outer变量,但由于Inner函数可以访问,如果在全局环境中获取Inner函数的话,就可以访问outer变量了:

1
2
3
4
5
6
7
8
9
10
11
var glob = 'Global';

function Outer() {
var outer = 'Outer';
function Inner() {
var inner = 'Inner';
}
return Inner;
}

var func = Outer(); //这个func就等于Inner函数,这样func就能访问outer的值了。

上述的func接受了Outer函数返回的Inner函数,所以func即可访问Outer函数中定义的outer变量了。举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Outer() {
var outer = 0;
function Inner() {
outer++;
console.log(outer);
}
return Inner;
}

var func = Outer();

func(); // 输出1
func(); // 输出2

这就是外部读取内部变量的经典方式。这个Inner的内部函数,就叫做闭包。闭包简单来说就是函数内部的函数,如果一旦该内部函数被返回到别的作用域,那么这个内部函数就形成一个闭包。因为引用了函数内部的一些变量,所以可以在别的作用域中进行访问。

同一个闭包可以同时存在多个,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Outer() {
var outer = 0;
function Inner() {
outer++;
console.log(outer);
}
return Inner;
}

var func = Outer();

func(); // 输出1
func(); // 输出2

var anotherFunc = Outer();
anotherFunc(); // 输出1
anotherFunc(); // 输出2

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

2. 闭包的副作用

首先由于闭包会携带包含它的作用域,所以闭包会比其他函数占用更多的内存。过度使用可能会导致内存泄漏。

另一方面,闭包还有一个副作用,就是闭包只能取得包含函数中任何变量的最后一个值。

1
2
3
4
5
6
7
8
9
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function() { // 闭包
return i;
}
}
return result;
}

你会发现,result这个数组其实是一串闭包的数组,因为都引用了i这个变量。

表面上看,数组里的每一项运行后都能返回它们各自的索引值,从0-9,但其实,每次运行的结果都是10。因为这10个函数中都引用了createFunctions里边的i变量,这个变量在函数执行完成的时候为10,所以所有的结果都是10。如果想要改成预期的结果,则可以这样做来避免上述情况:

1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
// 定义一个立即执行函数,赋值i的一份副本到num,这个时候,每个函数都会引用num了,num从0-9
return function() {
return num;
};
})(i);
}
return result;
}

另外,关于this对象,闭包有时候也会有一些需要注意的地方,举个例子:

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
var name = 'The window';

var object1 = {
name: 'Monar',
getName: function() {
return this.name;
}
};

var object2 = {
name: 'Sandy',
getName: function() {
return function() {
return this.name;
}
}
};

var object3 = {
name: 'Jack',
getName: function() {
var that = this;
return function() {
return that.name;
}
}
};

console.log(object1.getName()); // Monar
console.log(object2.getName()()); // The window
console.log(object3.getName()()); // Jack

上述三个例子,object1是正常情况,object2调用getName()()后发现this的指向是全局的,因为object2.getName()执行后返回的是匿名函数function(){return this.name},因为匿名函数的执行环境具有全局性,所以这个this指向全局环境中的name。如果想要返回的函数返回对象里边的name,那么就要像object3一样,构建一个闭包,引用内部的this,将值放到that中,然后返回调用即可。

3. 闭包的例子

1 实现私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var app = (function() {
var privateCounter = 0;
return {
// add和get即为两个闭包,可以访问匿名函数内部的privateCounter属性
add: function() {
privateCount++;
},
get: function() {
return privateCounter;
}
}
})();

// 由于privateCounter存在于匿名函数内部,所以除了app对象中包含的两个闭包方法外,没有任何方式可以访问privateCounter变量
app.add();
console.log(app.get()); // 1

2 knockout中的observable

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
var far = ko.observable('LinWei');

far(); // LinWei, 取值操作

far('ZiLiang'); // 赋值操作

far(); // ZiLiang,取值操作

// 源码
$.observable = function(value) {
// 闭包变量,保存在内存中,存储ko对象的值
var v = value;
// 返回闭包函数
function closure(val) {
// 如果有参数传进来,为赋值操作
if (arguments.length) {
// 且与上次的值不同的话,重新赋值
if (v !== val) {
v = val;
}
// 返回闭包供下次调用
return closure;
// 如果没有参数传进来,为取值操作,直接返回闭包存储的值
} else {
return v;
}
}
// 返回闭包
return closure;
};

3 未完待续…

4. 参考文献

  1. 学习Javascript闭包(Closure) 阮一峰
  2. Javascript高级程序设计(第五版) 第七章-函数表达式

React Quick Start - 5. State和生命周期

发表于 2017-01-06

State和生命周期

考虑之前时钟的例子,目前为止我们只学习了一种更新UI的方式,那就是通过调用ReactDOM.render()来渲染新的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}

setInterval(tick, 1000);

这一章,我们将学习如何让Clock组件变成可复用的和可封装的,它将会自己设置计时器并且每一秒更新自己。

首先先对clock进行组件封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}

function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}

setInterval(tick, 1000);

但是,这个组件忽略了一个关键的需求:应该由Clock自己来设置一个计时器并且每秒更新UI,这是Clock的实现细节。

理想状态下,我们想要的效果是只写一次Clock,并让其自己进行更新:

1
2
3
4
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

要实现这个效果,我们需要给Clock组件添加”state”。

State和props很像,但是State是私有的,并且完全由组件自己来控制。

之前我们曾谈到过,组件以类的方式来定义的话有一些额外的特性。本地的state就属于这种特性,只有类方式定义的组件才有。

将函数式组件改写为类方式

将一个函数式组件改写为类组件需要五步:

  1. 用相同的名字创建一个ES6的class,并且继承自React.Component。
  2. 添加一个空的方法render()。
  3. 将函数组件的内部代码转移到render()方法内部。
  4. 替换render()方法内部的props属性为this.props。
  5. 删除空的函数声明。
1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

如上所示,Clock组件现在已经从函数式定义转变为了类定义。这样一来,我们就可以使用一些额外的特性了,比如state和生命周期钩子。

添加本地State

将date从props变为state需要三步:

1 将render()方法中的this.props.date替换为this.state.date:

1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

2 为类添加一个构造函数constructor,并且声明和初始化this.state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

注意,我们传递了props给基类构造函数:

1
2
3
4
constructor(props) {
super(props);
this.state = {date: new Date()};
}

类组件应该在任何时候都传递props参数给基类构造函数并且调用。

3 移除<Clock />元素里边的date属性:

1
2
3
4
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

我们稍后添加回来计时器相关的代码。

目前的组件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

下一步,我们将给Clock逐渐设置计时器,并且让它每一秒都进行更新。

添加生命周期函数

在拥有很多组件的应用中,有一点非常重要,那就是当组件摧毁的时候一定要释放其占用的资源。

我们想在Clock第一次渲染在DOM树中的时候设置一个定时器,这在react中叫做”挂载(monting)”。

同样的,我们也想在Clock在DOM树中移除的时候清除掉定时器,这在react中叫做”卸载(unmounting)”。

我们可以在组件类中声明一些特殊的方法,这些方法可以在组件挂载或者卸载的时候运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() {

}

componentWillUnmount() {

}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

这些方法统称为”生命周期挂钩”。

componentDidMount()挂钩函数会在组件渲染到DOM中之后执行,将计时器放到这里非常合适:

1
2
3
4
5
6
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

注意这里我们将计时器的ID附在了this上边。

this.props、由React来设置,this.state相比而言有着特殊的意义,你可以任意添加额外的字段到class中如果你需要存储一些额外信息,并且不需要用来作为显示输出。

如果不需要在render()方法中使用一些变量,那么不就应该放置到state中。

此外,我们还需要在componentWillUnmount()生命周期挂钩中清除掉计时器:

1
2
3
componentWillUnmount() {
clearInterval(this.timerID);
}

最后,我们来实现tich()方法,用来每秒更新时间。

这个方法将会使用this.setState()来更新组件的本地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
32
33
34
35
36
37
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(
<Clock />,
document.getElementById('root')
);

到现在,这个时钟就可以正常工作了。

现在来快速回顾一下发生了什么以及这些方法调用的顺序:

  1. 当<Clock />传递给ReactDOM.render()的时候,React会调用Clock组件的构造函数。因为Clock需要显示当前时间,所以它用一个包含当前时间的对象来初始化this.state。之后我们还会更新这个state。
  2. 之后React调用Clock组件的render()方法。这一步React才知道需要在屏幕上展示什么内容。React接着更新DOM去匹配Clock的渲染输出。
  3. 当Clock组件的输出插入到DOM中的时候,React调用componentDidMount()方法。在这个方法中,Clock组件请求浏览器来设置一个定时器,每一秒钟调用一次tick()方法。
  4. 每一秒钟,浏览器都会调用tick()方法。这个方法中,Clock组件将一个包含当前时间的对象传递给setState(),以此来更新UI。通过setState()调用,React会知道state已经改变,然后再次调用render()方法去了解下一步要在屏幕上展示什么样的内容。这个时候,render()方法中的this.state.date会变化,所以渲染出来的结果将会包含更新过的时间。相应地,React更新DOM。
  5. 如果Clock组件从DOM中移除的话,React会调用componentWillUnmount()方法,这个时候计时器停止。

正确使用State

关于setState(),以下三点你必须知道。

不要直接去更改State

举个例子,直接去修改state的话不会重新渲染组件:

1
2
// Wrong
this.state.comment = 'Hello';

如果要修改state,请使用setState():

1
2
// Correct
this.setState({comment: 'Hello'});

唯一可以直接对this.state直接赋值的地方是构造函数。

State的更新可能是异步的

React可能会在一次更新操作中批处理多个setState()调用。

因为this.props和this.state可能异步更新,所以你不应该依赖他们当前的值去计算将来的状态(state)。

举个例子,下边的代码更新计数器可能会失败:

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

为了针对这种情况,有另一种调用setState()方法的方式,那就是不再传递一个对象给它,而是传递一个函数。这个函数接受前一个状态值作为第一个参数,第二个参数是执行更新操作的时候的props属性:

1
2
3
4
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));

上边使用的是箭头函数,当然使用常规的函数也可以:

1
2
3
4
5
6
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});

State的更新会被合并

当调用setState()的时候,React会合并你提供给当前state的所有对象。

举个例子,你的state可能包含若干相互独立的变量:

1
2
3
4
5
6
7
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}

之后你可能会分别调用setState()来互不影响地更新它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});

fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}

合并只是表面的,所以this.setState({comments})保证了this.state.posts的独立完整,但是完整替换了this.state.comments。

数据流自上而下流动

无论是父组件还是子组件都无法知道一个确定的组件是有状态的还是无状态的,而且它们也不应该关注组件是以何种方式定义。

正因为如此,所以说state经常被认为是本地的或者封装的。对于任何组件,无论谁拥有它或设置它,都无法访问它的内部。

一个组件可以将其state以props的方式向下传递给其子组件:

1
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

这种方式也适用于自定义组件:

1
<FormattedDate date={this.state.date} />

FormattedDate组件可以接受date参数在其props中,并且不需要知道是否来自Clock组件的state或者props或者仅仅是被手动输入的:

1
2
3
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

这通常被称为”自顶向下”或者”单向”的数据流。任何state都属于一些特定的组件,任何组件中的数据或者源于state的UI元素都只能影响该组件下游的子组件。

如果将一棵组件树想象成为props的瀑布,每一个组件的state都是一个额外的水源,这个水源可以并入瀑布任意一点,并且只能向下流动。

为了表示所有组件确实是相互隔离的,我们可以创建一个App组件来渲染三个Clock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}

ReactDOM.render(
<App />,
document.getElementById('root')
);

每一个时钟都会设置自己的计时器,并且互不影响地进行更新。

在React应用中,无论一个组件是有状态的还是无状态的都被认为是一个组件的实现细节,都可能随着时间而改变。你可以在有状态的组件中使用无状态的组件,反过来也一样。

React Quick Start - 4. 组件与Props

发表于 2017-01-05

组件与Props

组件可以将UI页面分割成为许多相互独立的、可复用的片段,从而使得考虑每一部分的设计时可以独立考虑。

概念上讲,组件类似于Javascript中的函数。组件接受任意的输入(称为props)并最终返回在屏幕上显示的React元素。

函数式与类形式的组件

最简单定义一个组件的方式是用Javascript的function:

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

上边这个函数是一个合法的React组件,它接受一个”props”对象作为参数,并返回一个React元素。我们称这种组件为”函数式的组件”,因为它就是字面上的Javascrit函数。

另一种定义组件的方法是使用ES6中的class:

1
2
3
4
5
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

以上两种组件在React里是完全等价的。利用class定义的组件会有一些额外的特性,下一章会重点讨论,目前为止都用函数式的组件来举例,因为简明一点。

渲染组件

之前我们只遇到过React元素代表DOM标签的情况:

1
const element = <div />;

其实,元素也可以表示用户自定义的组件:

1
const element = <Welcome name="Sara" />;

当React检测到一个元素代表的是一个用户自定义的组件的时候,它会将JSX的属性当做一个单对象传递给组件。我们称这个对象叫做”props”。

举个例子,下面的例子在页面上展示”Hello, Sara”:

1
2
3
4
5
6
7
8
9
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);

现在重点来看下在这个例子中发生了什么:

  1. 通过调用ReactDOM.render()来渲染<Welcome name="Sara" />元素。
  2. React 调用Welcome组件,并且将{name: 'Sara'}作为props对象传入。
  3. Welcome组件返回<h1>Hello, Sara</h1>元素作为组件输出结果。
  4. ReactDOM最终更新DOM,将<h1>Hello, Sara</h1>元素显示。

警告:通常情况下,组件名称都以大写开头。举个例子,<div />代表了一个普通的DOM元素标签,但是<Welcome />代表的是一个组件,并且需要Welcome在作用域内。

编写组件

组件在输出的时候也可以引用其他的组件。这可以让我们用组件抽象出各种层级的细节。一个按钮,一个表单,一个对话框等:在React应用中,所有的这些都是通过组件来表达的。

举个例子,我们可以通过创建一个APP组件来渲染多次Welcome组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}

ReactDOM.render(
<App />,
document.getElementById('root')
);

通常,React的应用拥有一个顶级的App组件。不过如果是将React集成进一个已经存在的应用里边,可能一开始是自底向上构建,比如从一个按钮一样的小组件开始,然后逐步向顶层构建。

警告:组件必须返回一个单结点的根元素。这也是为什么上边将所有<Welcome />放置在一个<div>里边的原因。

提取组件

不必害怕将一个大的组件分解成为小的组件。

举个例子,考虑将下边的Comment组件抽取成小的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar" src={props.author.avatarUrl} alt={props.author.name}/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}

这个组件接受anthor(一个对象),text(一个字符串)和date(一个日期类型)作为props,在一个社交媒体网站上描述一段评论信息。

这个组件很难去更改,因为所有的内容糅杂在一起了。这导致很难去复用组件里边的每一部分。让我们把这个组件里边的一些小的组件提取出来。

首先可以提取Avatar组件来表示头像:

1
2
3
4
5
function Avatar(props) {
return (
<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
);
}

Avatar组件并不关心Comment组件中渲染什么样的内容,这也是为什么传递给Avatar一个更加通用的名字:user而不是author。

推荐给props命名从这个组件自身作用的视角前去考虑,而不是这个组件将被用于什么样的场合。

现在可以将Comment组件稍微简化一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}

下一步提取一个UserInfo组件来渲染用户的姓名,这个组件包含用户的头像和姓名:

1
2
3
4
5
6
7
8
9
10
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}

这样可以让Comment组件进一步简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}

提取组件可能是一件比较繁琐的工作,但是在大的应用中,提取各种各样可复用的组件回报很大!

一个好的实践就是如果你的页面中某一部分被多次使用(比如Button,Panel,Avatar),或者这个组件自己很复杂(App,FeedStory,Comment),那么将其提取成各种可复用的组件会是一个不错的选择。

Props是只读的

当以函数式或者类形式声明一个组件的时候,这个组件就无法改变它自己的props。看下边这个sum函数:

1
2
3
function sum(a, b) {
return a + b;
}

这种函数被称为纯函数,因为它并不改变输入的值,而且相同的输入下总是返回相同的结果。

相应的,还有一种非纯函数,这种函数会改变它输入的值:

1
2
3
function withdraw(account, amount) {
account.total -= amount;
}

React非常灵活,但是有一条很严格的规则:

所有的React组件必须表现得像纯函数一样,不更改它们的props。

当然,所有的应用都是动态的,跟随时间所变化。下一节,我们会介绍一个新的概念state。

在不违背上述规则的前提下,State允许React组件随着时间根据用户的动作、网络响应或者其他事件来做出变动。

React Quick Start - 3. 渲染元素

发表于 2017-01-04

渲染元素

元素(Elements)是构建React应用的最小单元。

一个元素来描述你希望屏幕上展现的UI元素:

1
const element = <h1>Hello, world</h1>;

与DOM元素不同的是,React元素是普通的对象,创建简单。React DOM更加关注更新与React元素匹配的DOM树。

Tips: 一个常见的误区是将元素与组件(Components)弄混淆。元素只是构成组件的零部件。

渲染一个元素到DOM树中

首先假定在html文档中有一个<div>:

1
<div id="root"></div>

我们把这个称为”root”DOM结点,其中嵌套的所有东西都将被React DOM来接管。

通过React构建的应用通常只有一个单root结点。如果要将React集成在一个已存在的应用里,那么可以拥有多个相互独立的DOM根节点。

将一个React元素渲染到根DOM结点中,需要借助ReactDOM.render()函数:

1
2
3
4
5
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);

这会在页面上展示”Hello World”。

更新已渲染的元素

React的元素是不可变的。一旦创建了一个元素后就无法改变它的子节点或者属性。

一个元素就像是电影里边的一帧:它代表了一个UI元素在某一刻的展现。

就我们现在了解到的,唯一可以更新UI元素的方式就是创建一个新的元素,并通过ReactDOM.render()函数来渲染。

下边是一个时钟的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}

setInterval(tick, 1000);

这个例子中,setInterval()函数每过一秒就会重新调用ReactDOM.render()来渲染一个新的元素到页面上。

注意:实践中,大多数情况下React APP只调用ReactDOM.render()一次,在后边的章节中会学习如何利用具有状态的组件来封装代码。

React只在必要的时候才会更新

React DOM会将当前元素以及其子元素和之前的状态进行比对,只有在状态确实改变的情况下才会做出必要的更新。

React Quick Start - 2. JSX 简介

发表于 2016-12-30

JSX 简介

先看下这句声明:

1
const element = <h1>Hello, world</h1>;

以上这种标签语法既不是一个字符串也不是HTML。

这就是JSX,一种Javascript语法的一种扩展。官方推荐使用JSX和React搭配来描述UI组件。

JSX产出React的元素。下边主要介绍JSX的基本使用。

将表达式嵌入JSX

在JSX中可以使用{}来包含任何javascript表达式,比如说2+2,user.name或者方法调用formatName(user)等都是合法的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}

const user = {
firstName: 'Harper',
lastName: 'Perez'
};

const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);

ReactDOM.render(
element,
document.getElementById('root')
);

将JSX放入表达式中

经过编译,JSX表达式会变为普通的Javascript对象。
这意味着你可以在if语句中和for循环中插入JSX语法,也可以将JSX赋值给变量、接受其为参数或者从函数中返回JSX表达式。

1
2
3
4
5
6
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}

为JSX设定属性

可以通过双引号包括的字符串字面量来作为JSX的属性:

1
const element = <div tabIndex="0"></div>;

当然也可以通过{}包裹的Javascript表达式来作为属性:

1
const element = <img src={user.avatarUrl}></img>;

为JSX设定子元素

如果一个标签是空标签,可以使用闭合标签/>来结束标签,像XML一样:

1
const element = <img src={user.avatarUrl} />;

JSX标签可以包含子元素:

1
2
3
4
5
6
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);

注意:由于JSX语法较之HTML更加接近于Javascript,所以React DOM使用驼峰命名的方式来命名属性名,以此来替换HTML中的属性。
举个例子比如class在JSX中必须写成className,tabindex必须写成tabIndex。

JSX可以阻止注入攻击

将用户输入嵌入JSX是安全的:

1
2
3
4
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
);

默认情况下,对于任何嵌入到JSX中的值,React DOM在渲染之前都会进行编码。因此,它确保了在你的应用中你无法注入任何没有明确写明的代码,因为任何东西都会在渲染前被转化成字符串。
这种方式可以阻止XSS(跨域站点攻击)。

JSX代表了一种对象

Babel通过转码会将JSX编译为React.createElement()的调用:

以下两种方式是等价的:

1
2
3
4
5
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
1
2
3
4
5
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

React.createElement()可以确保你写出bug更少的代码,它创造出的对象像这样:

1
2
3
4
5
6
7
8
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};

这种对象称为React元素,你可以理解为他们就是页面上UI元素的描述信息。React解析这些对象,并通过他们来构建和更新DOM树。

React Quick Start - 1. 安装

发表于 2016-12-30

主要翻译的是React的官方文档Quick Start部分

安装

安装React

官方推荐使用Yarn或者npm来管理前端依赖。

如果使用Yarn,可以这样安装:

1
yarn add react react-dom

如果使用npm,可以这样安装:

1
npm install --save react react-dom

使用ES6和JSX

官方推荐使用Babel转义器来编译React代码,这样可以在React中使用ES6和JSX的语法。
ES6是最新一代的Javascript,它拥有诸多全新的特性与语法,能够使开发更加便捷轻松。
JSX是Javascript语言的一种扩展,可以与React友好地进行协作。

在使用babel之前确保已经安装了babel-preset-react和babel-preset-es2015,并且在.bablerc中做好了配置。

Hello World

官方推荐使用webpack或者Browserify等打包器来管理代码,一来可以让代码模块化,二来可以打包加载以减少加载时间。

下边是最小的一个React应用Hello World:

1
2
3
4
5
6
7
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);

这段代码构造了一个包含Hello, world!字样的h1Dom元素,并且将这个元素渲染进了页面上一个id为root的元素里。

Learn Git

发表于 2016-06-20

记录一些常用的git命令

基本配置

1
2
3
4
5
6
7
git config --global #全局配置

git config --list #配置清单

git config --global alias.co checkout #设置别名checkout

git help #帮助

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
git init #初始化仓库

git clone [url] [another name] #从远程仓库克隆

git status #查看当前状态

git add [file path] #跟踪当前某文件,纳入版本仓库,add就是放入暂存区的意思,如果是文件夹则会递归纳入

git diff [--cached] #查看修改文件之后还未暂存的变化,加了参数--cached是查看暂存之后与版本库的变化

git commit [-m] [#] #提交到版本库所有暂存区域的更新,-a参数会跳过暂存,直接将已跟踪的所有文件直接提交到版本库

git rm [--cached] [file name] #移除某个文件的跟踪,并将其从文件目录删除,如果加--cached参数的话只是从版本库移除跟踪

git mv [file form] [file to] #文件重命名

git commit --amend #修改最后一次提交

git reset HEAD [file] #取消暂存的文件

git checkout -- [file] #取消修改的文件,回到之前的提交状态文件

.gitignore 文件规则使用glob模式,所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。


日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git log #查看日志

git log -p -2 #-p显示文件差异,-2显示最近两次

git log --stat #显示摘要信息

git log --pretty=oneline/short/full/fuller #提供格式化输出方式

git log --pretty=format:"%h - %an, %ar : %s" #按照自定义格式输出log

git log --pretty=format:"%h %s" --graph #图形化展示

git log --since/until/after/before=2.weeks #特定时间的log

git log --author/committer

git log 选项 说明

-p 按补丁格式显示每个更新之间的差异。

–stat 显示每次更新的文件修改统计信息。

–shortstat 只显示 –stat 中最后的行数修改添加移除统计。

–name-only 仅在提交信息后显示已修改的文件清单。

–name-status 显示新增、修改、删除的文件清单。

–abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。

–relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。

–graph 显示 ASCII 图形表示的分支合并历史。

–pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。

format 选项 说明

%H 提交对象(commit)的完整哈希字串

%h 提交对象的简短哈希字串

%T 树对象(tree)的完整哈希字串

%t 树对象的简短哈希字串

%P 父对象(parent)的完整哈希字串

%p 父对象的简短哈希字串

%an 作者(author)的名字

%ae 作者的电子邮件地址

%ad 作者修订日期(可以用 -date= 选项定制格式)

%ar 作者修订日期,按多久以前的方式显示

%cn 提交者(committer)的名字

%ce 提交者的电子邮件地址

%cd 提交日期

%cr 提交日期,按多久以前的方式显示

%s 提交说明


远程仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
git remote [-v] #查看远程仓库

git remote add [short name] [url] #添加远程仓库

git fetch [remote name] #从远程仓库抓取更新,只抓取,并不合并

git push [remote name] [branch name] #向远程仓库推送更新,推送必须别人没有更新才可以,否则需要先取回本地合并

git remote show [remote name] #查看远程仓库详情

git remote rename [old name] [new name] #重命名远程仓库名

git remote rm [remote name] #移除远程仓库

标签

1
2
3
4
5
6
7
8
9
10
11
12

git tag # 查看现有标签

git tag -a [v1.0] -m [#] #新建带注释的标签

git show [v1.0] #查看某个版本信息

git tag [v1.1] #创建轻量级标签

git tag -a [v1.0] [hash] #给特定提交添加标签

git push [remote] [tag name]/[--tags] #推送标签

分支

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

git branch [branch name] #新建分支

git checkout [branch name] #切换分支

git checkout -b [branch name] #新建并切换到分支

git merge [branch name] #在当前分支上合并[branch name]分支

git branch -d [branch name] #删除分支

git branch [-v]/[-vv] #给出当前所有分支列表,*号代表当前所在分支

git branch [--merged]/[--no-merged] #查看那些分支合并进入了当前分支,即哪些分支是当前提交对象的直接上游

git fetch [origin] #获取服务器端的数据到本地,更新远程分支指向

git push [origin] [master] #向远程仓库推送自己的数据上去,更新远程分支的指向

git checkout -b [local branch name] [remote branch name] #本地新建分支与远程对应

git checkout --track [remote branch name] #本地开始跟踪远程分支

git push [origin] :[branch name] #删除远程分支名

git rebase [origin/master] #将自己的修改衍合到主干分支上去

git会维护一个HEAD的指针,指向当前分支的顶端。

切换分支最好保留一个干净的工作区域。

远程分支在本地无法移动,只是一个标记位。远程分支使用[origin]/[master]这种方式展现。

12
彭一

彭一

坚持与反复之后,柔水终成雕刀

11 日志
4 标签
© 2018 彭一
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4