0%

约定先行

  • 公共组件统一放到 components 目录下,公共组件建议添加 propTypesdefaultProps
  • 页面组件统一放到 routes 目录下
    • services目录定义接口调用
    • models目录定义 Dva 的 models
    • views目录处理页面渲染逻辑
    • constants.js 定义组件常量(可选)
    • config.js 定义组件配置(可选)
  • 所有事件监听的方法都用 handle 开头。
  • 把事件监听方法传给组件的时候,属性名用 on 开头
  • 有时候 render() 方法里面的内容会分开到不同函数里面进行,这些函数都以 render* 开头
  • 组件的内容编写顺序
    1. static 开头的类属性,如 defaultPropspropTypes
    2. 构造函数,constructor
    3. getter/setter方法。
    4. 组件生命周期。
    5. 组件私有方法。
    6. 事件监听方法,handle*
    7. render*开头的方法。
    8. render() 方法。

开发步骤

Step 1. 安装 dva-cli 并创建应用

先安装 dva-cli,并确保版本是 1.0.0-beta.2 或以上。

1
2
3
$ npm i dva-cli@next -g
$ dva -v
1.0.0-beta.4

然后创建应用:

1
2
$ dva new dashboard
$ cd dashboard

Step 2. 定义路由

修改页面组件index.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = [
{
url: "/demands/list",
view: "list",
models: ["list"]
},
{
url: "/demands/detail",
view: "detail"
}
];

Step 3. 构造 service 文件

新增src/routes/Demand/services/list.js,内容如下

1
2
3
4
5
6
7
8
export async function fetch(params) {
return request("/api/demand/list", {
method: "POST",
data: {
...params
}
});
}

Step 4. 构造 models 文件

新增 src/routes/Demand/models/list.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
import * as demandService from "../services/list";
import { PAGE_SIZE } from "../constants";

export default {
namespace: "demands",
state: {
list: [],
pageNo: 1
},
reducers: {
save(state,{payload: { data: list, pageNo }}) {
return { ...state, list, pageNo };
}
},
effects: {
*fetch({ payload }, { call, put }) {
const { result } = yield call(demandService.fetch, payload);
const { data } = result;
yield put({
type: "save",
payload: { data, pageNo: payload.pageNo }
});
}
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) => {
if (pathname === "/demands/list") {
dispatch({
type: "fetch",
payload: { pageSize: PAGE_SIZE, pageNo: 1 }
});
}
});
}
}
};

Step 5. 添加界面,让列表展现出来

我们把组件存在 src/routes/Demand/views/list.js

1
2
3
4
5
6
7
8
import React from "react";
import "../index.less";

function Demands() {
return <div className="demands">需求列表</div>;
}

export default Demands;

Step 7. 处理 loading 状态

dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。

然后在 src/routes/Demand/views/list.js 里绑定 loading 数据:

1
+ loading: state.loading.models.users,

Step 8. 处理数据 CURD

  • CURD 的功能调整基本都可以按照以下三步进行:

    1. services
    2. models
    3. views
  • 我们以删除功能为例

    1 . services, 修改 src/routes/Demand/services/list.js

    1
    2
    3
    export async function remove(id) {
    return request(`/api/demand/delete/${id}`);
    }

    2 . models, 修改 src/routes/Demand/models/list.js

    1
    2
    3
    4
    5
    6
    ...
    *remove({ payload }, { call, put }) {
    yield call(demandService.remove, payload);
    yield put({ type: 'fetch', payload: { pageSize: PAGE_SIZE, pageNo: 1 } });
    },
    ...

    3 . views,修改 src/routes/Demand/views/list.js,替换 handleDelete 内容

    1
    dispatch({ type: "demands/remove", payload: id });

技术栈一览

dva技术栈

前言

笔者使用的是 Mac 电脑,终端使用的是 Iterm + zsh,本文只是对常用命令的整理,方便自己记忆,没有参考意义

常用命令

查看

  • ls显示在存放在相应文件系统下的所有主要目录。

    1
    2
    ls /applications
    ls -a #显示所有隐藏的文件
  • cat 查看文件内容

  • ps 命令用于查看系统的进程状态

    1
    2
    3
    4
    5
    6
    7
    8
    ps -a
    ps -x
    # USER 所属用户
    # PID 进程ID
    # TTY 所属终端
    # VSZ 虚拟内存使用量(KB)
    # RSS 物理内存使用量(KB)
    # STAT 进程状态 R:运行 S:中断 D:不可中断 Z:僵死 T:停止
  • who 命令用于查看当前用户信息

  • history 用于显示运行过的命令,默认是记录最近的 1000 条命令
  • ifconfig 命令用于查看网卡配置和网络状态信息

