React添加环境变量

通常情况下,开发环境和线上环境用的 API 的 URL 不是同一个,所以并不能将 URL 写死,而是要根据不同的环境,使用不同的 API.

内置环境

create-react-app 创建的项目有内置的环境变量 NODE_ENV, 在 JavaScript 代码中可通过 process.env.NODE_ENV 读取它。

NODE_ENV 默认有三个可能的值,分别是 development, testproduction, 对应开发,测试,生产环境。

运行 npm start, NODE_ENV 的值为 development; npm run test 则是 test; npm run build 则对应 prodution.

比如开发环境的 API 的 URL 为 urlDev, 生产环境的 URL 为 urlProd, 就可通过环境变量,判断当前环境,使用相应的 URL:

1
2
3
4
5
6
7
8
9
10
11
12
let env = process.env.NODE_ENV
let baseUrl = ''
if (env === 'development') {
baseUrl = urlDev
} else if (env === 'production') {
baseUrl = urlProd
}
get(baseUrl) // 伪代码,表示请求动作
...

一般来说,这三个值够用了。

但是需要更多环境的话呢,比如,需要一个开发环境,一个测试环境,一个 UAT (User Acceptance Testing) 环境,一个 stage 环境,一个 production 环境。

掰掰手指数一数,发现上面三兄弟不够用了。

这个时候需要自定义环境变量。

多环境

分析

求助于文档,给了两个方法:

  1. shell 环境添加,写在命令行,类似运行下面这样的命令

    1
    set "REACT_APP_SECRET_CODE=abcdef" && npm start

    但是这种方式只是在 Shell 中添加临时的变量,JavaScript 是读取不到的。

  2. .env 文件添加,即在项目的根目录添加一个名为 .env的文件,在里面添加环境变量,这样的变量在 JavaScript 是可以读取到的。

    还有不同的文件名可在不同的环境进行覆盖,比如将文件命名为 .env.development, 在开发环境即可读取文件内定义的变量。同样,.env.test.env.production 分别对应测试环境和生产环境。

    这种方式,显而易见,只支持内置的三个环境。

文档里面还透漏一个重要信息,react 只读取 REACT_APP 开头的变量,其他的,表示不认识。

比如简单写个 MY_ENV, 是会被 React 忽略的,而 REACT_APP_ENV 就会被读取。

配置

有个思路,就是从命令行入手,然后搞一个 JavaScript 可以读到的永久环境变量。

这里需要 cross-env, cross-env 设计是为了定义全平台兼容的环境变量命令,因为 Windows 在设置环境变量的命令会出现阻塞问题。

首先安装 cross-env:

1
npm install --save-dev cross-env

然后在 package.jsonscripts 里面添加代码。

比如我要设置 stage 环境下的环境变量 REACT_APP_ENVstage, 只需在 package.json 添加如下代码

1
"build:stage": "cross-env REACT_APP_ENV=stage react-scripts build"

其中 cross-env REACT_APP_ENV=stage 定义了 REACT_APP_ENVstage.

然后得到:

1
2
3
4
5
6
7
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:stage": "cross-env REACT_APP_ENV=stage react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}

这样一来,运行 npm run build:stage 打包之后,process.env.REACT_APP_ENV 的值即为 'stage'.

其他要添加什么环境,以此类推。

JavaScript 作用域

作用域链

举个栗子

作用域和变量有关,先看简单的代码:

1
2
3
4
5
6
7
8
9
10
var a = 'a'
function foo () {
var b = 'b'
console.log(a)
}
foo() // 'a'
console.log(b) // Uncaught ReferenceError: b is not defined

从上面代码的执行结果可以看出,foo 函数取到了它外部的变量 a, 而最外层的 console.log(b) 操作并没能取得 foo 函数里面的变量 b.

上面代码中,最外层的变量对象保存了变量 a 以及函数 foo,代码执行所需的变量和函数都从变量对象里面取得。 变量对象和当前的执行环境相关联,当前执行环境的代码执行结束后,该环境被丢弃,其中保存的变量和函数也随之被丢弃。

每个函数都拥有自己的执行环境,当函数执行时,执行环境被拾起,函数执行完毕,执行环境被丢弃。

上面代码最外层的 console.log(b) 试图取一个变量 b, 但是当前的变量对象,也就是最外层的变量对象,并没有保存变量 b, 所以代码报错。

foo 为什么能够取到外部的变量 a 呢?

分析一下 foo 取得 a 的过程。

foo 执行的时候, JavaScript 创建了一个活动对象。函数的活动对象是在函数执行时产生的,包含了函数的形参和函数内声明的变量等。 foo 声明的 b 在就 foo 的活动对象里面。在函数的执行环境中,活动对象被当做变量对象。

可是,foo 需要的是 a 啊,foo 自身的变量对象没有这个变量。

不要着急,在创建 foo 的时候,也创建了作用域链,里面包含了外一层的变量对象,保存在函数的 [[Scope]] 属性当中。

在调用 foo 的时候,创建 foo 的执行环境和活动对象,然后复制 [[Scope]] 属性构建作用域链,再把当前活动对象插队放到作用域链的最前面。这个时候,foo 自身的活动对象和外部的变量对象就在这个作用域里面排队。

foo 在作用域链的最前面,也就是自身的活动对象里面,找不到变量 b 的时候,就会去作用域链的下一个对象里面查找。

