写点什么

揭秘 Vue 3.0 最具潜力的 API

  • 2020-02-15
  • 本文字数:5287 字

    阅读完需:约 17 分钟

揭秘 Vue 3.0 最具潜力的 API

尤雨溪 6 月份发布了 Vue Function-based API RFC,说是 3.0 最重要的 RFC。


文章发布后,引起了许多人的讨论和争执。


有人表示喜欢和赞赏,有人却表示“这不就是抄 React 吗?我干嘛不直接学 React 去了”。


从个人角度,相比 vue 之前的 class-component 提案,我更欣赏现在的 function-based 模型。表面上看它好像是 react-hooks 的翻版。其实它跟 react-hooks 走的函数增强路线不同,vue-hooks 是一个 value 增强的路线。


function 强化跟 value 强化,是一个能力相当的对偶模型。一个是 a -> data ,另一个则是 data -> a。后者也是现在函数式研究的一个方向,叫 codata。


react 路线:如何从普通的 value 中,通过函数管道,输出一个 view。


vue 路线:如何从一个特殊的(响应式的)值中,衍生出普通的值以及 view。


今天我们要揭示的,不是上面那个最重要的,而是最具潜力的、最能表征 Vue 路线的 API。


众所周知,Vue 当年的核心竞争力之一就是使用 ES5 的 Object.defineProperty 的 getter/setter 改良了当时的 MVVM 使用脏检查或者 get()/set() 函数。如今基于 ES2015 Proxy 升级成了新的 reactivity api。


它就是 Advanced Reactivity API,Provide standalone APIs for creating and observing reactive state。(注:请先大概看一下 API 介绍,它是理解后续内容的基础)。


某种意义上,vue 暴露的内部 api(reactivity api)比 react 暴露的内部 api(hooks),具有更强的表达能力和普适性。它比 react 更完整,因为 value 也可以衍生出 view$,它自带了状态管理和视图,且两者是无缝对接的。


react hooks 只能借鉴思路。在别的地方使用时,要去重新实现,是一种模式。而 reactvity api 可以直接作为 library 来用。比如,拥有了这个 API,我们可以实现出类似 cyclejs, rxjs, immer, react-hooks 的特性。


那么问题来了,vue 3.0 还没有发布,我们没有代码,怎么演示和证明 reactivity api 可以作为 library 来用呢


哼哼,这个难不倒我们。


我们来亲自手撸一个简单的 vue 3.0 reactivity api,不就行了吗?


具体如何实现,不是这篇文章的重点,按下不表。如果你等不急看代码和效果,可以点击这里访问DEMO(我基于 reactivity-api 实现了 counter 和 todo-list 效果)。


你会发现 reactivity.js 已经被编译和压缩过了,可读性很低。这是因为,最近前端社区有一些不良风气,一些小朋友,从各处抄了一点代码,就觉得实现了 vue/react 的核心。过分自信的在四处发表错漏百出、富有偏见的观点。因此我们特意做了一下处理,增加点抄袭成本,反正这不妨碍我们此次的演示目的。

如何实现 cyclejs-like 的 reactive-view


首先实现一个 watchable 函数,可以将任意对象或数组,变成可 watch 的,它有第二个参数,options,其中 options.map 决定 set 阶段时如何储存到 target。


state 采用递归的方式,将整颗树都 watch 起来。value 则只 watch value 字段。


尽管 vue 被认为不够 fp,不过我们其实可以插上一些 fp 的翅膀,比如将 value 视为 monad,实现 fmap, ap, bind 等函数。



fmap 是基于一个 watchable a,和 a -> b 的 f 函数,构造一个 watchable b 对象。这里简单 watch a 的阶段,调用 f(a) 构造新的值即可。


ap 则是 watchable f 和 watchable a,构造一个 watchable b,b 是 f(a) 的产物。


bind 是 watchable a 加上类型为 a -> watchable b 的 f,实现基于上一个 value 的值构造下一个 watchable 的功能。


至此,我们有了 state, value, 两个构造函数,有了 watch 监听函数, 有了 fmap, ap, bind 基于 value 构造下一个 value 的基本操作。


实现 reactive view 用不到 computed,因此我们没有去实现它。


