302020.4

React 状态管理的另一个世界,mutable state 状态管理器 react-tyshemo 发布

在 react 状态管理领域,react-redux 可谓是只手遮天了,基于 flux 思想实现,小巧,immutable 的思想让数据变化可控。但 immutable 所带来的编程代价太大了,如果你要更新一个深层结构的对象的某个节点,写作将会是极其麻烦的一件事,而且还保不准会出错。为了保证 immutable,redux 的 reducer 机制让开发者掉光了头发。于是有了类似 dva、rematch 等这样的项目,这些项目基于 redux 再做了一层封装,让开发者少写了很多 reducer 相关的代码,但是很无奈,immutable 的特性,让开发需要付出更多的精力来控制每一个更新。

再另一个世界,mutable state 其实也非常优秀。知名的 mobx 推出了 mobx-react 和 react-redux 竞争。然而,原本非常优秀的 mobx 却只管把自己的想法强加于人,而忽视了代码写作的便捷性,总之,使用起来虽然不用再为 reducer 头疼,却对组件的侵入和让人很不适应。你需要了解它的概念,特别是基于观察者模式的很多概念,它提供的 api 的形式也很丰富,基于接口的、装饰器的,总之,你在掀开它的魔法盒子时,会忍不住“wo\cao/”,但当你真正在项目中尝试使用它时,确会不由但发出“wo^cao\”,但就在心智折腾上,就让人抓狂,还不知道会产生多少副作用 bug。

在 mutable 数据管理最优秀的,莫过于 vue。无论初识还是长久,都会与 vue 的响应式数据相看两不厌。它简介的用法,以及把基本原理告诉你,让你尽情去修改数据。Mutable 相对于 immutable 的最大好处,就是可以对对象任意节点上的对象进行修改,而无需仔细的把握这个节点在整个 state 的什么位置上,在 redux 的实践中,你可能都已经厌烦了写 ... 来解构对象/数组了,但在 vue 中,根本不需要担心这一个点,直接逮住一个对象,立即修改它的某个属性,完事走人,不需要先构造出一个新的数据,不需要调用某个接口把这个新数据传入进去。虽然 redux 那种每一个状态都是全新的思想很好,但是,你的状态不可能是全新的,每一个被认为是全新的状态,都包含了老状态的一部分(对象节点),而这些引用,可能带来后续的错误。在 react 生态中,你可以使用 immutable.js 来尽可能避免这个问题。

但是在 vue 生态中,用于管理全局状态的 vuex 确非得要引入 mutions, actions 的概念,这都是从 flux 借鉴过来的,而且很奇怪的是,在 mutions 中定义的修改,又要到 actions 中重做一遍。实在是有点自废武功啊。有没有一种方法,可以在 react 中真正享受 vue 式的数据管理?你不妨来试试下面的代码:

function MyComponent(props) {
  const { one } = props
  const { name, age, height, married, sex, changeSex, me, book, updateBook } = one
  return (
    <>
      {!!me && (
        <>
          <span>{me.user_name_zh}, {me.user_position}</span>
          <br />
        </>
      )}
      <span>{name}: {age}, {height}, {married ? 'married,' : ''} {sex ? 'F' : 'M'}</span>
      <br />
      <span>book: {book.price || 0}</span>
      <br />
      <button type="button" onClick={() => one.age ++}>grow</button>
      <button type="button" onClick={() => changeSex(!sex)}>change</button>
      <button type="button" onClick={() => updateBook({ price: (Math.random() * 100).toFixed(2) })}>update</button>
    </>
  )
}

const mapToProps = (contexts) => {
  const { one } = contexts
  return {
    one,
  }
}

export default connect(mapToProps)(MyComponent)

看这代码啊,和 react-redux 有点像,完全可以理解是不。

在数据开始对组件进行注入的时候,保持和 react-redux 一致的使用效果,这实在是无缝的思想过渡,你觉得从原来写 react-redux 的代码到写这样的代码,会有压力吗?没有,一点都没有。下面来看看这种全局状态管理的真面目:

import React from 'react'
import { use, connect } from 'react-tyshemo'

class Book {}

