【三】React之组件

努力的废柴

是最爱啊

React组件

一个React应用就是构建在React组件之上的。
组件有两个核心概念:

  • props
  • state
    一个组件就是通过这两个属性的值在 render 方法里面生成这个组件对应的HTML结构。
    注意:组件生成的HTML结构只能有一个单一的根节点。

props

props 就是组件的属性,由外部通过JSX属性传入设置,一旦初始设置完成,就可以认为 this.props 是不可更改的,所以不要轻易更改设置 this.props 里面的值。
可以把 props 看作是组件的配置属性,在组件内部是不变的,只是在调用这个组件的时候传入不同的属性(比如这里的 name)来定制显示这个组件。

state

state 是组件的当前状态,可以把组件简单看成一个“状态机”,根据状态 state 呈现不同的UI展示。一旦状态(数据)更改,组件就会自动调用render重新渲染UI,这个更改的动作会通过this.setState方法来触发

划分状态数据

一条原则:让组件尽可能的少状态。这样组件逻辑就越容易维护。
什么样的数据属性可以当作状态?
当更改这个状态(数据)需要更新组件UI的就可以认为是state,下面这些可以认为不是状态:

  • 可计算的数据:比如一个数组的长度
  • 和props重复的数据:除非这个数据是要做变更的

无状态组件

可以用纯粹的函数来定义无状态的组件(stateless function),这种组件没有状态,没有生命周期,只是简单的接受 props 渲染生成 DOM 结构。无状态组件非常简单,开销很低,如果可能的话尽量使用无状态组件。比如使用箭头函数定义:

const HelloMessage = (props) => <div>Hello {props.name}</div>;
render(<HelloMessage name="John" />,mountNode);

上面的 HelloMessage 就是一个 React 构建的组件,最后一句 render 会把这个组件显示到页面上的某个元素 mountNode 里面,显示的内容就是 <div>Hello John</div>

因为无状态组件只是函数,所以它没有实例返回,这点在想用 refs 获取无状态组件的时候要注意,参见DOM 操作。

一、组件生命周期

(一)组件生命周期

一个组件类由 extends Component 创建,并且提供一个 render 方法以及其他可选的生命周期函数、组件相关的事件或方法来定义。

import React, { Compponent } from 'react';
import { render } from 'react-dom';

class LikeButton extends Component {
    constructor(props) {
        super(props);
        this.state = { liked:false};  // 设置初始状态
    }

    handleClick(e) {
        this.setState({ liked: !this.state.liked });  // 设置新的状态
    }

    render() {
        const text = this.state.liked ? 'like' : 'haven\'t liked';
        return (
            <p onClick={this.handleClick.bind(this)}>
                You {text} this. Click to toggle.
            <p>
        );
    }
}

render(
    <LikeButton />,
    document.getElementById('example')
);

(1) getInitialState

初始化 this.state 的值,只在组件装载之前调用一次。
如果是使用ES6的语法,你也可以在构造函数中初始化状态,比如:

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = { count:props.initialCount };
    }

    render() {
        // ...
    }
}

(2) getDefaultProps

只在组件创建时调用一次并缓存返回的对象(即在 React.createClass 之后就会调用)。

因为这个方法在实例初始化之前调用,所以在这个方法里面不能依赖 this 获取到这个组件的实例。

在组件装载之后,这个方法缓存的结果会用来保证访问 this.props 的属性时,当这个属性没有在父组件中传入(在这个组件的 JSX 属性里设置),也总是有值的。

如果是使用 ES6 语法,可以直接定义 defaultProps 这个类属性来替代,这样能更直观的知道 default props 是预先定义好的对象值:

Counter.defaultProps = { initialCount: 0 };

(3) render

必须
组装生成这个组件的 HTML 结构(使用原生 HTML 标签或者子组件),也可以返回 null 或者 false,这时候 ReactDOM.findDOMNode(this) 会返回 null。

(二)生命周期函数

(1) 装载组件触发

componentWillMount
只会在装载之前调用一次,在 render 之调用,可以在这个方法里面调用 setState 改变状态,并且不会导致额外调用一次 render 。

componentDidMount
只会在装载完成之后调用一次,在 render 之调用,从这里开始可以通过 ReactDOM.findDOMNode(this) 获取到组件的DOM节点。

(2) 更新组件触发

这些方法不会在首次 render 组件的周期调用

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate

(3) 卸载组件触发

  • componentWillUnmount

更多关于组件相关的方法说明,参见:

二、事件处理

(一)事件处理

import React, { Component } from 'react';
import { render } from 'react-dom';

class LikeButton extends Component {
    constructor(props) {
        super(props);
        this.state = { liked: false };
    }

    handleClick(e) {
        this.setState({ liked: !this.state.liked });
    }

    render() {
        const text = this.state.liked ? 'like' : 'haven\'t liked';
        return (
            <p onClick={this.handleClick.bind(this)}>
                You {text} this. Click to toggle.
            </p>
        );
    }
}

