React Ways1——函数即组件

未经审视的代码是不值得写的javascript

​ —— 沃兹吉硕德html

React 中有一个经典的公式:前端

const View = f(data)
复制代码

从这个公式里咱们能够提取出两个特色:vue

  • 视图由函数定义——函数即组件
  • 视图的展现与 data 有关——数据驱动

接下来,咱们就从这两点出发,来探讨探讨 React 的编程模式java

函数即组件——声明式编程

函数即组件,顾名思义,就是一个函数就能够是一个组件。 在 React 中,组件通常有两种形式:react

  • 类组件git

    class MyClassComp extends React.Component {
    	render () {
    		return <div className="my-comp"></div>
    	}
    }
    复制代码
  • 纯函数组件(无状态组件)github

    const MyPureFuncComp = () => (
    	<div className="my-comp"></div>
    )
    复制代码

纯函数描述的组件一目了然,可是类组件是否就不那么“函数即组件”了呢?算法

这就像偶像剧的剧情同样毫无惊喜——并不是如此。express

首先,咱们知道,在 JavaScript 中,class 其实更像是函数对象的语法糖,本质上仍是原型及原型链那一套,没出圈儿!

其次,在实际的开发场景下,囿于当前的浏览器形势,咱们产出的代码更多时候须要兼容到 es5 这个没有 class 概念的标准。

因此咱们会看到上面的 MyClassComp 在生产环境下会这样产出:

"use strict";

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

var MyClassComp =
/*#__PURE__*/
function (_React$Component) {
  _inheritsLoose(MyClassComp, _React$Component);

  function MyClassComp() {
    return _React$Component.apply(this, arguments) || this;
  }

  var _proto = MyClassComp.prototype;

  _proto.render = function render() {
    return React.createElement("div", {
      className: "myClassComp"
    });
  };

  return MyClassComp;
}(React.Component);
复制代码

其中 _inheritsLoose 函数用于实现继承,在它下面,MyClassComp 被编译成了一个函数!

好的,如今咱们无须担忧函数即组件这个概念的准确性了。同时,自 Hooks 在 React 16.8 正式 release 后,函数写法的组件会愈来愈多。

PS:代码中的 /#*__PURE__*/ 的做用是将纯函数(即无反作用)标记出来,方便编译器在作 tree-shaking 的时候能够放心的将其剥除

那么,为何 React 要使用函数做为组件的最小单元呢?

答案就是声明式编程(Declarative Programming)

声明式编程

In computer science, declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow. ——Wiki

根据维基的解释可知,与命令式编程相对立,声明式编程更加注重于表现代码的逻辑,而不是描述具体的过程。

也就是说,在声明式编程的实践过程当中,咱们须要更多的告知计算机咱们须要什么——好比调用一个具体的函数,而不是用一些抽象的关键字来一行一行的实现咱们的需求。

在这个模型下,数据是不可变的,这就避免如死锁等变量改变带来的问题。

这不就是封装方法?

是的,JavaScript 做为一门基于对象的语言,封装是很常见的 coding 方式。但封装的目的是为了经过对外提供接口来隐藏细节和属性,增强对象的便捷性和安全性。而声明式编程不只要将对象内部的细节封装起来,更要将各类流程封装起来,在须要实现该流程的时候能够直接使用该封装。

举个例子,现有以下一个数据结构,咱们要经过一个方法将其中名字的各个部分用空格链接起来,而后返回一个新数组,

const raw = [
    {
        firstName: 'Daoming',
        lastName: 'Chen'
    },
    {
        firstName: 'Scarlett',
        lastName: 'Johnson'
    },
    {
        firstName: 'Samuel',
        lastName: 'Jackson'
    },
    {
        firstName: 'Kevin',
        lastName: 'Spacy'
    }
]
复制代码

咱们很容易想到,一个 for 循环便可,

function getFullNames (data) {
    const res = []
    
    for (let i = 0; i < data.length; i++) {
        res.push(data[i].firstName + ' ' + data[i].lastName)
    }
    
    return res
}
复制代码

