React入门

2,624 阅读10分钟

React是一个用于构建用户界面的JavaScript库,用它可以让我们更便捷高效地进行前端开发。本文记录如何从零开始学习React,最后会用React进行小练习,实现一个简单的清单功能。

因为是入门,所以是以在html文件中引入js的方式进行练习的。如果想要直接创建并启动一个react项目(需要先安装好node和npm),可以使用命令:

 npx create-react-app my-app
 cd my-app
 npm start

官方文档地址:React。文章不涉及ReduxReact Router

准备工作

E6知识

熟悉ES6的话可以跳过这部分。

需要准备类、数组解构、对象解构等知识。ES6的学习可以参考ES6入门。在此只简单说一下类相关的内容:

// let container = new Container(100); // 类的声明不会提升,在此处使用Container会报错ReferenceError

// 类的声明
class Container {
  // 构造函数constructor用于创建和初始化Container的实例对象
  constructor ( totalCapacity ) {
    this.totalCapacity = totalCapacity;

    this.usedCapacity = 0;
    this.contained = [];

    // class中的方法不会自动绑定到实例对象,所以需要我们手动绑定
    this.add = this.add.bind(this);
    this.remove = this.remove.bind(this);
    this.printContained = this.printContained.bind(this);
  }
  // 静态方法,调用方式Container.containerInfo(),静态方法会被子类继承
  static containerInfo () {
    console.log('这个是静态方法呀\n');
  }
  // add、remove、printContained是定义在Container的prototype上的,可以被子类继承
  add (something, volume) { // 将某物加入容器
    const containedItem = {
      name: something,
      volume
    };
    let used = this.usedCapacity + volume;
    if (used > this.totalCapacity) {
      console.log(`此容器不能放下体积为${volume}的物品`);
      return;
    }
    this.contained.push(containedItem);
    this.usedCapacity = used;
  }
  remove (something, volume) { // 将某物移出容器
    let containedLen = this.contained.length;
    if (containedLen === 0) return;
    const index = this.contained.findIndex((item) => item.name === something);
    if (index < 0) return;
    const item = this.contained[index];
    const diff = item.volume - volume;
    switch (true) {
      case diff === 0:
        this.contained.splice(index, 1);
        break;
      case diff > 0:
        this.contained[index] = {
          name: item.name,
          volume: diff
        };
        break;
      case diff < 0:
        console.log(`容器中的${something}体积不足${volume}\n`);
        break;
    }
  }
  printContained () {
    this.contained.forEach((item) => {
      const { name, volume } = item;
      console.log(`${name} ${volume}`);
    })
  }
}

let container = new Container(100); // 使用class定义的类必须用new调用,直接执行Container()会报TypeError

container.add('苹果', 20);
container.printContained();
container.remove('苹果', 10);
container.printContained();

// 如果没有在constructor中手动给printContained绑定this,那么执行以下两行代码就会报错
// 因为printContained执行时的this是undefined,不是container(Container的实例)。
// const { printContained } = container;
// printContained();

// 静态方法的调用方式
// Container.containerInfo();

// 类的继承
class Box extends Container { // Box会继承Container原型链上的方法
  constructor (totalCapacity, material) {
    // super方法调用父类的构造函数,将父类属性添加到子类上。相当于Container.constructor.call(this, 参数1, 参数2...);
    super(totalCapacity);
    this.material = material; // Box类的属性
    this.printContained = this.printContained.bind(this);
  }
  printContained () {
    console.log(`箱子是${this.material}制的`);
    console.log('箱子中包含:');
    super.printContained(); // 可以使用super对象使用父类的方法,函数执行的过程中this是指向Box实例的
  }
}

let box = new Box(100, '木头');
box.add('芒果', 20);
box.add('西瓜', 10);
box.add('荔枝', 30);
box.printContained();
box.remove('芒果', 10);
box.printContained();

JSX

JSX是在React中使用的一种语法,以下代码对JSX进行了简单的说明:

let test = () => 1;
let divStyle = { // 注意这里不是直接写的style样式,而是一个样式对象,样式属性采用的是驼峰的命名方式
  backgroundColor: '#cfefe7',
  fontSize: '15px',
  textDecoration: 'underline'
};
let element = (
  <div> {/* ()里面只能有一个元素 */}
    <h1>JSX</h1>
    <div style={divStyle} > {/* 这是内联样式的添加方法 */}
      <p>这就是使用JSX语法创建的一个“元素”,它既不是HTML也不是字符串。</p>
      <p>可以通过{}包含任何JS表达式。比如:{test() + 1}。并且JSX本身也是表达式。</p>
      { test() > 0 && <p>当test()的值大于0的时候会展示这部分的内容</p> }
      <p>里面不光能使用原生元素比如div等,还能包含在React中的自定义组件。</p>
      <p className="nothing">JSX语法中元素的属性采用驼峰的命名方式。</p>
    </div>
  </div>
);

更多JSX相关内容,请查看官方文档:JSX

其实使用ES6和JSX在React中不是必须的,官方提供了相应的用法:不使用ES6不使用JSX。本文还是会在React中用ES6和JSX,因为使用ES6和JSX的代码更加简洁和直观。

React开发者工具

使用开发者工具能够方便查看组件以及组件的props,state。谷歌浏览器React开发者工具地址:React Developer Tools

简单的环境

我在html中直接引入js文件,然后在浏览器中打开html文件的时候报跨域错误了:

Access to script at 'file:///Users/.../practice/00.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

所以我起了一个简单的node服务,在本地服务器上打开html件。

首先创建文件夹practice,在文件夹中创建server.jspractice.htmlpractice.jspractice.css文件。 server.js用Node.js创建了一个简单的本地服务器,文件内容如下:

const http = require('http');
const fs = require('fs');
const url = require('url');

const server = http.createServer(function (req, res) {
  let pathname = url.parse(req.url).pathname;
  fs.readFile(pathname.substr(1), function(err, data){
    if (err) {
      res.writeHead(404, {'Content-Type': 'text/html'});
    } else {
      const index = pathname.lastIndexOf('.');
      const suffix = pathname.slice(index + 1);
      res.writeHead(200, {'Content-Type': `text/${suffix}`});
      res.write(data.toString());
    }
    res.end();
  });
});

server.listen(8080, function () {
  console.log(`server is running at http://127.0.0.1:8080`);
});

practice.html文件内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" type="text/css" href="practice.css" />
</head>
<body>
  <div id="root"></div>

  <!-- 这里引用了react和react-dom以及babel的压缩版  -->
  <script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

  <script type="text/babel" src="practice.js"></script>
</body>
</html>

practice.js 中内容如下:

// ReactDOM.render方法将React元素渲染到一个DOM元素中
ReactDOM.render(
  <h1>React学习</h1>, 
  document.getElementById('root') 
);

pracitice.css 中内容如下:

#root h1 {
  color: green;
}

在practice目录下执行命令node server 启动服务。然后访问地址http://127.0.0.1:8080/practice.html就能看到页面中的绿色的"React学习"这几个字。

开始学习React

元素

元素是构成 React 应用的最小砖块。

React元素和我们已经熟悉的html元素的元素名是一样的,例如divp等,但是React元素是一个普通的JavaScript对象,而html元素是真实的DOM节点。两者的使用方式也不同,React元素的属性名称以及事件绑定都是使用驼峰的命名方式,React元素还能在{}中使用任何表达式,React表单元素的使用也与html表单元素不同。

React元素:

<div className="card" onClick={test} style={ {color: 'green', fontSize: '12px'} }>点击</div>
<input type="text" value={this.state.inputValue} onChange={this.handleChange} />

在以上代码中,this.state.inputValue的值就是输入框的值,当设置state中的inputValue值的时候,输入框的值也会改变。state是组件中存放数据的地方,在下文的组件部分会进行说明。

想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render()

例如上文中的这段代码:

// ReactDOM.render方法将React元素渲染到一个DOM元素中
ReactDOM.render(
  <h1>React学习</h1>, // 这是React元素,是一个普通的对象
  document.getElementById('root') // 这是一个DOM象
);

html元素:

<div class="card" onclick="test()" style="color: green; font-size: 12px;">点击</div>
<input type="text" name="userName" value="RenMo" onchange="handleChange(this.value)" />

组件

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

  • 组件可以通过函数创建,也可以通过类创建。
  • 组件的名称必须是首字母大写。
  • 通过props获取组件的属性。
  • 通过state放置组件的数据,通过setState设置组件的数据。

通过函数创建组件