总结一下

作用域链依次保存了当前执行环境的变量对象,以及外层执行环境的变量对象。就像是变量对象在排队,越是内层的变量对象,越在前面。若无法在当前的变量对象找到一个变量,就会顺着作用域链,依次查找外部的变量对象。

闭包

闭包,看着这两个字,顾名思义也思不出什么来,不如先把它从脑中先抛开。

举个栗子

从文章的第一个例子,可以知道,在函数的外部是访问不了函数内部的变量的。

简单粗暴地去访问,当然访问不了,但也可以通过一系列操作,来访问函数内部变量。

看看简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function bar () {
var a = 0
return function () {
console.log(a++)
}
}
var fn = bar()
fn() // 0
fn() // 1
fn() // 2

上面的 bar 定义变量 a0, 并函数返回一个函数,返回的函数打印变量 a, 然后对变量 a 进行递增操作。

执行 bar ,并将返回的函数赋值给 fn. 执行 fn, 从执行结果可以看出,这个全局变量的 fn 居然访问到了 bar 内的变量 a.

现在去采访一下 fn, 看看他是怎么做到的。

fn 的值是 bar 函数的返回值,也就是 bar 里面返回的匿名函数。

当匿名函数被创建的时候,和所有函数一样,也是将外部的变量对象保存到自身的 [[Scope]] 属性当中,匿名函数保存的,自然是 bar 的活动对象,里面包含了变量 b.

现在慢动作回放一下 var fn = bar() 这个操作:

  1. 初始化 bar 的活动对象 ,将 a 保存到活动对象
  2. 执行 bar 内代码,给 a 赋值为 0
  3. 执行 bar 内代码,创建匿名函数,将 bar 的活动对象保存到匿名函数 [[Scope]] 属性
  4. bar 返回匿名函数,bar 执行完毕,其执行环境被丢弃
  5. fn 接受 bar 返回的匿名函数

注意到步骤 4, 虽然 bar 的执行环境被丢弃,但是 bar的活动对象依然在内存中,因为匿名的 [[Scope]] 属性还在引用这个活动对象,而匿名函数赋值给了 fn, fn 仍然存在于内存中。

执行 fn, fn 自身的活动对象并没有变量 a, 所以顺着作用域链查找 a, 可在 bar 遗留的活动对象中找到。

总结一下

重新捡起闭包这两个字,《JavaScript 高级程序设计》这样定义闭包:

闭包是有权访问另一个函数作用域中变量的函数。

结合上面的例子,可以知道 fn 就是一个闭包,因为它有权访问另一个作用域 bar 内的变量。

块级作用域

举个栗子

看简单的代码:

1
2
3
4
5
6
7
for (var i = 0; i < 3; i ++) {
if (i === 2) {
var b = 200
}
}
console.log(b) // 200

上面的代码输出了 200, 可不要被 forif 的花括号施了障眼法,误以为输出结果是 undefined.

上面的 forif 用花括号包起来的代码块,并没与自己的执行环境,所以上面的代码只有一个全局的执行环境,变量都保存在全局的变量对象里面。这就解释了外层为什么能访问花裤号内部的变量。

再举个栗子

看简单的代码:

1
2
3
4
5
6
7
8
for (let i = 0; i < 3; i ++) {
if (i === 2) {
const b = 200
}
}
console.log(i) // Uncaught ReferenceError: i is not defined
console.log(b) // Uncaught ReferenceError: b is not defined

ES6 中的 letconst 能够形成块级作用域,用 letconst 声明的变量,只能在花括号内部访问。

总结一下

简单地用花括号包起来的代码块,是没有自己的执行环境,形成不了作用域的。而 letconst 命令声明的变量,只能在花括号内部访问。

总结

作用域和执行环境有关,函数拥有自己的执行环境,创建活动对象,作为自己的变量对象,就形成了作用域。

而简单的用花括号包起来的代码,没有自己的执行环境,当然也形成不了作用域,但是 letconst 命令声明的变量,只能在花括号内部访问。

作用域链将内部执行环境的变量对象与外部的变量对象依次保存,使得内部可以访问外部的变量。

当函数 a 的活动对象被函数外的另一个函数 b 引用,函数 b 就是闭包,可以访问函数 a 内的变量。

JavaScript:简单粗暴浅拷贝,钻牛角尖深拷贝

深拷贝和浅拷贝都和对象的复制有关,它们是对象复制的两种不同方式。接下来就来看一下,浅拷贝是怎么个浅法,还有深拷贝深在哪里。

简单粗暴浅拷贝

来个栗子

先复制一个对象看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let a = {
name: 'a',
age: 99,
car: {
color: 'blue'
}
}
let b = {}
Object.keys(a).forEach((key) => {
b[key] = a[key]
})
console.log(b)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }

首先声明了一个对象 a 还有一个空对象 b. 然后遍历对象 a 的可枚举属性,将属性和属性值一一分配给 b. 这里,实现了对象的复制。

接下来,对 b 进行一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
b.name = 'b'
b.car.color = 'red'
console.log(b)
// {
// name: 'b',
// age: 99,
// car: {
// color: 'red'
// }
// }
console.log(a)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'red'
// }
// }

上面的代码,将 bname 属性值变成 'b', 然后再将 bcar 属性的 color 变为 'red'.

