资讯专栏INFORMATION COLUMN

React组件模型启示录

eternalshallow / 3265人阅读

摘要:另一种关于组件的常见说法,是组件是为了重用。这件事情是前端特有的,受限制于的结构。这一节的题目叫做混乱的组件通讯,我们来仔细掰扯一下细节,因为组件模型虽然很常说但是对通讯过程没有约定。

这个话题很难写。

但是反过来说,爱因斯坦有句名言:如果你不能把一个问题向一个六岁孩子解释清楚,那么你不真的明白它。

所以解释清楚一个问题的关键,不是去扩大化,而是相反,最小化。

Let"s begin.

组件

组件不是一个很清晰的编程概念。UML里的组件图基本上就是一个图示,远不能和具有数学完备性的State Diagram相比,也不能和静态结构的Class Diagram和时序交互的Sequence Diagram相比。但大家通常还是会画一个出来,便于程序员理解系统的运行时结构,或者代码结构。

你很容易在Google里搜索到一些Component Diagram的图例,所以这里不贴图了。你在组件图上可以看到有这样一些概念是重要的:

port,它指的是包含一组相关函数的接口,一个interface或者一个protocol;

user和provider,谁提供port和谁使用port;

这两个概念都不需要很复杂的阐述,直觉的理解没问题;

但问题是他们定义得特别粗,接口怎么实现的?是function call?rpc?message passing?event?没说,事实上是都行。

常见的Component Diagram画的一般是run-time的instance,black box逻辑,强调的是instance之间的依赖关系。

另一种关于组件的常见说法,是组件是为了重用。这把问题聊到了另一个空间去了。重用是静态概念,它指的是代码里的一个模块,类、结构等等,而不是指run-time实例。

但是这两种说法不矛盾。因为核心的问题,无论运行时还是静态代码,组件首先强调的是黑盒思维,这点不是问题,封装是开发者熟悉的逻辑;但是组件必须集成为系统,无论在静态代码层面还是运行时,组件之间都有依赖关系,在项目具有一定规模时这尤其重要。

在静态代码层面,任何语言都有库和源码模块话机制,include,import,require等语法关键字或函数建立了这种依赖关系;在运行时,组件(或对象实例)之间可能有动态产生的绑定关系;A对象要具有B的引用,才能使用B的方法;或者要降低耦合,采用观察者模式,消息总线或消息路由,Pub/Sub,等等。相对而言,后者更为重要一些,前者你总能通过分拆模块避免循环,静态依赖关系总归是比较清楚的,它定了就定了,不会运行时发生变化。

所以这篇文章主要谈运行时组件依赖关系的处理,特指在一个应用之内,不是微服务或者多个服务器组成的分布式系统。

React

可能可以不用一上来就谈React,但是这样做最简单。

React的基本代码单元称为React Component;它声称是View Component,但也可以是纯state的Component,在render方法里render其他view component即可;有经验的React开发者知道这被社区称为Container Component。

如果从Component的角度看,React的Component有一个非常特别的设计:Component之间只有一种通讯机制!就是通过Props传递对象或函数,原则上Component之间是不会通过引用互相调用方法甚至发送消息的。换句话说,所有Component都是匿名的。开发者不该在运行时查找某个Component实例访问其数据或方法,调用其方法的只有React框架。

从这个意义上说,React象一个Inversion of Control(IOC)模式,所有有态组件都插在React框架之上,他们可以在willReceiveProps或者render方法被调用时获得传递进来的数据或方法,他们也可以通过调用setState方法触发一次更新,但这几乎就是全部了。

React的官方开发者提供了一套叫做flux的数据流方式,需要持久化(生命周期比视图长)的状态存入store,社区也有很多改良的工作,包括流行的redux,mobx等等;但是本质上说,react自己具有完备的态处理能力,只要把两个有相关性的组件的关联状态放到他们的共同祖先即可;只是这样做,如果没有特殊的处理的化并不灵活,在设计变更时大量代码要修改,还不如使用redux等框架来得方便;anyway,这一点不是我们这篇文章要讨论的主题,它指的是静态代码层面的模块化问题,我找时间写文章专述。我们回到React组件是如何组合和互动这个话题上。

我们重新强调一下React组件的匿名问题。对一个组件而言,它要能工作,当然需要和外部组件互动,但是React组件在这里做了一个极致的设计:

一切依赖性都是注入的;注入的依赖性来自哪个外部组件,组件内部一无所知。

依赖性注入(Dependency Injection)一词,对熟悉可测试性(tesability)的开发者来说不陌生,但大多数情况下这停留在测试领域,很少影响设计。绝大多数应用在顶层都有一些类似全局变量的模块,也就是组件图中表达的那些;使用这些模块的其他模块都能用全局的name找到它们,找到就可以使用了。但是在React里,NO! 即使在顶层,每个组件的外部依赖也都是注入的。

