前端学习笔记(十五) --React 学习-1

462 阅读11分钟

今天开始学习 React。终于学到框架部分了。

1. React 介绍与基本配置

React 属于 mvc 框架的 v(view)层。是一个 JS 库。

1.1 安装

React 可以根据需求换安装方式,小项目可以使用 cdn。较大项目则可以用专门的工具链。
这里使用 create-react-app 工具链来安装。

  1. 执行 npx create-react-app <YourProjectName>,这个命令会在当前目录上生成一个叫 <YourProjectName>的文件夹。使用 npx 的好处是,这个命令会在当前目录上临时安装 create-react-app,执行完毕生成一个文件夹,然后再删除这个包。
  2. 转到刚生成的文件夹里,会发现里面几乎把能配置的都给你配置了,有 .git,有.gitignore,有package.json,有 yarn.lock,有 eslint 文件。而且其实在内部还有 webpack 等各种工具的集成,只是没有显示出来。
  3. 写这篇文章的时候 create-react-app 的最新版本(4.0.1)附带安装的是 webpack 4。

1.2 自定义 webpack 配置

由于 create-react-app 隐藏了 webpack 配置文件,如果我们想要自定义就要用以下方法,不过对于大部分中小项目,默认配置足够了
但是并不是没有办法,并且有多种办法。(注:这篇文章里的第一种办法也可以看一下,了解一下 eject 是干什么的)
↑ 根据这篇文章来看,最好的办法是使用 react-app-rewired。这是 react 社区开源的一个用于修改 CRA(creat-react-app)配置的工具。
使用 react-app-reqired 参考官方文档

  1. 安装 react-app-rewired
  2. 在根目录中创建一个叫 config-overrides.js 的文件,如下:
    • 这个函数接受一个 config 作为变量,返回一个 config。接受的 config 就是默认的 webpack 设置,你可以在这里进行一些配置修改,之后返回修改后的 config。
    • env 则为目前的环境。
  3. 如果要设置 webpack-dev-server,要单独设置,具体参考文章。
  4. 这个工具还能设置 Jest,具体参考文章。
  5. 设置完后,还需要更改 package.json 里的命令,除了 eject 都换成 rar 命令:

1.3 create-react-app 的 bug

现在最新的版本(4.0.1) 带有一个 bug,热更新不会更新 index.js 的内容,而且 wds 也不会刷新页面。
解决的办法是把 react-scripts 退回 3.4.4 版本。方法如下:

  1. 在 package.json 里把 react-scripts 的版本改成 3.4.4
  2. 删除 package-lock.json 或者 yarn.lock
  3. 删除 node_modules
  4. 执行 npm install 或者 yarn install (注:不能直接给 react-scripts 降级(如果你用 yarn 也许你会想这么做),很多包的依赖会出现问题)

解决方案是 CRA 社区里这位 ↓ 提出的:

2. JSX

JSX 是 React 的一种特殊语法,可以理解为 JS + XML。

2.1 JSX 里嵌入表达式

const name = "魔理沙·玛格特罗依德"; // 这句是普通 JS 语句
const element = <h1>Hello, {name}</h1>; // 这句是 JSX 语句

大括号里可以嵌入任何 JS 表达式。
最好将内容包括在括号里,以避免自动插入分号陷阱(JS 会给某些语句后面自动加入分号)。

2.3 JSX 本身也是表达式

比如拿以上的 element 来说,本身是一个对象。可以用对其他对象一样的用法。

2.4 JSX 的属性

即从 html 方面来看的属性。

const element = <div tableIndex="0">Bite the dust!</div>; // tableindex 属性

// 属性里也可以嵌入表达式
const img = <img src={myUrl} alt="But who care?"></img>; // src 属性

(注1:第一句这里的 tableIndex 属性名在 html 里的写法是 tableindex,因为 JSX 并不是真正的 html,使用的命名法是 JS 约定的驼峰命名法)

  • 属性值里大括号外面不要加引号。

2.5 JSX 指定子元素

JSX 标签里可以有很多子元素。

const element = (
    <h1>
        <p>No one can beat me</p>
        <p>I can</p>
    </h1>
);

如果是闭合标签,后接 />,和 html 写法一致。

const img = (<img src={url} alt="dead" />);