use({
  name: 'one',
  state: {
    name: 'one',
    age: 10,
    book: new Book(),
  },
  computed: {
    height() {
      return this.age * 5
    },
  },
  watch: {
    age({ value }) {
      if (value > 22) {
        this.married = true
      }
    },
  },
  methods: {
    changeSex(sex) {
      this.sex = sex
    },
    updateBook(data) {
      Object.assign(this.book, data)
      this.dispatch('book')
    },
  },
  hooks: {
    onUse() {
      fetch('/api/me').then(res => res.json()).then((json) => {
        const { data } = json
        this.me = data
      })
    },
  },
})

看完什么感受?“wo/cao/” 绝对是抄 vue!连属性名字都和 vue 组件一毛一样(多了一个 hooks)。用一个 use 函数注册一个 state 的 namespace,并且这这个注册定义对象中,传入 state, computed, methods, watch 等来实现状态数据的处理。由于对数据的操作和 vue 是一摸一样,所以,你不用担心 ajax 请求的异步问题了,不需要考虑一大堆 redux 带来的“解决问题带来的问题”。而且,react-tyshemo 只提供几个函数接口,避免 mobx-react 那般复杂。

看上去是不是很舒服呢?

马上体验

22:48:09 已有0条回复
222020.4

ESModule 和 commonjs module 混用

在一些项目中,使用 webpack 打包,难免会有 ESModule 和 commonjs 混用的情况,当然,全部用 ESModule 是最好的,符合标准。但是,在不得已或偷懒情况下必须混用时,要注意混用时的一些模块逻辑。

babel 会将 ESModule 导入进行处理,也就是说 import 的目标文件是 module.exports 导出的 commonjs 文件是可以的。但是前提是,必须先用 babel 进行编译。我们用 webpack babel-loader 的时候,为了获得 tree shaking 的效果,所以会关闭 modules 选项,这就导致在 webpack 进行打包时,babel 不会编译 ESModule,也就不会优化 import 逻辑,因此,这种情况下 import 的目标文件是 module.exports 时,运行时会报错,因为 webpack 认为 module 是不可写对象,你不能重写 module.exports,但你可以重写 exports。

这个逻辑是对的,webpack 按照标准进行了处理,并且是严格处理,这就要求模块输出者,必须输出 exports.xxx 这样的接口,从而可以通过 webpack 实现与 ESModule 完全对接。但是,很遗憾,由于历史原因,我们不可能把我们项目中全部的 module.exports 全部重新写过,而且对于 module.exports = function 的情况根本无解。

好消息是,我们也是有办法的,ESModule 和 commonjs 是可以混用的。关键就在 require 和 import 的区别。在 webpack 里面,目标文件究竟遵循 ESModule 还是 commonjs,完全由导入语句 require/import 来决定。例如,你的 a.js 是 ESModule 导出模块,但是你在外面用 require 导入,那么它可以很自然的被使用,可以说无缝对接。但是,如果你的 a.js 是 commonjs 导出模块,而外面是 import 进行导入,那就必须格外小心,module.exports 的导出方式不可以,但是 exports.xxx 的方式可以使用,当然,使用 export 导出是最优选择。

最终的结论是,不要用 import 去导入原来的 module.exports 的模块,而是要用 require,这在 webpack 中,你可以把这种操作当作日常操作,或者必须使用 require 的唯一特殊情况。

17:27:33 已有0条回复
172020.4

我来说一句反对typescript的话

世界上刚有typescript的时候,我就开始关注它了。我很明确,它是一门新语言,只是和javascript是近亲。在它的介绍文档中,我看到private关键字,甚是兴奋。但我开始去用它的时候,发现它的private的骗人的。除了private骗人外,它的所谓类型检查系统也有欺骗性。现在很多人已经叫嚣着,不懂typescript就是不会前端。说实话,除了对typescript不满意,我连对ES标准的#符也不满意。不是我挑剔,是有些东西人会天然不喜欢。