操作

  • cd 用来跳转(或“更改”)到一个目录的

    1
    cd Gym\ Playlist # 空格需要转译
  • mv将文件从一个文件夹转移到另一个文件夹

  • mkdir 创建目录或文件夹
  • rmdir 删除目录或文件夹
  • rm 删除文件
  • touch 创建新的、空的文件
  • vi/vim 编辑文件

权限

  • sudo 命令用于以 root 管理员权限运行某命令

    1
    2
    3
    sudo poweroff # 使用 root 权限执行 poweroff 命令
    su - # 切换到 root 账户
    sudo !! #以 root 权限运行上一条命令
  • poweroff 用于关闭系统,reboot 用于重启系统

    1
    2
    3
    poweroff #关闭系统

    reboot #重启系统·

输出

  • echo 命令用于将字符串和变量输出到屏幕上。

    1
    2
    echo hi # 输出字符串
    echo $SHELL # 输出变量
  • cal 命令用于输出一个日历

    1
    2
    3
    cal # 输出这个月日历
    cal 2018 # 输出 2018 年日历
    cal 6 2018 # 输出 2018 年 6 月日历
  • date 命令用于显示或设置系统的时间

    1
    2
    3
    4
    date # 显示当前日期
    date -s "20180428 7:15:00" # 设置指定日期
    date "+%j" # 显示当天是当年的第几天
    date "+%Y-%m-%d %H:%M:%S" # 以另一种格式显示当前日期

iterm 快捷键

标签

  • 新建标签:command + t
  • 关闭标签:command + w
  • 切换标签:command + 数字 1/2 command + 左右方向键
  • 切换全屏:command + enter
  • 查找:command + f

分屏

  • 垂直分屏:command + d
  • 水平分屏:command + shift + d
  • 切换屏幕:command + option + 方向键 command + [ 或 command + ]
  • 查看历史命令:command + ;
  • 查看剪贴板历史:command + shift + h

其他

  • 清除当前行:ctrl + u
  • 到行首:ctrl + a
  • 到行尾:ctrl + e
  • 前进后退:ctrl + f/b (相当于左右方向键)
  • 上一条命令:ctrl + p
  • 搜索命令历史:ctrl + r
  • 删除当前光标的字符:ctrl + d
  • 删除光标之前的字符:ctrl + h
  • 删除光标之前的单词:ctrl + w
  • 删除到文本末尾:ctrl + k
  • 交换光标处文本:ctrl + t
  • 清屏 1:command + r
  • 清屏 2:ctrl + l

编写语法

  • vue推荐的做法是webpack+vue-loader的单文件组件格式
  • react 使用 JSX 的语法
  • vue 模板更贴近我们的HTML,可以让我们更直观地思考语义结构,更好地结合CSS的书写。JSX 使用 js 编写 DOM,代码可控性更强,更易调试

构建工具

  • vue-cli & create-react-app 前者提供多个可选模版,扩展性强。后者提供默认选项,复杂度低

数据绑定

  • Vue采用数据劫持&发布-订阅模式的方式,vue在创建vm的时候,会将数据配置在实例当中,然后通过Object.defineProperty对数据进行操作,为数据动态添加了getter与setter方法,当获取数据的时候会触发对应的getter方法,当设置数据的时候会触发对应的setter方法,从而进一步触发vm的watcher方法,然后数据更改,vm则会进一步触发视图更新操作。
  • React是单向数据流,组件实例状态只能通过setState来进行更改。调用setState更新this.state,它不是马上就会生效的,它是异步的。所以不要认为调用完setState后可以立马获取到最新的值。多个顺序执行的setState不是同步的一个接着一个的执行,会加入一个异步队列,然后最后一起执行,即批处理

diff 算法

  • vue 中 diff 算法实现流程
    1. 在内存中构建虚拟 DOM
    2. 将内存中虚拟 DOM 树渲染成真是 DOM 结构
    3. 数据改变的时候,将之前的虚拟 DOM 树结合新的数据生成新的虚拟 DOM
    4. 将新的虚拟DOM 和上一次虚拟DOM 树进行对比(diff 算法进行比对),来更新只需要被替换的 DOM,而不是全部重绘。在Diff 算法中,只平层比较前后两颗DOM 树的节点,没有进行深度遍历
    5. 将对比出来的差异进行重新渲染
  • react 中 diff 算法实现流程
    1. 在内存中构建虚拟 DOM 描述页面元素
    2. state 变更的时候,将之前的虚拟 DOM 结合新的数据生成新的虚拟DOM
    3. DOM 结构发生改变 → 直接卸载并重新 create
    4. DOM 结构一样 → 不卸载 ,update 更新的属性
    5. 所有同一层级的子节点.他们都可以通过key来区分—–同时遵循1.2两点(key的存在与否只会影响diff算法的复杂度)
    6. diff 总共就是移动、删除、增加三个操作,而如果给每个节点唯一的标识(key),那么React优先采用移动的方式,能够找到正确的位置去插入新的节点。
  • vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制。

