这个话题很难写。前端
可是反过来讲,爱因斯坦有句名言:若是你不能把一个问题向一个六岁孩子解释清楚,那么你不真的明白它。java
因此解释清楚一个问题的关键,不是去扩大化,而是相反,最小化。node
Let's begin.react
组件不是一个很清晰的编程概念。UML里的组件图基本上就是一个图示,远不能和具备数学完备性的State Diagram相比,也不能和静态结构的Class Diagram和时序交互的Sequence Diagram相比。但你们一般仍是会画一个出来,便于程序员理解系统的运行时结构,或者代码结构。c++
你很容易在Google里搜索到一些Component Diagram的图例,因此这里不贴图了。你在组件图上能够看到有这样一些概念是重要的:程序员
port,它指的是包含一组相关函数的接口,一个interface或者一个protocol;数据库
user和provider,谁提供port和谁使用port;编程
这两个概念都不须要很复杂的阐述,直觉的理解没问题;redux
但问题是他们定义得特别粗,接口怎么实现的?是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 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方法,应该看做是父组件向子组件提供的一种触发状态变化的代理。好比你去酒店,你叫服务生来开门,这是一种相似function call或者message passing的机制,可是服务生也能够给你一张卡你本身去开门,这就是一种代理;和观察资源同样,由于使用了统一的Prop机制,在组件内部看,这种代理也是匿名的,组件并不知道究竟是谁在提供这项功能,它只是在须要的时候使用而已。
这件事情是前端特有的,受限制于HTML的结构。
不少功能组件都不仅是基于观察逻辑工做,他们还会须要提供功能性服务,功能性服务的入口从哪里触发,看应用和系统结构而定,它可能来自用户操做,可能来自操做系统,也可能来自API请求,后面咱们还会仔细说这个问题。
事实上绝大多数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,你看咱们的分类方法很是简单,就是单向的或者有去有回的。可是内在的故事不简单。
单向的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组合排查设计完备性和合理性,而同步更新是消灭态空间爆炸的利器,不然状态之间要排列组合了。
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的开发经验,只是看了半本书。
上面说的所有文字,均可以看做是基于事件模型的reactive编程;可是rx框架是另外一个故事,它没有事件模型假设,有不少语言实现,并且它考虑的问题不是一个应用级的,是分布式系统级的。
但rx是否是一个好的选择呢?好比说只用于数据层?
有可能。可是它用于组件层的话,它有几个问题:
1,它没约定单向,这个只能本身来;
2,它须要显式观察,即subscribe,我的认为这不如React的注入机制,后者真正让组件象乐高积木同样容易组合的,没有外部需求的组件才是真正的组件,才可能随意拆装使用;
因此我以为它写在组件内观察被注入进来的状态变化可能更合适,固然用于密集的异步IO更新的数据集是确定没问题的。
把关键点陈列一下,该说的前面都说过了。
依赖性注入的组件
状态机和资源建模
状态或资源变化即事件,不要额外发明语义了
理解State和IO的区别
全局级联的状态更新,同步!
~~~~~~~~~~~~~~~~
题外话:
最近在重构一个中等规模项目,在组件模型上想了不少;可是React的原做者们并无特别的以为他们的设计是unusual的。Jordan Walke的大部分视频都在谈react如何使用。
但在我来看,或者从后端或者系统程序的角度看,react的组件模型在使用上真正符合了组件的定义:无外部依赖,这一点比node里的module们require来require去高明太多。