打印 b,发现这些改动都预期发生了。

打印 a, 出现了一些意料之外的事情。aname 仍然是 'a' ,然而 acar 属性的 color 属性却发生了变化,变得和刚刚赋给 b 的一样了,成了 'red'.

这是怎么回事呢?

剧情解析

时光稍微倒流一下,回到复制对象的地方看一看。复制的关键代码:

1
2
3
Object.keys(a).forEach((key) => {
b[key] = a[key]
})

代码把 a 的可枚举属性悉数分配给 b.

分配 car 属性的时候,慢动作细节即为:

1
b['car'] = a['car']

a['car'] 的值是一个对象,准确地说,a['car'] 的值是一个指针,指向了对象 {color: 'blue'}

在 JavaScript 当中,把对象赋给一个值,实际上是把对象的内存地址赋给这个值,然后,这个值就指向了该对象。对象是引用类型,不同于基本类型,引用类型存放在堆中。

当把 a['car'] 的值赋给 b['car'] 之时,其实是把这个指针复制了一份,然后赋给 b['car'],最终, b['car'] 也指向了同一个对象。

后来执行 b.car.colorb.car 指向的对象进行操作,a.car 也产生了变化,因为它们指向的就是同一个对象。

上面这个例子,就属于浅拷贝。

归纳一下

浅拷贝,简单粗暴,只管复制,不考虑属性值是不是指向对象

可能上面这个归纳,在这个时候,还是有点模糊,感觉不甚清晰,接下来,来个深拷贝对比一下。

钻牛角尖深拷贝

来个栗子

先上代码:

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 deepCopy (target, source) {
Object.keys(source).forEach((key) => {
if (getType(source[key]) === 'Object') {
target[key] = {}
deepCopy(target[key], source[key])
} else if (getType(source[key]) === 'Array') {
target[key] = []
deepCopy(target[key], source[key])
} else {
target[key] = source[key]
}
})
return target
}
// Get type of parameter val.
function getType(val) {
let reg = /^\[object\s(\w*)\]$/
return reg.exec(Object.prototype.toString.call(val))[1]
}
var a = {
name: 'a',
age: 99,
car: {
color: 'blue'
}
}
let b = {}
deepCopy(b, a)
console.log(b)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }

上面的代码实现了一个深拷贝函数 deepCopy. 这个函数接受两个参数,target 是目标对象,source 是源对象,函数内将源对象的属性悉数拷贝到目标对象。

函数中首先遍历源对象的属性,分三种情况来处理,分别是当属性值指向对象,指向数组,或者是其他情况。

第一种情况,当 source[key] 指向对象时,先赋一个空对象给 target[key],然后再以 target[key] 这个空对象为 target 参数,以 source[key]source 参数,递归调用 deepCopy.

第二种情况,当 source[key]指向数组,就赋一个空数组给 target[key],接下来以 target[key] 作为 target 参数,以 source[key]source 参数,递归调用 deepCopy.

第三种情况,当 source[key] 不是对象也不是数组,那么直接把 source[key] 的值赋给 target[key].

遍历结束,deepCopy 返回 target.

接着,声明了函数 getType, 就是 deepCopy 里面用到的判断数据类型的函数。getType 接受一个参数 val, 并返回 val 的类型。函数内利用 Object.prototype.toString 得到 val 的类型,但这样得到的是类似 '[object String]'以及 '[object Array]' 这样的字符串,还需要加工一下,才能拿到代表类型的字符串,所以声明了一个正则表达式,来进行从 '[object String]' 提取出 'String' 这样的工作。

然后,声明对象 a, 接着,声明空对象 b, 再执行 deepCopy(b, a)a 深拷贝到 b.

console.log(b) 可知 b 已经成功拿到 a 的可枚举属性。

接下来,对 b 进行一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
b.name = 'b'
b.car.color = 'red'
console.log(b)
// {
// name: 'b',
// age: 99,
// car: {
// color: 'red'
// }
// }
console.log(a)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }

像第一个例子一样,将 bname 属性赋值为 'b', bcar 属性的 color 赋值为 'red'.

打印 b, 发现改变已经生效。

打印 a, 可见 a 没有像上一个例子那样发生改变。

剧情解析

在这个例子中,当遍历到 a[car] 属性的时候,发现属性值指向的是一个对象,就将一个空对象赋给 b[car], 再把 a[car] 所指向的对象当做 source, b[car] 指向的空对象当做 target, 执行 deepCopy 操作,将 a[car] 所指的对象属性拷贝给 b[car] 所指的空对象。这个时候, a[car]b[car] 所指的并非同一个对象,所以,任凭怎么操作 b[car], a[car] 都不会受到丝毫影响。

归纳一下

这就是深拷贝,深拷贝在遍历源对象属性的时候,遇到可遍历的,如对象和数组,会再深入遍历其属性,并进行拷贝

总结

从上面的例子可以知道,深拷贝和浅拷贝,区别在于,在遍历源对象的时候,遇到可遍历的属性值,会不会去进行遍历。浅拷贝是直接将属性值复制过去,而深拷贝则会继续深入遍历并拷贝。

JavaScript 事件代理

“代理”可以理解为代劳或者帮忙,或者类似你的事就是我的事这种乐于助人的良好品质。JavaScript 也有许多事件要处理,接着就演示下,如何在 DOM 元素中选出一位热心肠的好同学。