那么问题来了,以上的数据看起来至关的标准,所以咱们只须要链接 firstName 和 lastName。然而,有一天数据结构中加入了中间名,姑且叫 midName 吧,为了正确的输出全名,咱们不得不修改一下 getFullNames 方法——搞个 if…else 来判断 midName 是否可用?这个思路能够排除了,由于它并无考虑扩展性,若是未来录入了一个不知道多少个中间名的俄罗斯人怎么办?

好吧,咱们用 Object.keys() 先将全部部分提取出来,而后嵌套一个 for 循环将它们都拼在一块儿。

这看起来并无什么问题,但咱们仔细分析一下这个算法的目的——将名字用空格链接起来,咱们并不须要关心这些名字究竟属于什么部分,直须安顺序将这些值提取就行——对,咱们能够用 Object.values() 来实现这个改写,

function getFullNames (data) {
    const res = []
    
    for (let i = 0; i < data.length; i++) {
        res.push(Object.values(data[i]).join(' '))
    }
    
    return
}
复制代码

这甚至无须手动的拼接这些值,告诉计算机让 join 来完成它。

PS:Object.values 属于 es2017 标准,在使用它的时候须要加上对应的 preset 或 polyfill,固然了,也能够在你的方法库中实现一个。

It is the end?

No。既然咱们已经省去了一个 for 循环命令,何再也不省一个?来吧,

function getFullNames (data) {
    return data.map(item => Object.values(item).join(' '))
}

// 甚至再简单一点
const getFullNames = data => data.map(item => Object.values(item).join(' '))
复制代码

一行代码!

想一想原来的命令式写法,不支持中间名的状况下就有 9 行,如果再嵌套一层循环,这个如此简单的需求看着就不那么简单了。

咱们分析分析如今的 getFullNames:

  1. 咱们须要遍历里面全部的对象并返回一个新的数组,就调用 map 方法,它正好知足这个须要
  2. 对于里面每一个元素,有未知的属性个数,但实际上咱们只关注它们的值,那就用 Object.values 来提取这些值吧
  3. 拿到这些值后咱们要用空格将它们链接起来,Object.values 返回一个数组,那就顺势交给 join。
  4. 返回 map 的返回值
  5. 在该过程当中,没有额外的变量引用源数据,而全部的方法也未对源数据作修改,其中,惟一出现的变量 item 在整个流程中值是不变的,在使用完毕以后立刻就会被回收,无污染性。因此,综合来看,getFullNames 是安全的。

能够看到,在这个分析过程当中,咱们注重的是流程的逻辑,而实现每一个逻辑点的时候,咱们均可以用现成的方式去获得想要的结果,换言之,咱们是在一个个的求值过程当中去达到目的,而非在一大堆代码中挣扎。

简单总结一下声明式编程的优势:

  • 复用性:封装是声明式编程的一大要点,而封装的主要目的之一就是复用代码
  • 安全性:明确关注点,省去了没必要要的变量声明和对象引用,防止反作用。
  • 可读性:方法的调用代替直接编写流程,使得代码更加直观
  • 逻辑性:显然经过语义化的方法名能更加清楚的体现代码的逻辑

固然了,在这些优势之下,对开发者的编程素质也有至关的要求。好比代码规范,全部有意义的封装都是为了复用,那么其规范性就必须得提起来,包括注释以及代码格式,咱们都知道良好的代码规范是提高团队编程效率的重中之重;其次,前面提到了“有意义的封装”,这意味着,并不是全部的流程都须要隐藏起来,封装到什么程度?哪些东西须要被封装?该如何封装?这都是须要在实践中逐渐总结的,咱们也称其为封装的粒度问题。

好了,说了这么多,back to React!

首先 React 的核心思想是组件化,其最小的粒度单元就是组件,还记得前面提到的吗——函数即组件!

咱们能够将这种思惟理解成,React 就是将一个个函数按照必定的逻辑关系组合起来,最终构建出咱们想要的应用。这也几乎就是声明式编程的思惟。