性能优化

  • vue组件内部自动实现判断组件是否需要重新渲染 (defineObjectProptype)
  • react 当props或state发生改变的时候会触发,shouldComponentUpdate生命周期函数,优化 render 逻辑
  • react 引入 immutable 库配合 shouldComponentUpdate 优化
    原生支持
  • react native & weex

ssr 服务端渲染

  • Next.js (React)& Nuxt.js (Vue)

生命周期

  • Vue
    • 初始化阶段 beforeCreate → created → beforeMount→mounted
    • 更新阶段 beforeUpdated → updated
    • 销毁阶段 beforeDestroy → destoryed
    • keep-alive 标签 active → deactive
  • React
    • 初始化阶段 constructor → componentWillMount → shouldComponentUpdate → render → componentDidMount
    • 更新阶段 getDerivedStateFromProps(componentWillReceiveProps) → shouldComponentUpdate → componentWillUpdate → render → componentDidUpdate
    • 销毁阶段 componentWillUnmount

状态管理

  • vuex 的流程
    1. 将需要共享的状态挂载到state上:this.$store.state来调用,vuex提供了mapState辅助函数,帮助我们在组件中获取并使用vuex的store中保存的状态。
    2. 我们通过getters来创建状态:通过this.$store.getters来调用
    3. 使用mutations来更改state:通过this.$store.commit来调用
    4. 使用actions来处理异步操作:this.$store.dispatch来调用
  • redux 的流程
    1. 创建store: 从redux工具中取出createStore去生成一个store。
    2. 创建一个reducer,然后将其传入到createStore中辅助store的创建。 reducer是一个纯函数,接收当前状态和action,返回一个状态,返回什么,store的状态就是什么,需要注意的是,不能直接操作当前状态,而是需要返回一个新的状态。 想要给store创建默认状态其实就是给reducer一个参数创建默认值。
    3. 组件通过调用store.getState方法来使用store中的state,挂载在了自己的状态上。
    4. 组件产生用户操作,调用actionCreator的方法创建一个action,利用store.dispatch方法传递给reducer
    5. reducer对action上的标示性信息做出判断后对新状态进行处理,然后返回新状态
    6. 我们可以在组件中,利用store.subscribe方法去订阅数据的变化,也就是可以传入一个函数,当数据变化的时候,传入的函数会执行,在这个函数中让组件去获取最新的状态。

最近公司老项目打算使用React进行代码重构,交易模块有很多重复的逻辑,代码复用性非常重要,直接关系之后扩展业务的代码量。所以整理下在 React 中代码复用的方法。

纯函数(Pure Function)

在讲复用性之前,我不得不介绍一下一个函数式编程里面非常重要的概念 —— 纯函数(Pure Function)。
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数 我们把纯函数的定义拆开来看:

  1. 函数的返回结果只依赖于它的参数。
  2. 函数执行过程里面没有副作用。

具体代码我不展开了,有兴趣可以自行Google

为什么要提纯函数(Pure Function)

  • 因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。
  • 纯函数的特性正视我们在构建可复用代码的时候所需要的

代码复用方法

Render Props

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

使用 Render Props 来处理代码复用问题的类库包括 React RouterDownshift.

借用官网的例子:

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
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

{/* ...but how do we render something other than a <p>?
<p>The current mouse position is ({this.state.x}, {this.state.y})</p> */}

{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}


1
2
3
4
5
6
7
8
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}

高阶组件

a higher-order component is a function that takes a component and returns a new component.

最简单的高阶组件

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react'

export default (WrappedComponent) => {
class NewComponent extends Component {
// 可以做很多自定义逻辑
render () {
return <WrappedComponent />
}
}
return NewComponent
}

现在看来好像什么用都没有,它就是简单的构建了一个新的组件类 NewComponent,然后把传进入去的 WrappedComponent 渲染出来。但是我们可以给 NewCompoent 做一些数据启动工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react'