场景

想象一个平平无奇的表格,像下面这样:

需求是点击删除按钮可删除单条数据。

用 JavaScript 实现点击删除的操作,需要监听按钮点击事件,在事件点击回调中,发送删除数据的请求,请求成功后重新渲染表格。

基本实现

基本款的实现大概如下:

HTML:

1
2
3
4
5
6
7
8
9
...
<tr>
<td>1</td>
<td>汉堡</td>
<td>
<button data-id="1">删除</button>
</td>
</tr>
...

JavaScript:

1
2
3
4
5
6
7
8
9
10
const deleteItem = (id) => {
// 依据 ID 删除数据。
}
let btnList = document.getElementsByClassName('btn')
Array.prototype.forEach.call(btnList, (btn) => {
btn.addEventListener('click', function (e) {
deleteItem(this.dataset.id)
})
})

上面的代码给每一条删除按钮都添加了点击事件的回调。上面的代码利用自定义属性 data-id 将单条数据的 ID 传给数据的删除按钮,然后在点击事件的回调中通过 dataset 属性取得,将其传递给处理删除的函数。

这样可以实现上面的上面所说的删除功能。

副作用

可是这么写,当数据一多,将会对页面性能造成影响。

参考《JavaScript 高级程序设计》:

首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

面对这种状况,解决方案是事件代理。

事件代理实现

先看代码:

1
2
3
4
5
6
let table = document.getElementById('table')
table.addEventListener('click', function (e) {
if (e.target.tagName === 'BUTTON') {
deleteItem(e.target.dataset.id)
}
})

上面的代码,首先在回调中通过事件对象 etarget 属性取得触发点击事件的元素,然后通过 tagName 判断这个元素是不是按钮,如果是的话,就进行删除操作。

只添加了一次事件处理函数,就实现了功能。

这里利用的是事件流的事件冒泡。

原理

事件流

事件流有三个阶段,分别是事件捕获阶段,处于目标阶段,还有事件冒泡阶段。

按钮点击事件的事件流如下:

上图省略了 tr ,td, tbody 这些元素,只用关键的元素来展示事件流。

“事件捕获”是不太精确的目标先接收到事件,首先是文档,然后再到 body,逐渐精确到最具体的节点 button.

button 接收到事件,即为“处于目标阶段”。

之后,事件再向外层逐级传播到不太具体的节点,这便是“事件冒泡阶段”。

冒泡与捕获演示

假如有个外层元素称为 box, 它里面包含了一个按钮 btn

boxbtn 注册点击事件,代码如下:

1
2
3
4
5
6
box.addEventListener('click', function (e) {
console.log('box')
})
btn.addEventListener('click', function (e) {
console.log('button')
})

点击 btn

可以看到,首先打印的是 'button',接着才是 box. 因为 addEventListener 默认是在冒泡阶段执行回调函数的。这里,最具体的 btn 接受到事件之后,向外层冒泡,box 才能接受到事件。

改一下代码:

1
2
3
4
5
6
box.addEventListener('click', function (e) {
console.log('box')
}, true)
btn.addEventListener('click', function (e) {
console.log('button')
})

这里给 boxaddEventListener 传入了第三个参数 true, 表示要在捕获阶段处理 box 的点击事件。

addEventListener 第三个参数形参为 useCapture, 表示是否在捕获阶段处理事件,默认为 false.

运行一下:

这样一改,首先打印的就是 'box', 因为事件具体节点是 btn,而事件要从不太具体的节点,经过捕获阶段,才确定到 btn. 在捕获阶段,外层 box 要比 btn 早接受到事件,所以先执行了 box 捕获阶段的事件处理函数。

总结

事件代理的代码中,注册点击事件的是 table 元素,发生点击事件最具体的节点是 button. 事件在 button 发生后,由于冒泡,会传播到 table, 从而触发 table 上事先注册的回调函数。所以,用事件冒泡实现事件代理,可以只给热心肠同学 table 添加点击事件回调,而不必劳烦表格中的每个 button .

在类似的场景下,可利用事件代理,来减少冗余,节约资源。

你好,我是 JavaScript 的this

JavaScript 有个关键字 this, 时不时冒出来,让你猜一猜,“我到底指向谁?”

这个 this 指向 window, 那个 this 又是刚刚 new 出来的新对象,这里又冒出来一个指向 call 函数的第一个参数的。

到底什么鬼?一团乱麻。

线索

快刀斩乱麻,这里的快刀同学只会说一句话:”this 指向函数执行的环境对象。”

怎么理解

带着上面的线索,去几个场景,来看一看,this 在各种情况下,到底指向哪位。

函数调用

1
2
3
4
5
function a() {
console.log(this)
}
a() // window

在浏览器中,a 的执行环境即为 window, 所以 this 指向了 window.

方法调用

1
2
3
4
5
6
7
let obj = {
b: function () {
console.log(this)
}
}
obj.b() // obj

当作为对象的方法时,函数的 this 指向其所属对象。

构造函数

1
2
3
4
5
6
7
8
window.age = 100
function GirlFriend () {
this.age = 18
}
const girlFriend = new GirlFriend()
console.log(girlFriend.age) // 18

girlFriend.age18 可以看出,以构造函数的形式执行函数时,this 指向其返回的对象。