所以,一个好的 React 组件,也应当具备前文提到的声明式编程的优势,而且有更深的含义:

  • 复用性:对于组件来讲,复用性体如今其是否与其余组件有太多没必要要的耦合,你不必定会真的复用它,可是保持其独立性对于维护有着至关积极的意义

  • 安全性:因业务复杂程度的关系,组件不必定能保证彻底没有反作用(几乎不可能),可是它们对流程来讲应当是透明可见的。也就是说,开发者应当知道一个组件会产生哪些反作用,以及它们会在其余地方产生什么影响,尽力使得总体依然是可控的。

  • 可读性:这涉及到组件的总体设计,包含命名和接口等因素,举个例子,咱们设计一个时针组件:

    // 时针的英文为 hour hand,那么咱们有以下的选择
    const HH = () => (<div className="hh"></div>)
    const SZ = () => (<div className="sz" />)
    const HourHand = () => (<div className="hour-hand" />)
    const ShiZhen = () => (<div className="shizhen" />)
    复制代码

    显然,前两种方式容易让人摸不着头脑,它们须要进一步的阅读代码才能推断出其做用,这仍是对其逻辑性乐观的状况下。

    第三种方案则一目了然,几乎没有推理成本,对于项目的交接以及维护的便捷性都大有裨益。

    第四种方案呢,一样一目了然,但这只对懂汉语拼音的开发者有效,若是你的项目向全世界开源了,那么对于外国友人来讲,可能依然和没开源同样。

    好了,咱们肯定这个时针组件叫 HourHand 了,那咱们应该怎么使用它呢?

    // 联想到时钟的形态,咱们首先会意识到的就是旋转角度,那组件的接口或许是这样
    import PropTypes from 'prop-types'
    
    const HourHand = props => (
    	<div className="hour-hand" style={{ transform: `rotate(${props.deg}deg)` }} /> ) HourHand.propTypes = { deg: PropTypes.number } 复制代码

    这看起来并无什么问题,可是从逻辑上来讲,时针的含义是角度吗?固然不是,应该是当前的小时,而咱们知道小时之间的角度偏移为 30°,所以,为了使其总体更具备逻辑性,咱们优化一下:

    import PropTypes from 'prop-types'
    
    const HourHand = props => (
    	<div className="hour-hand" style={{ transform: `rotate(${this.props.hour * 30}deg)` }} /> ) HourHand.propTypes = { hour: (props, propName) { if (props[propName] < 0 || props[propName] > 12) { return new Error('Hour must be a number between 0 and 12.') } } } 复制代码

    如今这个时针组件的接口就与其自己的含义统一了,下次再使用它的时候,只需关注咱们熟悉的小时这个属性,而不用再去关心应当转换什么角度——这个流程已经被封装到组件内部了。

  • 逻辑性:其实这一点人为的因素比较大,由于不管多么优秀的编程模型,只要涉及到了业务,都能被 coding 成难以读懂的代码。而咱们使用 React 的最终目的就是实现咱们的业务需求,于是提高逻辑性须要咱们增强对应用的总体理解。

    不过这里咱们能够列举一个 JSX 在逻辑性上的优点。

    在经过模板编译的方式构建视图的框架中,每每须要先在父组件中注册子组件:

    <template>
    	<el-form>
            <el-form-item>
        		<el-input></el-input>
        	</el-form-item>
        </el-form>
    </template>
    
    <script>
        import { Form, FormItem } from 'element-ui'
        export default {
            components: {
                [Form.name]: Form,
                [FormItem.name]: FormItem
            }
        }
    </script>
    
    复制代码

    这样写其实已经足够语义化了,没有问题。

    然而咱们再仔细想一想,其实视图和逻辑自己是应该分离的,但在这个模式下咱们除了要在模板中查看组件结构以外,在逻辑中去关注组件的关系,而且 FormFormItem 的父子关系并未获得体现。

    How about JSX's way?

    import { Form, Input } from 'antd'
    
    export default () => (
    	<Form> <Form.Item> <Input /> </Form.Item> </Form> ) 复制代码

    显然,在这种模式下,组件结构能够完美的体现组件关系,咱们对视图的关注只须要集中在这个 JSX 代码块中。

    其实,在这个问题上,分离一下关注点彷佛也没什么大不了(同时,许多框架也兼容了 JSX);在 components 里注册也能够理解成是配置而非逻辑;甚至,根据习惯和 UI 库实现的不一样,咱们也可能解构的引入这些逻辑上的子组件。最重要的是,咱们如何在不一样的模型下优化咱们的逻辑。