render(
    <LikeButton />,
    document.getElementById('example')
);

可以看到 React 里面绑定事件的方式和在 HTML 中绑定事件类似,使用驼峰式命名指定要绑定的 onClick 属性为组件定义的一个方法 {this.handleClick.bind(this)}。
注意要显式调用 bind(this) 将事件函数上下文绑定要组件实例上

(二)"合成事件"和"原生事件"

React 实现了一个“合成事件”层(synthetic event system),这个事件模型保证了和 W3C 标准保持一致,所以不用担心有什么诡异的用法,并且这个事件层消除了 IE 与 W3C 标准实现之间的兼容问题。

“合成事件”还提供了额外的好处:

事件委托

“合成事件”会以事件委托(event delegation)的方式绑定到组件最上层,并且在组件卸载(unmount)的时候自动销毁绑定的事件。

什么是“原生事件”?

比如你在 componentDidMount 方法里面通过 addEventListener 绑定的事件就是浏览器原生事件。

使用原生事件的时候注意在 componentWillUnmount 解除绑定 removeEventListener

所有通过 JSX 这种方式绑定的事件都是绑定到“合成事件”,除非你有特别的理由,建议总是用 React 的方式处理事件。

Tips

关于这两种事件绑定的使用,这里有必要分享一些额外的人生经验

如果混用“合成事件”和“原生事件”,比如一种常见的场景是用原生事件在 document 上绑定,然后在组件里面绑定的合成事件想要通过 e.stopPropagation() 来阻止事件冒泡到 document,这时候是行不通的,参见 Event delegation,因为 e.stopPropagation 是内部“合成事件” 层面的,解决方法是要用 e.nativeEvent.stopImmediatePropagation()

”合成事件“ 的 event 对象只在当前 event loop 有效,比如你想在事件里面调用一个 promise,在 resolve 之后去拿 event 对象会拿不到(并且没有错误抛出):

handleClick(e) {
  promise.then(() => doSomethingWith(e));
}

详情见 Event pooling 说明。

(三)参数传递

给事件处理函数传递额外参数的方式:bind(this, arg1, arg2, ...)