读完这个答案之后,我更加确信,原来自己的反感是有根源的,并非我有问题,而是typescript有问题。有什么问题呢?先来说一些旁的不喜欢点:

  • 繁琐的类型定义方式,明明是一门新语言,非要按照js的语法来写,但是又发明一些莫名其妙的新语法
  • 类型声明实在受不了,我知道使用 : 是受了其他语言的启发,但是真正好的类型声明很明显要放在变量前面
  • 对象属性的类型声明?呵呵,太恶心。: 和 as 能让人写作烦躁10倍。
  • any?哎。。。
  • 结构类型检查!WTF
  • 据说类型检查能减少bug,但是,你怎么能确保你编写的类型本身没有bug?
  • 降低效率,用在解决(编写)一个类型问题上的时间,可能够我写完2个需求
  • 使代码可读性降低,你需要在阅读代码过程中,跳到类型定义的部分去阅读,阅读完再跳回来来,一去一来,我是谁,在哪里?
  • 增加运行难度,据说deno支持直接运行ts,但是实际上,还是先翻译为js后执行

我们来看上面那篇回答中的一个经典案例:

interface A {
    x: number;
}

let a: A = { x: 3 }
let b: { x: number | string } = a;

b.x = "unsound";

let x: number = a.x;

a.x.toFixed(0);

恶心死你。你不是静态类型检查吗?给你查,查破了你能告诉我 bug 来自哪里?人生啊,不要相信所谓了“在准确和效率之间找平衡”,忽悠。别的类型系统,之所以成立,是因为别的语言需要先编译,后执行,有健全类型系统,类型声明不用慌,而且类型本身就是运行时的。从我的不成熟的想法来看,不在运行时的类型系统,都是扯虎皮。使用typescript嘛,和语句末尾使用使用;结束一样,技术不到位,有;也避免不了各种错,技术到家,没;照样优雅健壮。

我理想中的js变量类型声明:

int a = 1
bigint b = 299300002390809238n
float c = 2.2
bigfloat d = 4.394085943789534809830543l
string e = 'xxx'
Date date = new Date()
Promise p = new Promise(...)

// 多层结构的对象
object o = {
  string name: 'my name',
  int age: 10,
  // 用<>表达其内部结构
  array<[
    object<{
      string name,
      number price,
    }>
  ]> books: [
    // 纯值,当然,也可以在内部进行类型定义,类型系统自己推演
    {
      name: 'book name',
      price: 6.6,
    },
  ],
}

let x = 1 // 相当于 any
const y = 'ok' // 不可变
var z = null // 带变量提升的 let

如果一个变量想要发生类型转化。。。不可以的。

int a = 10
string b = a + ''

这样操作是唯一的途径吧。

但是实际上,在运行时,你怎么知道后端接口会返回给你什么?大部分情况下,null 值无法避免。

// 用 type 关键字声明和定义类型
type ResponseType = object<{
  int code,
  object data: {
    string name,
    number|null age, // 支持 null
    array<[
      // array 内部可以有多种结构,只要元素满足其中之一,就可以通过类型检查
      object<{
        string name,
        number price,
      }>,
      object<{
        string name,
        int pages,
      }>,
    ]> books,
  },
  ?string error, // ? 开头表示可能不存在,存在的情况下才遵循类型
}>

现在我要使用它:

fetch('/api').then(res => res.json()).then(string (ResponseType data) => {
  // 如果data的类型检查不通过,直接抛出 TypeError
})

看看函数:

function int a(int x, int y) {
  return x + y
}

object o = {
  int a(int x, int y) {
    return x + y
  },
}

class Some {
  string _name = 'my name'

  // 类型声明永远放在变量名前面
  static get string name() {
    return this._name
  }
}
// 直接对函数进行参数和返回值类型声明
// <参数类型列表: 返回值类型> 这种表达式借鉴于 python lambda
// 如果没有参数类型列表,如function<int>,表示没有参数,返回值为 int
function<int, int: int> a(x, y) {
  return x + y
}

再搞一个泛型耍耍。我在这篇文章中已经说过了,泛型,实际上就是函数

// 用 <> => 定义泛型,其中 book<a, b> 中的 book 是泛型的名字,<a, b> 是替代符,=> 后面是返回的形式化结果
type book<a, b> => object<{
  a name,
  b price,
}>

怎么用?这样子啊:

book<string, number> book = {
  name: 'xxx',
  price: 12,
}

看看泛型的演化:

// 定义一个泛型,泛型的结果是一个 object 描述,(而非一个类型)
type a<x, y> => {
  x a,
  y b,
}

