Compare commits

...
4 Commits
Author SHA1 Message Date
ascoders 80fb60c6fb 160 2020-07-27 09:37:27 +08:00
ascoders 6a2031899d 159 2020-07-20 09:55:34 +08:00
ascoders aedcc2fd56 158 2020-07-13 10:26:30 +08:00
ascoders 757eb403ac update 2020-07-06 09:50:38 +08:00
5 changed files with 1137 additions and 14 deletions
@@ -0,0 +1,233 @@
## 1 引言
Object 类型的比较是非常重要的基础知识,通过 [How to Compare Objects in JavaScript](https://dmitripavlutin.com/how-to-compare-objects-in-javascript/) 这篇文章,我们可以学到四种对比方法:引用对比、手动对比、浅对比、深对比。
## 2 简介
### 引用对比
下面三种对比方式用于 Object,皆在引用相同是才返回 `true`
- `===`
- `==`
- `Object.is()`
```js
const hero1 = {
name: "Batman",
};
const hero2 = {
name: "Batman",
};
hero1 === hero1; // => true
hero1 === hero2; // => false
hero1 == hero1; // => true
hero1 == hero2; // => false
Object.is(hero1, hero1); // => true
Object.is(hero1, hero2); // => false
```
### 手动对比
写一个自定义函数,按照对象内容做自定义对比也是一种方案:
```js
function isHeroEqual(object1, object2) {
return object1.name === object2.name;
}
const hero1 = {
name: "Batman",
};
const hero2 = {
name: "Batman",
};
const hero3 = {
name: "Joker",
};
isHeroEqual(hero1, hero2); // => true
isHeroEqual(hero1, hero3); // => false
```
如果要对比的对象 key 不多,或者在特殊业务场景需要时,这种手动对比方法其实还是蛮实用的。
但这种方案不够自动化,所以才有了浅对比。
### 浅对比
浅对比函数写法有很多,不过其效果都是标准的,下面给出了一种写法:
```js
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
```
可以看到,浅对比就是将对象每个属性进行引用对比,算是一种性能上的平衡,尤其在 redux 下有特殊的意义。
下面给出了使用例子:
```js
const hero1 = {
name: "Batman",
realName: "Bruce Wayne",
};
const hero2 = {
name: "Batman",
realName: "Bruce Wayne",
};
const hero3 = {
name: "Joker",
};
shallowEqual(hero1, hero2); // => true
shallowEqual(hero1, hero3); // => false
```
如果对象层级再多一层,浅对比就无效了,此时需要使用深对比。
### 深对比
深对比就是递归对比对象所有简单对象值,遇到复杂对象就逐个 key 进行对比,以此类推。
下面是一种实现方式:
```js
function deepEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
const val1 = object1[key];
const val2 = object2[key];
const areObjects = isObject(val1) && isObject(val2);
if (
(areObjects && !deepEqual(val1, val2)) ||
(!areObjects && val1 !== val2)
) {
return false;
}
}
return true;
}
function isObject(object) {
return object != null && typeof object === "object";
}
```
可以看到,只要遇到 Object 类型的 key,就会递归调用一次 `deepEqual` 进行比较,否则对于简单类型直接使用 `!==` 引用对比。
值得注意的是,数组类型也满足 `typeof object === "object"` 的条件,且 `Object.keys` 可以作用于数组,且 `object[key]` 也可作用于数组,因此数组和对象都可以采用相同方式处理。
有了深对比,再也不用担心复杂对象的比较了:
```js
const hero1 = {
name: "Batman",
address: {
city: "Gotham",
},
};
const hero2 = {
name: "Batman",
address: {
city: "Gotham",
},
};
deepEqual(hero1, hero2); // => true
```
但深对比会造成性能损耗,不要小看递归的作用,在对象树复杂时,深对比甚至会导致严重的性能问题。
## 3 精读
### 常见的引用对比
引用对比是最常用的,一般在做 props 比较时,只允许使用引用对比:
```js
this.props.style !== nextProps.style;
```
如果看到有深对比的地方,一般就要有所警觉,这里是真的需要深对比吗?是不是其他地方写法有问题导致的。
比如在某处看到这样的代码:
```js
deepEqual(this.props.style, nextProps.style);
```
可能是父组件一处随意拼写导致的:
```jsx
const Parent = () => {
return <Child style={{ color: "red" }} />;
};
```
一个只解决局部问题的同学可能会采用 `deepEqual`,OK 这样也能解决问题,但一个有全局感的同学会这样解决问题:
```js
this.props.style === nextProps.style;
```
```jsx
const Parent = () => {
const style = useMemo(() => ({ color: "red" }), []);
return <Child style={style} />;
};
```
从性能上来看,`Parent` 定义的 `style` 只会执行一次且下次渲染几乎没有对比损耗(依赖为空数组),子组件引用对比性能最佳,这样的组合一定优于 `deepEqual` 的例子。
### 常见的浅对比
浅对比也在判断组件是否重渲染时很常用:
```jsx
shouldComponentUpdate(nextProps) {
return !shallowEqual(this.props, nextProps)
}
```
原因是 `this.props` 这个对象引用的变化在逻辑上是无需关心的,因为应用只会使用到 `this.props[key]` 这一层级,再考虑到 React 组件生态下,Immutable 的上下文保证了任何对象子属性变化一定导致对象整体引用变化,可以放心的进行浅对比。
最少见的就是手动对比和深对比,如果你看到一段代码中使用了深对比,大概率这段代码可以被优化为浅对比。
## 4 总结
虽然今天总结了 4 种比较 Object 对象的方式,但在实际项目中,应该尽可能使用引用对比,其次是浅对比和手动对比,最坏的情况是使用深对比。
> 讨论地址是:[精读《如何比较 Object 对象》· Issue #258 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/258)
**如果你想参与讨论,请 [点击这里](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)
+481
View File
@@ -0,0 +1,481 @@
## 1 引言
随着 [Typescript 4 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/) 的发布,又带来了许多新功能,其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾,使得这次更新非常有意义。
## 2 简介
### 可变元组类型
考虑 `concat` 场景,接收两个数组或者元组类型,组成一个新数组:
```typescript
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
```
如果要定义 `concat` 的类型,以往我们会通过枚举的方式,先枚举第一个参数数组中的每一项:
```typescript
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
```
再枚举第二个参数中每一项,如果要完成所有枚举,仅考虑数组长度为 6 的情况,就要定义 36 次重载,代码几乎不可维护:
```typescript
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(
arr1: [A1, B1, C1],
arr2: [A2]
): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(
arr1: [A1, B1, C1, D1],
arr2: [A2]
): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(
arr1: [A1, B1, C1, D1, E1],
arr2: [A2]
): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(
arr1: [A1, B1, C1, D1, E1, F1],
arr2: [A2]
): [A1, B1, C1, D1, E1, F1, A2];
```
如果我们采用批量定义的方式,问题也不会得到解决,因为参数类型的顺序得不到保证:
```typescript
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
```
在 Typescript 4,可以在定义中对数组进行解构,通过几行代码优雅的解决可能要重载几百次的场景:
```typescript
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
```
上面例子中,`Arr` 类型告诉 TS `T``U` 是数组类型,再通过 `[...T, ...U]` 按照逻辑顺序依次拼接类型。
再比如 `tail`,返回除第一项外剩下元素:
```typescript
function tail(arg) {
const [_, ...result] = arg;
return result;
}
```
同样告诉 TS `T` 是数组类型,且 `arr: readonly [any, ...T]` 申明了 `T` 类型表示除第一项其余项的类型,TS 可自动将 `T` 类型关联到对象 `rest`
```typescript
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
// type [2, 3, 4]
const r1 = tail(myTuple);
// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
```
另外之前版本的 TS 只能将类型解构放在最后一个位置:
```typescript
type Strings = [string, string];
type Numbers = [number, number];
// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
```
如果你尝试将 `[...Strings, ...Numbers]` 这种写法,将会得到一个错误提示:
```text
A rest element must be last in a tuple type.
```
但在 Typescript 4 版本支持了这种语法:
```typescript
type Strings = [string, string];
type Numbers = number[];
// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];
```
对于再复杂一些的场景,例如高阶函数 `partialCall`,支持一定程度的柯里化:
```typescript
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}
```
我们可以通过上面的特性对其进行类型定义,将函数 `f` 第一个参数类型定义为有顺序的 `[...T, ...U]`
```typescript
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...b: U) => f(...headArgs, ...b);
}
```
测试效果如下:
```typescript
const foo = (x: string, y: number, z: boolean) => {};
// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.
// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops");
// ~~~~~~
// error! Expected 4 arguments, but got 5.
// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");
// What can we do with f3 now?
f3(123, true); // works!
f3();
// error! Expected 2 arguments, but got 0.
f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'
```
值得注意的是,`const f3 = partialCall(foo, "hello");` 这段代码由于还没有执行到 `foo`,因此只匹配了第一个 `x:string` 类型,虽然后面 `y: number, z: boolean` 也是必选,但因为 `foo` 函数还未执行,此时只是参数收集阶段,因此不会报错,等到 `f3(123, true)` 执行时就会校验必选参数了,因此 `f3()` 时才会提示参数数量不正确。
### 元组标记
下面两个函数定义在功能上是一样的:
```typescript
function foo(...args: [string, number]): void {
// ...
}
function foo(arg0: string, arg1: number): void {
// ...
}
```
但还是有微妙的区别,下面的函数对每个参数都有名称标记,但上面通过解构定义的类型则没有,针对这种情况,Typescript 4 支持了元组标记:
```typescript
type Range = [start: number, end: number];
```
同时也支持与解构一起使用:
```typescript
type Foo = [first: number, second?: string, ...rest: any[]];
```
### Class 从构造函数推断成员变量类型
构造函数在类实例化时负责一些初始化工作,比如为成员变量赋值,在 Typescript 4,在构造函数里对成员变量的赋值可以直接为成员变量推导类型:
```typescript
class Square {
// Previously: implicit any!
// Now: inferred to `number`!
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
```
如果对成员变量赋值包含在条件语句中,还能识别出存在 `undefined` 的风险:
```typescript
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
// ~~~~~~~~~~~~~~~
// error! Object is possibly 'undefined'.
}
}
```
如果在其他函数中初始化,则 TS 不能自动识别,需要用 `!:` 显式申明类型:
```typescript
class Square {
// definite assignment assertion
// v
sideLength!: number;
// ^^^^^^^^
// type annotation
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}
```
### 短路赋值语法
针对以下三种短路语法提供了快捷赋值语法:
```typescript
a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b
```
### catch error unknown 类型
Typescript 4.0 之后,我们可以将 catch error 定义为 `unknown` 类型,以保证后面的代码以健壮的类型判断方式书写:
```typescript
try {
// ...
} catch (e) {
// error!
// Property 'toUpperCase' does not exist on type 'unknown'.
console.log(e.toUpperCase());
if (typeof e === "string") {
// works!
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}
```
PS:在之前的版本,`catch (e: unknown)` 会报错,提示无法为 `error` 定义 `unknown` 类型。
### 自定义 JSX 工厂
TS 4 支持了 `jsxFragmentFactory` 参数定义 Fragment 工厂函数:
```json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
```
还可以通过注释方式覆盖单文件的配置:
```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = (
<>
<div>Hello</div>
</>
);
```
以上代码编译后解析结果如下:
```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null, h("div", null, "Hello"));
```
### 其他升级
其他的升级快速介绍:
**构建速度提升**,提升了 `--incremental` + `--noEmitOnError` 场景的构建速度。
**支持 `--incremental` + `--noEmit` 参数同时生效。**
**支持 `@deprecated` 注释,** 使用此注释时,代码中会使用 ~~删除线~~ 警告调用者。
**局部 TS Server 快速启动功能,** 打开大型项目时,TS Server 要准备很久,Typescript 4 在 VSCode 编译器下做了优化,可以提前对当前打开的单文件进行部分语法响应。
**优化自动导入,** 现在 `package.json` `dependencies` 字段定义的依赖将优先作为自动导入的依据,而不再是遍历 `node_modules` 导入一些非预期的包。
除此之外,还有几个 Break Change
`lib.d.ts` 类型升级,主要是移除了 `document.origin` 定义。
覆盖父 Class 属性的 getter 或 setter 现在都会提示错误。
通过 `delete` 删除的属性必须是可选的,如果试图用 `delete` 删除一个必选的 key,则会提示错误。
## 3 精读
Typescript 4 最大亮点就是可变元组类型了,但可变元组类型也不能解决所有问题。
拿笔者的场景来说,函数 `useDesigner` 作为自定义 React Hook 与 `useSelector` 结合支持 connect redux 数据流的值,其调用方式是这样的:
```typescript
const nameSelector = (state: any) => ({
name: state.name as string,
});
const ageSelector = (state: any) => ({
age: state.age as number,
});
const App = () => {
const { name, age } = useDesigner(nameSelector, ageSelector);
};
```
`name``age` 是 Selector 注册的,内部实现方式必然是 `useSelector` + reduce,但类型定义就麻烦了,通过重载可以这么做:
```typescript
import * as React from 'react';
import { useSelector } from 'react-redux';
type Function = (...args: any) => any;
export function useDesigner();
export function useDesigner<T1 extends Function>(
t1: T1
): ReturnType<T1> ;
export function useDesigner<T1 extends Function, T2 extends Function>(
t1: T1,
t2: T2
): ReturnType<T1> & ReturnType<T2> ;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function,
T4 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner(...selectors: any[]) {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
}
```
可以看到,笔者需要将 `useDesigner` 传入的参数通过函数重载方式一一传入,上面的例子只支持到了三个参数,如果传入了第四个参数则函数定义会失效,因此业界做法一般是定义十几个重载,这样会导致函数定义非常冗长。
但参考 TS4 的例子,我们可以避免类型重载,而通过枚举的方式支持:
```typescript
type Func = (state?: any) => any;
type Arr = readonly Func[];
const useDesigner = <T extends Arr>(
...selectors: T
): ReturnType<T[0]> &
ReturnType<T[1]> &
ReturnType<T[2]> &
ReturnType<T[3]> => {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
};
```
可以看到,最大的变化是不需要写四遍重载了,但由于场景和 `concat` 不同,这个例子返回值不是简单的 `[...T, ...U]`,而是 `reduce` 的结果,所以目前还只能通过枚举的方式支持。
当然可能存在不用枚举就可以支持无限长度的入参类型解析的方案,因笔者水平有限,暂未想到更好的解法,如果你有更好的解法,欢迎告知笔者。
## 4 总结
Typescript 4 带来了更强类型语法,更智能的类型推导,更快的构建速度以及更合理的开发者工具优化,唯一的几个 Break Change 不会对项目带来实质影响,期待正式版的发布。
> 讨论地址是:[精读《Typescript 4》· Issue #259 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/259)
**如果你想参与讨论,请 [点击这里](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,115 @@
## 1 引言
在说低代码搭建之前,首先要理解什么是搭建(本文搭建指通过 Web 交互搭建一个自定义的新页面)。
**我认为搭建的本质是提效** ,而提效又分为对研发人员的提效,以及对客户的提效:
- 对研发人员的提效:相对于 Pro Code 模式,搭建的抽象程度更高,通过牺牲部分定制性换来更高效的开发方式。
- 对客户的提效:如果用户有任何搭建 Web 应用的诉求,本质上从阿里云购买服务器自建是最普适的方案,但由于专业性要求高,用户群会很窄,因此需要针对不同用户的诉求开发定制方案,本质上是通过降低通用性换取更低的上手成本,或者针对某个领域降低上手成本,比如 BI 搭建。
提效虽然被说烂了,但软件工程发展中,几乎大部分工作都能归结到在提效。比如 Vscode、Typescript 提升编码效率;React、Vue 框架提升程序研发效率;工作台、可持续集成提升协同开发效率,等等,连微软都称自己的使命是赋能全球每一人、每个组织成就不凡,很大程度上就是在说提升整个社会的生产效能。
低代码开发平台(Low-Code Development Platform)则更进一步,允许通过零代码或少量代码就可以快速创建应用。
从实践结果来看,完全零代码想要覆盖所有领域是不可能的,而 100% 全代码是可以覆盖所有领域,但研发成本太高,所以介于两者之间的低代码模式是值得尝试的,因为许多定制场景往往不需要太多高深的代码就能搞定,很多复杂逻辑可能几个简单的赋值语句、或者条件语句就可以搞定,但如果不允许写代码,其使用成本甚至比写少量代码还要高。
所以搭建本质解决的是提效问题,考虑提效就要看性价比,是使用者学习几行简单代码后,利用低代码平台效率更高,还是使用者坚持不写代码,使用繁琐的搭建交互成本更高?有人说代码学不会,但简单代码本质和搭建无异,都是对电脑指令的输入。
还有一些场景将背后复杂度转移到了其他链路,比如数据搭建场景,虽然搭建器没有低代码能力,但却能实现复杂业务逻辑,原因是这个复杂度被 SQL 层吃掉了,既然复杂度无法消除,那么哪一层实现的效率更高,就由哪一层去做才是合理的。
## 2 精读
低代码不仅仅包括 “能写代码”,主要具备如下四个特性:物料接入、编排能力、渲染能力、出码能力。
### 物料接入
通用搭建引擎要能够接入通用物料,即组件自身不关心搭建环境,就可以被搭建平台所使用。
这需要搭建平台本身不对组件代码实现有入侵,可以对组件暴露的 props 做完全控制,要做到自动识别组件有哪些 props 变量,并根据类型自动推荐编辑表单类型。
除了简单的文本、数字、下拉框等编辑器 Setter 之外,还有如下几种复杂编辑器:
- 回调函数编辑器。
- Node 节点编辑器。
- 文本国际化编辑器。
- 表达式编辑器。
回调函数编辑器与表达式编辑器都是低代码能力的体现,本质上就是利用代码描述某个变量值或者回调。
Node 节点编辑器专门处理节点类型 props 参数,比如 `props.header``propder.footer`,在代码模式描述为组件,在可视化模式需转化为画布下钻模式进行编辑。
### 编排能力
编排能力包含页面编排与逻辑编排,是低代码搭建的核心能力。
#### 页面编排
页面编排包含很多交互行为,比如拖拽组件、布局,其中布局大有可为,比如云凤蝶的编辑模式,通过自由拖拽布局,降低了使用者对 DOM 流式布局的理解成本,但通过自适应四周边距模拟出了流式布局自动撑开容器,容器间碰撞挤压的效果。
组件与组件形成的组合可以形成一个新的物料,一般称为模版,比如一个页面整体也可以称为模版,这个模版组件的 id 就是页面根节点的容器组件。但模版也有不能满足的场景,比如期望组件形成的组合拥有一套全新配置,此时就延伸出低代码业务组件的概念,可以认为将模版当作一个整体编辑,可以为模版设置任意的编辑表单,这个编辑表单的值可以透传到里面每个组件中读取。
#### 逻辑编排
逻辑编排是低代码能力的核心,在低代码引擎中,所有组件参数都可以用低代码描述,比如一个 `props.color` 可以通过颜色选择器选一个固定值,也可以转换为表达式模式写一段代码。
这段代码除了拥有普通 JS 能力外,还拥有基本状态管理的能力,即可以访问当前作用域下的状态 `this.state`,而状态作用域又被容器所分割,容器分为持有状态的容器与不持有状态的,一个持有状态容器内的子组件状态是互通的。
除了基本状态管理能力外,还拥有访问上下文能力,即调用引擎一些 API 对画布进行操作,一般都用于组件回调,在回调里调用 `this.setState` 设置状态也属于操作上下文的行为。除了上下文外,还有风格化、国际化、取数等能力可以通过 `this` 访问到,其中取数能力专门抽到引擎层做,就是为了让所有组件与取数逻辑解耦,组件只要拿到数据、isFetching,而不需要真正发送取数请求。
逻辑编排的另一个维度就是可视化,将上述低代码能力通过可视化方式表达为逻辑节点与线条,在描述与维护复杂逻辑时有一定优势。
### 渲染能力
搭建特殊之处在于,搭建过程几乎只能在 PC 端完成,但发布后的应用往往有多端渲染的诉求,比如越来越多的公司使用手机查看 BI 报表,甚至报表需要嵌入到微信、支付宝小程序中;PC 搭建的表单往往也有大量手机端填报的诉求。
所以编辑和渲染端应该是分离的,但为了保证逻辑一致性,核心代码需要复用,所以搭建引擎最好采用 UI 无关的内核 + 业务层拓展 UI 实现方式来做,UI 无关的内核只负责存储、操作画布数据,排除设计器附加的一堆 Panel 后,渲染时可以复用逻辑内核往往就足够了。
组件的跨端复用也是必须的,现在跨端渲染的技术方案也有不少。
### 出码能力
LowCode 与 ProCode 互转也是一大难题,首先互转的好处不必多说,可以自由的在提效与定制间切换,一定是最理想的开发模式,但实现起来有不少阻碍。
首先是 LowCode 转 ProCode,这个比较简单,原因是 LowCode 本身用 JSON 定义,代码是 JSON 的超集,从子集转换到超集本身没有技术障碍。
从 ProCode 转换到 LowCode 就麻烦了,一种方式是限定 ProCode 的能力,甚至用一种新的语法替代原生 JS,本质上都是通过将 ProCode 的能力范围限制住,使得 LowCode 可以接住。另一种方式是不对称转换,即从 ProCode 转换为 LowCode 后会存在功能缺失,或者即便功能不缺失,但 LowCode 无法对应的功能无法在搭建平台编辑。
### 运行时能力
只拥有上述低代码能力的搭建平台还是太通用了,虽然功能很强大,但在具体的业务场景不一定有多大的提效,具体的业务场景要有具体的解决方案,搭建本质是提效的,如果原子化、低代码的内容太多,就本末倒置,只是用另一种方式写代码罢了,并没有真正做到利用搭建提升开发效率。
通用的业务定制方式有如下三种:
- 定制业务组件:比如将某个复杂业务系统 80% 场景都要用到的组件固化为一个业务定制组件,省去了大部分配置时间,让使用者感受到提效。
- 定制业务模版和低代码业务组件:更进一步,将业务模版固化下来,本质上类似代码模版,或者利用低代码业务组件,在不开发新组件的前提下,制作一个针对某个业务场景的混合组件。
- 定制业务配置项:有些业务场景专业度很高,一方面是用户群不一样,一方面是搭建效率考虑,都应该提供一种基于业务角度出发的配置项,既符合业务思考逻辑,又节省配置步骤。
以上通用方式都是通过引擎已有的开放能力可以做到的,但对数据场景来说,有一些依赖引擎运行时能力场景,需要将引擎运行时能力抽象出来,配合低代码实现。
比如让当前页面所有配置相同数据集的组件自动建立筛选联动关联,虽然筛选联动关联可以通过低代码方式配置,但当画布组件数量变化时,或者有组件动态调用 API 新增组件时,静态的配置很难满足动态关联场景,此时我们可以拓展出一些全局运行时能力,让组件实现这些运行时能力时可以拿到画布信息,在引擎实际调用时再动态运行,而不是编辑生成一份静态 JSON 与渲染完全割裂。
运行时能力在不同平台针对不同垂直场景时会存在差异,如果希望打通底层引擎,可以提供拓展插槽,提供动态注册引擎运行时能力的机制。
## 3 总结
一个低代码搭建平台通吃一切场景是不可能的,只要有人愿意为垂直业务场景做 “量身定制”,用户就会立刻觉得搭建效率得到了提升,我们应当站在用户的角度,以用户利益最大化的方式做平台。
但搭建平台维护成本很高,每个业务场景都单独维护一套肯定不是长久之计,我们需要设计一套有弹性的低代码核心引擎,各个业务都可以基于他为自己的用户群 “量身定制” 一套专属设计器,共享搭建引擎通用的能力与协议,并自由拓展定制能力。
所以不仅渲染态是多态的,设计器也应该是多态的,其中可以被固化为标准的部分需要沉淀下来,比如物料接入规范、编排能力、出码能力、运行时能力,让各个搭建平台做到合而不同。
国内外都有非常多做的相当不错的搭建系统,但要不就太通用,具体场景提效不明显,要不就太垂直,换一个业务场景做不了。现在阿里中后台低代码搭建组织就在制定规范,将引擎通用能力固化为标准协议,让不同搭建平台可以对齐规范与功能,未来还会不断收敛核心引擎实现,基于它可以打造出千千万万个垂直领域的搭建平台,贴着业务做搭建提效,同时引擎内核与规范还能保持互通。
笔者所在阿里数据中台体验技术团队就是中后台低代码搭建组织的一员,将数据搭建领域做到极致。在技术上,我们在打通中后台搭建与数据搭建的技术方案,在产品上,我们正在逐渐统一阿里集团数据搭建平台,对外也携 QuickBI 成为国内唯一一家进入 Gartner 象限的 BI 产品,未来可期。
阿里数据中台体验技术团队正在火热招人中,如果感兴趣可以联系 ziyi.hzy@alibaba-inc.com 。
> 讨论地址是:[精读《对低代码搭建的理解》· Issue #260 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/260)
**如果你想参与讨论,请 [点击这里](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)
+308
View File
@@ -0,0 +1,308 @@
## 1 引言
函数缓存是重要概念,本质上就是用空间(缓存存储)换时间(跳过计算过程)。
对于无副作用的纯函数,在合适的场景使用函数缓存是非常必要的,让我们跟着 https://whatthefork.is/memoization 这篇文章深入理解一下函数缓存吧!
## 2 概述
假设又一个获取天气的函数 `getChanceOfRain`,每次调用都要花 100ms 计算:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
function showWeatherReport() {
let result = getChanceOfRain(); // Let the magic happen
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
```
很显然这样太浪费计算资源了,当已经计算过一次天气后,就没有必要再算一次了,我们期望的是后续调用可以直接拿上一次结果的缓存,这样可以节省大量计算。因此我们可以做一个 `memoizedGetChanceOfRain` 函数缓存计算结果:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
let isCalculated = false;
let lastResult;
// We added this function!
function memoizedGetChanceOfRain() {
if (isCalculated) {
// No need to calculate it again.
return lastResult;
}
// Gotta calculate it for the first time.
let result = getChanceOfRain();
// Remember it for the next time.
lastResult = result;
isCalculated = true;
return result;
}
function showWeatherReport() {
// Use the memoized function instead of the original function.
let result = memoizedGetChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
```
在每次调用时判断优先用缓存,如果没有缓存则调用原始函数并记录缓存。这样当我们多次调用时,除了第一次之外都会立即从缓存中返回结果:
```jsx
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
```
然而对于有参数的场景就不适用了,因为缓存并没有考虑参数:
```jsx
function showWeatherReport(city) {
let result = getChanceOfRain(city); // Pass the city
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated answer
```
由于参数可能性很多,所以有三种解决方案:
### 1. 仅缓存最后一次结果
仅缓存最后一次结果是最节省存储空间的,而且不会有计算错误,但带来的问题就是当参数变化时缓存会立即失效:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
let lastCity;
let lastResult;
function memoizedGetChanceOfRain(city) {
if (city === lastCity) {
// Notice this check!
// Same parameters, so we can reuse the last result.
return lastResult;
}
// Either we're called for the first time,
// or we're called with different parameters.
// We have to perform the calculation.
let result = getChanceOfRain(city);
// Remember both the parameters and the result.
lastCity = city;
lastResult = result;
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated result
```
在极端情况下等同于没有缓存:
```jsx
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
```
### 2. 缓存所有结果
第二种方案是缓存所有结果,使用 Map 存储缓存即可:
```jsx
// Remember the last result *for every city*.
let resultsPerCity = new Map();
function memoizedGetChanceOfRain(city) {
if (resultsPerCity.has(city)) {
// We already have a result for this city.
return resultsPerCity.get(city);
}
// We're called for the first time for this city.
let result = getChanceOfRain(city);
// Remember the result for this city.
resultsPerCity.set(city, result);
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Paris"); // (!) Triggers the calculation
```
这么做带来的弊端就是内存溢出,当可能参数过多时会导致内存无限制的上涨,最坏的情况就是触发浏览器限制或者页面崩溃。
### 3. 其他缓存策略
介于只缓存最后一项与缓存所有项之间还有这其他选择,比如 LRUleast recently used)只保留最小化最近使用的缓存,或者为了方便浏览器回收,使用 WeakMap 替代 Map。
最后提到了函数缓存的一个坑,必须是纯函数。比如下面的 CASE:
```jsx
// Inside the magical npm package
function getChanceOfRain() {
// Show the input box!
let city = prompt("Where do you live?");
// ... calculation ...
}
// Our code
function showWeatherReport() {
let result = getChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
```
`getChanceOfRain` 每次会由用户输入一些数据返回结果,导致缓存错误,原因是 “函数入参一部分由用户输入” 就是副作用,我们不能对有副作用的函数进行缓存。
这有时候也是拆分函数的意义,将一个有副作用函数的无副作用部分分解出来,这样就能局部做函数缓存了:
```jsx
// If this function only calculates things,
// we would call it "pure".
// It is safe to memoize this function.
function getChanceOfRain(city) {
// ... calculation ...
}
// This function is "impure" because
// it shows a prompt to the user.
function showWeatherReport() {
// The prompt is now here
let city = prompt("Where do you live?");
let result = getChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
```
最后,我们可以将缓存函数抽象为高阶函数:
```jsx
function memoize(fn) {
let isCalculated = false;
let lastResult;
return function memoizedFn() {
// Return the generated function!
if (isCalculated) {
return lastResult;
}
let result = fn();
lastResult = result;
isCalculated = true;
return result;
};
}
```
这样生成新的缓存函数就方便啦:
```jsx
let memoizedGetChanceOfRain = memoize(getChanceOfRain);
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);
```
`isCalculated``lastResult` 都存储在 `memoize` 函数生成的闭包内,外部无法访问。
## 3 精读
### 通用高阶函数实现函数缓存
原文的例子还是比较简单,没有考虑函数多个参数如何处理,下面我们分析一下 Lodash `memoize` 函数源码:
```jsx
function memoize(func, resolver) {
if (
typeof func != "function" ||
(resolver != null && typeof resolver != "function")
) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;
}
```
原文有提到缓存策略多种多样,而 Lodash 将缓存策略简化为 key 交给用户自己管理,看这段代码:
```jsx
key = resolver ? resolver.apply(this, args) : args[0];
```
也就是缓存的 key 默认是执行函数时第一个参数,也可以通过 `resolver` 拿到参数处理成新的缓存 key。
在执行函数时也传入了参数 `func.apply(this, args)`
最后 `cache` 也不再使用默认的 Map,而是允许用户自定义 `lodash.memoize.Cache` 自行设置,比如设置为 WeakMap:
```jsx
_.memoize.Cache = WeakMap;
```
### 什么时候不适合用缓存
以下两种情况不适合用缓存:
1. 不经常执行的函数。
2. 本身执行速度较快的函数。
对于不经常执行的函数,本身就不需要利用缓存提升执行效率,而缓存反而会长期占用内存。对于本身执行速度较快的函数,其实大部分简单计算速度都很快,使用缓存后对速度没有明显的提升,同时如果计算结果比较大,反而会占用存储资源。
对于引用的变化尤其重要,比如如下例子:
```jsx
function addName(obj, name){
return {
...obj,
name:
}
}
```
`obj` 添加一个 key,本身执行速度是非常快的,但添加缓存后会带来两个坏处:
1. 如果 `obj` 非常大,会在闭包存储完整 `obj` 结构,内存占用加倍。
2. 如果 `obj` 通过 mutable 方式修改了,则普通缓存函数还会返回原先结果(因为对象引用没有变),造成错误。
如果要强行进行对象深对比,虽然会避免出现边界问题,但性能反而会大幅下降。
## 4 总结
函数缓存非常有用,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存,仅限于计算耗时、可能重复利用多次,且是纯函数的。
> 讨论地址是:[精读《函数缓存》· Issue #261 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/261)
**如果你想参与讨论,请 [点击这里](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)
-14
View File
@@ -11,17 +11,3 @@
## 关注前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
## Special Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://e.coding.net/?utm_source=weekly" target="_blank">
<img width="300" src="https://img.alicdn.com/tfs/TB107D.QbrpK1RjSZTEXXcWAVXa-1000-332.png">
</a>
</td>
</tr>
</tbody>
</table>