所以你看到React的组件模型实际上只包含三个元素:

父组件向子组件传递的prop是对象或值

父组件向子组件传递的prop是方法(bound)

父子组件们用一个tree表示,是单向的传递数据或方法的(即层层注入)

观察者与资源建模

我们先说第一个要素:父组件向子组件传值。本质上,它是子组件对父组件或父组件可观察的某个资源状态的一个观察。

从语法上来说,它比写Observer Pattern要来得方便,因为子组件没有Subscribe的负担,是反过来做的,父组件把子组件的依赖性(需要观察的对象)塞进来。

因为React是function programming风格,这样写更方便;不方便的地方是观察变化的逻辑是在willReceiveProps里,需要自己比较新版本和副本的区别,如果有差别,调用setState方法更新自己。

但是这种观察能实现所有需要的观察吗?比如SomethingStarted,SomethingResumed,SomethingOpened?

确实可能遇到一些棘手的情况难以简单用值的变化来表述一种变化,但是我们反过来想这个问题,数据库是用CRUD实现的,Restful API设计采用资源建模也只有有限的verb,他们都工作的很好;工作的好的原因是他们都是用资源而不是行为建模的,如果确实需要为行为建模,我们也可以使用状态机,对单一模块而言,在各种粒度上状态机都是很好的建模方式;在状态机模型下,状态就一定可以用离散值来表示,比如运行状态可以是started, stopped, resumed, failed,等等。

这样的建模方式是否比自己发明很多message类型更为有效呢?个人看法是的,这是一种远好于用行为语义定义事件的方式。无论crud还是restful都有极为广泛的实践,可以被认为是被证实可行的方式。

从这些意义上说,React的组件建模方式具有类似crud或http verb的统一抽象,是避免出现大量程序员自己发明混乱语义的好办法。

Bound方法传递

父组件向子组件传递的Bound方法,应该看作是父组件向子组件提供的一种触发状态变化的代理。比如你去酒店,你叫服务生来开门,这是一种类似function call或者message passing的机制,但是服务生也可以给你一张卡你自己去开门,这就是一种代理;和观察资源一样,因为使用了统一的Prop机制,在组件内部看,这种代理也是匿名的,组件并不知道到底是谁在提供这项功能,它只是在需要的时候使用而已。

这件事情是前端特有的,受限制于HTML的结构。

很多功能组件都不只是基于观察逻辑工作,他们还会需要提供功能性服务,功能性服务的入口从哪里触发,看应用和系统结构而定,它可能来自用户操作,可能来自操作系统,也可能来自API请求,后面我们还会仔细说这个问题。

Tree

事实上绝大多数App,其组件都是可以用一个tree来表示的,只不过在项目规模不大的时候,大家更喜欢把顶层组件就堆在一起互相引用,这样变化的时候最灵活。

但React组件的Composition结构更符合组件设计的原则:组件和组件可以方便的组合起来实现更大的组件,而且最重要的,它仍然是只有React定义的只有Prop传递的组件。一种自相似性。或者叫做Composability。

组件更新

简单说一下React组件的更新过程;如果一个组件观察到变化,或者被子组件调用了方法,需要更新状态,这时如果变化只影响到自身和某些子组件,它只要直接setState触发变化即可,React回调用它的render方法触发一连串的变化,更新是自上至下的,所以比较容易做到更新收敛;如果变化会影响到组件树上某个非子组件的变化,那么应该通过上面传递下来的Bound方法触发更高层的组件先做状态迁移。这个设计会导致在状态设计上出现mediator模式,anyway,这也是常见模式和基本功了。

这里需要强调的是,理论上这种更新是同步的,虽然React因为效率问题做了其他的工作,它的VDOM渲染实际上是有Batch和异步的,细节不说了。

混乱的组件通讯

那么如果我们不说前端,如果写后端,或者写系统应用,用React的这个模式构建全部组件树可行吗?答案是不,也不必要。

这一节的题目叫做混乱的组件通讯,我们来仔细掰扯一下细节,因为组件模型虽然很常说但是对通讯过程没有约定。

第一个登场的是function call。

function call不管是同步的还是异步的,它没有区分(1)它是否改变了被调用对象的状态(2)它是否需要返回值。如果它不需要返回值,它就和emit了一个event没什么分别。如果它需要一个返回值,那么调用者是user角色,被调用者是provider角色,如果被调用者的状态发生了变化,这相当于crud里的cud,否则是read。

