做者:北溟小鱼hk
连接:https://www.zhihu.com/question/47686258/answer/107209140
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
css
1、引子html
这是关于一把玄铁重剑,一本经书,和一套轻功步法的故事。前端
让咱们先从普通程序猿们的平常工做内容提及,html5
通常来讲,程序猿们大部分时间关注的可能不是研发某个具体算法,这是算法工程师/数学家们擅长的东东。程序猿的工做主要是经过调用编程环境中现成的工具函数或接口来实现具体的应用功能,将各个底层接口或算法模块用代码有秩序地拼装联接起来,实现酷炫好用的产品功能,如同组装一件乐高玩具同样。node
<img src="https://pic1.zhimg.com/50/f18f94860ee3886b39c618e7df84ee20_hd.jpg" data-rawwidth="450" data-rawheight="405" class="origin_image zh-lightbox-thumb" width="450" data-original="https://pic1.zhimg.com/f18f94860ee3886b39c618e7df84ee20_r.jpg">序猿的不少工做每每不是围绕某个高大上的具体算法(“咱们不生产算法,咱们只是算法的搬运工”),而是像代码界的城管、或者清洁工同样,关注怎样组织文件结构,怎样理清编程思路,怎样命名变量,怎样下降代码耦合度,怎样提升代码的复用性和一致性,提升代码的可读性和健壮性,怎样优化分工协做、减小沟通成本等等。无论是OOP、FP等编程思想,仍是MVC等设计模式、或是各类编程语言下的应用开发框架,不少都是为了帮助程序猿完成这些脏活、累活儿。
具体到web应用开发而言,react以及他的好基友redux都是程序猿们出色的好帮手,所以让众多前端开发者一见钟情,俺也不例外。react
<img src="https://pic3.zhimg.com/50/fc7c13b8ae407494d6c9d99d1bae4a4f_hd.jpg" data-rawwidth="450" data-rawheight="500" class="origin_image zh-lightbox-thumb" width="450" data-original="https://pic3.zhimg.com/fc7c13b8ae407494d6c9d99d1bae4a4f_r.jpg">事实上,react和redux从使用的角度来讲,是如此轻量温馨,以致于咱们能够不把它们看成“开发框架”,而是一种编程模式,或是编程的“脚手架”,用起来很是“小清新”。这一点和angularjs这类“重口味”框架有很大区别(我不推荐使用angularjs,固然每一个人口味不一样,最好本身上手体验再作取舍)。其实本人接触react比较晚,但一试用就有种血槽猛涨的感受,强烈建议还没上手react的前端程序猿们试用一下!webpack
<img src="https://pic3.zhimg.com/50/bdf28c38b74ebbd9a747ad2493feac81_hd.jpg" data-rawwidth="450" data-rawheight="225" class="origin_image zh-lightbox-thumb" width="450" data-original="https://pic3.zhimg.com/bdf28c38b74ebbd9a747ad2493feac81_r.jpg">
2、正文css3
咱们首先举两个栗子:程序员
(一)江湖旧事1angularjs
在web1.0的纯网页时代,前端开发实际上是比较happy的,这是由于网页上几乎不须要什么交互,前端开发者基本上只须要根据后台提供的数据将网页内容排版呈现出来便可。用户的交互行为通常仅限于填写一个表单,而后把数据提交到服务器,提交成功后,直接刷新整个页面。
咱们以常见的todoList为例,当须要添加一条todo任务的时候,用web1.0的思路,典型的流程是这样的:
<img src="https://pic3.zhimg.com/50/1a187d2cdbf86dc563ac4cd4941b39d7_hd.jpg" data-rawwidth="690" data-rawheight="648" class="origin_image zh-lightbox-thumb" width="690" data-original="https://pic3.zhimg.com/1a187d2cdbf86dc563ac4cd4941b39d7_r.jpg">然而,当页面交互变得丰富细腻、内容变得庞大复杂以后,这种基于服务器维护state,而后页面总体刷新的web1.0方式存在两个严重的缺陷:
正是因为这两个显著缺陷,才致使ajax技术的出现。自从有了ajax通讯和局部页面更新的机制之后,妈妈不再用担忧咱们页面总体刷新致使的用户体验问题了!因而前端开发大踏步地进入web2.0时代,大量交互细腻,内容丰富的SPA(single page application)也应运而生。但与此同时,前端开发的工做也今后变得苦逼起来。。。由于如今根据后台数据排版生成页面,如今只是最基本的工做。当用户进行某种交互操做引发页面状态变化以后,为了不页面总体刷新,咱们须要当心翼翼地将各个相关的页面局部元素拣选出来,作适当修改,再放回原处,动做通常不会太优雅。(脑补一下《加勒比海盗》中,把眼球拿出来擦一擦又塞回去的那个海盗。。)
当页面逻辑简单,交互行为较少时,这种局部修改也许很容易搞定;然而,一旦页面中各个元素的关系变得复杂,各类交互操做耦合起来以后,这种局部修改就会消耗大量脑细胞,而且咱们须要同时对用户操做和服务器反馈作出响应,并确保页面状态和服务器状态的一致性,因而咱们就很容易变得左支右绌、顾此失彼。相信很多前端工程师都有过相似的体验。
总之,从上面的例子咱们得出两个经验:(二)江湖旧事2
其实,经过改变state,来让view自动更新,这个想法一点也不新鲜。css中各类基于className的样式声明,就是典型表明(改变div的className至关于改变它对应的state, 尤为是css3引入transition和animation以后,这种用css来偷懒的作法愈来愈常见),咱们再举个例子,好比说咱们要作一个菜单:
<img src="https://pic4.zhimg.com/50/c783e25f64e5474356a31830de6f292a_hd.jpg" data-rawwidth="330" data-rawheight="420" class="content_image" width="330">
生手碰到这个问题,也许会这样作:
当用户点击按钮的时候,先改变菜单名的色值,再改变按钮的背景图(由向下箭头,改为向上箭头),最后改变下拉列表的显示状态,有必要的话,还须要作个列表下拉的动画。当用户再次点击按钮的时候,把全部操做倒着来一遍,恢复菜单收起的状态,ok?
然而,对于好逸恶劳的前端老司机们来讲,通常会这样作:对整个菜单的容器定义两个className,好比一个是"menu-close",一个是“menu-open”。在menu-close的状态下,菜单名的色值为黑色,按钮背景图为向下箭头,下拉列表是隐藏状态;在menu-open的状态下,菜单名的色值为白色,按钮背景图是向上箭头,下拉列表是显示状态;须要的话,再用css3 transition给下拉列表加个动画过渡效果。当用户点击按钮的时候,只须要改变整个菜单容器的className便可。全部的交互效果都用css“平铺”的方式声明出来,这样作不只节省了多个手工步骤,并且更改整个容器的className是个单点操做,所以也方便维护和修改。这也是bootstrap框架里惯用的一招,很是好使。
经过这个例子,咱们看到“改变state,让view自动更新”的开发思想在纯粹的前端领域也由来已久。它可让开发者思惟更加清晰,代码更好维护,幸福指数飙升!
然而用css className来实现state功能,其应用范围是很是有限的。缘由很简单,css只能改变DOM元素的样式,却不能改变网页中DOM tree自己的结构(例如,例子1中todoList的增删操做就涉及li元素的append或者remove),更没有办法直接和具体的业务数据相关联。因而,knockoutjs、angularjs等前端框架纷纷登场,这些框架能够系统地实现view 和 state(通常在这些框架里称为ViewModel)的相互绑定,从而使代码更有秩序,帮前端开发省去很多麻烦。即使这些框架约定不少,有些束手束脚的感受,但若是没有另外一件大杀器重出江湖,程序猿们也能够将就过了,奈何既生瑜,何生亮。。这是后话。
(三)重剑无锋
好,抛开knockout、angular这些框架,如今若是让咱们本身设计一套根据states(包括后台业务数据和前端临时数据,例如表单input值、某个面板的显隐状态等等)自动更新页面的机制,咱们该怎么办呢?
对于每一种特定场景,咱们也许有不少种代码方式来根据state自动更新页面的某个局部view。可是,若是页面交互足够复杂,以致于咱们须要在页面的不少地方不断修修补补,而且这些“补丁”对应的state可能还彼此重叠,或者咱们但愿能一劳永逸地解决全部view自动更新的问题,而且还不引入更多繁琐的约定,彷佛除了刷新整个页面没有更好的办法(这有点像逐帧动画的原理,咱们通常不会真的根据物体运动轨迹一点一点修改画面,造成运动效果,而是直接一帧一帧*重绘整个画布,造成动画效果!)
但前面咱们说过,像web1.0的作法同样,重绘整个页面对浏览器的性能损耗是很严重的,用户体验也很糟糕。。怎么办?怎么办?!我脑补着facebook的某个程序员在一个月黑风高的晚上坐在公司电脑前,抿了一口浓浓的咖啡,忽然灵光一现,伴着屏幕上忽明忽暗的幽幽蓝光,在文本编辑器里写下这么一行文字:可不能够把浏览器里的DOM tree克隆一份完整的镜像到内存,也就是所谓的“virtual DOM”,当页面的state发生变化之后,根据最新的state从新生成一份virtual DOM(至关于在内存里“刷新”整个页面),将它和以前的virtual DOM作比对(diff),而后在浏览器里只渲染被改变的那部份内容,这样浏览器的性能损耗和用户体验不就都不成问题了吗?而咱们知道在绝大部分网页应用中js引擎的性能和内存彻底没有被充分利用,咱们正好能够火力全开,利用js的这部分性能红利,实现内存中virtual DOM的diff工做,完美!
因而React横空出世。
话虽简单,不过单单是在内存中模拟整个DOM tree,这个工做想一想就以为头大,因此不得不佩服facebook的那些前端大神们!表面上react花了很大气力却只作了view层跟virtual DOM相关的工做,但所谓“大巧不工、重剑无锋”,实际上facebook祭出的这件大杀器让“改变state,view自动更新”这种直观朴素的想法有了坚实的基础,“state-view”的开发模式今后一马平川!正由于如此,伴随着react的崛起,相似于redux这些专一于管理state的轻量级框架也变得煊赫一时起来。
有了React这把重剑,前端开发们第一次感受到彷佛又回到了web1.0美好的田园时代!而react很是具备表达力的jsx语法和完善的模块化结构,又让咱们以为像生活在酷炫的将来时代!这是咱们的下一话题。( 此处应有《Back to The Future》的电影配乐)
(四)庖丁解牛(view的模块化)
view的组件化和模块化很是有利于分工协做、代码的积累复用以及单元测试。这对于提升团队开发的效率无疑具备很是重要的意义,这也是react广受青睐的重要缘由之一,这一点就再也不赘述。这里想换个角度,聊一下react的模块化机制,对于开发者个体来讲有什么好处?
前面咱们提到,因为React的“state-view”模式可让开发者的大脑获得一种“单向流”的温馨体验。那为何单向流的思惟状态更加温馨呢?
这是由于在**单向流**状态下,要解决的问题如同一个函数映射,已知什么(好比state)是固定不变的,要获得什么(好比view)是定义明确,而人的思惟很是习惯于这种定义明确的、没有“分叉”和“环路”的函数式问题。
也就是说,让人抓狂崩溃的每每是那些包含“分叉”或“环路”的非函数式问题,这个时候大脑不得不思前想后,谋划全局,进入一种“多线程”工做状态,而“单线程”做业对于大脑来讲通常才是更加轻松高效。所谓“单线程”,就是每时每刻只专一于一个问题 —— one at a time! React的"state-view"模式帮助咱们在开发view的时候,只须要专一于view(即关注页面布局样式。view上的交互行为怎样对state产生反馈做用,咱们稍后再来讨论),而React简便的“模块化”机制(即只须要写不多量的boilerplate代码,就能够定义或引用一个新模块),让咱们能够根据须要,将整个页面的布局样式工做进一步拆分红各个小模块(view component),或者将各个小模块组装成大模块,从而进一步深刻贯彻“one at a time”的原则,给大脑减负,所以这时程序猿很容易进入一种温馨高效的状态(心理学中甚至有个“心流”的概念用来描述这种状态)。决定页面呈现的state能够经过模块属性(props)从父模块传递到子模块。这种"树状"分流机制,有点像植物将营养(state)从根部不断运输到细枝末叶的过程,如图所示:
<img src="https://pic2.zhimg.com/50/a6fa072f1c55533464fe616cbb06c67f_hd.jpg" data-rawwidth="300" data-rawheight="195" class="content_image" width="300">
好,到目前为止,咱们看到react已经几乎完美地帮咱们理顺了从state到view的开发流程。可是前面的讨论过程当中,彷佛还有一朵“小乌云”没清理,那就是怎样实现从view到state的反馈流程。也就是说,用户在view上的交互行为(好比点击提交按钮等)应当引发state改变的时候,这个流程该怎么处理?这是咱们要聊的下一话题。
(五)易筋经(flux思想)
a. 几点说明
若是要用一门武林绝学比喻flux思想,我首先想到的是易筋经。此功虽无固定招式,但意会以后,就会有种打通全身经络、气血通畅的感受,而且能够将前端开发中的其余武功串联起来,运用自如 : )
关于flux首先有几点须要说明:b. MVC模式
在讲flux以前,咱们不得不首先提一下大名鼎鼎的MVC开发模式。
所谓MVC开发模式, 主要讲的是在开发交互应用时,怎样将不一样功能的代码拆分到不一样文件或区块,以便下降代码的耦合度,提升代码的可读性和健壮性。简单理解就是:要将 Model-View-Controller 这三部分代码拆分到不一样文件。
对于服务器端开发,Model指的是和处理业务数据相关的代码,例如经过ORM实现数据库的增删改查等操做;View指的是和页面组装相关的代码,主要是和各类模版引擎(例如Java Velocity、PHP Smarty、nodejs ejs等等)相关的代码部分;Controller指的是和用户交互行为相关的代码,具体到网站后台应用,指的就是对各类http请求的handler,也就是根据不一样**url路径和http请求的参数**,将数据和模版绑定到一块儿,最终造成页面呈现给用户。
对于网站前端开发,在web1.0时代,因为js基本上只是个跑龙套的小角色,因此不须要什么设计模式;可是随着web应用功能变得愈来愈丰富、愈来愈复杂,js的地位也愈来愈靠近舞台中心。在目前状况下,若是没有必定的设计模式做为指导,其实很难开发出真正大型复杂的html5应用,也很难实现分工协做和持续维护。
因而MVC模式被很天然地引入到前端开发领域。也许某种意义上,前端开发的整个MVC,仅仅对应于后台开发眼中的View部分;但其实,现在前端MVC思想的深刻性和重要性,对于整个web应用来讲,其实一点也不逊色于服务器端的MVC。
具体而言,前端开发的Model至关于后台数据的镜像或缓存池,它和服务器端MVC中的Model概念一脉相承;View对应页面的呈现,主要指的是和html、css相关的代码,它和服务器端MVC中的View概念也很是相近。
显著的差异来自于controller:在后台应用中,用户和服务器之间的交互是经过http请求实现的,所以后台controller的表达形式是http请求的handler,而且和router(定义网站的url规则)紧密相关; 而前端应用中,用户和网页之间的交互主要是经过操做事件(例如点击鼠标、键盘输入等)实现的,所以前端的controller这里能够简单理解为各类交互事件的handler。
固然, 前端controller的概念是个大杂烩,好比angularjs中的controller被定义为一个做用域($scope)的闭包, 参考 AngularJS文档,这个闭包能够和一段html模版绑定在一块儿,最终将数据渲染到模版中造成页面。大约正是由于这种将数据和模版绑定的功能,很是相似于后台应用中的controller,所以不少框架包括angular将这种功能模块称为controller。为避免混淆,强调一下:后面咱们用controller指代 “凡是和交互事件handler相关的代码单元”
backbonejs 是一个广受欢迎的轻量级MVC前端框架,咱们先来看一个 Backbone.js Todo Example,下面是其View组件的代码片段:
<img src="https://pic4.zhimg.com/50/456c0ccb94fa025b9b3bd39069e8cbce_hd.jpg" data-rawwidth="470" data-rawheight="518" class="origin_image zh-lightbox-thumb" width="470" data-original="https://pic4.zhimg.com/456c0ccb94fa025b9b3bd39069e8cbce_r.jpg">
虽然这种View对Model直接修改的方式很是直截了当,适合小型web应用,然而一但web应用中存在多个Model,多个View,那么Model和View之间的决定关系就可能变得混乱,难以驾驭,以下图所示:
<img src="https://pic1.zhimg.com/50/df2d758b3fe498d20be49c6d3523066c_hd.jpg" data-rawwidth="426" data-rawheight="417" class="origin_image zh-lightbox-thumb" width="426" data-original="https://pic1.zhimg.com/df2d758b3fe498d20be49c6d3523066c_r.jpg">
怎么破??
c. Flux模式
前面咱们提到“单向流”的思惟状态可让大脑更加轻松驾驭,本质上而言,这也是为何上面这种杂乱的双向图示让咱们感到无所适从的缘由。咱们注意到:之因此图示中 Model-View (MVC中的Model大致上能够看做是前面提到的State)的“单向流”被破坏,是因为修改Model的Controller代码像一把黄豆同样散落在了各个View组件的内部,若是能够用某种方式把这些散落的代码单独收拢到一块儿,是否是就让这可让这张图示恢复秩序呢?好,咱们顺着这个思路想下去。
如今咱们又能够从服务器端的MVC模式中得到灵感了!由于咱们注意到,服务器端的controller一般也须要对不少Model产生修改,但在代码结构中却集中在一块儿,没有散落一地。缘由很简单,因为server和client是远程通讯的关系,所以为了尽可能减小通讯耦合,client每一个操做的所有信息都以http请求的形式被归纳成了精简的“做用量”(action)。请求的url路径约定了用户的操做意图(固然RESTful概念中,请求的method也能够反映操做意图),request参数表征了该“意图”的具体内容。正是基于这个action的抽象,client端的交互操做才能够被集中转移到server端的controller中作统一响应。
对比之下,咱们马上发现上述代码片段中前端MVC模式的“痛点”所在:不是MVC模式错了,而是咱们压根缺乏了一个和用户交互行为有关的action抽象!所以,对model的具体操做才无法从各个view组件中被剥离出来,放到一处。
参考http请求,咱们将要定义的action,须要一个typeName用来表示对model操做的意图(相似于http请求的url路径),还可能须要其余字段,用来描述怎样具体操做model(相似于http请求的参数)。
也就是说,当用户在view上的交互行为(例如点击提交按钮)应当引发Model发生变化时,咱们不直接修改model,而是简单地dispatch一个action(其实跟常见的event机制没有什么区别)以表达修改model的意图,这些action将被集中转移给数据端(models),而后数据端会根据这些action作出须要的自我更新。同时,咱们考虑到react中view组件的树状分流结构,因此有以下图所示:
<img src="https://pic2.zhimg.com/50/b3b8c82b1450675e2c1c9acf76825784_hd.jpg" data-rawwidth="600" data-rawheight="343" class="origin_image zh-lightbox-thumb" width="600" data-original="https://pic2.zhimg.com/b3b8c82b1450675e2c1c9acf76825784_r.jpg">
图中A表示Action,V表示View组件,Models部分的结构会进一步讨论。稍微总结一下:从代码层面而言,flux无非就是一个常见的event dispatcher,其目的是要将以往MVC中各个View组件内的controller代码片段提取出来放到更加恰当的地方进行集中化管理,并从开发体验上实现了温馨清爽、容易驾驭的“单向流”模式。 因此我以为,Flux与其说是对前端MVC模式的颠覆,倒不如说是对前端MVC思想的补充和优化。
但为了区分于以往的MVC模式,并向facebook的贡献表达敬意,后面咱们将把这种优化后的 Model-View-Controller 开发模式在React背景下正式称为Flux模式
好,易筋经Flux练到这里,打完收工。到目前为止,咱们看到React的独孤九剑能够经过View Component把页面呈现进行“原子化”拆分(即上图中兰色区域的树状分流结构);Flux打通了State-View的任督二脉(绿色区域),并经过action抽象把用户交互行为进行了“原子化”拆分;那么联系上面的图示,咱们天然要问数据端(紫色区域)的处理,能否一样被“原子化”拆分?这是咱们要聊的下一门武林绝学。
(六)凌波微步(数据端的“原子化”)
redux 登场
凌波微步指的是redux中的reducer机制,能够用来将state端的数据处理过程做“原子化”拆分。redux是来自函数式编程(Functional Programming)的一朵奇葩,听说颇有背景([参考连接](Prior Art | Redux) )。本人尚未深究过,但一接触redux,就马上被其reducer机制的轻盈小巧惊艳到(redux库自己也只有几kb,有必要的化,本身重写也不是难事),所以称其为“凌波微步”。
reducer,从代码上说,其实就是一个函数,具备以下形式:
(previousState, action) => newState
即,reducer做为一个函数,能够根据web应用以前的状态(previousState)和交互行为(经过flux中提到的action来表征),决定web应用的下一状态(newState),从而实现state端的数据更新处理。这个函数行为和大名鼎鼎的“Map-Reduce”概念中的Reduce操做很是相似,于是称这个函数为“Reducer”。
"shut up and show me the code"
ok,咱们仍是以todoList应用为例, 此处有[完整代码](Example: Todo List)。这里不打算详细讲解Redux的具体使用,而只想经过一个Redux对state数据进行操做的代码片段,管窥一下reducer机制对数据进行拆分和组装的简洁过程。代码片段以下:
<img src="https://pic3.zhimg.com/50/095d385bcdd8b0bd7114ac7bc3e6497f_hd.jpg" data-rawwidth="566" data-rawheight="624" class="origin_image zh-lightbox-thumb" width="566" data-original="https://pic3.zhimg.com/095d385bcdd8b0bd7114ac7bc3e6497f_r.jpg">
visibilityFilter是和列表显示状态相关的另外一个reducer;combineReducers将visibilityFilter和todos合并为整个应用的reducer,也就是todoApp。这个过程,从感受上也和react中view组件的合并过程很是相像。
createStore是一个工厂函数。经过它,todoApp(至关于一个数据处理的引擎)被装配到整个应用的state容器,也就是store中。能够经过store的getState方法获取整个应用的state;同时,store也是一个event dispatcher,能够经过其dispatch和subscribe方法,分别实现触发action事件和注册对action事件的响应函数。总言之,从概念上来讲 Redux = Reducer + Flux
3、总结
全体亮相
好,如今React开发模式中的几个核心概念已经所有出场亮相。咱们俯瞰一下整个开发流程:首先,react框架为咱们理顺了 store --> view 的“单向”工做流(store是state的容器);而后,redux框架为咱们理顺了 view --> store 的**“单向”**工做流。而且,react和redux都以组件化的形式能够将各自负责的功能进行灵活地组装或拆分,最大程度上确保咱们“一次只须要专一于一个局部问题”。具体来讲,分为如下步骤:
用图示的方式来表达,即,
<img src="https://pic4.zhimg.com/50/1f81a280ccdbedf6a3c6757f7ff4d9c7_hd.jpg" data-rawwidth="600" data-rawheight="345" class="origin_image zh-lightbox-thumb" width="600" data-original="https://pic4.zhimg.com/1f81a280ccdbedf6a3c6757f7ff4d9c7_r.jpg">
要你命三千
固然实际的react-redux开发步骤中也有很多变通,例如为了能在适当条件下节省些参数传递的代码,redux中提供了provider机制,它像《星际迷航》中的瞬间传送机同样,能够绕过view组件的hierarchy把state一步到位地传递到末端view组件;再好比redux虽然鼓励使用pure function(即不含side effect的函数), 但咱们通常仍是会经过side effect实现store和服务器端的通讯。这些变通,以为是好事。毕竟再严格的框架,也经不起胡乱使用;再完美的框架,也很难说能包治百病,倒不如提供一些台阶,让程序猿们去自由发挥吧。
除了React、Reducer、Flux这三驾马车的主线情节外,react开发模式的周边生态也有不少振奋人心的新鲜事物,例如:拥抱函数式编程和模块化的es6语法,日趋成熟的模块资源管理工具npm,自动化编程及打包神器webpack, 让初始化性能和SEO再也不成为问题的server-side rendering方案,将触角伸到原生开发领域的React-Native系列项目等等,这些零零总总加起来,不禁得让人想起《国产凌凌漆》中集各类武器于一身的终极武器 “要你命三千”!
<img src="https://pic1.zhimg.com/50/07bbeff3e0379855a772fe3d40cf2612_hd.jpg" data-rawwidth="400" data-rawheight="225" class="content_image" width="400">
<img src="https://pic1.zhimg.com/50/b4203f67905e47e8d80bc3dc1e0d3946_hd.jpg" data-rawwidth="400" data-rawheight="280" class="content_image" width="400">