将上文创建的practice.js 文件的内容替换如下:

function ShowText(props) {
  const { style, content } = props;
  return <p style={style}>{content}</p>;
}
const element = <ShowText content="React学习" style={ {color: 'green', fontSize: '30px'} }/>;
ReactDOM.render(
  element,
  document.getElementById('root')
);

打开http://127.0.0.1:8080/practice.html,就能看见绿色的“React学习”几个字。

ShowText是一个通过函数方式创建的组件,将它渲染到页面中同样需要使用ReactDOM.render函数。函数的参数props中包含了所定义的组件的属性。组件的props是只读的。

函数定义的组件,如果内容有需要改变的,那么必须重新调用一遍ReactDOM.render,比如要改变content的内容,就必须定义一个新的元素并调用ReactDOM.render进行渲染:

const element = <ShowText content="React学习测试测试" style={ {color: 'green', fontSize: '30px'} }/>;
ReactDOM.render(
  element,
  document.getElementById('root')
);

从官方文档中了解到,要在不使用class的情况下使用Reactstate等特性,需要使用HookHook会在下文中提到。

通过类创建组件

将上文创建的practice.js 文件的内容替换如下(一个简单的待办清单的例子):

class TodoItem extends React.Component {
  constructor (props) {
    super(props);
    this.state = {
      complete: false
    };
    // 组件中定义的方法必须手动绑定this
    this.changeTodoState = this.changeTodoState.bind(this);
  }
  componentDidMount() { // 生命周期方法 ------组件第一次渲染到DOM中会触发
    this.timer = null; // 为了展示任务选中的效果,设置一个timer
  }
  componentDidUpdate () { // 生命周期方法 ------组件更新的时候会触发
    console.log('组件更新了');
  }
  componentWillUnmount() { // 生命周期方法 ------组件从DOM中删除的时候会触发
    clearTimeout(this.timer);
  }
  // 如果在输入框的点击事件中既要传入参数,又要使用event,那么把event放在参数的最后一个位置即可
  changeTodoState (content, event) {
    let value = event.target.value;
    this.timer = setTimeout(() => {
      if (value) {
        // 通过setState改变状态的值
        this.setState({
          complete: true
        });
        // setCompleteItems是从父组件传过来的方法,调用这个方法能将子组件中的内容传入父组件中
        // 也就是待办清单的某一项完成后,将该清单的content传入父组件中
        this.props.setCompleteItems(content);
      }
    }, 200);
  }
  render () {
    let complete = this.state.complete;
    let content = this.props.content;
    // 当complete完成的时候,返回的内容就是false,而不是组件的内容,所以清单的某一项就会隐藏
    return (
      (!complete) && 
      <li className="todo-item" key={content.toString()}>
        {/* 如果不需要绑定参数,在{}中直接写this.changeTodoState就行,如果要传入参数,则需要使用bind方法 */}
        <input type="checkbox" onChange={this.changeTodoState.bind(this, content)}/>
        <span>{content}</span>
      </li>
    );
  }
}
class TodoList extends React.Component {
  constructor (props) {
    super(props);
    this.state = {
      completeItems: []
    };
    this.setCompleteItems = this.setCompleteItems.bind(this);
  }
  // 这个方法是为了将子组件中的内容传到父组件中,item就是从子组件传过来的内容
  setCompleteItems (item) {
    let completeItems = this.state.completeItems;
    completeItems.push(item);
    this.setState({
      completeItems
    });
  }
  render () {
    let list = this.props.list;
    let todoList = list.map((item) => <TodoItem content={item} setCompleteItems={this.setCompleteItems}/>);
    return (
      <div>
        <ul className="todo-list">
          {todoList}
        </ul>
        <p>已完成: {this.state.completeItems.join(',')}</p>
      </div>
    )
  }
}
let list = ['洗衣服', '买水果', '追剧'];
const todoList = <TodoList  list={list} />;
ReactDOM.render(
  todoList,
  document.getElementById('root')
);

其中props是组件的属性,是不可变的。state是组件中的数据,可以通过setState进行改变。

定义的事件需要在constructor中手动绑定this,否则就不能在组件中使用。

继承自React.Component的类ShowText中包含一个render方法,每次组件更新render方法都会被调用,返回的内容可以查看render方法官网地址,上面的练习例子中,返回的内容是一个React元素。