vue 跟 rxjs 这种特殊的值,可以直接衍生出 view。首先实现一个 combinaLatest([value 范畴内构造数组的方式,然后通过 [[key, value]] ,从处理数组的方式中,配合 fromEntries 衍生出 value$ 层面构造 object 的方式。而 virtual-ui-model 就是用特定的 object 表征 ui 对象。


因此,我们基于 object,它代表了一个在时间序列中动态输出的视图流,并且因为 combinaLatest 自动复用未变化的值,使得 view$ -> view 输出的结构,总是结构共享的,利于 diff 算法。



实现 combineArray:如果一个数组里存在一个 reactive value,那么它也返回一个 reactive array,每次输出一个纯数组。如果数组里不包含 reactive value,它什么也不包装,直接返回该数组。相当于 Promise.all(list),只不过它有可能不返回 promise/reactive-value。



有了 combineArray,可以实现 combineObject,正如前面说的,就是 entries 和 fromEntries 的转换。



再封装一下,得到一个 combine 函数,可以将任意结构,构造成 reactive-value,只要子结构了包含 reactive-value,它就 wrap 成一个整体。


现在我们除了 vue-like 的 reactivity api,还有 combine 函数了,可以去 combine react-element 了。为什么是 combine react-element ?因为我们就是要证明 vue 3.0 的 reactivity api 可以作为 library,脱离 vue 来用。因此就用在其竞争对手 react 身上(其实是因为我比较熟悉 react)。


我们会将 jsx 的编译函数从 React.createElement,切换成我们自己构造的 createElement。



createElement 将可能包含 reactivity-value 的 type, props, children,给 combine 起来。检测到 component 用 monad 的 bind,此时我们将组件描述为 bind 的 f 参数。检测到 element 我们用 functor 的 fmap,将 props 映射成 react-element。



最后,实现一个 map 函数,用来 map 一个 reactive-value,既支持数组,也支持非数组。


准备工作做好了,把它们 import 进来。



回顾一下我们的 combineArray 是如何更新的,它不是直接赋值,而是先浅拷贝,再赋值。



这意味着,它总是返回 immutable-list,因为它跟 immer 一样 copy-on-write。


我们免费得到了一个行走的 immer,不需要 produce 包裹。combine 一下,然后随便改,watch 函数都会拿到结构共享的 immutable data。


如果没有实现这一点,combine react-element 时,子树直接被修改,react 进行 diff 时检测不出来子树有变化,就不会去更新视图了。


现在可以实现一个 Counter 组件试试。



看这个代码,是不是觉得非常有趣?既像 vue 那样可以用 js 赋值操作,又像 react-hooks 那样的形式,还像 cycle.js 一样在组件内部可以操作 reactive value。


它怎么做到自动更新视图的呢?


因为 let count = value(0),它是一个 reactive-value。它被 handleIncre 和 handleDecre 修改,它同时用在了 jsx 里。我们的 createElement 会检测到这个 jsx 结构里包含一个 reactive-value,因此它会被整个 combine 起来,成为一个大的响应式的值 view.value。



前面我们将 jsx 编译从 React.createElement 切换到我们的 createElement 函数,因此 <Counter /> 组件不是返回 react-element,而是返回我们的 reactive-value,它是一个响应式的值,可以被 watch。我们 watch 这个 <Counter />,然后拿到它真正的 react-element,再用 ReactDOM 渲染到 root 节点。


看起来像下面那样。



只支持一个 counter,看起来可能是一个特例,我们可以再实现一个 todo list。



TodoApp 组件里构造一个 reactive-state,然后传递给 TodoInput 和 Todos。



TodoInput 里构造一个 reactive text,作为局部状态,绑定到 input 元素。


点击 add 按钮时,构造一个 todo,直接 push 到 todos 里即可。


其它用到 todos 的地方,会自动检测到 todos 变化而进行局部渲染。比如我们的 Todos。



它通过 map 函数,将 reactive todos 映射成 Todo 组件,每当 todos 变化时,这个 map 函数就会自动再次执行,然后 top-level 的 app 就会拿到一个 immutable vdom,除了 todos 以外,其它结构复用原来的引用。



Todo 里面很简单,就是展示一下,支持 toggle 和 remove 什么的。


整体看上去像下面那样。



可以看到,我们从未调用 setState/setValue 等触发函数,只用到了原生 js 的方法和赋值操作。以一种符合直觉的方式,构建了我们这个 reactive todo-list。


react 在这里只是起来了一个 renderer 的作用,理论上,用任意 vdom library 都行。

如何用 combine 函数实现行走的 immer


上面的 test 是一个 reactive state,里面深层节点里包含了 reactive-value。


mobx 作者的 immer,是现用现抛,nextState = produce(state, update)。


我们 reactive-state 的版本则是,draftState 不必限制在 update 函数里,可以在外面随意传递和使用,watch 函数拿到的总是 immutable 的。


我们构造了 3 个方法,分别深度更新不同的字段,然后随机使用这些更新方法。它们不会引起其它字段的引用变化,共享没有变化的结构。



比如,randomMethod a 只引起了 a 字段的更新,因此 c 和 g 字段跟 prev 对比是相等的。

如何用 reactivity api 实现 react-hooks 的机制?

vue 3.0 的 reactivity api,更多的是承担 connect, computed, combine 等结构关联的动作,它没有作为 source 去 produce data。data 是外部传入 state/value,以及 reactive-state 在别的地方被 mutate 出新数据。


而 react-hooks 其实是一个 producer,它不断的 re-execute 自身,产生很多次 return data 的过程。


react-hooks 跟 reactivity api 的结合,就得到了一个 producer + combinator。比如,我们要构造一个 count,它不只是在 count.value += 1 的时候被动产生新的 value,而是可以通过某个机制,不断自动产生。



这个结构看起来跟 rxjs 倒很像。有 next, cleanup/unsubscribe,默认自带 startWith 操作符。后续我们可以实现 merge, combine, concat, filter, take 等其它 operator。这样直接 vue, react, rxjs 的 pattern 一家亲了~


不过,额外引入 react-hooks,跟 vue-reactivity 并行,会显得很奇怪,应该用后者实现前者的机制。就是 re-run 时,重用 state/value,并且 state/value 的变化,会引起函数的 re-run。


useEffect 应该是 watch 自身,是一个语法糖。watch(self, effect)。


如此,区分出了两种 reactivity 形态,一种是在 producer 外部的 free-order-reactivity,一种是在 producer 内部的 fixed-order-reactivity。


实现起来很简单。



实现 3 个增强函数的函数,resumable 增强函数 re-run 自身的能力,referencable 增强函数持久化内部状态的能力。reactive 增强函数使用 reactivity api 的能力。


首先存在一个 env 内部环境,它会被 resumeable, referencable, reactivie 等 enhancer 进行拓展。



reactivie 就是将 prodcuer 的返回值,挂载到 value,因此外部总是拿到一个 reactive value。



useRef 的实现,直接使用 referencable 提供的 storage 方法即可。



useEffect 在使用 storage 方法时,通过 reactive enhancer 拿到了 value$,watch 它,并返回 unwatch。



useReactive 在内部构造 reactive value/state,watch 它,然后使用 resumable enhancer 提供的 resume 方法,触发重新执行。



然后用 useReactive,分别实现 useState,和 useValue。再用它们实现一个 interval 函数,可以输出一个自行变化的 count。



把 interval 用在我们之前的 Counter 组件里。



效果,有一个 tick 自动随时间而变化,不需要额外的地方去 count.value += 1。


如何用 reactivity api 实现 rxjs-like 的功能?

先实现一个 rxjs 那样的 pipe,用法是 pipe(source, operator1, opeator2, operator3) 这类。



map operator 的实现,可以直接用 functor 的 fmap,参数映射一下 pipe 函数的要求。



因为 map 函数已经定义过了,因此这个 map operator 只好改名为 mapx。


filter operator 就是通过 predicate 函数,有选择的将 source



take 和 scan 则分别是内部计数和累加 acc,代码都很简单。



将这些 operators 用在我们的 tick 上。



输出 10 个奇数的数组。如下图所示。


总结

需要说明的是,目前的模拟是一个粗糙的做法,有很多没有处理,比如 unwatch 的时机,它几乎一定会内存泄露。需要更精细的去实现和控制,才能得到一个可用的形态,当下只是演示一下思路 。


这些 demo 只是演示一些能力。没有考虑实际项目里怎么用,不管大小,都不要用这个方案。


等有人基于这个思路做出一个完成度更好的库或者框架,再考虑吧。


到目前为止,我们差不多填完了用 vue reativity api 实现 immer-like, rxjs-like, react-hooks-like, cyclejs-like(就是最初的那个 reactive view) 的坑,应该足以展示 vue reactivity api 是一个更加 primitive 的机制了(毕竟基于 Proxy)。


vue 3.0 reactivity api 的能力还不局限于上面演示的,感兴趣的同学,可以自行探索一下。


作者介绍


古映杰,携程研发高级经理,负责前端框架和基础设施的设计、研发与维护。开源项目 react-lite 和 react-imvc 作者。


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


https://mp.weixin.qq.com/s/TwUubgCH0c0tue12CBNTzg


2020-02-15 17:461116

评论

发布
暂无评论
发现更多内容

「信创」风口,国产数据库的新机遇

BinTools图尔兹

数据库 数据安全 dba 数据库管理 tdsql

暑期 2021 | Serverless Devs 最全项目申请攻略来啦!

阿里巴巴云原生

开源 Serverless 开发者 云原生 活动

不愧是Alibaba技术官,Kafka的精髓全写这本“限量笔记”里,服了

Java 大数据 架构 面试

文本分析基本流程

Qien Z.

文本分析 5月日更

深入剖析 MySQL 自增锁

leonsh

MySQL 数据库

大数据采集和常见问题

数据社

大数据 数据采集 5月日更

One-on-One Meeting

escray

学习 5月日更 朱赟的技术管理课

促成“零碳”社会的全面实现,华为云让技术更有温度

xiaotan

华为云

“四大模型”革新NLP技术应用,揭秘百度文心ERNIE最新开源预训练模型

百度大脑

开源 nlp

量化马丁策略系统搭建,网格策略交易系统

不含敌意的坚决|靠谱点评

无量靠谱

IoT系列,树莓派监控开关状态

IT蜗壳-Tango

IT蜗壳 IT蜗壳教学 5月日更

人生算法:愿景,设计人生导航系统

石云升

读书笔记 愿景 5月日更

唵嘛呢叭咪吽|靠谱点评

无量靠谱

网络攻防学习笔记 Day31

穿过生命散发芬芳

5月日更 网络攻防

第五课作业

杰语

持续测试 | DevOps 时代的高效测试之钥

CODING DevOps

DevOps 持续测试 迭代式测试

阿里云携手 VMware 共建云原生 IoT 生态,聚开源社区合力打造领域标准

阿里巴巴云原生

阿里云 容器 开发者 云原生 k8s

5分钟速读之Rust权威指南(十三)

wzx

rust

99% 的同学写不出好代码,都是因为这个问题!

程序员鱼皮

Java c++ Python 自学编程 经验分享

Serverless Devs 的官网是如何通过 Serverless Devs 部署的

阿里巴巴云原生

Serverless 开发者 运维 云原生 存储

dubbo-go v3 版本 go module 踩坑记

阿里巴巴云原生

容器 开发者 云原生 中间件 dubbogo

公安重点人员情报研判分析系统,可视化大屏系统

Logstash-数据流引擎

进击的梦清

大数据 Linux 运维 后端 Logstash

刚刚接触视频剪辑,怎么快速剪视频?

奈奈的杂社

从外包到拿下阿里offer,这2年5个月13天到底发生了什么?

Java 程序员 架构 面试

iOS基础原理题目汇总

程序员 面试 iOS 知识体系

通证经济— 激励机制、社会生产、后资本主义

CECBC

思想与落地

型火🔥

架构 分布式 微服务 哲学

简单又灵活的权限设计?

蛋先生DX

数据库设计 权限系统 权限 权限架构 rbac

腾讯云大神亲码“redis深度笔记”,字字珠玑,全是精华

Java 程序员 架构 面试

揭秘 Vue 3.0 最具潜力的 API_技术管理_古映杰_InfoQ精选文章