React Note - Basic

February 8, 2017

React 学习笔记(基础篇)。

安装

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

实践:create 这一步会同时执行 npm install 因此有失败的可能,多尝试几次就成功了。

这个程序跟 vue-loader 很像,会造出一个简单的手脚架,包含了 Babel 编译器以及打包工具等等。但是细看它的 package.json 文件并没有包含上述内容:

"devDependencies": {
  "react-scripts": "0.8.5"
},
"dependencies": {
  "react": "^15.4.2",
  "react-dom": "^15.4.2"
}

因此,跟 vue-loader 不一样的是,react 这个手脚架把无关内容都封装了。这么做我觉得有利有弊:它让人用起来更方便,然而不可能达到直接使用原组件的自由度了。相比之下,这里我更喜欢 vue-loader 的处理方式。

Hello World

最简示例:

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

JSX 语法

JSX 是 JavaScript 的一种语法扩展,实际上可以看做是语法糖。通过编译器,以下语法:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

相当于:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world'
  }
};

后者就是编译后的结果,JSX 语法块变成了一个对象(称之为 React element)。

(JB 家的 IDE 已经对 JSX 语法提供了默认支持,不然这篇笔记就到此为止了)

JSX 支持一些稍微高级的用法,如:

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:

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

元素

上面有说到 React element(元素),元素的概念与组件不同:元素是组件的组成部分。

元素渲染

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

显然,它掌控了 DOM 中一个 ID 为 root 的节点,并往里面插入了元素。

元素更新

已创建的元素是无法更新属性的。因此,如果要改变它,只能够重新创建并渲染一次。

然而,托虚拟 DOM 的福,重新渲染并不代表重新渲染整个 DOM,React 会查找并只更新有改变的节点。

但是一般不回这么做。因为有一点很重要:在设计一个元素的时候就要考虑到它在所有状态下的表现。这个其实在其它框架下也是一样的。

组件

React 是组件化框架,因此组件是组成一个应用的基础。组件的特点:独立、可重用。

组件有两种定义方法:

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

或者:

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

组件渲染

一个简单的例子:

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

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

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

可以看到元素组成了组件,组件又组成了元素,最后渲染在 DOM 上的是元素。

这个跟 Vue 很像了,区别是 Vue 没有区分所谓的“元素”跟“组件”,通通都是组件。

需要注意的是,在 React 世界中有个约定:自定义控件以大写字母打头。这是为了跟 HTML 元素有所区分。

组件使用与拆解

一个简单的例子:

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

需要注意的是,组件只能有一个根节点。(如例子中的 3 个 Welcome 必须包裹在 div 中)

参数只读

简单地说,React 不允许在控件内修改参数(包括值的修改以及对象修改)。允许修改的称之为“状态”(约等于 Vue 中的 component data)

状态管理与生命周期

添加状态管理

组件的更新依赖于状态,因此需要实时更新的组件应在其内部建立状态管理机制(低耦合高内聚)。

需要状态管理机的组件,必须使用 ES6 方式声明,如:

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

但是,此时,组件是无法更新的:因为状态在创建时就已经被决定了。

添加生命周期

代码有注释:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  // 组件渲染到 DOM 后调用
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  // 组件将销毁后调用
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
  
  tick() {
    this.setState({
      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')
);

整个流程很简单清晰了:

  1. ReactDOM 渲染 Clock,并对 state 做第一次初始化
  2. render 方法被调用,插入 DOM
  3. componentDidMount 方法被调用,计时器启动,tick 每秒钟执行一次
  4. 每次 tick 执行都调用 setState 方法去更新状态,这样 React 就知道需要更新 DOM 了
  5. 当组件被从 DOM 移除后,componentWillUnmount 执行

正确使用状态

直接更改 state 属性是不会触发 UI 更新的。因此,有一些规则需要遵守。

不直接修改状态

在组件内进行修改状态操作,使用 setState 方法:

// Wrong
this.state.comment = 'Hello';

// Correct
this.setState({comment: 'Hello'});

关于异步更新

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

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

状态合并

在进行 setState 的时候,只关心需要更改的属性即可,没有传入的属性会被保留。就好像新的状态被“合并”进入旧状态一样。

数据流

在 React 世界,组件与组件之间的状态传递是单向的,传值的方式就是将 state 当做 prop 传给子组件。

事件处理

跟 DOM 操作很像,区别:

  1. 事件命名使用驼峰式
  2. 直接向 JSX 中传入方法
  3. 不支持 return false 操作

例:

// DOM
<button onclick="activateLasers()">
  Activate Lasers
</button>

// React
<button onClick={activateLasers}>
  Activate Lasers
</button>

// A prevent default sample
function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

// A class sample
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>
    );
  }
}

注意:这个 e 是 React 封装过的,但遵循 W3C 标准,因此无需做浏览器差异化处理。

另外,this.handleClick.bind 方法是为了保证在 onClick 中调用了正确的 this,但使用箭头函数可以避免这个累赘的方法:

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

条件渲染

例子:

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

行内判断

例子:

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

这段代码的工作方式跟 JavaScript 一致:

  • true && expression -> expression
  • false && expression -> false

因此,当 unreadMessages.length > 0 为真时,后面的 JSX 会被渲染,反则不会。

除此以外还有三元表达式:

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

阻止渲染

在组件的 render 方法内 return null 会阻止组件的渲染,但是其生命周期不受影响。

循环

一个简单的例子:

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

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

循环组件

一个列表组件示例:

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

注意,这里对列表项添加了一个 key 属性。

Key

Key 是 React 用来追踪列表项的一个属性。跟 angular 以及 vue 中 track-by 的概念一样。

如果列表项没有唯一标识,也可以用索引作为 key (不推荐):

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

注意:Key 只能直接在数组循环体内定义。如:

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


Loading Disqus......