改变 this 指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.hello = 'Hello, I am window.'
let obj = {
hello: 'Hello, I am obj.'
}
function sayHello () {
console.log(this.hello)
}
sayHello() // 'Hello, I am window.'
sayHello.call(obj) // 'Hello, I am obj.'
sayHello.apply(obj) // 'Hello, I am obj.'
const hi = sayHello.bind(obj)
hi() // 'Hello, I am obj.'

call 方法和 apply 方法接收的第一个参数,是一个为函数指定的 this. 上面的代码中,第一次函数调用形式执行 sayHello, this 指向的是 window. 后面的 callapply 指定的 this 是对象 obj, sayHello 函数执行这两个方法时,this 指向了 obj.

bind 方法的第一个参数也是指定的 this, 执行 bind 方法,返回一个拥有指定 this 的函数。上面代码中的 hi 指向一个函数,这个函数是 thisobjsayHello.

箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
window.hello = 'Hello, I am window.'
let obj = {
hello: 'Hello, I am obj.',
foo: function () {
return () => {
console.log(this.hello)
}
}
}
let a = obj.foo()
a() // 'Hello, I am obj.'
let b = obj.foo
let c = b()
c() // 'Hello, I am window.'

箭头函数没有自己的 this, 当箭头函数内部出现 this, 可将它理解为一个变量——“我没有这个变量,去我的外面一层找一找有没有”。所以,箭头函数的 this, 指向的是定义箭头函数时的外部环境。

上面代码中,对象 obj 的方法 foo, 返回一个箭头函数,该箭头函数打印 this.hello.

变量 a 的值是 obj.foo() 的返回值,即为箭头函数。obj.foo() 是方法调用,此时,foothis 指向的是对象 obj. 而 foo 中的箭头函数用到的 this 是从箭头函数的外部(即 foo)拿到的,也指向 obj.

对象 objfoo 赋值给了变量 b, let b = obj.foo 相当于下面的代码:

1
2
3
4
5
let b = function () {
return () => {
console.log(this.hello)
}
}

接下来执行 b, 把返回的箭头函数赋值给 c. 此时,执行 b 是一次函数调用,函数 bthis 指向的是它的运行环境 window. 箭头函数拿到的 this, 是它外层函数的 this, 也就是 window.

严格模式

严格模式下,会对函数的 this 指向造成影响。

先上代码:

1
2
3
4
5
6
7
8
9
'use strict'
window.hello = 'Hello, I am window.'
function sayHello () {
console.log(this.hello)
}
sayHello() // Uncaught TypeError: Cannot read property 'hello' of undefined.

在严格模式下,函数调用的情景会受到影响。这个时候,函数内的 this 不再指向全局对象。

总结

this 指向函数执行的环境对象。

JavaScript 函数防抖了解一下

说到防抖,想必多数人首先想到的是相机的防抖。因为我们并不是机器人,所以拿手机拍照的时候,手都会有不易察觉的抖动,这样的抖动会影响相片的质量。手机对这些情况做的一些补偿操作,减小了手抖对成像造成的影响。

我们都知道,JavaScript 是一门编程语言,不是人类也不是机器人。那什么情况下,会产生“抖动”呢?

场景

联想一个平平无奇的搜索框,当用户输入的时候,需要实时给出搜索建议。

第一反应肯定是监听输入框的 input 事件,随着内容的变化,发出请求,拉取建议的数据。

如下面的例子(这里 input 被触发时,执行 console.log('input'), 并且用 console.log('fetch') 代指请求):

可以看到,每一个字符的增减,都会触发请求。

这种情况,就属于“抖动”。

服务器接收到这样的请求,肯定是一脸懵啊,这谁顶得住?

这个时候,就需要像手机相机一样,做一些操作,减少抖动对网络请求的影响,减轻服务器的压力。

怎么做

“抖动”情景下,连续输入,导致发送了多次一样的请求。函数防抖的处理方式是:先规定一个时间段,比如一秒,输入内容触发 input ,一秒之后再发送请求,假如一秒内又产生了新的输入,那么重新计时,点击过后一秒再发送请求。

这样一来,规定时间段内的所有输入,只会产生一次请求。不管打字多快的手速,也战胜不了防抖的函数。

怎么写

直接上代码:

1
2
3
4
5
6
7
8
9
const debounce = (func, delay = 200) => {
let timeout = null
return function () {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}

debounce 函数接受一个函数 func 和一个默认为 200 毫秒延迟时间 delay 作为参数。返回一个函数,触发返回的函数,开始计时,delay 毫秒后触发 func, 假如 delay 时间段内,再次触发这个函数,那么重新计时,delay 毫秒后触发 func.

debounce 首先声明变量 timeout, 用于存放之后 setTimeout 函数返回的定时器编号。

然后返回一个函数,函数内执行 clearTimeout 来依据先前声明的 timeout 来清除定时器。当然,一开始,传入的 timeout 值为 null, 这时的清除操作忽略不计。

接着,执行 setTimeout, 在至少 delay 规定的毫秒后,将 setTimeout 的回调函数添加到当前事件队列,回调内执行 func 函数。并且把返回的定时器编号赋值给 timeout , 这样,下一次触发 debounce 返回的函数时,就可以清除通过上面的 clearTimeout(timeout) 来清除定时器 。

注意到上面执行 func 用的是 func.apply(this, arguments), 这样一来,就可以对 debounce 返回的那个函数传递参数,func 执行的时候,再把参数传给 func.

来用一下:

1
2
3
4
5
6
7
8
9
10
11
const suggest = () => {
console.log('fetch')
}
const debounceSuggest = debounce(suggest, 500)
let btnSearch = document.getElementById('search')
btnSearch.addEventListener('input', () => {
console.log('input')
debounceSuggest()
})

这里将 suggest 函数传入 debounce 函数,并设置延迟时间为 500 毫秒。 debounce 返回的函数赋给 debounceSuggest , 然后在提交按钮 btnSearchinput 事件回调中执行 debounceSuggest.

看下效果:

上图中,一开始的几次连续输入,都不会触发 suggest,打印 fetch ,直到整个单词 helllo 输入完成,才触发了一次 suggest. 接着连续删除,也只触发了一次 suggest.

总结

函数的防抖将一定时间内的多次操作,减少为一次,去除冗余,节约资源。

JavaScript 节流函数

简介

“节流”,字典上这么解释:

[throttle]∶节制流入或流出,尤指用节流阀调节。

那节流函数 (throttle) 就是 “节制流入或流出的函数”,越来越绕了,这是什么鬼?刚开始接触这种名词,反正我是有点摸不着头脑,略抽象,不好理解。

那放下这术语不管,先看看为什么要用它。

场景

页面事件中,scroll, resize 和 mousemove 事件都有一个共同点,就是会频繁地被触发。举个例子,那 scroll 来看好了。

先写一段代码:

1
2
3
4
let num = 1
window.onscroll = () => {
console.log(num++)
}

这里监听了页面的 scroll 事件,当页面滚动时,打印出递增的数字。

下图是运行结果,可以看到,console 里面疯狂输出:

回想一下最常用的 click 事件,是不是老老实实,点一次,执行一次。而 scroll 呢,滚动一小段,好几次就去了。这里还好,不涉及到 DOM 操作,要是在 scroll 事件中处理 DOM 元素,那页面性能肯定受到影响,笔记本风扇说不定随着滚动的页面呼呼作响,或者页面一卡一卡卡出心脏病。

让它消停点的办法,就是让回调少执行几次。提到让疯狂执行的函数少执行几次,限制它的行为,“节流”这个概念是不是隐隐约约出现,不再那么抽象了?就好比把水龙头关小一点,让水慢慢流。

方法

怎么让这个函数慢慢流呢?

设想一下,页面一直在滚动,根本停不下来。这个时候,回调也一直在触发,噼里啪啦。

和它商量一下: “你能不能隔几秒执行一次?”

“隔几秒?”

“两百毫秒吧。”

节流函数就是给这种高频率触发的函数,规定一个时间间隔,就是这一次执行和下一次执行的时间间隔。

有点像游戏里面的技能冷却时间,放了技能,进入冷却时间,这期间,就算不停地点技能,也放不了,只能等冷却好,再放。

千言万语不如上代码:

1
2
3
4
5
6
7
8
9
10
const throttle = (func, ms = 200) => {
let prev = new Date().getTime()
return function(...args) {
let now = new Date().getTime()
if (now - prev >= ms) {
func()
prev = now
}
}
}

这里的 throttle 函数接受一个函数 func 和一个毫秒数 ms 作为参数,返回一个函数。当返回的函数不停地被触发,只有满足两次触发间隔大于或者等于规定的毫秒数,传入的 func 才执行。

这个间隔是用时间戳来判断的。首先定义变量 prev 记录一个时间,在返回的函数中,取得它执行时的时间戳 now,比较两个时间戳,假如间隔大于规定的毫秒数 ms,就执行传入的 func 函数。

拿上面的例子再举个例子:

1
2
let num = 1
window.onscroll = throttle(()=> {console.log(num++)}, 2000)

这里监听页面滚动事件,每两秒触发一次回调函数。如下所示(没有慢放):

当然,实际开发中,不会两秒一次这么夸张。这里只是为了更明显地观察到节流函数的作用。

总结

节流函数为高频率触发的回调设定一个执行周期,减少对页面性能的损耗。

可用于 resize, scroll, mousemove 这些事件的回调函数。

Vuex 基本使用

简单介绍

查看官方文档可以知道:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

怎么理解呢?就拿我两位数的资产的银行卡来说吧,基本的存钱取钱,就是状态管理。

怎么用

我们将以一个简单的 Todo 作为例子,来讲解 Vuex 的基本使用。

安装

1
npm install vuex --save

初始化

首先需要在项目的 src 目录下,新建一个 Vuex 的目录 store,结构如下:

初始化 Vuex:

1
2
3
4
5
6
7
8
9
10
// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state.js'
Vue.use(Vuex)
export default new Vuex.Store({
state
})

引入 Vuex 是理所当然的,除此之外执行 Vue.use(Vuex) 来全局安装 Vuex. Vuex.Store 即 Vuex 的构造函数,来初始化 Vuex 实例。这里可以看到,我们在 Vuex 构造函数中传入了一个 state 选项,那这个 state 是什么呢?

State

State 从字面意思理解,就是状态,在 Vuex 里面,什么代表了状态呢?数据。State 是 Vuex 这一状态管理工具的唯一的数据源,所有的数据都储存在里面。
State 的写法如下:

1
2
3
4
5
6
// state.js
const state = {
todoList: JSON.parse(localStorage.getItem('todoList')) || []
}
export default state

这里声明了一个 state, 里面有一个 todoList 的字段,todoList 的数据是去 localStorage 里面拿的,如果没有,就是一个空数组。
好了,现在我们的状态已经有了,接下来就是要在组件里面,获取这个 state, 也就是让我们的组件拿到这里的数据。

Getter

Getter, 顾名思义,就是一个“取”的操作,来拿 state 里面的数据。Getter 的写法如下:

1
2
// getters.js
export const todoList = state => state.todoList

这里声明并输出了一个 todoList 函数,函数的参数是 state, 返回值 state.todoList.
Getter 函数接受 state 作为它的第一个参数。这里我们就取到了上一节 state 里面的 todoList.
在需要 todoList 数据的组件当中,可以利用 mapGetters 将数据映射到计算属性。写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { mapGetters } from 'vuex'
...
export default {
...
computed: {
...mapGetters([
'todoList'
])
}
...
}

调用的时候,和普通的计算属性别无二致:

1
2
3
4
5
<ul class="todo-list">
<li v-for="(item, index) in todoList" :key="index" class="todo-item" >
...
</li>
</ul>

这样,todoList 就被渲染到了页面中:
todoList

当然,不管是标记一个事项为完成,还是添加删除事项,todoList 都会产生变化,也就是 state 会变化。那我们怎么去改变 state 呢?这就需要 mutation 的帮助 。

Mutation

Mutation 是 Vuex 当中改变 state 唯一的方法。Mutation 使用与事件处理函数非常相似,都具有类型和回调函数。
这里把 mutation 比作事件,首先来规定“事件类型”:

1
2
// mutation-types.js
export const SET_TODO_LIST = 'SET_TODO_LIST'

这里规定了一个 SET_TODO_LIST 的类型。
类型是不可变的,所以我们将其声明为常量。
然后再来写“事件”的“回调”:

1
2
3
4
5
6
7
8
9
// mutations.js
import * as types from './mutation-types'
const mutations = {
[types.SET_TODO_LIST] (state, todoList) {
state.todoList = todoList
}
}
export default mutations

可以看到, mutations 是一个对象,一个“事件类型”就对应可一个处理函数。处理函数接受 state 作为它的第一个参数,第二个参数是额外的,一般称之为“荷载 (payload) ”。
这里我们的荷载是一个 todoList,这个处理函数将 state 原来的 todoList 改为传入的荷载。
要使用 mutations,在 Vuex 的构造函数中,就要将 mutations 选项加进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state.js'
import * as getters from './getters'
import mutations from './mutations'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
state,
getters,
mutations,
plugins: debug ? [createLogger()] : []
})