componentDidMountcomponentWillUnmountcomponentDidUpdate是常用的React的生命周期函数,分别在React组件的挂载、卸载、更新时被调用。还有其他的生命周期钩子函数:详情可看React生命周期

如果要从子组件中改变父组件的内容,可以像上面那个例子中一样,在父组件中定义一个方法,上例的方法是setCompleteItems,将其作为某属性的值传入子组件中,在子组件中通过this.props获取到父组件传来的方法,再根据需要做出相应的修改。

Hook

通过上文可以了解到如果要使用statesetState等,就必须用类的方式定义组件,这样会导致组件的状态逻辑等有冗余并且可能比较复杂,使用hook就是为了提供更直接简单的方法来进行开发。hook能在不使用class的情况下使用ReactReact从16.8.0版本开始支持hook

接下来就使用hook的方法来实现上文中的待办清单的例子,还是将practice.js的内容替换如下:

// 在项目中可以使用 import { useState } from 'react'; 引入useState
// 因为在浏览器中会将import转换为require,于是会报错require is not defined,所以使用下面这个句子引入useState
let useState = React.useState; 
let useEffect = React.useEffect; 

function TodoItem (props) {
  const { content, setCompleteItems } = props;
  // 声明一个名为complete的state变量,可以通过setComplete改变变量的值,complete的初始值为false。
  const [ complete, setComplete ] = useState(false);
  let timer = null; // 定时器
  // useEffect的参数为一个函数,这个函数在组件挂载和更新时都会执行
  useEffect(() => {
    // 在useEffect中返回一个函数,这个函数会在组件清除的时候执行
    return () => {
      clearTimeout(timer)
    }
  });
  function changeTodoState (content, event) {
    let value = event.target.value;
    timer = setTimeout(() => {
      if (value) {
        // 通过setComplete改变complete的值
        setComplete(true);
        setCompleteItems(content); // 这里原本是this.props.setCompleteItems(content);
      }
    }, 200);
  }
  return (
    (!complete) && 
    <li className="todo-item" key={content.toString()}>
      {/* 如果不需要绑定参数,在{}中直接写this.changeTodoState就行,如果要传入参数,则需要使用bind方法 */}
      <input type="checkbox" onChange={changeTodoState.bind(this, content)}/>
      <span>{content}</span>
    </li>
  );
}

function TodoList (props) {
  const { list } = props;
  const [ completeItems, setCompleteItemsState ] = useState([]);
  useEffect(() => {
    // 更新文档的title
    document.title = completeItems.join();
    return () => {
      document.title = '清单';
    }
  });
  const setCompleteItems = (completedItem) => {
    setCompleteItemsState([...completeItems, completedItem]);
  };
  const unmountComponent = () => {
    ReactDOM.unmountComponentAtNode(document.getElementById('root'));
  };
  let todoList = list.map((item) => <TodoItem content={item} setCompleteItems={setCompleteItems}/>);
  return (
    <div>
      <ul className="todo-list">
        {todoList}
      </ul>
      <p>已完成: {completeItems.join()}</p>
      <p onClick={unmountComponent}>点此手动卸载组件</p>
   </div>
  )
}
let list = ['洗衣服', '买水果', '追剧'];
const todoList = <TodoList  list={list} />;
ReactDOM.render(
  todoList,
  document.getElementById('root')
);

可以看见使用hook之后,就不用像在类中那样,需要使用this.state.varName,直接使用varName就可以了。

  (!complete) && 
  <li class="todo-item" key={content.toString()}>
  ...

state hook用于处理数据,对于effect hook,直接引用官方文档的说明:

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

在使用useEffect的时候,如果需要在组件卸载的时候进行一些处理,那么在useEffect返回一个函数,在清除effect的时候,就会执行返回的函数的内容。比如以下代码,每完成一项的时候,document.title都会改变,在组件卸载的时候,会将document.title会变成''清单''。

useEffect(() => {
  // 更新文档的title
  document.title = completeItems.join();
  return () => {
    document.title = '清单';
  }
});
const setCompleteItems = (completedItem) => {
  setCompleteItemsState([...completeItems, completedItem]);
};

useStateuseEffectuseContext是几个基础的hook,也可以自定义hook

最后

代码练习的最后结果是这样的:源码地址,使用DownGit能够拿到git仓库中某文件夹的代码。