摘要:致力于为应用提供一个类型安全表达力强可组合的状态管理方案。是一组的命名空间。是内置组件的镜像,但允许组件的额外接受类型的数据。这次内容更新,是由组件处理的。这些小的组件不必知道所有的应用状态数据。这是因为大部分对的研究来自于。
Focal
Focal 致力于为 React 应用提供一个类型安全、表达力强、可组合的状态管理方案。
用一个不可变的 (immutable) 、响应式的 (observable) 单一数据源,来表达整个应用的 state.
将响应式对象无缝嵌入到 React 的组件中
借助 Rx.JS 的威力,来增强、组合应用的 state,来精确控制数据流
使用 lenses 将应用的 state 分解为若干个较小的部分,帮助你更整洁地解耦 ui 组件,更方便地操作 state
要编写的代码更少,更容易理解
Example我们将通过一个经典的计数器的例子,来展现 Focal 在一个完整应用中的用法。
import * as React from "react" import * as ReactDOM from "react-dom" import { Atom, // this is the special namespace with React components that accept // observable values in their props F } from "@grammarly/focal" // our counter UI component const Counter = (props: { count: AtomTutorial}) => {/* use observable state directly in JSX */} You have clicked this button {props.count} time(s). // the main "app" UI component const App = (props: { state: Atom<{ count: number }> }) =>Hello, world!// create the app state atom const state = Atom.create({ count: 0 }) // track any changes to the app"s state and log them to console state.subscribe(x => { console.log(`New app state: ${JSON.stringify(x)}`) }) // render the app ReactDOM.render(x.count) } /> , document.getElementById("app") )
在 Focal 中,state 被存储在 Atom
import { Atom } from "@grammarly/focal" // 创建一个初始值为 0 的 Atomconst count = Atom.create(0) console.log(count.get()) // => 0 // 赋值为 5 count.set(5) console.log(count.get()) // => 5 // 基于当前值进行重新赋值 count.modify(x => x + 1) console.log(count.get()) // => 6
你还可以追踪 Atom
import { Atom } from "@grammarly/focal" const count = Atom.create(0) // 订阅 count 值的变化,每次变化后就往控制台输出新值 // NOTE: 注意它将如何立即输出当前值 count.subscribe(x => { console.log(x) }) // => 0 console.log(count.get()) // => 0 // 赋值后,它会在控制台自动输出 count.set(5) // => 5 count.modify(x => x + 1) // => 6Atom 属性 Atom properties
每个 Atom 都拥有这些属性:
一旦被订阅 (.subscribed),立即触发响应,返回当前值( emit the current value)
如果新值和当前值相等,就不触发响应
单一数据源 Single source of truth在 Focal 中,我们用 Atom
理想情况下,我们期望应用的 state 都来自一个单一数据源,后面我们会讨论如何用这种新方法来管理应用的 state 数据。
我们已经了解了如何创建、修改和订阅应用的 state 数据。下面我们需要了解如何展示这种数据,从而帮助我们有效地编写 React UI。
Focal 允许你直接把 Atom
不过它们还是不太一样:
在 Focal 里描述数据操作时,你编写的就是标准的 JavaScript 或 TypeScript 代码,而不必像 Vue 那样需要借助模板引擎语法。Focal 在语法层面上没有魔法,所以你原来的语言工具栈都可以继续使用。
既然 Focal 的数据绑定本质还是原生的 TypeScript (or JavaScript) 表达式,你的 IDE 特性就不会失效,比如说自动补全、跳转定义、命名重构、用法搜索等。比起模板引擎来说,UI 层代码维护起来更简单。
你可以继续享受类似于 TypeScript 这样的静态分析工具带来的好处。因此你的 UI 代码将和其它代码一样可靠。
数据(指 Atom
通常来说,你不需要考虑组件何时被渲染,一切皆由 Focal 自动处理。
说了这么多,我们看看实际写起代码来到底怎么样:
import * as React from "react" import * as ReactDOM from "react-dom" import { F, Atom } from "@grammarly/focal" // 创建 state const count = Atom.create(0) // 定义一个 props 里带有 Atom的 stateless 组件 const Counter = (props: { count: Atom }) => {/* 直接把 state atom 嵌入到 JSX 里 */} Count: {count} ReactDOM.render(, document.getElementById("test") ) // => Count: 0
那么问题来了,这跟平常我们写 React 有什么不同呢?
F-component在 Focal 里,我们用
React 本来就允许你在 JSX 中嵌入 js 代码,但是它有诸多限制,会把表达式转为字符串或其它 React elements。
F-component 就不一样。F 是一组 lifted componenets 的命名空间。lifted component 是 React 内置组件的镜像,但允许组件的 props 额外接受 Atom
我们知道,一个 React JSX 元素中,它的子元素内容会被解析为 children prop。Focal 所做的就是支持嵌入 Atom
好了,让我们来试试修改 state 的值:
// 下面这行代码将修改 atom `count` 的当前值。 // 因为我们在 `Counter` 组件中使用了这个 atom `count`,所以修改了它的值后会使得组件更新 count.set(5) // =>Count: 5
你可能已经发现了,我们并没有修改任何的 React 组件的 state (即没有通过 Component.setState 的方式),但 Counter 还是不可思议地渲染了新内容。
实际上,从 React 的角度来说,Counter 组件的 props 和 state 都没有改变,照道理这个组件也不会被更新渲染。
这次内容更新,是由
那么根据这个原理,修改 count 的值以后,子元素
下面我们来编写稍微复杂一点的计数器组件。
// 给我们的计数器组件加点佐料 const Counter = (props: { count: Atom}) => Count: {count}. {/* 输出当前计数的奇偶性 */} That"s an {count.view(x => x % 2 === 0 ? "even" : "odd")} number! // =>Count: 5. That"s an odd number!
我们加了一行 :That"s an odd/even number!,它是由 state atom 的 view 创建的。
创建一个 view 本质上是创建了一个 atom,这个 atom 输出 state 时,可以表现为它经过修改后的值,对其修改的操作逻辑定义在 view 函数中。
这实际上和 array 或 Observable 的 map 方法差不多,主要的区别在于,和原生的 atom 一样,这种衍生 atom (被称为 view )只会在新值和当前值不相等时才响应新值。
我们再看一个例子
const Counter = (props: { count: Atom}) => `rgba(255, 0, 0, ${Math.min(16 * x, 255)})`) }} > Count: {count}. That"s an {count.view(x => x % 2 === 0 ? "even" : "odd")} number! // =>Count: 5. That"s an odd number!
在这里,我们用 state atom 来为组件创建动态的样式。如你所见,atom 配合 F-component 几乎无所不能。它能让你更简单地去用声明式的手段,来描述组件对 state 的依赖。
组合 Composition我们已经了解了如何声明式地创建基于应用状态数据的 UI 层。接下来,为了使用它们来构建规模更大更复杂,同时又不致于分崩离析的应用,我们还需要两样东西:
既然应用的状态数据都来自于一个单一数据源( 唯一的 atom ),那么当应用的不同部分彼此交互时,这些交互行为不会破坏彼此之间的同步性,同时应用的状态数据作为一个整体应始终保持一致。
Have the application state come from a single place (a single atom), so that when different parts of the application interact with each other, these interactions can"t fall out of sync with each other and the application state is consistent as a whole.
将应用的状态数据划分为若干部分,这样我们可以通过组合若干个小的组件的方式创建我们的应用层。这些小的组件不必知道所有的应用状态数据。
这两个需求可能乍看起来互相矛盾,所以就需要 lenses 登场了。
Lens让我们快速复习下 lens 的概念
(不知道 lens 的可以参考维基 Haskell/Lens)
一种对不可变数据的一部分进行读写的抽象
一组 getter 、setter 函数的组合
lens 的泛型接口可以用 TypeScript 表达为:
interface Lens{ get(source: TSource): T set(newValue: T, source: TSource): TSource }
来看一个用例
import { Lens } from "@grammarly/focal" // 后面我们会在 obj 上进行数据操作 const obj = { a: 5 } // 用 lens 来查看对象的属性 `a` const a = Lens.create( // 定义一个 getter:返回 obj 的属性 (obj: { a: number }) => obj.a, // setter: 返回一个新对象,新对象的属性 a 被更新为一个新值 (newValue, obj) => ({ ...obj, a: newValue }) ) // 通过 lens 来访问属性 console.log(a.get(obj)) // => 5 // 通过 lens 来写入一个新值 console.log(a.set(6, obj)) // => { a: 6 }
注意我们是如何通过 .set 方法返回一个新对象的:我们并没有执行修改操作,当我们想要 .set 数据的某部分时,我们通过 lens 创建了一个新对象。
这看起来好像没啥用。为什么我们不直接访问 obj.a 呢? 当我们需要返回新对象来避免修改操作时,为什么不直接 { ...obj, a: 6 } 呢?
好吧。想象你的对象结构相当复杂,比如 { a: { b: { c: 5 } } },而它甚至仅仅只是一些更大的对象的一部分:
const bigobj = { one: { a: { b: { c: 5 } } }, two: { a: { b: { c: 6 } } } }
lenses 的一大特性就是你可以组合 lenses(把它们串联起来)。假设你定义了一个 lens 用来把属性 c 从对象 { a: { b: { c: 5 } } } 解构出来,那么在 bigobj 的 one 和 two 这两个部分上,你都能复用这个 lens。
// 该 lens 用于操作对象 { a: { b: { c: 5 } } }` 里深度嵌套的属性 c const abc: Lens<...> = ... // 该 lens 用于访问 `bigobj` 的一部分: `one` const one: Lens= ... // 该 lens 用于访问 `bigobj` 的一部分: `two` const two: Lens = ... // 把 lens `one` 或 `two` 和 lens `abc` 组合起来 // 然后我们可以在结构类似为 // `{ one: { a: { b: { c: 5 } } } }` 或 `{ two: { a: { b: { c: 5 } } } }` // 的数据中操作 c const oneC = one.compose(abc) const twoC = two.compose(abc) console.log(oneC.get(bigobj)) // => 5 console.log(twoC.get(bigobj)) // => 6 console.log(oneC.set(7, bigobj)) // => { one: { a: { b: { c: 7 } } }, two: { a: { b: { c: 6 } } } }
Focal 也提供了相当方便的定义这些 lenses 的手段。
// 只需要定义一个 getter 函数就可以创建上述的 lenses¹ const abc = Lens.prop((obj: typeof bigobj.one) => obj.a.b.c) const one = Lens.prop((obj: typeof bigobj) => obj.one) const two = Lens.prop((obj: typeof bigobj) => obj.two) // ¹ 注意使用限制!(RESTRICTIONS APPLY!) // 在这个例子里,getter 函数只能是一个简单的属性路径访问函数 // 该函数仅包括一个属性访问表达式,没有副作用 (side effects)
其中最棒的一点是,这种方式是完全类型安全的,所有的 IDE 工具(比如说自动补全、命名重构等)都仍然有效。
可能比较奇怪的一点是,lens 照道理应该还可以修改该值,但我们只定义了一个 getter 函数。这确实不可思议,因为我们在这里施了点魔法。但是,这只能被视为一个实现细节,因为这些特性在将来可能在 TypeScript 编译器中就过时了。
简单解释下,我们用的方案可能类似于 WPF 里用来实现类型安全的 INotifyPropertyChanged 接口的标准实践。我们通过调用 .toString 函数,把 getter 函数转换成一个字符串,然后根据函数的源码解析出属性的访问路径。这种实现方式比较 hacky ,还有着明显的限制,不过还是很有效的。
关于 lenses 的更多资料希望上一章能让你稍微领略一下 lenses 的威力,当然你还可以用这个抽象来做更多的事情。遗憾的是我们没法在这个简短的教程里覆盖 lens 所有有趣的部分。
不幸的是,大部分关于 lenses 的文章和介绍都是用 Haskell 来描述的。这是因为大部分对 lenses 的研究来自于 Haskell。不过很多其它语言也采用了 lenses ,包括 Scala, F#, OCaml, PureScript 和 Elm 等。
Program imperatively using Haskell lenses
WikiBooks article on Haskell lenses
School of Haskell lens tutorial
lens over tea blog series
Atoms 和 lenses好,言归正传。到此为止,我们已经知道了如何管理应用状态数据,如何把状态数据嵌入到我们的 UI 层代码中。
我们还学习了如何抽象对不可变数据的操作,以便方便地对大型的不可变对象的部分进行操作。我们正是需要用它来拆分应用的状态数据。我们想要这样构造我们的应用:UI 组件的各部分仅和整个应用状态数据中和它有关的那部分交互。
为了实现这个目的,我们可以通过结合 atom 和 lens 来生成 lensed atom。
Lensed atom 也还是一个 Atom
import { Atom, Lens } from "@grammarly/focal" // 创建一个维护我们所需对象(的值)的 atom const obj = Atom.create({ a: 5 }) // 创建一个观察属性 a 的 lens const a = Lens.prop((x: typeof obj) => x.a) // 创建一个 lensed atom,这个 lensed atom 会维护对象 obj 的属性 a 的值 const lensed = obj.lens(a) console.log(obj.get()) // => { a: 5 } console.log(lensed.get()) // => 5 // 为 lensed atom 设置新值 lensed.set(6) console.log(obj.get()) // => { a: 6 }
注意,当我们为 lensed atom 设置新值的时候,源 atom 的值是如何变化的。
我们还有一种更简洁的方法来创建 lensed atom:
const lensed = obj.lens(x => x.a) // ¹ // ¹ 还是要注意使用限制 SAME RESTRICTIONS APPLY! // 和 `Lens.prop` 方法一样,atom 的 `lens` 方法接受一个 getter 函数作为参数, // 这个 getter 函数只能是一个简单的属性路径访问函数, // 它仅包括一个属性访问表达式,没有副作用。
我们无需显式地去创建 lens,atom 的 lens 方法已经提供了几个重载来帮助你立即创建 lensed atom。另外需要注意的是,我们不需要在此为对象添加类型标注,编译器已经知道了我们正在操作的数据的类型,并且为我们自动推断出来(比如在上面那个例子里,根据 obj 的类型 Atom<{ a: number }>,编译器可以自动推断出 x 的类型)
基于这种能力,现在我们可以拆分应用的单一数据源为几个小的部分,使其适用于独立的 UI 组件中。让我们来尝试把这一方案用在上述的计数器例子中:
import * as React from "react" import * as ReactDOM from "react-dom" import { Atom, F } from "@grammarly/focal" // 应用的状态数据 const state = Atom.create({ count: 0 }) // 原先写好的计数器组件 const Counter = (props: { count: Atom}) => Count: {props.count}. // app 组件,其 prop.state 携带整个应用的状态数据 const App = (props: { state: Atom<{ count: number }> }) =>Hi, here"s a counter: {/* 在此,我们拆分应用状态数据,把其中的一部分给 counter 组件使用 */}x.count)} />
我们就用这个例子作为 Focal 基础教程的总结吧。
希望你现在能理顺上面这些东西是如何结合起来的。另外,还请务必看看一些其它例子
。尝试搭建并尝试跑通它们,方便进一步了解你可以用 focal 来做什么。
Focal 不是一个框架,换句话说,它并不限制你非要用要某种特定的方式来编写整个应用。Focal 提供了命令式的接口 (回想下,你可以用 .set 或 .modify 方法来操作 atom ),并且可以完美地配合原生的 React 组件。这意味着,在同一个应用里,你可以只在某些部分使用 Focal。
性能尽管我们还没有建立一套全面的评测基准 (benchmarks),目前为止,在类似 TodoMVC 的例子中,Focal 的性能表现至少近似于原生 React。
一般来说,当一个被嵌入到 React 组件里的 Atom
这意味着,在一个复杂的 React 组件中,如果你在该树某处相当深的可见部位,有一个频繁变更的值,那么当该值变化时,只有对应的那部分会更新,而不是整个组件树都会更新。在很多场景下,这使得我们很容易为 VDOM 的重计算做优化。
商业应用略
JavaScript 支持尽管从技术上来说可以把 Focal 用于纯 Javascript 项目,但是我们还没尝试过这样做。所以,如果你在搭建这种项目时遇到了问题,欢迎前来提交 issues。
Prior artFocal 起初只是想把 Calmm 转接到 TypeScript ,但随后我们因为一些显著的差异而放弃了。
一开始,我们更专注于快速开发产品和类型安全。基于此,许多东西都被简化了,所以在当时(TypeScript 版本为 1.8 时)Focal 还很难和类型系统搭配得很好,API 也不够直观,也很难让新入门函数式编程的 React 老用户快速上手。
和 Calmm 的区别Calmm 是模块化的,由几个独立的库组成。而 Focal 没必要模块化,因为我们只有一种使用场景,所以我们只需要在一个库里维护所有东西。
Calmm 最初大量借助 Ramda 的 curry 和 Partial Application。这不利于搭配类型系统,所以我们决定放弃这种做法。不过随着 TypeScript 编译器的进步,现在要去实现上面那种做法可能变得容易多了,所以这也许会是一个有趣的话题。
Calmm 最初还借用了 Ramda 里的 lens ,这种 lens 使用的是 van Laarhoven 表示法。相反,Focal 使用的是含有一对 getter/setter 的 naїve 表示法。由于我们无需去做遍历或多态更新 (traversals or polymorphic updates),所以这对我们来说足够了。不过有可能我们会在以后重新考虑这个问题。
Calmm 的主要实现 (kefir.atom 和 kefir.react.html) 都基于 Kefir 的 observables。一开始我们也用 Kefir,不过很快迁移为 RxJS 5.x。最主要的原因是,RxJS 功能更丰富,它有一些 Kefir 还不支持的对 observables 的操作。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/84957.html
摘要:要求通过要求数据变更函数使用装饰或放在函数中,目的就是让状态的变更根据可预测性单向数据流。同一份数据需要响应到多个视图,且被多个视图进行变更需要维护全局状态,并在他们变动时响应到视图数据流变得复杂,组件本身已经无法驾驭。今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案。 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? ...
摘要:避免脆弱的基类问题。红牌警告没有提到上述任何问题。单向数据流意味着模型是单一的事实来源。单向数据流是确定性的,而双向绑定可能导致更难以遵循和理解的副作用。原文地址 1. 你能说出两种对 JavaScript 应用开发者而言的编程范式吗? 希望听到: 2. 什么是函数编程? 希望听到: 3. 类继承和原型继承的不同? 希望听到 4. 函数式编程和面向对象编程的优缺点? ...
showImg(https://segmentfault.com/img/bV6aHV?w=1280&h=800); 社区优秀文章 Laravel 5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 - 自造车轮。 API 文档神器 Swagger 介绍及在 PHP 项目中使用 - API 文档撰写方案 推荐 Laravel API 项目必须使用的 8 个...
摘要:以管理员身份打开分别输入输入完成后重启电脑,以完成安装并更新到。将设置为默认版本在微软商店内下载分发版,这里我下载的是。且被视为管理员,能够运行管理命令。 目录 ...
摘要:函数通常是面向对象编程风格,具有副作用。因为在函数式编程中,很有可能这些引用指向的并不是同一个对象。记住,函数并不意味着函数式编程。函数可以用函数式编程风格编写,避免副作用并不修改参数,但这并不保证。 软件构建系列 原文链接:Functional Mixins 译者注:在编程中,mixin 类似于一个固有名词,可以理解为混合或混入,通常不进行直译,本文也是同样。 这是软件构建系列教...
阅读 2620·2021-11-25 09:43
阅读 2723·2021-11-04 16:09
阅读 1633·2021-10-12 10:13
阅读 880·2021-09-29 09:35
阅读 879·2021-08-03 14:03
阅读 1775·2019-08-30 15:55
阅读 2988·2019-08-28 18:14
阅读 3489·2019-08-26 13:43