现在,构造函数中已经传入了 mutations,接下来,就是在组件中使用 mutations(这里的第四个选项 plugins 并不影响 mutation 的功能,用途是在控制台打印 Vuex 操作的信息).

TodoList 组件中,我们在“添加事项”这一动作提交 mutation, 改变 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
// todo-list.vue
import { mapGetters, mapMutations } from 'vuex'
export default {
...
methods: {
...
addItem () {
let itemText = window.prompt('请输入要添加的事项:')
if (itemText) {
let list = this.todoList.slice();
list.push({
text: itemText,
done: false,
checked: false
})
this.setTodoList(list)
}
}
...
...mapMutations({
setTodoList: 'SET_TODO_LIST'
})
}
}

接下来就去 todo 里面添加一个条目。如图,添加了一个 coding 事项:
添加事项

可在 vue 的 devtool 观察到如下结果:

可以看到 mutation 的 payload 以及 type 信息。
再来观察 Vuex 的 createLogger 插件在控制台输出的信息:

可以看到,这里进行了一次 SET_TODO_LIST 的 mutation 操作。从 prev state可以知道 ,操作前, todoList 只有三个条目。进行 SET_TODO_LIST 操作时,传入的荷载为四个条目的 todoList。操作后,观察 next state 可知,操作后的 todoList 已经有四个事项。
在线上的 Demo
中可打开控制台,添加或者删除条目,观察 Vuex 的状态变化。

总结

这里用一个 todo 的例子,简单介绍了 Vuex 基本的 getters 和 mutatiosn 操作,更多的细节,可参考官方文档

Vuex 的应用场景是,当 n 多个组件之间的相互通信让人眼花缭乱,那么借助 Vuex 保存多个组件共享的状态,只需操作 state,就能在组件之间同步状态。

Vim 基本操作

初次接触到 Vim 是在 Git 命令行中。改写提交的 git commit --amend 命令执行后,需要按下 i 才能修改 commit 信息,修改之后需要按下 Esc 然后输入 :wq 来保存修改并退出,若不想保存修改可输入 :q 退出。
最近的工作中涉及到在虚拟机里面编辑简单的文本,自然要用到 Vim,可还不知道怎么在 Vim 里面进行复制粘贴的基本操作呢,于是搜索一番,并记录下一些基本的操作。

新建/打开文件

1
$vim filename

该命令可打开文件,如果文件不存在,则新建一个文件。

编辑文件

i 进入 Insert 模式,终端窗口下方可见 Insert 字样。此时可进行文件的编辑。
Insert 模式下,按下 Esc 退出该模式,输入 :q 退出,:q! 退出并忽略修改,:wq 保存并退出。