2.6 防止 xss 攻击

ReactDOM 在渲染所有输出内容之前,会默认进行转义,也就是说在渲染之前所有的东西都会变成字符串。因此 可以有效防止 DOM 操作相关的 XSS 攻击。

2.7 React 元素

JSX 在运行的时候,会预先被 babel 转译成一个 React.createElement() 的调用。
JS 运行时,这个调用会生成一个对象,被叫做 React 元素(也是虚拟 DOM 对象),描述了屏幕上的内容。
React 会读取这些对象来将它们渲染成 DOM。

3. 元素渲染

React 元素是对象,开销较小。而 React DOM 会负责更新 DOM 来保持与元素的一致。

3.1 将元素渲染为 DOM

或者说将虚拟 DOM 渲染为 DOM。
假设 index.html 上有一个 <div class="root"></div>(CRA 的 index.html 在 public 文件夹里,默认带一个 <div class="root"></div>),被叫做根节点。仅由 React 构建的应用通常只有一个根节点,React 负责管理根节点里所有的内容。
把元素渲染到根节点使用如下代码:

const element = ( <h1>no one</h1> );
ReactDOM.render( element, document.getElementById("root") );

3.2 更新元素

很可惜,React 元素是一个不可变对象,一旦被创建,包括子元素都无法被更改。
如果要更新,只能先删除,再创建一个新的元素插入。如以下时钟:

3.2.1 只更新必要部分

虽然每次都是删除再插入新的元素,但是这并不代表每次都要重新渲染所有的部分。React DOM 会比较前后的状态,只更新需要更新的部分。如下图所示:
虽然在元素层面来看,每次整个 root 都在更新,但是经过 React DOM 处理(diff 算法)使得 DOM 只更新需要更新的部分。

4. 组件

组件是一个独立可复用的代码片段。

4.1 函数组件, class 组件

组件的定义是接受唯一参数对象(props),返回 React 元素。
组件有两种定义方式,函数组件和 class 组件。

  1. 函数组件如下,函数组件其实可以说就是函数,除了:
    • 函数名要用 pascal 命名法(小写字母开头会被当做原生标签)。
    • 接受唯一参数,该参数将会作为一个对象被传入。
    • 不能更改 props(这种函数也被成为纯函数)
    function MyComponent(props){
      return (<h1>no one is {props.name}</h1>);
    }
    
  2. 类组件如下:

    • 继承自 React 组件类,接受一个唯一参数 props 对象。
    • 截图里下面这个使用方式最好不用,因为这个会返回一个不会变的元素,而不是作为组件来被使用。具体问题后面会解释。

4.2 渲染组件

组件的特性之一是可以当做标签来使用,创建元素:

const element = (<myComponent name="alice">);

该标签的属性便会作为 props 传入组件。

4.3 组合组件

组件使用标签(或者不使用 JSX)作为元素来使用,因此可以与其他组件或者原生标签等组合,从而创建包含多层组件的元素。
组件会被经常用到用作各种内容,甚至整个页面内容都会被作为一个组件。CRA 自动生成的默认文件打开后的那一整个页面就是在 App.js 里的一个组件。
一般来说,App 是最顶层的组件。

4.4 提取组件

并不是新的语法,简单来说就是当一个组件很复杂的时候,最好将其分为多个组件。
一般来说,当组件中有一至多个代码片段多次重复,或者一个组件虽然没有重复片段但是本身比较复杂难以阅读的时候,最好就去提取组件。

4.5 组件的 state 和 生命周期

如之前所说,props 是无法更改的,但是我们的页面经常是动态变化的。为了解决这个问题,提出了这些概念。
之前有写过时钟函数,通过删除再插入更新 DOM。现在用组件的方式去封装这个时钟。

4.5.1 外观上封装

先从最基础的方式封装,只封装元素外观。

这样子计时器仍然在外部。

4.5.2 给组件增加计时器

由于不能更改 props,没法在 Clock 中改变时间,为了实现这个功能,需要给组件添加 state。 要使用 state 或者其他额外特性,就必须将函数组件转成 class 组件。

4.5.2.1 添加 state