type b<z> => a<z, z> // 直接运行 a<z, z> 将得到的结果作为 b<> 泛型结果

// b<string> 的结果,作为 object<> 的参数
object<b<string>> o = {
  a: 'xxx',
  b: 'yyy',
}

实际上,我不大推崇这种声明,因为通过这样的声明,我并不能马上看清楚在声明 book 这个变量时,它的结构,我更喜欢:

object<{
  string name,
  number price,
}> book

即使这个时候,我并不给 book 赋值,这样声明 book 这个变量我就知道它的内部结构是啥。虽然这样我可能需要写很多重复代码,但是相对来说,理解成本更低。

当然,在函数上,泛型确实还是有好处:

function int|string a(int|string x, int|string y) {
  return x + y
}

这种声明明显不好,因为你怎么知道 x=1, y='a' 这种情况不会发生。所以,这种情况,要使用泛型啊:

// 通过泛型,实现函数参数和返回值统一类型
type f<T> => function<T, T: T>
f<int|string> a(x, y) {
  return x + y
}

// 或者通过一个匿名的泛型来简化写作
function<int|string as T => T, T: T> a(x, y) {
  return x + y
}

来详细解释一下这个语法

function<...> // 函数类型,<> 中是对函数类型的描述,上面说过函数的描述格式为 params:result
<类型 as 泛型> // as 关键字表示,将“类型”传入“泛型”作为参数,运行泛型得到结果
T => T, T: T // 一个匿名的泛型,如果泛型有多个参数,可以表达为 a, b => a, b: a|b
int|string as T => T, T: T // 将 int|string 作为参数 T 运行泛型 T => T, T: T,当 T 为 int 时,结果为 int, int: int,当 T 为 string 时,结果为 string, string: string
function<int|string as T => T, T: T> // 运行结果是 function<int, int: int> 或者 function<string, string: string>

其中 | 操作符,会带来一些逻辑上的混淆,需要明确:

function<int|string, string|number as a, b => a, b: a|b>
// 相当于
function<(int, number) | (string, number) | (string, string) | (int, string) as a, b => a, b: a|b>
// 但是我可能只想要
function<(int, number) | (string, number) as a, b => a, b: a|b>

到底应该是将 | 至于多个参数列表之间,还是置于参数位置本身?很显然,置于参数位置本身,还是会产生歧义,所以要有置于参数列表之间的 | 语法,需要通过 () 分组的逻辑。

这一套东西,是在运行时做的,这样的定义,岂不是爽歪歪。当然,我依靠 tyshemo 还是有可能把这套东西给实现的。对了,有兴趣可以看下 C# 的类型系统。

22:21:56 已有2条回复
  1. 不敢苟同,ts 的认知吾辈经历了反复的几次变化之后,只能说:真香!
    #921 rxliuli 2020-04-18 14:11 回复
  2. whh
    #922 bigsir 2020-04-22 16:11 回复
142020.4

重新理解 rxjs 的事件流

在做上一期 robust 的时候,受到这篇文章的启示,重新理解了 rxjs 的事件流这个概念。结合响应式编程范式的两个目标,对 rxjs 的理解又有了新的认识。

rxjs 是观察者模式、迭代器模式、函数式编程的结合体。其中,我一直对迭代器模式这个点不是很理解。在阅读上面那篇文章之后,我的思维豁然开朗了。在此之前,我只能强迫性的理解,new Observable 本质上是在决定什么时候进行迭代动作,也就是调用 next。但是,迭代器本身是怎么来的呢?我刚开始默认认为,迭代器需要用户自己构造。但在读完上面那篇文章之后,有了新的见解。rxjs 将一个对象的某个事件(例如 DOM 节点的 click 事件),在时间上的发生,抽象为一个虚拟的数组,它的示意图:

这个虚拟数组,和普通数组不一样,它不是在一开始就确定的,而是随着时间的流逝,不断往数组中新加元素,知道该对象被销毁,该数组也自动被销毁。而往数组中新增元素的过程,就是 next 被调用的时候。你可以想象成,这是一个无限个数坑位的列表,一个 click 发生,一个坑位就被填了,这个填坑位的过程,就是 next。而这个虚拟的数组,就是事件流的抽象表示。

