React Quick Start笔记

最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习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,Babel和ESLint,但是你可以自行配置他们。

将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-react和babel-preset-es2015以及在.babelrc中正确配置好了。

ES6和JSX的HELLO WORLD

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

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  

Hello, world!

, 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加速如下:



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



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


第二节 Hello World

笔记原文

最小的React例子如下:

ReactDOM.render(
  

Hello, world!

, document.getElementById('root') );

在root节点渲染h1标签

关于js

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


第三节 JSX介绍

笔记原文

show U the code:
const element =

Hello, world!

;
这个不是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 = (
  

Hello, {formatName(user)}!

); ReactDOM.render( element, document.getElementById('root') );

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

JSX中制定属性

const element = 
; const element = ;

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

JSX中指定子元素

const element = ;

const element = (
  

Hello!

Good to see you here.

);

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

JSX 防止注入攻击

const title = response.potentiallyMaliciousInput;
// This is safe:
const element = 

{title}

;

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

JSX 代表对象

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

const element = (
  

Hello, world!

);
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 = 

Hello, world

;

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

将元素渲染到DOM中

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

const element = 

Hello, world

; ReactDOM.render( element, document.getElementById('root') );

更新已经渲染的元素

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

function tick() {
  const element = (
    

Hello, world!

It is {new Date().toLocaleTimeString()}.

); 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 

Hello, {props.name}

; }

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

class Welcome extends React.Component {
  render() {
    return 

Hello, {this.props.name}

; } }

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

渲染组件

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

const element = ;

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

function Welcome(props) {
  return 

Hello, {props.name}

; } const element = ; ReactDOM.render( element, document.getElementById('root') );

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

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

组合组件

组件可以引用组件。

function Welcome(props) {
  return 

Hello, {props.name}

; } function App() { return (
); } ReactDOM.render( , document.getElementById('root') );

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

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

提取组件

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

属性只读

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


State和生命周期

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

ReactDOM.render(
  ,
  document.getElementById('root')
);

原来的代码如下:

function Clock(props) {
  return (
    

Hello, world!

It is {props.date.toLocaleTimeString()}.

); } function tick() { ReactDOM.render( , 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 (
      

Hello, world!

It is {this.props.date.toLocaleTimeString()}.

); } }

在类中添加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 (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } }

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

  1. 将原来代码里将Clock的date属性删除,最后的代码如下:
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } } ReactDOM.render( , 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将数据传递到子控件中。

It is {this.state.date.toLocaleTimeString()}.

function FormattedDate(props) { return

It is {props.date.toLocaleTimeString()}.

; }

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


响应事件

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



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


  Click me

在React中,可以这么写:

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    
      Click me
    
  );
}

这里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 (
      
    );
  }
}

ReactDOM.render(
  ,
  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 (
      
    );
  }
}

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

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // This syntax ensures `this` is bound within handleClick
    return (
      
    );
  }
}

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


条件渲染

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

function UserGreeting(props) {
  return 

Welcome back!

; } function GuestGreeting(props) { return

Please sign up.

; } function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return ; } return ; } ReactDOM.render( // Try changing to isLoggedIn={true}: , document.getElementById('root') );

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

元素变量

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

function LoginButton(props) {
  return (
    
  );
}

function LogoutButton(props) {
  return (
    
  );
}

下面的代码,我们生成了一个包含状态的组件,命名为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 = ;
    } else {
      button = ;
    }

    return (
      
{button}
); } } ReactDOM.render( , document.getElementById('root') );

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

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

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    

Hello!

{unreadMessages.length > 0 &&

You have {unreadMessages.length} unread messages.

}
); } const messages = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render( , document.getElementById('root') );

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

行内If-Else条件操作符

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

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

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

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    
{isLoggedIn ? ( ) : ( )}
); }

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

隐藏组件

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

function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }

  return (
    
Warning!
); } 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 (
); } } ReactDOM.render( , 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) =>
  
  • {number}
  • );

    然后我们将listItems

      元素包裹起来,最后[渲染到DOM][react-render]中。

      ReactDOM.render(
        
        {listItems}
      , document.getElementById('root') );

      基本的列表组件

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

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

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

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

      键(keys)

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

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

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

      const todoItems = todos.map((todo) =>
        
    • {todo.text}
    • );

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

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

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

      与键一起抽取组件

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

    • 标签上。
      错误示例:

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

      正确示例:

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

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

      在一个列表中键必须唯一

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

      const content = posts.map((post) =>
        
      );
      

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

      在JSX中嵌入map()

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

      function NumberList(props) {
        const numbers = props.numbers;
        return (
          
        {numbers.map((number) => )}
      ); }

      这种方法很清晰,但是不能滥用。如果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 (
            
      ); } }

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

      textarea标签

      React中,