以上部份内容看起来有些像是在聊函数式编程,没有错,函数式编程是最多见的声明式编程的子范式之一,在实际开发中,咱们还体会到许多函数式编程的理念。

以上就是对”函数即组件“这一大概念的基本诠释,理解起来并不难,可是最重要的是如何将其最优的实践到实际开发中,这是须要咱们不断探索的。Ok,Let‘s next into the next plate。

数据驱动——单向数据流

在 JS 的世界中,对象是最基本的数据形式之一;而在 React 的世界中,驱动视图更新的因可能来自 state 的更新,也可能来自 props 的更新——它们都是对象。也就是说数据决定了 React 应用的展现形态,这些形态当且仅当数据发生改变的时候才会更新(这里先回避直接操做 dom 的场景),这是彻彻底底的数据驱动

顺势的,咱们来理解一下 React 的数据驱动方式——单向数据流

单向数据流

单向数据流双向数据流相对,是一种通道内只容许单向数据传递的数据流形式。也就是说,在同一链路上,只有一种数据流向,相似于通讯工程中的单工信道

而双向数据流则是上容许同一链路有两种数据流向,如 MVVM 的双向绑定形式。其在概念上相似于双工信道。进一步的,在代码层面,同一时段只能有一种方向的工做,因此在实际的工做方式上它更接近半双工

那么,咱们就从两种信道的角度来理解探索两种数据流的特色及使用场景:

  • 单工信道一般被应用在电视、广播等纯内容输出的场景。在这种状况下,数据的来源及其下发的目标都是可追踪可记录的,而且能够保证数据的统一性
  • 半双工信道最多见的应用便是对讲机,其特色是在一方在响应(发)的时候,另外一方只能接收(收)。若是两边同时响应(收发并行),那么信道便不能正常工做了

回过头来,咱们前端开发的主要目的就是将数据视觉化的输出给用户,那么,单向数据流(单工信道)天然是具备得天独厚的优点的——从 QA 到线上,前端的一大目标就是在同一状态下对同一角色的用户有同一的展现形式。

对,提到用户,用户的行为是如何引发的视图变化的呢?

再一次回到 React。前面咱们提到了,在 React 中全部的视图变化都来自于数据的变化,而数据存储在状态中,所以,不管是用户仍是其余反作用,引发视图变化的缘由都是他们修改了状态,咱们看看在 React 中这是如何进行的:

class MyComp extends React.Component {
    state = {
        showName: true
    }

    hideName = () => {
        this.setState({
            showName: false
        })
    }

    render () {
		return (
        	<div> {this.state.showName ? (<div>Samuel</div>) : null} <button onClick={this.hideName}>hide name</button> </div>
        )
    }
}
复制代码

在上面的代码中,咱们实现了经过一个按钮来隐藏显示名字的组件,能够看到,点击按钮后,会触发 hideName 方法,而这个方法中只作了一件事,就是调用 setState 方法来修改 state,而 setState 方法则会去开启更新视图的流程。

看到了吗,hideName 自己并不知道会对视图会有什么影响,它只是影响了状态,而 render 才知道如何根据这些状态来渲染界面。咱们能够获得一个简单的示意图:

single flow

如此就清晰多了——行为到状态一样也是单向的!

所以,在 React 中,状态到视图的更新行为到状态的修改,是两条相互独立的通道,这意味着,在这个基础上,全部的行为和变化均可以追踪,可控性很是的强。