光标移动

键盘上的上下左右即可实现光标移动,此外以下按键也可移动光标:

  • h 向左
  • j 向下
  • k 向上
  • l 向右

复制粘贴

首先按下 v 进入 Vim 的 Visual 模式,移动光标可选择文本。
y 可将选中的文本复制到剪贴板,d 则是剪切。
p 可实现粘贴。

查找/替换

查找

  • :/cat 向下查找cat
  • ?/cat 向上查找cat
  • n 下一个匹配
  • N 上一个匹配

    替换

  • :s/cat/dog 替换当前行的第一个 cat 为 dog
  • :s/cat/dog/g 替换当前行所有 cat 为 dog
  • :%s/cat/dog 替换所有行第一个 cat 为 dog
  • %s/cat/dog/g 替换所有 cat 为 dog

行跳转/翻页

  • :n 跳转到第 n 行
  • Ctrl + f (forward)下一页
  • Ctrl + b (backward) 上一页

    删除/插入行

  • dd 删除当前行
  • o 向下插入一行
  • O 向上插入一行

撤销操作

  • u (undo) 撤销操作
  • Ctrl + r(redo) 恢复操作

以上即是 Vim 的一些操作,可应付简单的文本处理。

Stylus基本使用

介绍

在学习一个Vue.js项目的过程中,注意到源码中样式的部分并没有用熟悉的.css样式文件,而是发现了代码长得和css相像的.styl文件。这个.styl以前没见过啊,你是谁?于是开始一顿搜索。

发现文件后缀是.styl的这个哥们儿学名叫stylus,是css的预处理框架。

css预处理,顾名思义,预先处理css。那stylus咋预先处理呢?stylus给css添加了可编程的特性,也就是说,在stylus中可以使用变量、函数、判断、循环一系列css没有的东西来编写样式文件,执行这一套骚操作之后,这个文件可编译成css文件。

安装

首先,安装stylus(确保之前已经安装nodejs)。

1
$ npm install stylus

安装之后,运行stylus -h 可查看帮助。
运行stylus example.styl可将example.styl文件编译成example.css文件。

基本使用

一段简单的stylus代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$background-color = lightblue
add(a, b = a)
a = unit(a, px)
b = unit(b, px)
a + b
.list-item
.text-box
span
background-color: $background-color
margin: add(10)
padding: add(10, 5)
&:hover
background-color: powderblue

编译后生成的css代码:

1
2
3
4
5
6
7
8
9
10
.list-item span,
.text-box span {
background-color: #add8e6;
margin: 20px;
padding: 15px
}
.list-item:hover,
.text-box:hover {
background-color: #b0e0e6;
}

由此我们看到了stylus代码以及由它编译而成的css代码,下面来一步一步看一看为啥编译成这样。

变量

1
$background-color = lightblue

上面的代码声明了变量$background-color,并为其赋值lightblue
声明之后,就可以使用这个变量了:

1
2
span
background-color: $background-color

翻译成css就是:

1
2
3
span{
background-color: lightblue;
}

函数

1
2
3
4
add(a, b = a)
a = unit(a, px)
b = unit(b, px)
a + b

代码声明了函数add,add接受两个参数ab,其中b的默认值是a
add中调用了stylus的内置函数unit,此处,unit函数为ab赋予了单位px
最后将ab相加,并返回结果,是的,你没有看错,没有return,但是返回了结果。
return居然是可以省略的,很任性了。

函数调用:

1
2
3
span
margin: add(10)
padding: add(10, 5)

翻译成css就是:

1
2
3
4
span{
margin: 20px;
padding: 15px;
}

选择器

1
2
3
4
5
6
7
8
.list-item
.text-box
span
background-color: $background-color
margin: add(10)
padding: add(10, 5)
&:hover
background-color: powderblue

这一段是与css代码长得最像的了,虽说最为相像,可有些差别还是一眼就能看出的。
比如没有了css的花括号,没有了css的分号,却多了一些奇奇怪怪的缩进,还有那个&是什么鬼?还&:hover???
预知后事如何,这次接着分解。

先说符号,这里提到的花括号和分号在stylus中是可以省略的,不仅如此,冒号也是可以省略的,color: powderblue你可以写成color powderblue,没有问题。

再说缩进,先提出两个有缩进关系的选择器,上面代码片段第二行的.text-box和第三行的span,其实很容易理解,这哥俩写到css里面是这样的:

1
.text-box span{ ... }

span.text-box的子选择器,stylus中以缩进表示这种关系,更加清晰明了,好看。

还有&,这是个新鲜东西。它是父级的引用,还是来看代码。

stylus这样写:

1
2
3
4
.list-item
.text-box
&:hover
background-color: powderblue

翻译成css是这样:

1
2
3
4
.list-item:hover,
.text-box:hover {
background-color: #b0e0e6;
}

相信代码中已经能很明确地体现出&父级引用的角色了。
同时,我们也不难看出,.list-item.text-box这两个同一级的选择器在stylus中是可以换行写的,只要保证缩进相同,它们就属于同一级的选择器。当然,沿用css的方式,将同一级的选择器用逗号分隔开在stylus中也是可以的。

总结

上面就是stylus的基本使用,普遍情况可以用上。
全面的stylus知识可以参考官方文档,或者张鑫旭翻译的中文文档