既然是一个数组,那么就可以调用迭代器方法,rxjs 内置了非常多迭代器方法,比如常见的 map, filter, reduce。而 new Observable 本质上就是在构造这个迭代器数组。叫 Observable 很傻,还是叫 Stream 比较直观。

10:06:45 已有0条回复
072020.4

纯库

随着时间的流逝,越来越对很多看似花里胡哨的库感到失望,其中以 react 和基于 react/vue 所做起来的库为最。对 vue 报以热烈的喜爱,在于,它的开箱即用性和易用性。虽然是一个框架,但你完全可以把它当作一个库来使用。这就是我认为的纯库。所谓纯库,就是 cdn 加载进来,立即可用的库,不需要复杂的构建环境,不需要上下游的 babel 插件,只需要一个 <script src> 就可以使用其功能的库,而这样的库,以 jquery 为最,以 bootstrap 为最,这才是 web 开发的本质,简洁的声明式编程。react 给 web 开发开了一个坏头,但这和 react 自己没有太多关系,而是类 react 的 web 框架,以及趴在 react 上的 web 组件。react 不是最好的 react 库。但要知道,react 的设计理念,是要抽象视图层为 virtual dom,从而可以在跨平台上发挥作用,因此,它有自己的一套事件系统,如果不是为了跨平台,没必要自己做事件系统,而那些只管用 react 做 web 开发的开发者,实际上,和 flutter 做 web 应用一样憋足。而 vue 一个 cdn 挂载进来,把各类插件用 Vue.use 加载进来,马上就可以用。这就像 jquery 一样,插件加载进来就可以用,真正做到的 web 开发的便捷性。失去便捷性的 web 开发,应该交给另外一门语言去做,比如 ts,甚至 go,总之,便捷性是我认为 web 开发的真谛。为了工程的稳定和可持续发展,引入庞大的构建体系,这没有错,但是如果随便一个东西,都一定要走这么庞大的构建体系,那么真的是背道而驰。

19:49:17 已有2条回复
  1. 是的,现代前端的工具链依赖真的是太重了,所以吾辈选择 vue-cli/cra 这种脚手架工具绕过去
    #919 rxliuli 2020-04-07 20:22 回复
  2. 同是前端,多交流呀
    #920 回复给#919 否子戈 2020-04-10 13:15 回复
052020.4

有生之年,很难想象,那个曾经在社区仰望的技术大神,就这么草草的离开了人世。回顾司徒正美一生,最主要的成就或许在于亮点:1.推出 avalon 框架,并且基于该框架思想后续发展出来的 anu 等框架或 ui 库;2. 在 DOM 编程上所展现的黑魔法技巧,以及传道授业所写下的大量技术文章。

avalon 是在 virtual dom 出现之前,能够兼容到 ie6 的 mvvm 响应式 js 框架。当然,要兼容一些特定浏览器(包含低版本 css),还需要加载一些额外的 js 文件,但是,它自己会自动加载。它在用法上很像 angularjs,但是采用了 Object.defineProperty 来实现响应式。在 react, vue 出现之前,avalon 是一个令人感到神奇的框架。但是随着 virtual dom 的统治,他也在 avalon2 中采用这项技术。虽然 avalon 早期版本令人惊叹,但后续紧跟潮流,也不免让人有些唏嘘,脱离了 DOM,他的想象力也被当红的技术所框定。

36 岁去世,未婚。假如,假如时间允许,他或许还会出一本新书,或者在框架上突发奇想提供一个新的思路。但是,总归是没有假如。这世界来过,也留下了些东西,等待年月渐渐将这些痕迹抹去。再过几年,便不会再有人记得这个名字,随之而去的,还有 avalon 框架。

在感叹年轻的开发者们,要注意自己的身体,要用于对 996 说不的同时,作为档案专业毕业的学生,我一直对“档案是人类社会的记忆”这一点耿耿于怀。为什么我们不能把这个人给我们带来的都留住?再过一段时间,他的博客域名过期了,他的开源项目停更了,那他就像未曾来过这个世界一样,一切烟消云散。有没有一种方法,让人类的思想瑰宝可以永世长存?

21:51:23 已有0条回复