美文网首页
React Quick Start笔记

React Quick Start笔记

作者: gaoer1938 | 来源:发表于2017-03-26 11:09 被阅读0次

    最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习React,所以我打算每天坚持一篇React笔记。

    第一节:安装

    笔记原文

    尝试React

    可以在CodePen中尝试React,但是既然选择了React,那就直接下载一个脚手架吧。

    创建单页面应用

    使用官方脚手架可以快速的创建React单页面应用。它提供了开发环境,可以使用最新的js特性,提供了很好的开发方法以及能够给发布版本做优化。

    npm install -g create-react-app
    create-react-app hello-world
    cd hello-world
    npm start
    

    需要注意的是,这个脚手架只是一个前端的脚手架,你可以使用任何的后台。该脚手架使用了webpack,BabelESLint,但是你可以自行配置他们。

    将React添加到现有应用中

    这种情况下,可以在原有应用中,分离出部分单独的界面,用React来尝试。我们也推荐用构建工具来开发,现在的构建工具都包含一下几个工具:

    • 包管理工具:Yarn或者npm,它们可以让你尽情的安装、升级三方库
    • 打包工具:webpack或者Browserify,它们可以让你组件化开发、将资源打包、优化加载时间
    • 编译器:Babel,它将最新的js语法编译为浏览器兼容的版本。
    安装React
    npm init
    npm install --save react react-dom
    

    //todo 了解yarn和npm的区别。
    我只知道npm的使用方法,所以我这里我先选择npm。Yarn和Npm都是使用npm的仓库。

    使用ES6和JSX

    安装Babel就可以了。Babel安装指南说了如何在不同的环境中配置Babel。确认你安装了babel-preset-reactbabel-preset-es2015以及在.babelrc中正确配置好了。

    ES6和JSX的HELLO WORLD

    强烈建议用webpackBrowserify,这样你可以模块开发。

    import React from 'react';
    import ReactDOM from 'react-dom';
    
    ReactDOM.render(
      <h1>Hello, world!</h1>,
      document.getElementById('root')
    );
    

    这里将新元素渲染到了root元素中,所以html文件中必须包含这么一个元素。同理,你也可以将它渲染到通过其他Javascript UI库生成的节点中。

    开发版本和发布版本

    开发版本,react提供了很多有用的警告信息。但是发布的时候,需要用到发布版本。
    原文中罗列了Brunch,Browserify,Create React App,Rollup和webpack的优化方法。这里只记录Create React App和Webpack。

    Create React App

    如果使用了Create React App脚手架,使用npm run build命令,会在build目录中生成优化版本。

    Webpack

    根据这篇配置指导来配置,要配置DefinePluginUgligyJsPlugin

    使用CDN加速

    npm包里的dist目录里包含了编译好的库。可以直接拿来用。CDN加速如下:

    <script src="https://unpkg.com/react@15/dist/react.js"></script>
    <script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
    

    这两个文件只适用于开发,没有优化处理。
    适合发布版的优化版本如下所示:

    <script src="https://unpkg.com/react@15/dist/react.min.js"></script>
    <script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
    

    如果想要使用特定版本的react,直接替换里边的版本号15就可以了


    第二节 Hello World

    笔记原文

    最小的React例子如下:

    ReactDOM.render(
      <h1>Hello, world!</h1>,
      document.getElementById('root')
    );
    

    在root节点渲染h1标签

    关于js

    react是js库,所以要掌握好js。这里推荐js知识给大家。ES6的语法也是可以使用的,但是要谨慎些。推荐学习下箭头函数模版字符串letconst


    第三节 JSX介绍

    笔记原文

    show U the code:
    const element = <h1>Hello, world!</h1>;
    这个不是html也不是字符串的家伙就是JSX了,它是js的扩展。JSX可能会让你想到模版语言,但是它是不折不扣的js。JSX为React提供渲染所需的“元素”(element)。

    在JSX中插入表达式

    在JSX中插入js表达式需要用花括号{}括起来:

    function formatName(user) {
      return user.firstName + ' ' + user.lastName;
    }
    
    const user = {
      firstName: 'Harper',
      lastName: 'Perez'
    };
    
    const element = (
      <h1>
        Hello, {formatName(user)}!
      </h1>
    );
    
    ReactDOM.render(
      element,
      document.getElementById('root')
    );
    
    

    分行显示,有利于阅读;用小括号()括起来是为了防止编辑器自动插入分号;

    JSX中制定属性

    const element = <div tabIndex="0"></div>;
    
    const element = <img src={user.avatarUrl}></img>;
    

    以上两种都是可以的:提供一个字符串或者一个表达式。需要注意的是,不要将表达式既用花括号包裹,又用引号包裹,那就出错了。鱼({})和熊掌(‘’)兼得,可能啥也没有哈。(或许有个大bug)

    JSX中指定子元素

    const element = <img src={user.avatarUrl} />;
    
    const element = (
      <div>
        <h1>Hello!</h1>
        <h2>Good to see you here.</h2>
      </div>
    );
    

    如果没有子元素,标签可以马上关闭。如果有呢,就像html一样用就好了。
    注意:虽然JSX和html很像,但是React DOM 使用驼峰标记法转化html的属性。
    例如:class-->className,tabindex-->tabIndex

    JSX 防止注入攻击

    const title = response.potentiallyMaliciousInput;
    // This is safe:
    const element = <h1>{title}</h1>;
    

    将用户输入嵌入到JSX中是安全的。默认情况下,在渲染之前,React DOM会转义所有嵌入的值。这保证了只能插入你程序中写好的东西。还有所有的嵌入的都会转换为字符串,有效的防止了XXS(cross-site-scripting)攻击。

    JSX 代表对象

    babel会把JSX转换为React.createElement(),所以下面的代码块是一致的

    const element = (
      <h1 className="greeting">
        Hello, world!
      </h1>
    );
    
    const element = React.createElement(
      'h1',
      {className: 'greeting'},
      'Hello, world!'
    );
    

    React.createElement()还会帮你检查代码,最终会转换成如下对象:

    // Note: this structure is simplified
    const element = {
      type: 'h1',
      props: {
        className: 'greeting',
        children: 'Hello, world'
      }
    };
    

    这些对象,就叫做React元素了。用来渲染视图以及更新视图。

    提示:强烈建议你搜索下编辑器的“babel”语法方案(syntax scheme),它会让你的JSX和ES6代码高亮显示。


    第四节 渲染元素

    笔记原文
    React元素是构建React应用的最小代码块了。元素描述的就是要渲染的界面了。

    const element = <h1>Hello, world</h1>;
    

    React元素和html的dom是不一样的,相比之下,Rect元素更加轻量级。
    注意:元素和组件是不同的,组件是由元素构成的。

    将元素渲染到DOM中

    <div id="root"></div>
    

    你有一个id为root的div,通常React应用都会渲染到这个根结点里。如果你是将React结合到现成的应用里,你也可以有多个节点,用来渲染React元素。
    调用ReactDOM.render()将React元素渲染进DOM节点中。

    const element = <h1>Hello, world</h1>;
    ReactDOM.render(
      element,
      document.getElementById('root')
    );
    

    更新已经渲染的元素

    React元素是不可变的。一旦创建了就不能再更改了。就像是电影里的一帧图像,代表一个时间点固定的图像。
    以目前我们所学的,如果要更新界面,就要调用函数ReactDOM.render()

    function tick() {
      const element = (
        <div>
          <h1>Hello, world!</h1>
          <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
      );
      ReactDOM.render(
        element,
        document.getElementById('root')
      );
    }
    
    setInterval(tick, 1000);
    

    setInterval()回调,每隔一秒钟调用一次ReactDOM.render()
    注意:实际上,大多数React应用只会调用一次ReactDOM.render()。通常我们用state来显示动态UI。

    React 只更新需要更新的

    更新界面的时候RactDOM会和之前的元素进行比较,并且只会更新改变了的部分。所以上面的代码只会更新时间,其他节点都不会更新。写代码的时候就只要考虑如何显示,不用考虑怎么改变了。


    第五节 组件以及属性(props)

    date:20170329
    笔记原文
    组件将这个界面分割成几个独立的,可以重用的部分。开发的时候,可以单独的对一个模块进行思考。从概念上来说,组件很像js函数。它们接收参数(props)并且返回需要渲染的React元素。

    函数组件和类组件

    我们可以像定义js函数一样定义组件:

    function Welcome(props) {
      return <h1>Hello, {props.name}</h1>;
    }
    

    我们也可以用ES6类的语法来定义组件。

    class Welcome extends React.Component {
      render() {
        return <h1>Hello, {this.props.name}</h1>;
      }
    }
    

    这两者实现是都是一样的,但是类组件更加强大些,而函数组件会简介些。

    渲染组件

    之前我们一直渲染的DOM元素,如div。其实React也是可以呈现用户定义的组件:

    const element = <Welcome name="Sara" />;
    

    当React遇到用户定义的标记时,会将属性通过对象的方式传递给组件,这个对象就是props。

    function Welcome(props) {
      return <h1>Hello, {props.name}</h1>;
    }
    
    const element = <Welcome name="Sara" />;
    ReactDOM.render(
      element,
      document.getElementById('root')
    );
    

    以上代码,一目了然,直接将Welcome的name属性,封装到props对象里,并且传递给组件,组件在内部,就可以通过props对象,获取到外边传递过来的值。

    警告:组件名称首字母一定要大写。小写的是DOM原生标记,大写的代表组件,并且需要将组件引入到作用域中。

    组合组件

    组件可以引用组件。

    function Welcome(props) {
      return <h1>Hello, {props.name}</h1>;
    }
    
    function App() {
      return (
        <div>
          <Welcome name="Sara" />
          <Welcome name="Cahal" />
          <Welcome name="Edite" />
        </div>
      );
    }
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    

    通常新的React应用只有一个App组件。但是如果你要把React结合到已存在的项目中,最好就是自底向上的方式开发。

    警告:组件必须返回一个元素,所以上边的例子需要用div包裹三个Welcome组件。

    提取组件

    不要担心把组件分割为更小的组件。原文列举了如何把评论界面提取出头像组件,用户信息组件。
    细分组件保证了代码重用和降低代码复杂度。

    属性只读

    Props对象的值都是不能变化的。一种“纯”的函数是不会改变输入的函数,而改变输入参数的函数都是不纯的函数。React是很灵活的框架,但是有一个很苛刻的条件:所有的react组件都必须像纯函数一样,不改变输入的props。
    界面是时常要变的,所以就有了State概念。state可以在用户交互的时候,网络响应或者其他情况下,作出界面改变。


    State和生命周期

    date:20170330
    笔记原文
    这一节将用state实现时钟组件。最终要求要实现的效果如下:

    ReactDOM.render(
      <Clock />,
      document.getElementById('root')
    );
    

    原来的代码如下:

    function Clock(props) {
      return (
        <div>
          <h1>Hello, world!</h1>
          <h2>It is {props.date.toLocaleTimeString()}.</h2>
        </div>
      );
    }
    
    function tick() {
      ReactDOM.render(
        <Clock date={new Date()} />,
        document.getElementById('root')
      );
    }
    
    setInterval(tick, 1000);
    

    state和props很相似,但是state是私有的,并且是控件自己维护的。之前我们说过的,类组件有一些另外的功能。state是只支持类组件的一种特性。

    将函数组件转化为类组件

    通过以下5步可以将函数组件转化为类组件。

    1. 创建一个名字相同的ES6类,该类继承自React.Component
    2. 添加一个名为render()的函数。
    3. 将函数体内的代码添加到render()函数里。
    4. 将代码里的props替换为this.props
    5. 将剩下的函数体删除。
    class Clock extends React.Component {
      render() {
        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
          </div>
        );
      }
    }
    

    在类中添加State

    1. render()中的this.props.date替换为this.state.date
    2. 类的构造函数中初始化state
    class Clock extends React.Component {
      constructor(props) {
        super(props);
        this.state = {date: new Date()};
      }
    
      render() {
        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
          </div>
        );
      }
    }
    

    注意这里我们通过构造函数来传递props

    1. 将原来代码里将Clock的date属性删除,最后的代码如下:
    class Clock extends React.Component {
      constructor(props) {
        super(props);
        this.state = {date: new Date()};
      }
    
      render() {
        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
          </div>
        );
      }
    }
    
    ReactDOM.render(
      <Clock />,
      document.getElementById('root')
    );
    

    在代码里添加生命周期方法

    本例中,需要在组件加载(mounting)好之后添加一个计时器,然后在卸载(unmounting)的时候将计时器停止。
    componentDidMount()componentWillUnmount()这两个就是生命周期里的回调方法。

     componentDidMount() {
        this.timerID = setInterval(
          () => this.tick(),
          1000
        );
      }
    

    这里,我们将timerID保存到this中。this.props是React自身维护的,this.state具有特殊的含义,一般都用在需要更新界面的地方。我们可以在类里边随意添加不需要在界面上显示的数据。也就是说,不是在render()中显示的都不需要用state。
    实现tick(),代码如下:

      tick() {
        this.setState({
          date: new Date()
        });
      }
    

    正确的使用State

    这里强调3点关于setState()的内容

    不要直接修改State

    如果直接通过this.state.XXX=XXX来修改内容,界面是不会刷新的。用setState方法。

    State更新是异步的

    如下的代码是有问题。

    this.setState({
      counter: this.state.counter + this.props.increment,
    });
    

    this.statethis.props可能异步的更新的,所以不应该依赖它们来计算新的state。
    这里要修改的话,就把setState的参数,从对象改为函数,代码如下所示:

    this.setState((prevState, props) => ({
      counter: prevState.counter + props.increment
    }));
    

    函数第一个参数是之前的state,第二个参数是更新之后的props。上面的函数是箭头函数,也可以普通函数。

    State的更新是合并的

    这个可以理解,不然,难道要每次更新就把所有的state都列出来吗。

      constructor(props) {
        super(props);
        this.state = {
          posts: [],
          comments: []
        };
      }
      
      componentDidMount() {
        fetchPosts().then(response => {
          this.setState({
            posts: response.posts
          });
        });
    
        fetchComments().then(response => {
          this.setState({
            comments: response.comments
          });
        });
      }
    

    以上的代码,都是各自跟新自己的属性,其他的属性都会毫发无伤的维持原样。这就是合并的含义。

    向下的数据流

    父控件和子控件都不知道一个控件是包含状态的还是不包含状态的,也不关心是函数组件还是类组件。所以state只属于自身控件的,其他控件都访问不了。
    一个组件可以通过propsstate将数据传递到子控件中。

    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    <FormattedDate date={this.state.date} />
    function FormattedDate(props) {
      return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
    }
    

    FormattedDate并不关心props中的数据是来自哪里的,可能是state,props或者手写的。
    这里,数据流就是自顶向下或者说是单向的。


    响应事件

    20170331
    笔记原文
    React响应事件和DOM响应事件是差不多的,但是有些语法差异:
    * React的事件命名采用驼峰法,而不是小写
    * 在JSX中传递的是一个函数,而不是字符串

    <button onclick="activateLasers()">
      Activate Lasers
    </button>
    
    <button onClick={activateLasers}>
      Activate Lasers
    </button>
    

    另一个差异就是不能通过返回false来阻止事件,必须手动调用preventDefault
    例如以下代码可以防止a标签打开新页面:

    <a href="#" onclick="console.log('The link was clicked.'); return false">
      Click me
    </a>
    

    在React中,可以这么写:

    function ActionLink() {
      function handleClick(e) {
        e.preventDefault();
        console.log('The link was clicked.');
      }
    
      return (
        <a href="#" onClick={handleClick}>
          Click me
        </a>
      );
    }
    

    这里e是一个合成的事件,React的事件都是通过W3C 定义,所以你也不用担心兼容性。详情见SyntheticEvent.
    React中不需要调用addEventListener,只需要在元素初始化的时候提供一个监听函数。
    如果你通过ES6类来定义组件,类中本身有一组模版函数可以使用。例如Toggle组件可以让用户改变打开和关闭状态:

    class Toggle extends React.Component {
      constructor(props) {
        super(props);
        this.state = {isToggleOn: true};
    
        // This binding is necessary to make `this` work in the callback
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        this.setState(prevState => ({
          isToggleOn: !prevState.isToggleOn
        }));
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            {this.state.isToggleOn ? 'ON' : 'OFF'}
          </button>
        );
      }
    }
    
    ReactDOM.render(
      <Toggle />,
      document.getElementById('root')
    );
    

    必须要注意的是JSX回调函数中的this关键字。在js中,类方法并没有默认绑定
    到this中。所以如果忘了绑定,那么this.handleClickthis就是为定义的。
    这个也不是React中的特性,而是js函数运行机理的一部分
    如果引用的时候不用(),例如onClick={this.handleClick},那么你就应该要绑定下。
    两种方法可以来绑定this。第一种是采用试验性功能属性初始化语法(propery initializer syntax)

    class LoggingButton extends React.Component {
      // This syntax ensures `this` is bound within handleClick.
      // Warning: this is *experimental* syntax.
      handleClick = () => {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Click me
          </button>
        );
      }
    }
    

    这个语法在官方脚手架Create-Reac-App中默认支持。
    第二种方法是使用箭头函数

    class LoggingButton extends React.Component {
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        // This syntax ensures `this` is bound within handleClick
        return (
          <button onClick={(e) => this.handleClick(e)}>
            Click me
          </button>
        );
      }
    }
    

    这个方法的弊端是每次渲染LoggingButton的时候都会生成一个回调函数。多数情况下是不影响的。但是如果要通过props将回调函数传递给子控件的时候,就会造成多次渲染。为了避免这一类的性能问题,建议用第一种方法或者在构造函数中初始化。


    条件渲染

    20170331
    笔记原文
    React中的条件渲染和JS中的条件渲染是一样的。使用[if][js-if]或者条件操作符根据条件来呈现不同的状态。

    function UserGreeting(props) {
      return <h1>Welcome back!</h1>;
    }
    
    function GuestGreeting(props) {
      return <h1>Please sign up.</h1>;
    }
    
    function Greeting(props) {
      const isLoggedIn = props.isLoggedIn;
      if (isLoggedIn) {
        return <UserGreeting />;
      }
      return <GuestGreeting />;
    }
    
    ReactDOM.render(
      // Try changing to isLoggedIn={true}:
      <Greeting isLoggedIn={false} />,
      document.getElementById('root')
    );
    

    以上的代码就是依靠isLoggedIn属性来判断渲染不同的界面。

    元素变量

    可以通过变量来存储元素,以达到部分界面更新,而其他界面不变的目的。

    function LoginButton(props) {
      return (
        <button onClick={props.onClick}>
          Login
        </button>
      );
    }
    
    function LogoutButton(props) {
      return (
        <button onClick={props.onClick}>
          Logout
        </button>
      );
    }
    

    下面的代码,我们生成了一个包含状态的组件,命名为LoginControl

    class LoginControl extends React.Component {
      constructor(props) {
        super(props);
        this.handleLoginClick = this.handleLoginClick.bind(this);
        this.handleLogoutClick = this.handleLogoutClick.bind(this);
        this.state = {isLoggedIn: false};
      }
    
      handleLoginClick() {
        this.setState({isLoggedIn: true});
      }
    
      handleLogoutClick() {
        this.setState({isLoggedIn: false});
      }
    
      render() {
        const isLoggedIn = this.state.isLoggedIn;
    
        let button = null;
        if (isLoggedIn) {
          button = <LogoutButton onClick={this.handleLogoutClick} />;
        } else {
          button = <LoginButton onClick={this.handleLoginClick} />;
        }
    
        return (
          <div>
            <Greeting isLoggedIn={isLoggedIn} />
            {button}
          </div>
        );
      }
    }
    
    ReactDOM.render(
      <LoginControl />,
      document.getElementById('root')
    );
    

    行内if操作符结合&&操作符

    你可以在JSX中结合任何的表达式,表达式需要用花括号包裹起来。这是语法。结合&&可以实现条件渲染。

    function Mailbox(props) {
      const unreadMessages = props.unreadMessages;
      return (
        <div>
          <h1>Hello!</h1>
          {unreadMessages.length > 0 &&
            <h2>
              You have {unreadMessages.length} unread messages.
            </h2>
          }
        </div>
      );
    }
    
    const messages = ['React', 'Re: React', 'Re:Re: React'];
    ReactDOM.render(
      <Mailbox unreadMessages={messages} />,
      document.getElementById('root')
    );
    

    这个原因很简单哈。短路与就是这么神奇的。true && expression返回truefalse && expression返回false.因此,条件满足了&&后面的内容就会渲染出来,否则就直接忽略了。

    行内If-Else条件操作符

    另一个条件渲染的方法是JS的条件操作符condition?true:false

    render() {
      const isLoggedIn = this.state.isLoggedIn;
      return (
        <div>
          The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
        </div>
      );
    }
    

    如果看不清,就换一种样式,看起来会明显些。

    render() {
      const isLoggedIn = this.state.isLoggedIn;
      return (
        <div>
          {isLoggedIn ? (
            <LogoutButton onClick={this.handleLogoutClick} />
          ) : (
            <LoginButton onClick={this.handleLoginClick} />
          )}
        </div>
      );
    }
    

    如果条件比较复杂的时候,记得把组件抽取出子组件

    隐藏组件

    通过返回null的方式,来阻止渲染。

    function WarningBanner(props) {
      if (!props.warn) {
        return null;
      }
    
      return (
        <div className="warning">
          Warning!
        </div>
      );
    }
    
    class Page extends React.Component {
      constructor(props) {
        super(props);
        this.state = {showWarning: true}
        this.handleToggleClick = this.handleToggleClick.bind(this);
      }
    
      handleToggleClick() {
        this.setState(prevState => ({
          showWarning: !prevState.showWarning
        }));
      }
    
      render() {
        return (
          <div>
            <WarningBanner warn={this.state.showWarning} />
            <button onClick={this.handleToggleClick}>
              {this.state.showWarning ? 'Hide' : 'Show'}
            </button>
          </div>
        );
      }
    }
    
    ReactDOM.render(
      <Page />,
      document.getElementById('root')
    );
    

    render函数返回null的时候并不会触发组件的生命周期方法。但是componentWillUpdatecomponentDidUpdate还是会被调用到。


    列表与键(key)

    20170401
    [笔记原文][]
    我们先看看js如何改变一个数组,直接上代码:

    const numbers = [1, 2, 3, 4, 5];
    const doubled = numbers.map((number) => number * 2);
    console.log(doubled);
    

    在控制台输出[2,4,6,8,10],React中对数组中的[元素(element)][react-element]的处理也是如此。

    渲染多个组件

    还是使用map函数,遍历numbers数组,每次返回一个li元素。最后我们把数组存储在listItem中。

    const numbers = [1, 2, 3, 4, 5];
    const listItems = numbers.map((number) =>
      <li>{number}</li>
    );
    

    然后我们将listItems<ul>元素包裹起来,最后[渲染到DOM][react-render]中。

    ReactDOM.render(
      <ul>{listItems}</ul>,
      document.getElementById('root')
    );
    

    基本的列表组件

    通常我们需要把在[组件][react-component]中渲染一个列表。
    重构以上的代码:

    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        <li>{number}</li>
      );
      return (
        <ul>{listItems}</ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    

    在跑这段代码的时候,它会警告你需要给列表项提供一个键(key)。键的作用参看下一节,我们修改这个问题之后的代码如下:

    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        <li key={number.toString()}>
          {number}
        </li>
      );
      return (
        <ul>{listItems}</ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    

    键(keys)

    键是react用来判断列表项是否有改变,添加以及删除。所以我们在列表数据中,需要添加一个固定的项:

    const numbers = [1, 2, 3, 4, 5];
    const listItems = numbers.map((number) =>
      <li key={number.toString()}>
        {number}
      </li>
    );
    

    在一个列表中最好指定一个唯一确定的数值。所以通常使用数据的ID就可以了。

    const todoItems = todos.map((todo) =>
      <li key={todo.id}>
        {todo.text}
      </li>
    );
    

    如果没有id,那也可以使用索引。

    const todoItems = todos.map((todo, index) =>
      // Only do this if items have no stable IDs
      <li key={index}>
        {todo.text}
      </li>
    );
    

    如果列表的数据顺序会变的情况下,我们是不推荐使用索引的,因为速度慢。参看[关于键的高级进阶][react-key-explanation]。

    与键一起抽取组件

    键只有在列表的上下文中才有意义。
    例如,我们抽取了ListItem组件,我们需要将键同时抽出,放置在ListItem上,而不是<li>标签上。
    错误示例:

    function ListItem(props) {
      const value = props.value;
      return (
        // Wrong! There is no need to specify the key here:
        <li key={value.toString()}>
          {value}
        </li>
      );
    }
    
    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        // Wrong! The key should have been specified here:
        <ListItem value={number} />
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    

    正确示例:

    function ListItem(props) {
      // Correct! There is no need to specify the key here:
      return <li>{props.value}</li>;
    }
    
    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        // Correct! Key should be specified inside the array.
        <ListItem key={number.toString()}
                  value={number} />
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    
    

    总之,map()里的元素需要键。

    在一个列表中键必须唯一

    同一个列表的键必须唯一,但是在全局上,不同的列表就没有限制了。
    由于key是react的机制,key的值并不会传递到组件props里,所以如果需要使用数据的时候,要再传递一次。

    const content = posts.map((post) =>
      <Post
        key={post.id}
        id={post.id}
        title={post.title} />
    );
    

    Post组件可以获取到props.id,但是获取不到props.key

    在JSX中嵌入map()

    之前的例子我们用一个变量来存储列表元素。JSX是可以[嵌入任何表达式][react-embed-expressions]的,所以我们将map()函数直接嵌入到JSX中。

    function NumberList(props) {
      const numbers = props.numbers;
      return (
        <ul>
          {numbers.map((number) =>
            <ListItem key={number.toString()}
                      value={number} />
          )}
        </ul>
      );
    }
    

    这种方法很清晰,但是不能滥用。如果map()中嵌入太多,就需要[抽取组件][react-extract-component]了


    表单

    date:20170402
    笔记原文
    表单和其他元素有些不同,因为表单包含了一些交互。通常我们会用js来控制提交信息。所以React提供了控制组件

    控制组件

    html中的表单组件通常都是自己维护用户输入,但是在React中,只能用setState()

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {value: ''};
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert('A name was submitted: ' + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Name:
              <input type="text" value={this.state.value} onChange={this.handleChange} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    

    每当表单控件变化的时候,就会触发onChange事件,从而调用监听方法。在监听方法中,改变state中的值。

    textarea标签

    React中,<textarea>标签也会有value属性,这样使用起来会比较方便。

    class EssayForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: 'Please write an essay about your favorite DOM element.'
        };
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert('An essay was submitted: ' + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Name:
              <textarea value={this.state.value} onChange={this.handleChange} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    

    select 标签

    在html中,select能够生成一个下菜单:

    <select>
      <option value="grapefruit">Grapefruit</option>
      <option value="lime">Lime</option>
      <option selected value="coconut">Coconut</option>
      <option value="mango">Mango</option>
    </select>
    

    注意,Coconut这一项中,具有select属性来指定默认选择。在React中,是value属性。所以我们只要更新state就可以了。

    class FlavorForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {value: 'coconut'};
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert('Your favorite flavor is: ' + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Pick your favorite La Croix flavor:
              <select value={this.state.value} onChange={this.handleChange}>
                <option value="grapefruit">Grapefruit</option>
                <option value="lime">Lime</option>
                <option value="coconut">Coconut</option>
                <option value="mango">Mango</option>
              </select>
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    

    总之,<input type="text">,<textarea><select>都是类似的机制,

    监听函数处理多个表单元素输入

    如果要实现这样的功能,可以在表单元素里添加name属性,监听函数可以根据event.target.name的值来作出不同的处理。

    class Reservation extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isGoing: true,
          numberOfGuests: 2
        };
    
        this.handleInputChange = this.handleInputChange.bind(this);
      }
    
      handleInputChange(event) {
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name;
    
        this.setState({
          [name]: value
        });
      }
    
      render() {
        return (
          <form>
            <label>
              Is going:
              <input
                name="isGoing"
                type="checkbox"
                checked={this.state.isGoing}
                onChange={this.handleInputChange} />
            </label>
            <br />
            <label>
              Number of guests:
              <input
                name="numberOfGuests"
                type="number"
                value={this.state.numberOfGuests}
                onChange={this.handleInputChange} />
            </label>
          </form>
        );
      }
    }
    

    这里要留意下我们使用了ES6的计算属性名称(computed property name)。下面两段代码都是一样的效果。

    this.setState({
      [name]: value
    });
    
    var partialState = {};
    partialState[name] = value;
    this.setState(partialState);
    

    同时,由于setState()自动的混合部分属性,所以也只是更新变化的部分。

    替换控制组件

    如果有时候写控制组件太麻烦了,或者说要从非React代码变更为React的时候,你也可以考虑下非控制组件,这是控制组件的替代技术。


    玩转State

    date:20170404
    笔记原文

    通常很多组件都要使用同一条数据,这时候,就要把数据提出来,放在这些控件最近的父组件中。
    举个温度计例子:BoilingVerdict组件接收一个温度参数,返回水是否会沸腾。

    function BoilingVerdict(props) {
      if (props.celsius >= 100) {
        return <p>The water would boil.</p>;
      }
      return <p>The water would not boil.</p>;
    }
    

    然后Calculator组件渲染一个<input>使用户可以输入温度数据,并且把温度保存在this.state.temperature中,然后渲染BoilingVerdict组件,用来指示当前温度。

    class Calculator extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {temperature: ''};
      }
    
      handleChange(e) {
        this.setState({temperature: e.target.value});
      }
    
      render() {
        const temperature = this.state.temperature;
        return (
          <fieldset>
            <legend>Enter temperature in Celsius:</legend>
            <input
              value={temperature}
              onChange={this.handleChange} />
            <BoilingVerdict
              celsius={parseFloat(temperature)} />
          </fieldset>
        );
      }
    }
    

    添加第二个输入框

    新需求是添加一个华氏度的输入框,要求华氏温度和摄氏温度同时变化。刚开始,我们从Calculator中提取TemperatureInput组件,添加scaleprop属性,来指示温度的单位。

    const scaleNames = {
      c: 'Celsius',
      f: 'Fahrenheit'
    };
    
    class TemperatureInput extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {temperature: ''};
      }
    
      handleChange(e) {
        this.setState({temperature: e.target.value});
      }
    
      render() {
        const temperature = this.state.temperature;
        const scale = this.props.scale;
        return (
          <fieldset>
            <legend>Enter temperature in {scaleNames[scale]}:</legend>
            <input value={temperature}
                   onChange={this.handleChange} />
          </fieldset>
        );
      }
    }
    

    好了,我们可以重新渲染Calculator了。

    class Calculator extends React.Component {
      render() {
        return (
          <div>
            <TemperatureInput scale="c" />
            <TemperatureInput scale="f" />
          </div>
        );
      }
    }
    

    现在虽然有了两个输入框,但是数据还是不会更新,同时BoilingVerdict也没有显示出来,因为输入的温度,都在TemperatureInput里,

    添加转换功能

    首先,写几个功能函数,实现温度互相转换:

    function toCelsius(fahrenheit) {
      return (fahrenheit - 32) * 5 / 9;
    }
    
    function toFahrenheit(celsius) {
      return (celsius * 9 / 5) + 32;
    }
    
    function tryConvert(temperature, convert) {
      const input = parseFloat(temperature);
      if (Number.isNaN(input)) {
        return '';
      }
      const output = convert(input);
      const rounded = Math.round(output * 1000) / 1000;
      return rounded.toString();
    }
    
    

    提出State

    目前,两个TemperatureInput控件里的数据都是在它们自己的控件中。这是满足不了需求的。在React中,State共享是将数据提出到最近的父容器中。我们称之为提出State。我们需要将TemperatureInput中的state提出到Calculator。这样,两个输入控件具有相同的数据源,能够实现同步变化的需求。
    一步一步来解析如何实现这个功能。
    第一步,将state替换为prop,从Calculator传递数据到输入控件中。

    render() {
        // Before: const temperature = this.state.temperature;
        const temperature = this.props.temperature;
    

    我们知道,props是只读的。如果是state的话,直接调用this.setState()就好了。但是prop的话就没有办法控制了。
    在React中,类比之前的控制组件,就像<input>标签有valueonChange属性,所以TemperatureInput可以从父控件Caculator里传入temperatureonTemperatureChange属性,这样,TemperatureInput想要跟新温度的时候,就可以通过调用this.props.onTemperatureChange

      handleChange(e) {
        // Before: this.setState({temperature: e.target.value});
        this.props.onTemperatureChange(e.target.value);
    

    这里函数名称是可以自己随便定义的,而不是定死的。onTemperatureChange作为回调,是从父控件与温度数据一同传进来的。调用的时候,就会相应的改变父容器的state,然后更新视图。

    class TemperatureInput extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
      }
    
      handleChange(e) {
        this.props.onTemperatureChange(e.target.value);
      }
    
      render() {
        const temperature = this.props.temperature;
        const scale = this.props.scale;
        return (
          <fieldset>
            <legend>Enter temperature in {scaleNames[scale]}:</legend>
            <input value={temperature}
                   onChange={this.handleChange} />
          </fieldset>
        );
      }
    }
    

    Calculator组件中,将温度和单位存储在state中。这些state是从input组件里提取出来的。这是满足需求的最少数据集。例如,当我们在摄氏度一栏输入37,数据如下:

    {
      temperature: '37',
      scale: 'c'
    }
    

    当我们在华氏度里输入212,数据如下:

    {
      temperature: '212',
      scale: 'f'
    }
    

    我们之前存储了两个输入值,但是是没有必要的。我们完全可以从一个输入里推出另一个数据。

    class Calculator extends React.Component {
      constructor(props) {
        super(props);
        this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
        this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
        this.state = {temperature: '', scale: 'c'};
      }
    
      handleCelsiusChange(temperature) {
        this.setState({scale: 'c', temperature});
      }
    
      handleFahrenheitChange(temperature) {
        this.setState({scale: 'f', temperature});
      }
    
      render() {
        const scale = this.state.scale;
        const temperature = this.state.temperature;
        const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
        const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
    
        return (
          <div>
            <TemperatureInput
              scale="c"
              temperature={celsius}
              onTemperatureChange={this.handleCelsiusChange} />
            <TemperatureInput
              scale="f"
              temperature={fahrenheit}
              onTemperatureChange={this.handleFahrenheitChange} />
            <BoilingVerdict
              celsius={parseFloat(celsius)} />
          </div>
        );
      }
    }
    

    好了,功能实现了。我们总结下,我们具体做了哪些工作:

    • TemperatureInput组件中,<input>中指定onChange的回调函数
    • 在上述onChange回调中,执行父容器提供的回调this.props.onTemperatureChange(),将变化值传递给父容器。
    • 两个输入控件都有自己的回调函数
    • 输入控件的回调函数中,将子控件传递过来的新的数据,通过setState来重新设置,
    • 根据当前的温度和单位,渲染界面

    他山之石

    React奉行的是单一数据源原则。如果数据可能要有变化,那就使用State。如果这个数据其他控件需要用到,那么就提出State。如果要在不同的控件中同步数据,那么就要遵循单向数据流
    从数据上来说,如果一个数据可以从其他数据中计算得到,那么就不用保存在state中。这个例子中,我们只是保存了一个输入的数据。
    React提供了一套React调试开发工具来查看属性和状态更新,有利于调试代码。


    组合VS继承

    date:20170405
    笔记原文

    建议不用继承来复用代码,而是用组合模式。

    控制

    有些组件并不知道他们要包含的子组件。例如幻灯片和对话框就是这样。
    我们建议这类组件为这类组件创建特殊的prop属性。

    function FancyBorder(props) {
      return (
        <div className={'FancyBorder FancyBorder-' + props.color}>
          {props.children}
        </div>
      );
    }
    

    这使得其他组件可以通过在JSX中包含标签来传递任何的子标签。

    function WelcomeDialog() {
      return (
        <FancyBorder color="blue">
          <h1 className="Dialog-title">
            Welcome
          </h1>
          <p className="Dialog-message">
            Thank you for visiting our spacecraft!
          </p>
        </FancyBorder>
      );
    }
    

    <FancyBorder>里的任意JSX标签都会通过{props.children}属性传递到FancyBorder中。由于在FancyBorder里的<div>中渲染了{props.children},最后这些子标签就出现在了页面中。
    有时候,我们需要在组件里定义插槽,通过插入不同的组件,就可以实现不同的效果。这样就不用使用props.children了。

    function SplitPane(props) {
      return (
        <div className="SplitPane">
          <div className="SplitPane-left">
            {props.left}
          </div>
          <div className="SplitPane-right">
            {props.right}
          </div>
        </div>
      );
    }
    
    function App() {
      return (
        <SplitPane
          left={
            <Contacts />
          }
          right={
            <Chat />
          } />
      );
    }
    

    这里我们像传递数据一样,传递控件。

    特殊化

    有时候我们需要将一些组件特殊化。例如欢迎对话框是特殊化的对话框了。在React中,还是通过组合的方式,将一个一般性的控件,渲染为特殊化的控件。

    function Dialog(props) {
      return (
        <FancyBorder color="blue">
          <h1 className="Dialog-title">
            {props.title}
          </h1>
          <p className="Dialog-message">
            {props.message}
          </p>
        </FancyBorder>
      );
    }
    
    function WelcomeDialog() {
      return (
        <Dialog
          title="Welcome"
          message="Thank you for visiting our spacecraft!" />
      );
    }
    

    组合也同样适用于类组件:

    function Dialog(props) {
      return (
        <FancyBorder color="blue">
          <h1 className="Dialog-title">
            {props.title}
          </h1>
          <p className="Dialog-message">
            {props.message}
          </p>
          {props.children}
        </FancyBorder>
      );
    }
    
    class SignUpDialog extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.handleSignUp = this.handleSignUp.bind(this);
        this.state = {login: ''};
      }
    
      render() {
        return (
          <Dialog title="Mars Exploration Program"
                  message="How should we refer to you?">
            <input value={this.state.login}
                   onChange={this.handleChange} />
            <button onClick={this.handleSignUp}>
              Sign Me Up!
            </button>
          </Dialog>
        );
      }
    
      handleChange(e) {
        this.setState({login: e.target.value});
      }
    
      handleSignUp() {
        alert(`Welcome aboard, ${this.state.login}!`);
      }
    }
    

    那么继承呢?

    facebook通过成百上千个组件构成,但是还没有用到需要用到继承的情况。Props和组合已经具有足够的灵活性和安全性来自定义控件。记住,porps可以传递原型数据,React元素和函数。
    如果想要服用功能函数,建议用单独的js模块来分离代码,实现复用。


    Think in React笔记

    date:20170405
    笔记原文
    得益于react的模块化,我们可以很方便的开发大型web应用。

    从设计稿开始

    1. 将设计稿根据功能分块处理。根据单一性原则,分为不能再精简的单一模块。
    2. 用固定的数据生成静态页面,因为我们不需要思考交互。
      • 简单的页面可以通过自顶向下的方式开发,复杂的页面可以通过自底向上的方式开发
      • 这里不需要用到state,通过props将数据传递到模块中。
    3. 确认数量最少但是能满足条件的state
      • DRY原则:能够从元数据获取到的信息,就不要提炼出state。如TodoList的长度,直接使用数组的长度,而不是提炼出新的state。
      • 如何分辨需要state还是props:是否是父控件通过props传递进来的;是否是一成不变的信息;是否可以通过其他信息得到。
    4. 确认state应该在哪里,react的数据是单向的,新手可以通过以下步骤去掌握它。
      • 确认每个组件依赖的state
      • 找到一个共同的父控件
      • 共同父控件或者该父控件的父控件拥有这个state
      • 如果你不知道将这个state放在哪里的时候,就新建一个控件,专门来维护这个state,然后将这个控件放置在共同父控件的上层
    5. 添加反向数据流,通过底层控件改变上层控件的状态

    这就是React

    代码写的比平常多了,但是在构建大项目的时候,你就会发现好处了


    相关文章

      网友评论

          本文标题:React Quick Start笔记

          本文链接:https://www.haomeiwen.com/subject/ivahottx.html