state 和 props 类似,只是 state 可以被更改,并且是私有的。(props 不是私有的,因为只能作为参数被传入,并不是组件自己创建的,且不能更改)

  1. 建立一个构造函数,给 state 赋值为 new Date()。
  2. 在 render() 中把 props 改成 state。
  3. 去掉 <Clock> 标签中的属性,因为已经在 state 中赋值了。

4.5.2.2 添加生命周期

组件被加入到 DOM 中叫做 “挂载(mount)”,组件从 DOM 中被移除叫做 “卸载(unmount)”。
生命周期是组件类中的一些特殊命名的方法,在组件被挂载和卸载时会被调用。
这里使用其中的两个,componentDidMountcomponentWillUnmount。前者会在组件被挂载完成的时候被调用,后者会在组件将要被销毁的时候被调用(也是唯一的销毁时期的生命周期函数)。

重要的几点如下:

  1. 要改变 state 只能通过 setState,对 state 的赋值只能出现在构造函数中。
  2. 这里用 this.timer 存储计时器,没有使用 props 和 state,这当然是可以的。props 首先自然不能赋值因为不能改变 props。而 state 则是专门为了要改变的状态提供的属性,当 state 改变的时候组件就会重新调用 render()。如果不是有这种需求的话,就可以不用。
  3. 这里的定时器使用了箭头函数把 tick 又包围了一次,因为定时器里会默认把 this 指向 window 而不是这个实例对象。而 ES6 引入的箭头函数解决了 this 指向的问题。如果这里把箭头函数改成普通匿名函数,那就会出问题,因为 this 的指向还是 window。(或者使用 bind 也可以解决,给 this.tick() 绑上实例的 this)

现在 Clock 可以直接当一个完整的时钟来用了。

4.5.2.3 整个流程发生了什么

上面写的这个 Clock 组件,在整个调用过程是如下这样子的:

  1. Clock 组件类被创建。
  2. 使用 <Clock /> 引用组件,创建了一个元素(对,<Clock /> 是元素,因为这是 JSX 语法,而 JSX 语法就是 React.createElement() 的语法糖,创建出来的就是元素)。此时没有被实例化。(以及,如果是函数式组件,因为不是类,甚至都没有实例化这种东西)
  3. 当调用 ReactDOM.render() 的时候,组件才会被实例化。那么一个类被实例化当然是要使用构造函数了,因此调用构造函数。因为构造函数里必须有 super(),因此传入 props。然后给 this.state 赋值,这也是唯一能给 state 赋值的地方。
  4. 然后运行组件里的 render() 方法,很明显这是个原型方法,同时也是个特殊命名方法,因为会被 React 特殊对待。每次组件实例改变的时候(即 state 改变的时候),render() 方法便会被调用,由 React 检查需要更新的地方后,渲染到 DOM 上。
  5. 此时即为被装载,因此触发 componentDidMount() 方法,触发定时器,然后存储定时器,以便之后删除。由于定时器不参与元素渲染数据流,直接绑定在实例上,而不是 state 上。
  6. 本例中定时器每 10ms 都会调用 tick() 函数,使用 setState() 更新 state,因为不在构造函数里。
  7. setState() 用于通知组件该重新运行 render() 了,而组件并不会检查 state 的真实值是否真的改变了,只要 setState() 被运行了,就会调用 render()。因此,尽管每 100 次调用中只有 1 次 state 会改变,但是每次调用的时候都会重新运行 render(),也就是每 1 秒 render() 都会运行 100 次。但是由于 react 出色的特性,使得其中 99 次都不会更新 DOM。如果打开浏览器看,依然只有在秒数改变的时候 DOM 会更新。

4.5.3 state 使用注意事项

4.5.3.1 state 和 props 的更新是异步的

也就是说并不是同时更新,因此使用 setState 的时候是不能下面这样的:

this.setState({ count: this.state.count + this.props.incrementNum });

要解决这个问题,可以通过把函数作为 setState 的参数来解决:

setState( (state, props) => {
  count: state.count + props.incrementNum
} );

4.5.3.2 state 的更新会被合并

也就是说每次更新的时候可以只更新 state 的部分属性。可以看看这段文章

4.6 数据是向下流动的

不好描述,看看这篇文章。(文章中有状态组件和无状态组件的区别就是有没有 state。)
一个组件的 state,其他组件是无法访问的。数据只能向下传递(子元素),子元素/组件也无法知道数据来自哪里。