render: function() {
  return <p onClick={{this.handleClick.bind(this, 'extra param')}>
},
handleClick: function(param, event) {
  // handle click
}

React 支持的事件列表

三、DOM操作

(一)DOM操作

大部分情况下你不需要通过查询 DOM 元素去更新组件的 UI,你只要关注设置组件的状态(setState)。但是可能在某些情况下你确实需要直接操作 DOM。

ReactDOM.render 会返回对组件的引用也就是组件实例(对于无状态状态组件来说返回 null),注意 JSX 返回的不是组件实例,它只是一个 ReactElement 对象。例如(用纯JS来构建JSX的方式)

// A ReactElement
const myComponent = <MyComponent />

// render
const myComponentInstance = ReactDOM.render(myComponent, mountNode);
myComponentInstance.doSomething();

(二)findDOMNode()

import { findDOMNode } from 'react-dom';

// Inside Component class
componentDidMound() {
    const el = findDOMNode(this);
}

findDOMNode() 不能用在无状态组件上。

(三)Refs

另外一种方式就是通过在要引用的 DOM 元素上面设置一个 ref 属性指定一个名称,然后通过 this.refs.name 来访问对应的 DOM 元素。

比如有一种情况是必须直接操作 DOM 来实现的,你希望一个 <input/> 元素在你清空它的值时 focus,你没法仅仅靠 state 来实现这个功能。

class App extends Component {
    constructor() {
        return { userInput: '' };
    }

    handleChange(e) {
        this.setState({ userInput: e.target.value });
    }

    clearAndFocusInput() {
        this.setState({ userInput:'' },() => {
            this.refs.theInput.focus();
        });
    }

    render() {
        return (
            <div>
                <div onClick={this.clearAmdFocusInput.bind(this)}>
                    Click to Focus and Reset
                </div>
                <input
                    ref="theInput"
                    value={this.state.userInput}
                    onChange={this.handleChange.bind(this)}
                />
            </div>
        );
    }
}

如果 ref 是设置在原生 HTML 元素上,它拿到的就是 DOM 元素,如果设置在自定义组件上,它拿到的就是组件实例,这时候就需要通过 findDOMNode 来拿到组件的 DOM 元素。

因为无状态组件没有实例,所以 ref 不能设置在无状态组件上,一般来说这没什么问题,因为无状态组件没有实例方法,不需要 ref 去拿实例调用相关的方法,但是如果想要拿无状态组件的 DOM 元素的时候,就需要用一个状态组件封装一层,然后通过 ref 和 findDOMNode 去获取。

总结

  • 你可以使用 ref 到的组件定义的任何公共方法,比如 this.refs.myTypeahead.reset()
  • Refs 是访问到组件内部 DOM 节点唯一可靠的方法
  • Refs 会自动销毁对子组件的引用(当子组件删除时)

注意事项

  • 不要在 render 或者 render 之前访问 refs
  • 不要滥用 refs,比如只是用它来按照传统的方式操作界面 UI:找到 DOM -> 更新 DOM

四、组合组件

(一)组合组件

使用组件的目的就是通过构建模块化的组件,相互组合组件最后组装成一个复杂的应用。

在 React 组件中要包含其他组件作为子组件,只需要把组件当作一个 DOM 元素引入就可以了。

一个例子:一个显示用户头像的组件 Avatar 包含两个子组件 ProfilePic 显示用户头像和 ProfileLink 显示用户链接:

import React from 'react';
import { render } from 'react-dom';

const ProfilePic = (props) => {
    return (
        <img src={'http://graph.facebook.com/' + props.username + '/picture'} />
    );
}

const ProfileLink = (props) => {
    return (
        <a hrdf={'http://www.facebook.com/' + props.username}>
            {props.username}
        </a>
    );
}

const Avatar = (props) => {
    return (
        <div>
            <ProfilePic username={props.username} />
            <ProfileLink username={props.username} />
        </div>
    );
}

render(
    <Avatar username="wpr" />,
    document.getElementById('example')
);

通过 props 传递值。

(二)循环插入子元素

如果组件中包含通过循环插入的子元素,为了保证重新渲染 UI 的时候能够正确显示这些子元素,每个元素都需要通过一个特殊的 key 属性指定一个唯一值。具体原因见这里,为了内部 diff 的效率。
key 必须直接在循环中设置:

const ListItemWrapper = (props) => <li>{props.dara.text}</li>;

const MyComponent = (props) => {
    return (
        <ul>
            {props.results.map((result) =>{
                return <ListItemWrapper key={result.id} data={result}/>;
            })}
        </ul>
    );
}

你也可以用一个 key 值作为属性,子元素作为属性值的对象字面量来显示子元素列表,虽然这种用法的场景有限,参见Keyed Fragments,但是在这种情况下要注意生成的子元素重新渲染后在 DOM 中显示的顺序问题。

实际上浏览器在遍历一个字面量对象的时候会保持顺序一致,除非存在属性值可以被转换成整数值,这种属性值会排序并放在其他属性之前被遍历到,所以为了防止这种情况发生,可以在构建这个字面量的时候在 key 值前面加字符串前缀,比如:

render() {
  var items = {};

  this.props.results.forEach((result) => {
    // If result.id can look like a number (consider short hashes), then
    // object iteration order is not guaranteed. In this case, we add a prefix
    // to ensure the keys are strings.
    items['result-' + result.id] = <li>{result.text}</li>;
  });

  return (
    <ol>
      {items}
    </ol>
   );
}

(三)this.props.children

组件标签里面包含的子元素会通过 props.children 传递进来。
比如:

React.render(<Parent><Child /></Parent>, document.body);
React.render(<Parent><span>hello</span>{'world'}</Parent>, document.body);

HTML 元素会作为 React 组件对象、JS 表达式结果是一个文字节点,都会存入 Parent 组件的 props.children。
一般来说,可以直接将这个属性作为父组件的子元素 render:

const Parent = (props) => <div>{props.children}</div>;

props.children 通常是一个组件对象的数组,但是当只有一个子元素的时候,props.children 将是这个唯一的子元素,而不是数组了。

五、组件间通信

(一)父子组件间通信

就是通过 props 属性传递,在父组件给子组件设置 props,然后子组件就可以通过 props 访问到父组件的数据/方法,这样就搭建起了父子组件间通信的桥梁。

import React, { Component } from 'react';
import { render } from 'react-dom';

class GroceryList extends Component {
    handleClick(i) {
        console.log('You clicked: ' + this.props.items[i]);
    }

    render() {
        return (
            <div>
                {this.props.items.map((item, i)=>{
                    return (
                        <div onClick={this.handleClick.bind(this, i)} key={i}>{item}</div>
                    );
                })}
            </div>
        );
    }
}

render(
    <GroceryList items={['Apple','Banana','Cranberry']} />,mountNode  // mountNode表示渲染到哪个节点上
);

div 可以看作一个子组件,指定它的 onClick 事件调用父组件的方法。
父组件访问子组件?用 refs

(二)非父子组件间通信

使用全局事件 Pub/Sub 模式,在 componentDidMount 里面订阅事件,在 componentWillUnmount 里面取消订阅,当收到事件触发的时候调用 setState 更新 UI。

这种模式在复杂的系统里面可能会变得难以维护,所以看个人权衡是否将组件封装到大的组件,甚至整个页面或者应用就封装到一个组件。

一般来说,对于比较复杂的应用,推荐使用类似 Flux 这种单项数据流架构,参见Data Flow

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容