理解了对function call的分类方式,那么event和message passing也就好理解了。message和function call一样是模棱两可的。在Sequence Diagram里,有去必然有回的message被称为synchronous message,有去无回的叫做asynchrnous;但是我们避免这个术语,和我们在JS里说的不是一回事。但是这个分类方式是对的。

相比之下只有event很纯粹,它就是有去无回的。

OK,你看我们的分类方法非常简单,就是单向的或者有去有回的。但是内在的故事不简单。

State & IO

单向的event,它有可能trigger一个模型内的state或者resource变化(后面统称为State)。

双向的通讯,是一种承诺,即使是失败或错误也要有返回,我们称之为IO。注意这个定义是我自己发明的,它仅仅表示双向通讯。

双向通讯难道就不会trigger模型内的state变化吗?这当然是非常可能的。但是问题的关键点就在这里:

对于一个提供IO服务也可能因为IO改变其内部状态的模块,你是否在代码层面上把IO和State分离了呢?

我们专注于说JavaScript的事件模型;如果你把模块写成状态机,模块接收到的event会race吗?当然不会。IO呢?很可能。并发的本质就是IO的并发,event在单线程的事件模型下没有并发的概念。

那么这里就有一个特别简单的建模方式,你可以脑补一个鸡蛋三明治。

三明治两边的面包(其实只有一边有面包的逻辑也是一样的),可以看作一个是向外提供的IO服务,另一个是自己需要使用的IO服务;而中间的鸡蛋,是这个模块的State,全部State,状态机。

进来的IO如果有资源冲突,可以排队;出去的IO如果有返回结果,返回结果要当作一个Event来处理。如果某些Event导致当前正在服务或者排队的IO请求失败,进来的IO请求队列清空,全部返回错误;如果对象出现生命周期结束,其发出的和服务的IO都要清空,返回失败或者abort。你看这超级容易,就是callback队列和handle队列而已。

我们把中间这层鸡蛋,称为该模块的模型(model),它封装了共享资源,实现了内部和外部状态。

在这个模型上前端和后端有没有区别呢?还是有的,虽然两者都可以看作在对外提供服务,一个是服务机器另一个是服务人。后端的服务在对外提供服务的那层面包上,前端呢,前端都是Event进来的。

级联

在级联这个问题上,React的组件模型显示出了它的简单抽象的威力。如果我们能够把所有模块的鸡蛋部分,象React组件那样级联起来:

React的框架的render过程要自己手写,而且也不大现实搞成functional风格的,只要遵循其自上至下的更新逻辑即可。

React的依赖性全注入的组件形式是非常诱人的,但是在设计变更时要修改mediator在组件树上的所在位置也有些恼人。这里会有一些比较tricky的写法,但是好消息是对大多数应用而言,其实粗粒度的组件数量还没有一个React写的网页里的组件数量多,所以这件事情也不见得要做到极致去,组件数量不多的时候Pub/Sub工作的也很好。但是对于明确的Leaf Node组件,这样写是推荐的。

同步更新。能全部组件同步更新鸡蛋层是非常值得追求的目标。因为它让你的模型具有一个全局的显式状态设计,包含组件相关的数据完整性定义;如果到处是异步状态更新,这个设计本身就有麻烦,其逻辑完备性不容易检验,状态机很容易根据State/Event组合排查设计完备性和合理性,而同步更新是消灭态空间爆炸的利器,否则状态之间要排列组合了。

Event Model

JavaScript是Event Model。Event Model编程的核心就是用状态建模,状态同步更新容易保证数据完整性。建模的开始是看有那些共享资源需要封装,把组件一个一个写出来,然后组合起来。

过程在这里是二等公民,它主要致力于上面说的面包层的IO处理。从这个意义上说,callback还是promise还是async根本不是重点,没有什么值得争执的,哪个合适用哪个。在状态建模之下,IO过程都被碎片化了,试图用长途奔袭的方式串联大量IO操作很难保障设计正确性,光写出来能跑几次成功测试的代码是没意义的,从这个意义上说我不赞同那些伪线程框架。

事务锁的问题不是这篇讨论的重点。事件模型下用状态机和IO排队解决冲突是第一方法,90%以上用这个方法;剩下10%是用opportunistic lock的方式一次性commit多个数据更新状态,这个也很容易,但需要注意读入的数据是尽量同步的(有时这无法保证,但应该去detect非法组合和重试)。

理想的事件模型应该是计算不消耗时间的;实际上这当然不可能。所以主进程的主要目的是维护全局状态层,即所有的鸡蛋;文件和网络IO操作Node大多做得很好,需要算力的任务要用Cluster/Worker了,这是Node的短板,只是要求不高的情况下可用。