export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: null }
}

componentWillMount () {
let data = localStorage.getItem(name)
this.setState({ data })
}

render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent

我们可以这样使用它

1
2
3
4
5
6
7
8
9
10
import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

React hooks

总结

本来想整理以下 React 中代码复用的知识的,但写到最后发现这是一个很大的题目。光靠一篇文章根本讲不好,权当我把自己的想法写出来吧,后续得再深入研究再来分享吧。逃ing….

我算是半个工具控,有段时间会追求效率工具,希望工具能提高生产力。现在意识到工具可以给你提供帮助,但真正想提高效率还是要靠自己,提高自己才是最大的收益。当然也没必要排斥工具,善假于物而不为物所迷即可

君子生非异也,善假于物也 –荀子《劝学》

效率类

  • XMind 思维导图工具
  • 原生邮箱 勉强够用
  • 原生备忘录 同步速度快,适配性好
  • 原生提醒事项 简单够用
  • 1Password 我的密码管家
  • Dropbox 保护隐私,同步速度快
  • Paste 剪贴板工具
  • 极光词典 移动端字典
  • Office Lens 微软家的 OCR 工具 够用
  • 白描 中文 OCR
  • 印象笔记 启动速度慢已弃用

娱乐

  • Apple Music 国区便宜
  • Spotify 版权全
  • 网易云音乐 国内歌曲

阅读

  • Instapper 稍后读工具
  • Fiery Feeds Rss 阅读器
  • Price Tag 了解一些软件降价信息
  • Kindle 阅读书籍
  • 微信读书
  • PPHub 刷 GitHub 用

社交

  • 微信 天朝必装吧
  • iMessage 家人交流
  • 钉钉 办公交流用
  • Telegram 了解一些 qiang 外资讯

开发

  • VS code 现阶段最佳前端编辑器
  • Lepton 代码片段管理
  • Dash 离线文档
  • iTerm 终端工具
  • SwitchHosts 一键切换 hosts
  • Axure RP 看原型

其他

  • shadoworcket 够用
  • MoneyWiz 管钱的

防抖 debounce

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!

简单代码实现


1
2
3
4
5
6
7
function debounce(fn, wait) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(fn, wait);
};
}

这只是简单的实现函数防抖,存在很多问题和局限性。这次来分析一下 lodash 中的 debounce 实现。

存在问题


在看 lodash 源码之前先来分析一下简单实现存在的问题

this 指向问题


上面的代码 debounce 返回的函数 this 指向了 Window 对象。在实际需求中我们希望 this 指向正确的对象(一般为触发事件的 DOM 元素)。

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(func, wait) {
var timeout;

return function() {
var context = this;

clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context);
}, wait);
};
}

event 对象


我们使用 debounce 方法包装之后,我们会丢失原来事件绑定的 event 对象,我们需要把他找回来。

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;

clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}

立即执行问题


上面的版本解决了 this 指向和 event 对象的问题,但是在实际使用中我们会有一个很常见的需求,我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。我们再来改造原来的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function debounce(func, wait, leading) {
var timeout;

return function() {
var context = this;
var args = arguments;

if (timeout) clearTimeout(timeout);
if (leading) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
};
}

lodash 的 debounce 实现

上面的代码已经比较完善了,下面来看下 lodash 中的代码实现

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
function debounce(func, wait, options) {
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;

// 参数初始化
let lastInvokeTime = 0; // func 上一次执行的时间
let leading = false;
let maxing = false;
let trailing = true;

// 基本的类型判断和处理
if (typeof func != "function") {
throw new TypeError("Expected a function");
}
wait = +wait || 0;
if (isObject(options)) {
// 对配置的一些初始化
}

function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// 为 trailing edge 触发函数调用设定定时器
timerId = setTimeout(timerExpired, wait);
// leading = true 执行函数
return leading ? invokeFunc(time) : result;
}

function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime; // 距离上次debounced函数被调用的时间
const timeSinceLastInvoke = time - lastInvokeTime; // 距离上次函数被执行的时间
const timeWaiting = wait - timeSinceLastCall; // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置

// 两种情况
// 有maxing:比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
// 无maxing:在下一次trailing时执行 timerExpired
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}

// 根据时间判断 func 能否被执行
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;

// 几种满足条件的情况
return (
lastCallTime === undefined || //首次
timeSinceLastCall >= wait || // 距离上次被调用已经超过 wait
timeSinceLastCall < 0 || //系统时间倒退
(maxing && timeSinceLastInvoke >= maxWait)
); //超过最大等待时间
}