这种模式就比如开发者为应用构建了一个神经中枢,整个应用躯干都受这个神经中枢的控制,若是应用出了问题,即可以在中枢中进行行为的回溯,对症下药。

同时,在前端很是流行的 Flux 应用架构也一样采用了单向数据流模型,由其衍生出的各类状态管理框架则在不断地体现着这个模型的优越性。

与 双向绑定 共存

说到这儿,咱们仍是得提一提双向绑定,尽管 React 0.15 版开始就不提供这种数据绑定方式了,但它依然是被其余框架所采用的现代前端开发的关键技术之一。

在 Vue 和 Angular 中,双向绑定是一个很常见的交互处理方案,对于各种表单控件,它有很强的即时同步能力。然而,这也意味着,它的工做频率会很是的高,对于一些规模较小的应用来讲,这种鸡毛蒜皮儿的小事儿影响可能不大,但应用一旦扩展起来,状态树将会愈来愈复杂,咱们就应该尽可能减小这种可控性较差的实践。

譬如,Vuex 的诞生,在技术栈层面从新梳理了 Vue 的状态管理方式,而 Vuex 的模式也是由 Flux 思想演变过来的,一样具备单向数据流的特色。这时候,Vue 的开发者们能够从新思考双向绑定与总体状态的结合形式,以在保证应用稳定性的状况下最大化发挥这种高效数据处理方式的能力。