如果你的后端或者系统应用是非常stateful的,包括文件持久化的资源,node是很好的选择;如果只是对称的无态逻辑,资源都在数据库里,node没什么意义;如果算力要求高,数据集也大,不适合在进程间抛来抛去,千万别用node,go/java/c++都是好得多的选择。

Rx

我基本没有Rx的开发经验,只是看了半本书。

上面说的全部文字,都可以看作是基于事件模型的reactive编程;但是rx框架是另一个故事,它没有事件模型假设,有很多语言实现,而且它考虑的问题不是一个应用级的,是分布式系统级的。

但rx是不是一个好的选择呢?比如说只用于数据层?

有可能。但是它用于组件层的话,它有几个问题:

1,它没约定单向,这个只能自己来;
2,它需要显式观察,即subscribe,个人认为这不如React的注入机制,后者真正让组件象乐高积木一样容易组合的,没有外部需求的组件才是真正的组件,才可能随意拆装使用;

所以我觉得它写在组件内观察被注入进来的状态变化可能更合适,当然用于密集的异步IO更新的数据集是肯定没问题的。

Final

把关键点陈列一下,该说的前面都说过了。

依赖性注入的组件

状态机和资源建模

状态或资源变化即事件,不要额外发明语义了

理解State和IO的区别

全局级联的状态更新,同步!

~~~~~~~~~~~~~~~~

题外话:

最近在重构一个中等规模项目,在组件模型上想了很多;但是React的原作者们并没有特别的觉得他们的设计是unusual的。Jordan Walke的大部分视频都在谈react如何使用。

但在我来看,或者从后端或者系统程序的角度看,react的组件模型在使用上真正符合了组件的定义:无外部依赖,这一点比node里的module们require来require去高明太多。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83932.html

相关文章

  • 2017-07-10 前端日报

    摘要:前端日报精选入门指南入口,输出,加载器和插件中数据类型转换让我印象深刻的面试题大话大前端时代一与的组件化庖丁解牛一发布中文第期手把手教你用管理状态上个快速编程技巧众成翻译中执行顺序组件解耦之道众成翻译组件模型启示录有个梨作 2017-07-10 前端日报 精选 Webpack入门指南: 入口,输出,加载器和插件JavaScript中数据类型转换让我印象深刻的javascript面试题大...

    Heier 评论0 收藏0
  • 状态决定视图——基于状态的前端开发思考

    摘要:前端与状态现在的前端开发中,对于状态的管理是重中之重。有限状态机那么如何更好的管理前端软件的复杂度的状态机思想给出了自己的答案。有限状态机并不是一个复杂的概念简单说,它有三个特征状态总数是有限的。 前提 在现在的前端社区,关于MVVM、Model driven view 之类的概念,已经算是非常普及了。React/Vue 这类框架可以算是代表。而自己虽然有 React/Vue 的使用经...

    miya 评论0 收藏0
  • [ 一起学React系列 -- 5 ] 如何优雅得使用表单控件

    摘要:假如我们从后台拉取一个数据要填入输入框,那么必须得使用受控组件,因为非受控组件只能被用户输入。不影响正常输入填充该输入框的默认值,此时不显示内容。 网页中使用的form表单大家肯定都再熟悉不过了,它主要作用是用来收集和提交信息。React中的表单组件与我们普通的Html中的表单及其表现形式没有什么不同,所以如何使用表单我觉得再拿出来说可能是画蛇添足、毫无意义。不过再怎么样也不能辜负大家...

    Charlie_Jade 评论0 收藏0
  • React组件:拖拽布局Dragact v0.1.6 发布

    摘要:新特性性能提升通过对组件渲染的优化以及内部算法的优化,把大量的遍历和渲染都省掉。新特性不一样的挂件渲染依赖注入式的挂件可以从最简单的例子看出,我们渲染子组件的方式和以往有些不同。通过获取组件的实例,我提供了一个,用于获取当前的布局信息。 showImg(https://segmentfault.com/img/remote/1460000013377768?w=600&h=375); ...

    since1986 评论0 收藏0
  • React组件:拖拽布局Dragact v0.1.6 发布

    摘要:新特性性能提升通过对组件渲染的优化以及内部算法的优化,把大量的遍历和渲染都省掉。新特性不一样的挂件渲染依赖注入式的挂件可以从最简单的例子看出,我们渲染子组件的方式和以往有些不同。通过获取组件的实例,我提供了一个,用于获取当前的布局信息。 showImg(https://segmentfault.com/img/remote/1460000013377768?w=600&h=375); ...

    caozhijian 评论0 收藏0

发表评论

0条评论

eternalshallow

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<