Compare commits

..
6 Commits
Author SHA1 Message Date
ascoders a513286318 126 2019-10-28 08:54:07 +08:00
ascoders 509dfe2c97 125 2019-10-21 08:55:05 +08:00
ascoders 806ee0177a fix: 修复歧义 2019-10-16 20:30:07 +08:00
ascoders fe4afdf89c 124 2019-10-14 09:09:13 +08:00
ascoders caec16066a 123 2019-10-08 09:33:26 +08:00
ascoders 7f53bde9a3 122 2019-09-29 09:11:00 +08:00
6 changed files with 1605 additions and 4 deletions
+9 -4
View File
@@ -813,25 +813,30 @@ function Parent() {
换一个例子就可以看得更清楚:
```js
function Parent() {
function Parent(props) {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const drag = useDraggable(count, step); // 封装了拖拽函数
const drag = useDraggable(props.dom, count, step); // 封装了拖拽函数
useEffect(() => {
// dom 变化时重新实例化
drag()
}, [drag])
}
```
假设我们使用 [Sortablejs](https://github.com/SortableJS/Sortable) 对某个区域进行拖拽监听,这个函数每次都重复执行的性能损耗非常大,**然而这个函数内部可能因为仅仅要上报一些日志,所以依赖了没有实际被使用的 `count` `step` 变量:**
```js
function useDraggable(count, step) {
function useDraggable(dom, count, step) {
return useCallback(() => {
// 上报日志
report(count, step);
// 对区域进行初始化,非常耗时
// ... 省略耗时代码
}, [count, step]);
}, [dom, count, step]);
}
```
@@ -0,0 +1,293 @@
## 1 概述
本期精读的是有限状态机管理工具 [robot](https://github.com/matthewp/robot) 源码。
有限状态机是指有限个数的状态之间相互切换的数学模型,在业务与游戏开发中有限状态都很常见,包括发请求也是一种有限状态机的模型。
笔者将在简介中介绍这个库的使用方式,在精读中介绍实现原理,最后总结在业务中使用的价值。
## 2 简介
这个库的核心就是利用 `createMachine` 创建一个有限状态机:
```typescript
import { createMachine, state, transition } from 'robot3';
const machine = createMachine({
inactive: state(
transition('toggle', 'active')
),
active: state(
transition('toggle', 'inactive')
)
});
export default machine;
```
如上图所示,我们创建了一个有限状态机 `machine`,包含了两种状态:`inactive``active`,并且可以通过 `toggle` 动作在两种状态间做切换。
与 React 结合则有 [react-robot](https://github.com/matthewp/react-robot):
```tsx
import { useMachine } from 'react-robot';
import React from 'react';
import machine from './machine'
function App() {
const [current, send] = useMachine(machine);
return (
<button type="button" onClick={() => send('toggle')}>
State: {current.name}
</button>
)
}
```
通过 `useMachine` 拿到的 `current.name` 表示当前状态值,`send` 用来发送改变状态的指令。
至于为什么要用有限状态机管理工具,官方文档举了个例子 - 点击编辑后进入编辑态,点击保存后返回原始状态的例子:
![](https://img.alicdn.com/tfs/TB16AvLhAL0gK0jSZFAXXcA9pXa-998-96.png)
点击 Edit 按钮后,将进入下图的状态,点击 Save 后如果输入的内容校验通过保存后再回到初始状态:
![](https://img.alicdn.com/tfs/TB1LeYLhpP7gK0jSZFjXXc5aXXa-1013-97.png)
如果不用有限状态机,我们首先会创建两个变量存储是否处于编辑态,以及当前输入文本是什么:
```js
let editMode = false;
let title = '';
```
如果再考虑和后端的交互,就会增加三个状态 - 保存中、校验、保存是否成功:
```js
let editMode = false;
let title = '';
let saving = false;
let validating = false;
let saveHadError = false;
```
就算使用 React、Vue 等框架数据驱动 UI,我们还是免不了对复杂状态进行管理。如果使用有限状态机实现,将是这样的:
```js
import { createMachine, guard, immediate, invoke, state, transition, reduce } from 'robot3';
const machine = createMachine({
preview: state(
transition('edit', 'editMode',
// Save the current title as oldTitle so we can reset later.
reduce(ctx => ({ ...ctx, oldTitle: ctx.title }))
)
),
editMode: state(
transition('input', 'editMode',
reduce((ctx, ev) => ({ ...ctx, title: ev.target.value }))
),
transition('cancel', 'cancel'),
transition('save', 'validate')
),
cancel: state(
immediate('preview',
// Reset the title back to oldTitle
reduce(ctx => ({ ...ctx, title: ctx.oldTitle })
)
),
validate: state(
// Check if the title is valid. If so go
// to the save state, otherwise go back to editMode
immediate('save', guard(titleIsValid)),
immediate('editMode')
)
save: invoke(saveTitle,
transition('done', 'preview'),
transition('error', 'error')
),
error: state(
// Should we provide a retry or...?
)
});
```
其中 `immediate` 表示直接跳到下一个状态,`reduce` 则可以对状态机内部数据进行拓展。比如 `preview` 返回了 `oldTitle`,那么 `cancle` 时就可以通过 `ctx.oldTitle` 拿到;`invoke` 表示调用第一个函数后,再执行 `state`
通过上面的代码我们可以看到使用状态机的好处:
1. 状态清晰,先罗列出某个业务逻辑的全部状态,避免遗漏。
2. 状态转换安全。比如 `preview` 只能切换到 `edit` 状态,这样就算在错误的状态发错指令也不会产生异常情况。
## 3 精读
[robot](https://github.com/matthewp/robot) 重要的函数有 `createMachine, state, transition, immediate`,下面一一拆解说明。
### createMachine
[createMachine](https://github.com/matthewp/robot/blob/master/machine.js#L122) 表示创建状态机:
```js
export function createMachine(current, states, contextFn = empty) {
if(typeof current !== 'string') {
contextFn = states || empty;
states = current;
current = Object.keys(states)[0];
}
if(d._create) d._create(current, states);
return create(machine, {
context: valueEnumerable(contextFn),
current: valueEnumerable(current),
states: valueEnumerable(states)
});
}
```
可以看到,如果传递了一个对象,通过 `Object.keys(states)[0]` 拿到第一个状态作为当前状态(标记在 `current`),最终将保存三个属性:
- `context` 当前状态机内部属性,初始化是空的。
- `current` 当前状态。
- `states` 所有状态,也就是 `createMachine` 传递的第一个参数。
再看 `create` 函数:
```js
let create = (a, b) => Object.freeze(Object.create(a, b));
```
也就是创建了一个不修改的对象作为状态机。
这个是 `machine` 对象:
```js
let machine = {
get state() {
return {
name: this.current,
value: this.states[this.current]
};
}
};
```
也就是说,状态机内部的状态管理是通过对象完成的,并提供了 `state()` 函数拿到当前的状态名和状态值。
### state
[state](https://github.com/matthewp/robot/blob/master/machine.js#L70) 用来描述状态支持哪些转换:
```js
export function state(...args) {
let transitions = filter(transitionType, args);
let immediates = filter(immediateType, args);
let desc = {
final: valueEnumerable(args.length === 0),
transitions: valueEnumerable(transitionsToMap(transitions))
};
if(immediates.length) {
desc.immediates = valueEnumerable(immediates);
desc.enter = valueEnumerable(enterImmediate);
}
return create(stateType, desc);
}
```
`transitions``immediates` 表示从 `args` 里拿到 `transition``immediate` 的结果。
方法是通过如下方式定义 `transition``immediate`:
```js
export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);
function filter(Type, arr) {
return arr.filter(value => Type.isPrototypeOf(value));
}
```
**那么如果一个函数是通过 `immediate` 创建的,就可以通过 `immediateType.isPrototypeOf()` 的校验,此方法适用范围很广,在任何库里都可以用来校验拿到对应函数创建的对象。**
如果参数数量为 0,表示这个状态是最终态,无法进行转换。**最后通过 `create` 创建一个对象,这个对象就是状态的值**。
### transition
[transition](https://github.com/matthewp/robot/blob/master/machine.js#L53) 是写在 `state` 中描述当前状态可以如何变换的函数,其实际函数是 `makeTransistion`:
```js
function makeTransition(from, to, ...args) {
let guards = stack(filter(guardType, args).map(t => t.fn), truthy, callBoth);
let reducers = stack(filter(reduceType, args).map(t => t.fn), identity, callForward);
return create(this, {
from: valueEnumerable(from),
to: valueEnumerable(to),
guards: valueEnumerable(guards),
reducers: valueEnumerable(reducers)
});
}
```
由于:
```js
export let transition = makeTransition.bind(transitionType);
export let immediate = makeTransition.bind(immediateType, null);
```
可见 `from``null` 即表示立即转换到状态 `to``transition` 最终返回一个对象,其中 `guards` 是从 `transition``immediate` 参数中找到的,由 `guards` 函数创建的对象,当这个对象回调函数执行成功时此状态才生效。
`...args` 对应 `transition('toggle', 'active')``immediate('save', guard(titleIsValid))`,而 `stack(filter(guardType, args).map(t => t.fn), truthy, callBoth)` 这句话就是从 `...args` 中寻找是否有 `guards``reducers` 同理。
最后看看状态是如何改变的,设置状态改变的函数是 [transitionTo](https://github.com/matthewp/robot/blob/master/machine.js#L136):
```js
function transitionTo(service, fromEvent, candidates) {
let { machine, context } = service;
for(let { to, guards, reducers } of candidates) {
if(guards(context)) {
service.context = reducers.call(service, context, fromEvent);
let original = machine.original || machine;
let newMachine = create(original, {
current: valueEnumerable(to),
original: { value: original }
});
let state = newMachine.state.value;
return state.enter(newMachine, service, fromEvent);
}
}
}
```
可以看到,如果存在 `guards`,则需要在 `guards` 执行返回成功时才可以正确改变状态。同时 `reducers` 可以修改 `context` 也在 `service.context = reducers.call(service, context, fromEvent);` 这一行体现了出来。最后通过生成一个新的状态机,并将 `current` 标记为 `to`
最后我们看 `state.enter` 这个函数,这个函数在 [state](https://github.com/matthewp/robot/blob/master/machine.js#L79) 函数中有定义,其本质是继承了 `stateType`:
```js
let stateType = { enter: identity };
```
`identity` 这个函数就是立即执行函数:
```js
let identity = a => a;
```
因此相当于返回了新的状态机。
## 4 总结
有限状态机相比普通业务描述,其实是增加了一些状态间转化的约束来达到优化状态管理的目的,并且状态描述也会更规范一些,在业务中具有一定的实用性。
当然并不是所有业务都适用有限状态机,因为新框架还是有一些学习成本要考虑。最后通过源码的学习,我们又了解到一些新的框架级小技巧,可以灵活应用到自己的框架中。
> 讨论地址是:[精读《robot 源码 - 有限状态机》 · Issue #209 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/209)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,486 @@
## 1 引言
在写这次精读之前,我想谈谈前端精读可以为读者带来哪些价值,以及如何评判这些价值。
前端精读已经写到第 123 篇了,大家已经不必担心它突然停止更新,因为我已养成每周写一篇文章的习惯,而读者也养成了每周看一篇的习惯。所以我想说的其实是一种更有生命力的自媒体运作方式,定期更新。一个定期更新的专栏比一个不不定期更新的专栏更有活力,也更受读者喜爱,因为读者能看到文章之间的联系,跟随作者一起成长。个人学习也是如此,养成定期学习的习惯,比在培训班突击几个月更有用,学会在生活中规律的学习,甚至好过读几年名牌大学。
前端精读想带给读者的不仅是一篇篇具体的内容和知识,知识是无穷无尽的,几万篇文章也说不完,但前端精读一直沿用了“引言-概述-精读-总结”这套学习模式,无论是前端任何领域的问题,还是对人生和世界的思考都可以套用,希望能为读者提供一套学习思维框架,让你能学习到如何找到好的文章,以及如何解读它。
至今已经选择了许多源码解读的题材,与培训思维的源码解读不同,我希望你不要带着面试的目的学习源码,因为这样会让你只局限在 react、vue 这种热门的框架上。前端精读选取的框架类型之所以广泛,是希望你能静下心来,吸取不同框架风格与作者的优势,培养一种优雅编码的气质。
进入正题,这次选择的文章 [《用 Babel 创造自定义 JS 语法》](https://lihautan.com/creating-custom-javascript-syntax-with-babel/) 也是培养编码气质的一类文章,虽然对你实际工作用处不大,但这篇文章可以培养几个程序员梦寐以求的能力:深入理解 Babel、深入理解框架拓展机制。理解一个复杂系统或培养框架思维不是一朝一夕的,但持续阅读这种文章可以让你越来越接近掌握它。
之所以选择 Babel,是因为 Babel 处理的一直是语法树相关的底层逻辑,编译原理是程序世界的基座之一,拥有很大的学习价值。所以我们的目的并不是像文章标题说的 - 创造一个自定义 JS 语法,因为你创造的语法只会让 JS 复杂体系更加混乱,但可以让你理解 Babel 解析标准 JS 语法的原理,以及看待新语法提案时,拥有从实现层面思考的能力。
最后,不必多说,能重温 Babel 经典的插件机制,你可以发现 Babel 的插件拓展机制和 Antrl4 很像,在设计业务模块拓展方案时也可以作为参考。
## 2 概述
我们要利用 Babel 实现 `function @@` 的新语法,用 `@@` 装饰的函数会自动柯里化:
```js
// '@@' makes the function `foo` curried
function @@ foo(a, b, c) {
return a + b + c;
}
console.log(foo(1, 2)(3)); // 6
```
可以看到,`function @@ foo` 描述的函数 `foo` 支持 `foo(1, 2)(3)` 这种柯里化调用。
实现方式分为两步:
1. Fork babel 源码。
2. 创建一个 babel 转换器插件。
不要畏惧这些步骤,“如果你读完了这篇文章,你将成为同事眼中的 Babel 大神” - 原文。
首先 Fork babel 源码到本地,执行下面的命令可以初始化并编译 babel:
```bash
$ make bootstrap
$ make build
```
babel 使用 [Makefile](https://opensource.com/article/18/8/what-how-makefile) 执行编译命令,并且采用 monorepo 管理,我们这次要关心的是 `package/babel-parser` 这个模块。
### 词法
首先要了解词法知识,更详细的可以阅读原文或精读之前的一篇系列文章:[精读《词法分析》](https://github.com/dt-fe/weekly/blob/v2/064.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md)。
要解析语法,首先要进行词法分析。任何语法输入都是一个字符串,比如 `function @@ foo(a, b, c)`,词法分析就是要将这个长度为 24 的字符拆分为一个个有语义的单词片段:`function` `@@` `foo` `(` `a` ..
由于 `@@` 是我们创造的语法,所以我们第一个任务就是让 babel 词法分析可以识别它。
下面是 `package/babel-parser` 的文件结构:
```text
- src/
- tokenizer/
- parser/
- plugins/
- jsx/
- typescript/
- flow/
- ...
- test/
```
可以看到,分为词法分析 `tokenizer`,语法分析 `parser`,以及支持一些特殊语法的插件,以及测试用例 `test`
推荐使用 **Test-driven development (TDD) - 测试驱动开发的方式**,就是先写测试用例,再根据测试用例开发。这种开发方式在后端或者 babel 这种底层框架很常见,因为 TDD 方式开发的逻辑能保证测试用例 100% 覆盖,同时先看测试用例也是个很好的切面编程思维。
```js
// packages/babel-parser/test/curry-function.js
import { parse } from '../lib';
function getParser(code) {
return () => parse(code, { sourceType: 'module' });
}
describe('curry function syntax', function() {
it('should parse', function() {
expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();
});
});
```
可以利用 jest 直接测试这段代码:
```bash
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/c
```
结果会出现如下报错:
```text
SyntaxError: Unexpected token (1:9)
at Parser.raise (packages/babel-parser/src/parser/location.js:39:63)
at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16)
at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18)
at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23)
at Parser.parseIdentifier (packages/babel-pars
```
第 9 个字符就是 `@`,说明程序现在还不支持函数前面的 `@` 解析。我们还可以在错误堆栈中找到报错位置,并把当前 Token 与下一个 Token 打印出来:
```js
// packages/babel-parser/src/parser/expression.js
parseIdentifierName(pos: number, liberal?: boolean): string {
if (this.match(tt.name)) {
// ...
} else {
console.log(this.state.type); // current token
console.log(this.lookahead().type); // next token
throw this.unexpected();
}
}
```
`this.state.type` 代表当前 Token`this.lookahead().type` 表示下一个 Token。`lookahead` 是词法分析的专有词,表示向后查看。打印之后,我们会发现输出了两个 `@` Token:
```js
TokenType {
label: '@',
// ...
}
```
下一步,我们需要让 babel 词法分析识别 `@@` 这个 Token。首先需要注册这个 Token
```js
// packages/babel-parser/src/tokenizer/types.js
export const types: { [name: string]: TokenType } = {
// ...
at: new TokenType('@'),
atat: new TokenType('@@'),
};
```
注册了之后,我们要在遍历 Token 时增加判断 “如果当前字符是 `@` 且下一个字符也是 `@`,则整体构成了 `@@` Token 并且光标向后移动两格”:
```js
// packages/babel-parser/src/tokenizer/index.js
getTokenFromCode(code: number): void {
switch (code) {
// ...
case charCodes.atSign:
// if the next character is a `@`
if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {
// create `tt.atat` instead
this.finishOp(tt.atat, 2);
} else {
this.finishOp(tt.at, 1);
}
return;
// ...
}
}
```
再次运行测试文件,输出变成了:
```js
// current token
TokenType {
label: '@@',
// ...
}
// next token
TokenType {
label: 'name',
// ...
}
```
到这一步,已经能正确解析 `@@` Token 了。
## 语法
词法已经可以将 `@@` 解析为 `atat` Token,下一步我们就要利用这个 Token,让生成的 AST 结构中包含柯里化函数的信息,并利用 babel 插件在解析时实现柯里化功能。
首先我们可以在 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 看到 AST 解析的结构,我们拿 generator 函数测试,因为这个函数结构与柯里化函数类似:
![](https://img.alicdn.com/tfs/TB1H4HvioT1gK0jSZFrXXcNCXXa-1180-442.png)
可以看到,babel 通过 `generator` `async` 属性来标识函数是否为 generator 或者 async 函数。同理,增加一个 `curry` 属性就可以实现第一步了:
![](https://img.alicdn.com/tfs/TB1c8jviXP7gK0jSZFjXXc5aXXa-1180-464.png)
要实现如上效果,只需在词法分析 `parser/statement` 文件的 `parseFunction` 处新增 `atat` 解析即可:
```js
// packages/babel-parser/src/parser/statement.js
export default class StatementParser extends ExpressionParser {
// ...
parseFunction<T: N.NormalFunction>(
node: T,
statement?: number = FUNC_NO_FLAGS,
isAsync?: boolean = false
): T {
// ...
node.generator = this.eat(tt.star);
node.curry = this.eat(tt.atat);
}
}
```
`eat` 是吃掉的意思,实际上可以理解为吞掉这个 Token,这样做有两个效果:1. 为函数添加了 `curry` 属性 2. 吞掉了 `@@` 标识,保证所有 Token 都被识别是 AST 解析正确的必要条件。
关于递归下降语法分析的更多知识,可以参考 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md),或者阅读原文。
我们再次执行测试函数,发现测试通过了,一切都在预料中。
## babel 插件
现在我们得到了标记了 `curry` 的 AST,那么最后需要一个 babel 解析插件,实现柯里化。
首先我们通过修改 babel 源码的方式实现的效果,是可以转化为自定义 babel parser 插件的:
```js
// babel-plugin-transformation-curry-function.js
import customParser from './custom-parser';
export default function ourBabelPlugin() {
return {
parserOverride(code, opts) {
return customParser.parse(code, opts);
},
};
}
```
这样就可以实现修改 babel 源码一样的效果,这也是做框架常用的插件机制。
其次我们要理解如何实现柯里化。柯里化可以通过柯里函数包装后实现:
```js
function currying(fn) {
const numParamsRequired = fn.length;
function curryFactory(params) {
return function (...args) {
const newParams = params.concat(args);
if (newParams.length >= numParamsRequired) {
return fn(...newParams);
}
return curryFactory(newParams);
}
}
return curryFactory([]);
}
// from
function @@ foo(a, b, c) {
return a + b + c;
}
// to
const foo = currying(function foo(a, b, c) {
return a + b + c;
})
```
柯里化函数通过构造参数数量相关的递归,当参数传入不足时返回一个新函数,并持久化之前传入的参数,最后当参数齐全后一次性调用函数。
我们需要做的是,将 `@@ foo` 解析为 `currying()` 函数包裹后的新函数。
下面就是我们熟悉的 babel 插件部分了:
```js
// babel-plugin-transformation-curry-function.js
export default function ourBabelPlugin() {
return {
// ...
visitor: {
FunctionDeclaration(path) {
if (path.get('curry').node) {
// const foo = curry(function () { ... });
path.node.curry = false;
path.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(path.get('id.name').node),
t.callExpression(t.identifier('currying'), [
t.toExpression(path.node),
])
),
])
);
}
},
},
};
}
```
`FunctionDeclaration` 就是 AST 的 visit 钩子,这个钩子在执行到函数时被触发,我们通过 `path.get('curry')` 拿到 **柯里化函数**,并利用 `replaceWith` 将这个函数构造为一个被 `currying` 函数包裹的新函数。
剩下最后一个问题:`currying` 函数源码放在哪里。
第一种方式,创建类似 `babel-plugin-transformation-curry-function` 这样的插件,在 babel 解析时将 `currying` 函数注册到全局,这是全局思维的方案。
第二种是模块化解决方案,创建一个自定义的 `@babel/helpers`,注册一个 `currying` 标识:
```js
// packages/babel-helpers/src/helpers.js
helpers.currying = helper("7.6.0")`
export default function currying(fn) {
const numParamsRequired = fn.length;
function curryFactory(params) {
return function (...args) {
const newParams = params.concat(args);
if (newParams.length >= numParamsRequired) {
return fn(...newParams);
}
return curryFactory(newParams);
}
}
return curryFactory([]);
}
`;
```
在 visit 函数使用 `addHelper` 方式拿到 `currying`
```js
path.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(path.get('id.name').node),
t.callExpression(this.addHelper("currying"), [
t.toExpression(path.node),
])
),
])
);
```
这样在 babel 转换后,就会自动 import helper,并引用 helper 中导出的 `currying`
最后原文末尾留下了一些延伸阅读内容,感兴趣的同学可以 [点击到原文](https://lihautan.com/creating-custom-javascript-syntax-with-babel/)。
## 3 精读
读完这篇文章,相信你不仅对 babel 插件有了更深刻的认识,而且还掌握了如何为 js 添加新语法这种黑魔法。
我来帮你从 babel 这篇文章总结一些编程模型和知识点,借助 babel 创造自定义语法的实例,加深对它们的理解。
### TDD
Test-driven development 即测试驱动的开发模式。
从文章的例子可以看出,创造一个新语法,可以先在测试用例先写上这个语法,通过执行测试命令通过报错堆栈一步步解决问题。这种方式开发可以让测试覆盖率更高,目的更专注,更容易保障代码质量。
### 联想编程
联想编程不属于任何编程模型,但从简介的思路来看,作者把 “为 babel 创建一个新 js 语法” 看作一种探案式探索过程,通过错误堆栈和代码阅读,一步一步通过合理联想实现最终目的。
在 AST 那一节,还借助了 [Babel AST explorer](https://lihautan.com/babel-ast-explorer/#?eyJiYWJlbFNldHRpbmdzIjp7InZlcnNpb24iOiI3LjYuMCJ9LCJ0cmVlU2V0dGluZ3MiOnsiaGlkZUVtcHR5Ijp0cnVlLCJoaWRlTG9jYXRpb24iOnRydWUsImhpZGVUeXBlIjp0cnVlfSwiY29kZSI6ImZ1bmN0aW9uICogZm9vKCkge30ifQ==) 工具查看 AST 结构,通过联想到 generator 函数找到类似的 AST 结构,并找到拓展 AST 的突破口。
随着解决问题的不同,联想方式也不同,如果能够举一反三,对不同场景都能合理的联想,才算是具备了技术专家的软素质。
### 词法、语法分析
词法、语法分析属于编译原理的知识,理解词法拆分、递归下降,可以帮助你技术走的更深。
不论是 Babel 插件的使用、还是 Babel 增加自定义 JS 语法,都要具备基本编译原理知识。编译原理知识还能帮助你开发在线编辑器,做智能语法提示等等。
### 插件机制
如下是 babel 自定义 parser 的插件拓展方式:
```js
export default function ourBabelPlugin() {
return {
parserOverride(code, opts) {
return customParser.parse(code, opts);
},
};
}
```
这只是插件拓展的一种,有申明式,也有命令式;有用 JS 书写的,也有用 JSON 书写的。babel 选择了通过对象方式拓展,是比较适合对 AST 结构统一处理的。
做框架首先要确定接口规范,比如 parser,先按照接口规范实现一套官方解析,对接时按照接口进行对接,就可以自然而然被用户自定义插件替代了。
可以参考的文章: [精读《插件化思维》](https://github.com/dt-fe/weekly/blob/v2/053.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8F%92%E4%BB%B6%E5%8C%96%E6%80%9D%E7%BB%B4%E3%80%8B.md)
### 柯里化
柯里化是面试经常考察的一个知识点,我们能学到的有两点:理解递归、理解如何将函数变成柯里化。
这里再拓展一下,我们还可以想到 JS 尾递归优化。如何快速写一个支持尾递归的函数?
```js
const fn = tailCallOptimize(() => {
if ( /* xxx */ ) {
fn()
}
})
```
通过封装 `tailCallOptimize` 函数,可以很方便的构造一个支持尾递归的函数,这个函数可以这么写:
```js
export function tailCallOptimize<T>(f: T): T {
let value: any;
let active = false;
const accumulated: any[] = [];
return function accumulator(this: any) {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = (f as any).apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
```
感兴趣的读者可以在评论里解释一下这个函数的原理。
### AST visit
遍历 AST 树常采用的方案是做一个遍历器 visitor,所以在遍历过程中进行拓展常采用 babel 这种方式:
```js
return {
// ...
visitor: {
FunctionDeclaration(path) {
if (path.get('curry').node) {
// const foo = curry(function () { ... });
path.node.curry = false;
path.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(path.get('id.name').node),
t.callExpression(t.identifier('currying'), [
t.toExpression(path.node),
])
),
])
);
}
},
},
};
```
`visitor` 下每一个 key 名都是遍历过程中的拓展点,比如上面的例子,我们可以对函数定义位置进行拓展和改写。
### 内置函数注册
babel 提供了两种内置函数注册方式,一种类似 polyfill,在全局注册 window 级的变量,另一种是模块化的方式。
除此之外,可以学习的是 babel 通过 `this.addHelper("currying")` 这种插件拓展方式,在编译后会自动从 helper 引入对应的模块,前提是 `@babel/helper` 需要注册 `currying` 这个 helper。
babel 将编译过程隐藏了起来,通过一些高度封装的函数调用,以较为语义化方式书写插件,这样写出来的代码也容易理解。
## 4 总结
《用 Babel 创造自定义 JS 语法》这篇文章虽然说的是 babel 相关知识,但可以从中提取到许多通用知识,这就是现在还去理解 babel 的原因。
从某个功能点为切面,走一遍框架的完整流程是一种高效的进阶学习方式,如果你也有看到类似这样的文章,欢迎推荐出来。
> 讨论地址是:[精读《用 Babel 创造自定义 JS 语法》 · Issue #210 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/210)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,372 @@
## 1 引言
Flex 与 Grid 相比就像功能键盘和触摸屏。触摸屏的控制力相比功能键盘来说就像是降维打击,因为功能键盘只能上下左右控制(x、y 轴),而触摸屏打破了布局障碍,直接从(z 轴)触达,这样 **无论 UI 内部布局再复杂,都可以通过 touch 直接定位。**
Flex 是一维布局方式,我们需要不断嵌套 Div 才能形成复杂结构,而一旦布局产生了变化,原有嵌套结构如果不能 “兼容变化” 到新结构,代码就需要重构。而 Grid 就像触摸屏一样,可以二维布局,即便布局方式做了翻天覆地的调整,也仅需少量修改就能适配。
这就是这次精读 [用 css grid 重新思考布局](https://www.freecodecamp.org/news/css-grid-changes-how-we-can-think-about-structuring-our-content/) 的原因,理解这个革命性布局技术给布局,甚至代码逻辑组织带来的变化。
## 2 概述
作者首先抛出了 Flex 的问题,其实是 `block` `float` `flex` 这三种布局模式的通病:
- 布局结构由 Div 层级结构描述,导致 Div 层级复杂且遇到结构变更时难以维护。
- 定制能力弱。Flex 布局有一些不受控制的智能设定,比如宽度 50% 的子元素会被同级元素挤到 50% 以下,这种智能化在某些场景是需要的,但由于没有提供像 Grid 的 `minmax` 之类的 API,所以定制型不足。
![](https://img.alicdn.com/tfs/TB1X8Wvi4D1gK0jSZFyXXciOVXa-608-324.png)
举个例子,上图的结构用 Flex 描述可能是这样的:
```html
<div class="card">
<div class="profile-sidebar">
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
<ul class="social-list">
<li>
<a href="#" class="social-link"
><i class="fab fa-dribbble-square"></i
></a>
</li>
<li>
<a href="#" class="social-link"
><i class="fab fa-facebook-square"></i
></a>
</li>
<li>
<a href="#" class="social-link"
><i class="fab fa-twitter-square"></i
></a>
</li>
</ul>
</div>
<div class="profile-body">
<h2 class="profile-name">Ramsey Harper</h2>
<p class="profile-position">Graphic Designer</p>
<p class="profile-info">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
dignissimos odit accusantium repellat quidem, sit molestias dolorum
placeat quas debitis ipsum esse rerum?
</p>
</div>
</div>
```
利用 HTML 嵌套结构,我们将图形纵向分成两大块,然后在每块内部继续嵌套划分布局,这是最经典的布局行为了。
![](https://img.alicdn.com/tfs/TB17_Oqi2b2gK0jSZK9XXaEgFXa-608-324.jpg)
样式文件里,我们需要对每层布局进行描述,同时支持多分辨率弹性布局,包括顶层 `card` 容器在内的一些样式需要做一定调整:
```scss
.card {
width: 80%;
margin: 0 auto;
display: flex;
flex-direction: column;
max-width: 600px;
background: #005e9b;
flex-basis: 250px;
color: white;
padding: 2em;
text-align: center;
}
.profile-info {
font-weight: 300;
opacity: 0.7;
}
.profile-sidebar {
margin-right: 2em;
text-align: center;
}
.profile-name {
letter-spacing: 1px;
font-size: 2rem;
margin: 0.75em 0 0;
line-height: 1;
}
.profile-name::after {
content: "";
display: block;
width: 2em;
height: 1px;
background: #5bcbf0;
margin: 0.5em auto 0.65em;
opacity: 0.25;
}
.profile-position {
text-transform: uppercase;
font-size: 0.875rem;
letter-spacing: 3px;
margin: 0 0 2em;
line-height: 1;
color: #5bcbf0;
}
.profile-img {
max-width: 100%;
border-radius: 50%;
border: 2px solid white;
}
.social-list {
list-style: none;
justify-content: space-evenly;
display: flex;
min-width: 125px;
max-width: 175px;
margin: 0 auto;
padding: 0;
}
.social-link {
color: #5bcbf0;
opacity: 0.5;
}
.social-link:hover,
.social-link:focus {
opacity: 1;
}
.bio {
padding: 2em;
display: flex;
flex-direction: column;
justify-content: center;
}
@media (min-width: 450px) {
.bio {
text-align: left;
max-width: 350px;
}
}
.bio-title {
color: #0090d1;
font-size: 1.25rem;
letter-spacing: 1px;
text-transform: uppercase;
line-height: 1;
margin: 0;
}
.bio-body {
color: #555;
}
.profile {
display: flex;
align-items: flex-start;
}
@media (min-width: 450px) {
.card {
flex-direction: row;
text-align: left;
}
.profile-name::after {
margin-left: 0;
}
}
```
让我们看看 Grid 是怎么做的吧!Grid 有许多 API,我们重点看 `grid-template-areas` 这个属性,利用它,我们可以不关心模块的 HTML 结构,直接平铺方式描述:
```html
<div class="card">
<img src="https://i.pravatar.cc/125?image=3" alt="" class="profile-img" />
<ul class="social-list">
<li>
<a href="#" class="social-link"><i class="fab fa-dribbble-square"></i></a>
</li>
<li>
<a href="#" class="social-link"><i class="fab fa-facebook-square"></i></a>
</li>
<li>
<a href="#" class="social-link"><i class="fab fa-twitter-square"></i></a>
</li>
</ul>
<h2 class="profile-name">Ramsey Harper</h2>
<p class="profile-position">Graphic Designer</p>
<p class="profile-info">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere a tempore,
dignissimos odit accusantium repellat quidem, sit molestias dolorum placeat
quas debitis ipsum esse rerum?
</p>
</div>
```
可以看到,使用 Grid 可以将 UI 结构与 HTML 结构分离,HTML 结构仅描述包含关系,我们只需在样式文件中描述具体 UI 结构。
样式文件只截取 Grid 相关部分:
```scss
.card {
width: 80%;
margin: 0 auto;
display: flex;
flex-direction: column;
max-width: 600px;
background: #005e9b;
flex-basis: 250px;
color: white;
padding: 2em;
text-align: left;
display: grid;
grid-template-columns: 1fr 3fr;
grid-column-gap: 2em;
grid-template-areas:
"image name"
"image position"
"social description";
}
.profile-name {
grid-area: name;
}
.profile-position {
grid-area: position;
}
.profile-info {
grid-area: description;
}
.profile-img {
grid-area: image;
}
.social-list {
grid-area: social;
}
```
可以看到,`grid-template-areas` 是进一步抽象的语法,将页面结构通过直观的文本描述,无论是理解还是修改都更为轻松。
这种描述方式适配不同分辨率下也具有优势,只要重组 `grid-template-areas` 即可:
```scss
@media (min-width: 600px) {
.card {
text-align: left;
grid-template-columns: 1fr 3fr;
grid-template-areas:
"image name"
"image position"
"social description";
}
}
```
归根结底,Grid 通过二维结构描述,将子元素布局控制收到了父级,使布局描述更加直观。
最后作者也提到,Flex 依然有使用场景,即简单的一维结构,或者 `space-between` 等 Flex 独有语法的情况。因此推荐整体、复杂的二维布局采用 Grid,一维的简单布局采用 Flex。
## 3 精读
Grid 的布局思路给了我很多启发,HTML 结构与 UI 结构的分离有助于减少 DIV 的层级结构,使代码看上去更清晰。
也许有人会疑惑,Grid 无非将 HTML 布局部分功能挪到了 CSS,整体复杂度应该不变。其实,从 `grid-template-areas` 这个 API 可以看到,Grid 不仅仅将布局功能抽到 CSS 中,更是将布局描述进行了一层抽象,使代码更易维护。
### 抽象,再抽象
为什么 Grid 可以对布局进行抽象?因为 Grid 将二维结构都掌握在手中,得到了更大的布局能力,才能进一步将结构化语法抽象为字符串的描述。
抽象的好处是不言而喻的,你觉得一堆嵌套的 DIV 与下面的代码,哪个更易读呢?
```scss
.card {
grid-template-areas:
"image name"
"image position"
"social description";
}
```
这就是抽象的好处,一般来说,代码抽象程度越高就越易读,越易维护。
再看一个 Chrome Grid 插件,将 Grid 可视化显示出来,并可以以 UI 方式进行调整:
![](https://img.alicdn.com/tfs/TB1cAmui2b2gK0jSZK9XXaEgFXa-640-400.jpg)
UI 是对文本的再抽象,同时可以规避一些不可能存在的语法,比如:
```scss
.card {
grid-template-areas:
"image name"
"image position"
"social image";
}
```
布局只能以凸多边形方式拓展,不可能分离,也不可能突然插入一个其他模块而变成凹多边形。因此 UI 可以将这个错误规避,并简化为横竖多条线的方式对 UI 进行划分,显然这种描述方式效率更高。
不得不说,Grid 以及图形化插件的探索,是布局领域的一大进步,是不断抽象的尝试,要解决的问题只有一个:如何提供一种更直观的描述 UI 的方式。
### 布局对模块化的影响
Grid 将布局方式提高了一个维度,会直接影响到 JS 模块化方式。
尤其是以 JSX 组织代码的情况下,一个模块等于 UI + JS,通过嵌套方式的布局会让我们更倾向于站在 UI 视角划分模块。
![](https://img.alicdn.com/tfs/TB1WQCvi.Y1gK0jSZFMXXaWcVXa-1052-750.png)
比如对于上图模块,如果用 Flex 方式布局,我们可能会首先创建模块 X 作为左侧容器,子元素是 A 和 B,创建模块 Y 作为右侧容器,子元素是 C 以及新容器 Z,Z 容器的子元素是 D 和 E。
如果你的第一印象是这么组织代码,不得不承认模块化会受到布局方式的影响。虽然许多时候这样划分是正确的,但当这 5 个模块各自没有关联时,我们创建的容器 X、Y、Z 就失去了复用性,在新的组合场景我们又要重新组合一遍。
但是在 Grid 语法中,我们不需要 X、Y、Z,只需要用 [css grid generator](https://cssgrid-generator.netlify.com/) 按照上图的方式拖拖拽拽即可自动生成如下布局代码:
```scss
.parent {
display: grid;
grid-template-columns: 3fr repeat(2, 1fr);
grid-template-rows: repeat(5, 1fr);
grid-column-gap: 0px;
grid-row-gap: 0px;
}
.div1 {
grid-area: 1 / 1 / 3 / 2;
}
.div2 {
grid-area: 3 / 1 / 6 / 2;
}
.div3 {
grid-area: 1 / 2 / 2 / 4;
}
.div4 {
grid-area: 2 / 2 / 6 / 3;
}
.div5 {
grid-area: 2 / 3 / 6 / 4;
}
```
其实 `grid-template-columns` `grid-template-rows` 组合起来使用比 `grid-template-areas` 更强大,但是纯代码方式描述没有 `grid-template-areas` 直观,可是配合一些可视化系统就非常直观了:
![](https://img.alicdn.com/tfs/TB1E.9AiYj1gK0jSZFuXXcrHpXa-2006-1470.png)
将 A ~ E 这 5 个模块布局抽出来后,它们之间的关系就打平了,我们可以完全从逻辑视角审视如何做模块化了。
## 4 总结
CSS Grid 本质上是一种二维布局的语法,相比 [Block](https://www.w3schools.com/Css/css_inline-block.asp)、[Flex](https://www.w3schools.com/Css/css3_flexbox.asp) 等一维布局方案,多了一个维度可以同时从行与列角度定义布局,因此派生出 `grid-template-areas` 等语法,整体上更内聚更直观,抽象度也更高了。
理解了这些也就理解了布局未来的发展方向,**让布局与 Dom 分离** 一直是前端的一个梦想,开发 UI 部分时,只需关心页面由哪些模块组成,去实现这些模块就行了,而不需要关心模块之间应该如何组合。在描述组合时,可以通过可视化或比较抽象的字符串描述布局的结构,并对应到写好的模块上,这样的代码维护性远高于用 DIV 描述结构的方案。
> 讨论地址是:[精读《用 css grid 重新思考布局》 · Issue #211 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/211)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,136 @@
## 1 引言
函数式语言在深度学习领域应用很广泛,因为函数式与深度学习模型的契合度很高,[The Beauty of Functional Languages in Deep Learning — Clojure and Haskell](https://www.welcometothejungle.co/fr/articles/btc-deep-learning-clojure-haskell) 就很好的诠释了这个道理。
通过这篇文章可以加深我们对深度学习与函数式编程的理解。
## 2 概述与精读
深度学习是机器学习中基于人工神经网络模型的一个分支,通过模拟多层神经元的自编码神经网络,将特征逐步抽象化,这需要多维度、大数据量的输入。[TensorFlow](https://www.tensorflow.org/) 和 [PyTorch](https://pytorch.org/) 是比较著名的 Python 深度学习框架,同样 [Keras](https://blog.rstudio.com/2017/09/05/keras-for-r/) 在 R 语言中也很著名。然而在生产环境中,基于 **性能和安全性** 的考虑,一般会使用函数式语言 [Clojure](https://www.clojure.org/) 或 [Haskell](https://www.haskell.org/)。
在生产环境中,可能要并发出里几百万个参数,因此面临的挑战是:如何高效、安全的执行这些运算。
**所以为什么函数式编程语言可以胜任深度学习的计算要求呢?** 深度学习的计算模型本质上是数学模型,而数学模型本质上和函数式编程思路是一致的:数据不可变且函数间可以任意组合。这意味着使用函数式编程语言可以更好的表达深度学习的计算过程,因此更容易理解与维护,同时函数式语言内置的 Immutable 数据结构也保障了并发的安全性。
另外函数式语言的函数之间都是相互隔离的,即便在多线程环境下也不会发生竞争和死锁的情况,函数式编程语言会自动处理这些情况。
比如说 [Clojure](https://www.clojure.org/),**它甚至可在两个同时修改同一引用的程序并发运行时,自动重试其中之一,而不需要手动加锁**:
```clojure
(import (java.util.concurrent Executors))
(defn test-stm [nitems nthreads niters]
(let [refs (map ref (repeat nitems 0))
pool (Executors/newFixedThreadPool nthreads)
tasks (map (fn [t]
(fn []
(dotimes [n niters]
(dosync
(doseq [r refs]
(alter r + 1 t))))))
(range nthreads))]
(doseq [future (.invokeAll pool tasks)]
(.get future))
(.shutdown pool)
(map deref refs)))
(test-stm 10 10 10000) -> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)
```
上面的代码创建了引用(refs),同时创建了多个线程自增这个引用对象,按理说每个线程都修改这个引用会导致竞争状态出现,但从结果来看是正常的,说明 Clojure 引擎在执行时会自动解决这个问题。实际上当两个线程出现竞争而失败时,Clojure 会自动重试其中之一。
> [原文介绍](https://clojure.org/about/concurrent_programming)
**Clojure 的另一个优势是并行效率高:**
```clojure
(defn calculate-pixels-2 []
(let [n (* *width* *height*)
work (partition (/ n 16) (range 0 n))
result (pmap (fn [x]
(doall (map
(fn [p]
(let [row (rem p *width*) col (int (/ p *height*))]
(get-color (process-pixel (/ row (double *width*)) (/ col (double *height*))))))
x)))
work)]
(doall (apply concat result))))
```
使用 `partition` 结合 `pmap` 可以使并发效率达到最大化,也就是 CPU 几乎都消耗在实际计算上,而不是并行的任务管理与上下文切换。Clojure 凭借 `partition` 对计算进行分区,采取分而治之并对分区计算结果进行合并的思路优化了并发性能。
> [原文介绍](http://www.fatvat.co.uk/2009/05/jvisualvm-and-clojure.html)
Clojure 另一个特性是函数链式调用:
```clojure
;; pipe arg to function
(-> "x" f1) ; "x1"
;; pipe. function chaining
(-> "x" f1 f2) ; "x12"
```
其中 `(-> "x" f1 f2)` 等价于 `f2(f1("x"))`,这种描述不仅更简洁清晰,也更接近于实际数学模型。
> [原文介绍](http://xahlee.info/clojure/clojure_function_chaining.html)
最后,Clojure 还具备计算安全性,计算过程不会修改已有的数据,因此在神经网络的任何一层的原始值都会保留,每层计算都可以独立运行且函数永远幂等。
[Haskell](https://www.haskell.org/) 也有独特的优势,**它具有类型推断、惰性求值等特性**,被认为更适合用于机器学习。
类型推断即 Haskell 类型都是静态的,如果试图赋予错误的类型会报错。
Haskell 的另一个优势是可以非常清晰的描述数学模型。
想想一般数学模型是怎么描述函数的:
```text
fn =>
f1 = 1
f2 = 9
f3 = 16
n > 2, fn = 3fn-3 + 2fn-2 + fn-1
```
一般语言用 `if-else` 描述等价关系,但 Haskell 可以几乎原汁原味的还原函数定义过程:
```haskell
solve :: Int -> Interger
solve 1 = 1
solve 2 = 9
solve 3 = 16
solve n = 3 * solve (n - 3) + 2 * solve (n - 2) + solve (n - 1)
```
这使得阅读 Haskell 代码和阅读数学公式一样轻松。
> [原文](https://blog.jle.im/entry/purely-functional-typed-models-1.html)
Haskell 另一个优势是惰性求值,即计算会在真正用到时才进行,而不会在计算前提前消费掉,比如:
```haskell
let x = [1..]
let y = [2,4 ..]
head (tail tail( (zip x y)))
```
可以看到,`x``y` 分别是 `1,2,3,4,5,6...``2,4,6,8...` 的无限数组,而 `zip` 函数将其整合为一个新数组 `(1,2),(2,4),(3,6),(4,8)...` 这也是无限数组,如果将 `zip` 函数执行完那么程序就会永远执行下去。但 Haskell 却不会陷入死循环,而是直接输出第一位数字 `1`。这就是惰性计算的特性,无论数组有多长,只有真正用到某项时才对其进行计算,所以哪怕初始数据量或计算量很大,实际消耗的运算资源只取决于这次计算实际用到的部分。
由于深度学习数据量巨大,惰性求值可以忽略海量数据输入,大大提升计算性能。
## 3 总结
本文介绍了为什么深度学习更适合使用函数式语言,以及介绍了 Clojure 与 Haskell 语言的共性:安全性、高性能,以及各自独有的特性,证明了为何这两种语言更适合用在深度学习中。
在前端领域说到函数式或函数之美,大部分时候想到的是 Class Component 与 Function Component 的关系,这个理解是较为片面的。通过本文我们可以了解到,函数式的思想与数学表达式思想如出一辙,以写数学公式的思维方式写代码,就是一种较好的函数式编程思路。
函数式应该只有表达式,没有语句,这是因为函数式是为了处理运算而诞生的,因此很适合用在深度学习领域。
> 讨论地址是:[精读《深度学习 - 函数式之美》 · Issue #212 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/212)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
+309
View File
@@ -0,0 +1,309 @@
## 1 引言
[Nuxt](https://github.com/nuxt/nuxt.js) 是基于 Vue 的前端开发框架,这次我们通过 [Introduction toNuxtJS](https://www.youtube.com/watch?v=NS0io3Z75GI) 视频了解框架特色以及前端开发框架的基本要素。
> nuxt 与 [next](https://github.com/zeit/next.js) 结构很像,可以结合在一起看
视频介绍了 NuxtJs 的安装、目录结构、页面路由、导航模版、asyncData、meta、vueX。
这是一个入门级视频,所以上面所列举的特征都是一个前端开发框架的最核心的基本要素。一个前端开发框架,安装、目录结构、页面路由、导航模版一定是最要下功夫认真设计的。
asyncData 和 Vuex 都在解决数据问题,meta 则是通过约定语法控制网页 meta 属性,这部分值得与 React 体系做对比,在精读部分再展开。
Nuxtjs 前端开发框架不仅提供了脚手架的基本功能,还对项目结构、代码做了约定,以减少代码量。从这点可以看出,脚手架永远围绕两个核心目标:**让每一行源码都在描述业务逻辑;让每个项目结构都相同且易读**。
20 年前,几百行 HTML、Css、Js 代码就能完成一个完整的项目,只需要遵守 W3C 的基本规范就足够了,每一个项目代码都简单清晰,而且由于没有复杂的业务逻辑,导致代码结构也非常简单。但现在前端项目复杂度逐渐升高,一个大型项目源码数量可能达到几十万行、几百万行,这是 W3C 规范没有设想到的,因此出现了各种工程化与模块化方案解决这个复杂度问题,也引发了各个框架间约定的割裂,且设计合理程度各不相同。
Nuxtjs 等框架要做的就是定义支持现代大型项目的前端研发标准,这个规范具有网络效应,即用的人越多,价值越大。
接下来我们进入正题,看看 Nuxt 脚手架定义了怎样的开发规范。
## 2 概述
### 安装
使用 `npx create-nuxt-app app-name` 创建新项目。这个命令与 `create-react-app` 一样,区别主要是模版以及配置不同。
这个命令本质上是拉取一个模版到本地,并安装 `nuxt` 系列脚本作为项目依赖,并自动生成一系列 npmScripts
```json
{
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"test": "jest"
},
"dependencies": {
"nuxt": "^2.0.0"
}
}
```
之后即可通过 `npm start` 等命令开发项目,对大部分项目来说,npmScripts 启动是最能达成共识的。
这种安装方式另一个好处是,依赖都被安装在了本地,即开发环境 100% 内置在项目中。Nuxt 没有采用全局 cli 命令方式执行,第一是 npmScripts 更符合大家通用习惯,不需要记住不同脚手架繁琐的名称与不同约定的启动命令,第二是全局脚手架一旦进行不兼容升级,老项目就面临维护难题。
### 目录结构
```text
├── .nuxt
├── layouts
├── pages
├── store
├── assets
├── static
├── middleware
├── plugins
├── nuxt.config.js
```
**pages**
页面文件存放的目录,路径 + 文件名即路由名,关于更多约定路由的信息,在下一节页面路由详细说明。
**layouts**
模版文件存放的目录,文件名即模版名,页面可以通过定义模版在选择使用的模版。
**store**
全局数据流目录,在 vueX 章节介绍。
**assets**、**static**
分别存放不需被编译的资源文件与非 `.vue` 的静态文件,比如 scss 文件。
由于 `.vue` 文件集成了 html、js、css,因此一般不会再额外定义样式文件在 static 文件夹中。
当然,这是 Vue 生态的特别之处,在 React 生态中会存在大量 `.scss` 文件混杂在各个目录中,比较影响阅读。
**middleware**、**plugins**
中间件与插件,这两个目录是可选的,作为一种定制化拓展能力。
**.nuxt**
为实现约定路由等便捷功能,启动项目时需要自动生成一些文件作为真正项目入口,这些文件就存储在 `.nuxt` 目录下,gitingore 且无需手动修改。
**nuxt.config.js**
nuxt 使用 js 文件作为配置文件,比 json 配置文件拓展性更好一些,这个文件也是整个项目唯一的配置文件。
基本上 **pages**、**layouts**、**store**、**assets**、以及唯一的配置文件基本成为现代前端开发框架的标配。
### 页面路由
nuxt 支持约定路由:
```text
├── pages
│ ├── home.vue
│ └── index.vue
```
上述目录结构描述了两个路由:`/``/home`
也支持参数路由,只要以下划线作为前缀命名文件,就定义了一个动态参数路由:
```text
├── pages
│ ├── videos
│ │ └── _id.vue
```
`/videos/*` 都会指向这个文件,且可以通过 `$route.params.id` 拿到这个 url 参数。
另一个特性是嵌套路由:
```text
├── pages
│ ├── videos
│ │ └── index.vue
│ └── videos.vue
```
`videos.vue``videos/index.vue` 都指向 `/videos` 这个路由,如果这两个文件同时存在,那么外层的 videos 就会作为外层拦截所有 `/videos` 文件夹下的路由,可以通过 `nuxt-child` 透出子元素:
```html
# pages/videos.vue
<template>
<div>
videos
<nuxt-child />
</div>
</template>
```
### 导航模版
页面公共逻辑,比如导航条可以放在模版里,模版的目录在 `layouts` 文件夹下。
默认 `layouts/default.vue` 对所有页面生效,但也可以创建例如 `layouts/videos.vue` 特殊导航文件,在 `pages/` 页面文件通过如下申明指定使用这个模版:
```html
<script>
export default {
layout: "videos"
};
</script>
```
### asyncData
`asyncData` 是 nuxt 支持的异步取数函数,可以替代 `data`
`data` 函数:
```html
<script>
export default {
data() {
return {};
}
};
</script>
```
对于异步场景,可以用 `asyncData` 替代:
```html
<script>
export default {
async asyncData() {
return await fetch("/");
}
};
</script>
```
### meta
nuxt 允许在 `.vue` 页面文件自定义 head 标签信息:
```html
<script>
export default {
headr() {
return {
title: "",
meta: {
charset: "utf-8"
}
};
}
};
</script>
```
这是开发框架提供的特性,不过在 React 体系下可以通过 `useTitle` 等自定义 Hooks 解决此问题,将框架功能降维到代码功能,会更容易理解些。
### vueX
nuxt 集成了 [vuex](https://github.com/vuejs/vuex),在 `store/` 文件夹下创建数据模型:
```js
export const state = () => ({
videos: [],
currentVideo: {}
})
export const mutations = {
SET_VIDEOS (state, videos) {
state.videos = videos
}
SET_CURRENT_VIDEO (state, video) {
state.currentVideo = video
}
}
```
接下来就能在 `pages` 文件夹下的页面组件使用了:
```html
<script>
import { mapState } from "vuex";
export default {
async fetch({ $axios, params, store }) {
const reponse = await $axios.get(`/videos/${params.id}`);
const video = response.data.data.arrtibutes;
store.commit("SET_CURRENT_VIDEO", video);
}
};
</script>
```
`return` 替换为 `store.commit` 即可,更多语法可以参考 [vuex 文档](https://github.com/vuejs/vuex)。
## 3 精读
Nuxtjs 框架做了几件事情:
1. 统一执行命令。
2. 统一开发框架。
3. 统一目录与代码规范。
4. 内置公共 utils 函数。
### 统一执行命令
命令行是所有开发者每天都要用上十几次甚至几十次的场景,试想一下团队中项目分别有如下这么多不同的启动命令会怎么样?
1. npm start.
2. monkey dev.
3. npm run ng.
4. npm run bootstrap & banana start.
5. ...
我永远不知道下一个项目该如何启动,这大大降低了开发效率。更严重的是,有的项目可以通过 `npm run docs` 查看文档,有的项目不能;有的项目 `npm run build` 可以触发编译,有的项目却无需编译,等等,所谓的环境不一致或者说迁移成本,学习成本,都是由最开始负责搭建项目脚手架的同学对架构设计不一致导致的,**然而没有必须用 `monkey dev` 才能运行起来的项目,但项目却可能因为被设计为 `monkey dev` 启动而显得与其他项目格格不入,甚至难以统一维护。**
Nuxtjs 等前端开发框架统一执行命令就是为了解决这个问题,统一开发者习惯需要很长的时间周期,但这个趋势不可挡。
### 统一开发框架
**虽然现在 React、Vue、Angular 框架各有利弊,但如果一个团队的项目同时使用了两个以上的框架,没有人会觉得这是一件好事。**
诚然每个框架都有自己的特点,在不同维度都一些优势,但三大框架能并存,说明各自都没有绝对的杀手锏来消灭对方。
对开源来说,多元化是活力的源动力,但对一家公司来说,多元化就是一场灾难,至今没有一个框架敢说自己的优势是 “与其他框架混合使用可以提升整体开发效率”。
前端开发框架要解决的最重要问题也是这一点,无论如何只能选择一种开发框架,Nuxtjs 选择了 VueNextjs 选择了 React。
### 统一目录与代码规范
目录和代码规范不会从根本上影响项目的通用性,因为不同的目录结构可以通过映射来兼容,不同的代码规范不会影响代码执行。所以目录与代码规范真正影响的是一个程序员对项目的 “解码成本”。
所谓解码成本,就是程序员理解项目逻辑所需要的成本。如果你是一个销售主管,让团队周报统一用一种格式汇总绝对比 “用自己喜欢的方式汇总” 效率高,而对编程也一样,一个完全不同的目录结构和代码规范对程序员来说是巨大的阅读阻碍,甚至可能引发恶心反应。
所以不同的目录结构和代码规范是没有必要的壁垒,除非你的团队已经对某种规范产生达成了牢固的共识,否则最好和其他团队共享相同的目录结构与代码规范。改变代码规范是一件很难得事情,但只要不同规范的团队间产生了长期合作关系,规范统一就势必会被提上议程,那么为何不能在公司层面早一点达成共识,提前消除这种痛苦呢?
所以统一目录与代码规范是前端开发框架需要优先确定的,很多时候不要去质疑为什么目录叫 `layouts` 而不叫 `layout`,因为这个规范背后形成的协同网络规模越大,叫什么名字就越不重要。
### 内置公共 utils 函数
让业务开发更聚焦,还可以通过抽取通用的逻辑的方式解决,但需要解决两个问题:
1. 虽然将公共函数抽成 npm 包可以解决代码复用问题,但关键是怎么保证你的代码能被别人复用?
2. 如何让业务通用的 utils 代码有效沉淀并从项目中移除?
脚手架内置公共 utils 函数就为了解决这个问题。上面几个小节解决了通用命令、框架、规范,但实际代码中,`router` `history` `fetch` `store` 等等概念也都是可以统一的,**没有一个项目必须用定制的 `fetch` 函数才能取数,但一开始就定制了 `fetch` 会导致耦合了不可预期的、没有必要的业务逻辑,成为理解与提效的阻碍。**
所以统一这些能统一的包,是进一步提效的关键。也许有人会觉得断了自己造轮子的路,但就像我们如今都不会重写浏览器内核逻辑一样,稳定的逻辑不仅带来了全行业的提效,还催生了前端岗位带来大量的就业,同样的,统一底层通用函数,其实是断了无意义产出这条路,每个人都有追求更高价值事情的权利,不要把自己困在反复造 `fetch` 函数这个低水平的活里。
## 4 总结
如果一个项目没有使用类似 Nuxtjs 开发框架,它面临的不仅仅是技术选型不统一的问题,久而久之这种项目势必成为 **代码孤岛**,当尘封在代码仓库几年后,一系列文档工具链接都失效后,就成为谁也不想碰,不敢碰的高危代码。
所以我们今天不仅要看到 Nuxtjs 提供的能力对项目开发有多么便捷,更要看到这类框架带来的协同效应有多么巨大,如果它不能成为整个前端的标准,至少要成为你们公司,或者你们团队的标准。
> 讨论地址是:[精读《Nuxtjs》 · Issue #213 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/213)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)