function timerExpired() {
const time = Date.now();
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 重启定时器,保证下一次时延的末尾触发
timerId = setTimeout(timerExpired, remainingWait(time));
}

function trailingEdge(time) {
timerId = undefined;

// 有lastArgs才执行,意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
if (trailing && lastArgs) {
return invokeFunc(time);
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
lastArgs = lastThis = undefined;
return result;
}

function cancel() {}

function flush() {}

function pending() {}

function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time); //是否满足时间条件

lastArgs = args;
lastThis = this;
lastCallTime = time; //函数被调用的时间

if (isInvoking) {
if (timerId === undefined) {
// 无timerId的情况有两种:1.首次调用 2.trailingEdge执行过函数
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}

lodash 中的优化

  1. maxWait 参数保证超过一定时间保证调用一次函数
  2. trailing 参数保证延迟结束后调用一次函数
  3. 加了取消(cancel)、刷新(flush)、暂停(pending) 防抖的方法。
  4. 兼容了被包装函数有返回值的情况

具体使用可以查看官方中文文档

什么是 throttle 和 debounce


throttle 和 debounce 的功能主要是控制函数调用的频率。

  • throttle 函数节流,将一个函数的调用频率限制在一定阈值内,例如 1s 内一个函数不能被调用两次。
  • debounce 函数防抖,当调用函数 n 秒后,才会执行该动作,若在这 n 秒内又调用该函数则将取消前一次并重新计算执行时间,举个简单的例子,我们要根据用户输入做 suggest,每当用户按下键盘的时候都可以取消前一次,并且只关心最后一次输入的时间就行了。

throttle 和 debounce 的应用场景


throttle 应用场景

  • DOM 元素的拖拽功能实现(mousemove)
  • 监听滚动事件判断是否到页面底部自动加载更多

debounce 应用场景

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

简单实现防抖 debounce

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(fn, wait) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(fn, wait);
};
}

function log() {
console.log(1);
}

window.onscroll = debounce(log, 1000);

throttle 就是设置了 maxwait 的 debounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function throttle(fn, wait, maxwait) {
var timer = null;
var previous = null; //记录上一次运行时间
return function() {
var now = +new Date();
previous = previous || now;
if (now - previous > maxwait) {
clearTimeout(timer);
fn();
previous = now;
} else {
clearTimeout(timer);
timer = setTimeout(fn, wait);
}
};
}

function log() {
console.log(2);
}
window.onscroll = throttle(log, 500, 2000);

Lodash 中的防抖 debounce


上面的代码只是最简单的实现,存在问题和局限性。下次有机会分享一下 Lodash 中防抖的实现

代理转发的前世今生


代理对于中国人来说真的是太熟悉不过了,如果你想到外面看看,你就得需要所谓的 vpn。大多数体验过 vpn 上网的就知道代理起到什么样的作用。

为什么使用代理

  • 分担服务器压力
  • 缓存资源
  • 安全
  • 负载均衡
  • 压缩资源文件(Gzip)
  • ……

前端开发中使用代理


  • 前后端联调
  • 解决前端跨域问题
  • ……

常用的 web 服务都能实现代理转发


  • Tomcat
  • Apache
  • nginx
  • node

正向代理和反向代理


正向代理代理的是客户端,而反向代理代理的是服务器端。
正向代理让服务端不知道谁在请求服务,而反向代理让客户端不知道服务来自哪里。

nginx 常见配置


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
# 配置可访问域名,注意需要添加相应host配置
server_name xxx.dev;
#charset koi8-r;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

location /api/v1 {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
# API Server
proxy_pass http://localhost:4000;
proxy_next_upstream error;
}

}

node 实现代理转发


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require("express");
var httpProxy = require("http-proxy");

var options = {
target: "http://jsonplaceholder.typicode.com/users",
changeOrigin: true
};
var httpProxyServer = httpProxy.createProxyServer(options);

function jsonHttpProxy(req, res) {
httpProxyServer.web(req, res);
}
var app = express();
app.use("/users", jsonHttpProxy);
app.listen(3002);

http-proxy-middleware 简单用法


1
2
3
4
5
6
7
8
9
10
11
12
13
var express = require("express");
var proxy = require("http-proxy-middleware");

var app = express();

app.use(
"/api",
proxy({
target: "http://www.example.org",
changeOrigin: true
})
);
app.listen(3000);

工作中配合脚手架使用


  1. 在配置文件中proxyTable 代理映射
  2. 使用 http-proxy-middleware 应用代理服务器