最后,咱们再进一步的考察一下 Vue 和 Angular 双向绑定的本质看看会发生什么,咱们以 input 为例:

  • Vue:在 Vue 中,实现 input (包括 textarea) 标签双向绑定的源码以下:

    // input 和 textarea 是比较基础的表单组件,除此以外还有 genRadioModel、genCheckBoxModel 等方法进行对应的标签绑定
    function genDefaultModel ( el: ASTElement, value: string, modifiers: ?ASTModifiers ): ?boolean {
      const type = el.attrsMap.type
      const { lazy, number, trim } = modifiers || {}
      const needCompositionGuard = !lazy && type !== 'range'
      
      // v-model.lazy 的实如今这里
      const event = lazy
        ? 'change'
        : type === 'range'
          ? RANGE_TOKEN
          : 'input'
    
      let valueExpression = '$event.target.value'
      if (trim) {
        valueExpression = `$event.target.value.trim()`
      }
      if (number) {
        valueExpression = `_n(${valueExpression})`
      }
    
      // 看这里,生成的 code 被传入到了下面的 addHanlder 中 
      let code = genAssignmentCode(value, valueExpression)
      // 若是有输入法守卫,就增长一个判断,当正在输入的时候不触发 code
      if (needCompositionGuard) {
        code = `if($event.target.composing)return;${code}`
      }
    
      // 给该标签设置 value 属性
      addProp(el, 'value', `(${value})`)
      // 给该标签添加事件处理函数
      addHandler(el, event, code, null, true)
      if (trim || number || type === 'number') {
        addHandler(el, 'blur', '$forceUpdate()')
      }
    }
    复制代码

    总体下来,就是一个为该元素添加 handler 的过程,深刻 genAssignmentCode 彷佛能够找到数据绑定的答案,来看看:

    /** * Cross-platform codegen helper for generating v-model value assignment code. */
    export function genAssignmentCode ( value: string, assignment: string ): string {
      const res = parseModel(value)
      if (res.key === null) {
        return `${value}=${assignment}`
      } else {
        return `$set(${res.exp}, ${res.key}, ${assignment})`
      }
    }
    复制代码

    一目了然,该方法的做用就是生成将genDetaultModel 中的 valueExpression 赋值给要绑定的 value ,要么直接触发 setter 以启动依赖检测,要么经过 $set 方法通知检测器——最终都是状态树更新引起数据下流带来的视图影响,本质上,这依然是单向数据流。那么是否说明 MVVM 的本质实际上都是单向数据流呢?咱们继续往下

    • Angular:Angular 经过 ngModel 指令进行双向绑定,其源码以下(篇幅较长只提取了重要部分,完整源码可戳这里):

      @Directive({
        selector: '[ngModel]:not([formControlName]):not([formControl])',
        providers: [formControlBinding],
        exportAs: 'ngModel'
      })
      export class NgModel extends NgControl implements OnChanges,OnDestroy {
        public readonly control: FormControl = new FormControl();
      
        /** * @description * Tracks the value bound to this directive. */
        // 根据注释,这里即指令要跟踪的值
        @Input('ngModel') model: any;
      
        @Input('ngModelOptions')
        options !: {name?: string, standalone?: boolean, updateOn?: FormHooks};
      
        @Output('ngModelChange') update = new EventEmitter();
      
        // 定义 ngOnChange( Angular 对 change 事件的封装) 的 handler
        ngOnChanges(changes: SimpleChanges) {
            this._checkForErrors();
            if (!this._registered) this._setUpControl();
            if ('isDisabled' in changes) {
                this._updateDisabled(changes);
            }
      
            if (isPropertyUpdated(changes, this.viewModel)) {
                // 调用 _updateValue 来应用新的值
                this._updateValue(this.model);
                this.viewModel = this.model;
            }            
         }           
      
                    
        // 用 control.setValue 给绑定的属性赋值
        private _updateValue(value: any): void {
            resolvedPromise.then(
            () => { this.control.setValue(value, {emitViewToModelChange: false}); });
                   }
      
        }
      }
      复制代码

      像以前同样,咱们来看看这个 control.setValue 方法干了些什么:

      setValue(value: any, options: {
          onlySelf?: boolean,
          emitEvent?: boolean,
          emitModelToViewChange?: boolean,
          emitViewToModelChange?: boolean
        } = {}): void {
          (this as{value: any}).value = this._pendingValue = value;
          if (this._onChange.length && options.emitModelToViewChange !== false) {
            this._onChange.forEach(
                (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
          }
          this.updateValueAndValidity(options);
        }
      复制代码

      从这里知道,setValue 方法先是将新值下发给了 change 事件的订阅者们,而后调用了 updateValueAndValidity

      可见,除了将新值赋值给 model 外,ngModel 还“手动”调用了相关的方法进行后续工做,说明在 Angular 中,ngModel 实现的是一个真正的双向通道。

      综上所述,在 Angular 中,单向数据流和双向数据流共同存在。但更需注意的是,上面实现双向绑定的过程当中用到的 control 对象,是 FormControl 类的一个实例,也就是说,Angular 将这种模式内聚到了表单控制器之中,使咱们有了明确的问题域,这便是在框架层面就将双向绑定的场景进行了规划,从而为庞大而复杂的应用作了准备——聊到 Ng 的时候不就是几乎在聊这样的应用吗?

总结

总结一下本文说了些什么:

  • 函数即组件
    • 函数式 React 组件的本质
    • 这是一种声明式编程的实践
    • 声明式编程使得代码更加灵活、复用性更强
    • 理解这一切,构建更好的 React 应用
  • 单向数据流
    • 这是目前前端世界最流行的数据流形式
    • 它使得应用的数据和行为能更好的被监听和捕获
    • 提高了应用表现的一致性
    • 它能够与双向数据流共存,方式的合理能够发挥它们各自最大的效能

以上贯穿 React 开发的两个最基本的概念,能够说“函数即组件”是 React 应用的各个器官;“单向数据流”就是这个应用的血液管道,支持着各个组件呈现出它们应该的样子;而开发者,就是大脑,为应用注入了灵魂。

理解它们,咱们就知道了 React 的基本形态;而能在开发过程当中正确地实践它们,应用将会更加优秀。

Hold it if you agree with it~!

若是您还没有尝试 React,或许本文并不能让您立刻着手开发,若不嫌弃,还有后文。

若是您已经在 React 的世界中自由翱翔,但愿本文能对您有益,或是获得您的批评。

Thanks

相关文章
相关标签/搜索