React(一):核心概念
React 简介
背景
React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在 2013 年 5 月开源了。
react 是什么?
React 是一个用于构建用户界面的 JavaScript 库。用于构建快速、高效的用户界面;它是一个轻量级库,遵循组件化设计模式、声明式编程范式以及函数式编程概念。它使用虚拟 DOM 来有效地操作 DOM;遵循从高阶组件到低阶组件的单向数据流。
在整个 Web 应用的 MVC 架构中,你可以将 React 看作为视图层,并且是一个高效的视图。React 提供了和以往不一样的方式来看待视图,它以组件开发为基础。对 React 应用而言,你需要分割你的页面,使其成为一个个的组件。也就是说,你的应用是由这些组件组合而成的。你可以通过分割组件的方式去开发复杂的页面或某个功能区块,并且组件是可以被复用的。这个过程大概类似于用乐高积木去拼装不同的物体。
特点
- 采用组件化模式、声明式编码;提高开发效率和组件复用率
- 使用虚拟 DOM 和 diffing 算法,减少与真实 DOM 的交互
- 与已知的库或框架完好配合,相对灵活
- 使用 JSX 语法糖
- 单向数据流
Virtual DOM
为了跟踪模型层的变化,并且将其应用到 DOM 中(也就是渲染),我们需要注意两个重要的事情:
- 数据是什么时候改变的
React 提供了一个观察者模型用于替代传统的脏检查(dirty checking),也就是持续的检查模型的变化。这也就是解释了为什么 React 不需要计算哪些发生了改变的原因,因为它会立即知道。这个过程减少了计算量,使应用程序变得更平滑。 - 哪一个(些)DOM 元素需要被更新
React 在内存中构建了 DOM 的树形表示,并且计算出哪个 DOM 元素应该被改变。对浏览器而言,DOM 操纵是比较耗费性能的,因此我们更倾向于让其变得最小化。幸运的是,React 视图尽可能少的触及到 DOM 元素。给予对象表示而言, 更少的 DOM 操纵意味着计算会更快,因此 DOM 改变也被尽可能的减少。
React 在底层实现了一个 diffing 算法,该算法使用 DOM 的树形表示法,当某个节点发生变化(标记为 dirty)时它会重新计算整个子树,你会注意到你的模型发生了改变,因为整个子树在之后会被重新渲染。
什么是虚拟 DOM?
在 react 中,简单来说虚拟 DOM 其实就是 JS 对象,将真实 DOM 抽象成一个 javascript 对象。
1 | function el(name, props, child) { |
这里的 dom 就是一个对象,里面包含了 name,props,child。
将对象转换成真实的 DOM:
1 | el.prototype.render = function () { |
原理
在 React 中,也有一个 render 函数来将虚拟 DOM 树转换成真实 DOM,并且,React 中有 state 转移的过程,所以每次 state 有变化之后,就会触发 render 函数,重新构造一个虚拟 DOM 树。对比新旧虚拟 DOM 树的差别,记录下差异,然后只针对差异部分对应的真实 DOM 进行操作。
- 如何进行新旧虚拟 DOM 树的对比?
这里采用的是 Diff 算法。Diff 算法比较复杂,主要的思路是这样的。
首先,每一次生成的虚拟 DOM 树上的各个节点都对应唯一的一个 id,当第二次生成了新的 DOM 树时,对原来树上的每一个节点对比新树上对应节点,如果不同,就记录下来这个差异。同时,差异也分为很多种:
- 替换节点
- 删除、移动、增加节点
- 修改属性
- 对文本内容做修改
对每一类记录下差异后,针对不同的差异做不同的操作。
1 | 比如:替换节点就需要调原生JS的repaceChild()接口;对于修改属性,则要调setAttribute()接口等等。 |
JSX 语法
1 | const element = <h1>Hello, Word!</h1>; |
像这样的形式,被称为 JSX;是 javascript 的语法扩展。JSX 的基本语法和 XML 基本相同,都是使用成对标签的形式构成树状结构的数据。
为什么使用 JSX?
我们来看看 React 官方的回答:
简单来说,react 认为组件才是王道;而组件是和模板紧密关联的。组件模板和组件逻辑分离让问题复杂化了。
所以就有了 JSX 这种语法,就是为了把 HTML 模板直接嵌入到 JS 代码里面,这样就做到了模板和组件关联,但是 JS 不支持这种包含 HTML 的语法,所以需要通过工具将 JSX 编译输出成 JS 代码才能使用。
JSX 是可选的
因为 JSX 最终是输出成 JS 代码来表达的,所以我们可以直接用 React 提供的这些 DOM 构建方法来写模板,比如一个 JSX 写的一个链接:<a href="https://react.docschina.org/">Hello!</a>
用 js 来写的的话就是:React.createElement('a', {href: 'https://react.docschina.org/'}, 'Hello!')
JSX 语法糖允许前端开发者使用我们最熟悉的类 HTML 标签语法来创建虚拟 DOM 在降低学习成本的同时,也提升了研发效率与研发体验。
使用
标签类型
- DOM 类型的标签(div,span 等), 使用时,标签首字母必须小写
- React 组件类型的标签,使用时,组件名称的首字母必须大写
React 通过首字母的大小写来判断渲染的是 DOM 类型标签还是 React 组件类型标签。
JS 表达式
JSX 的本质是 javascript,在 JSX 中使用 JS 表达式需要用{}包裹起来。
- 使用场景
- 通过表达式给标签属性赋值
const dom = <Child data={ 1 + 1 } />
- 通过表达式定义子组件
1 | const arr = ['li01','li02','li03'] |
标签属性
- 当 JSX 标签是 DOM 类型的标签时,对应的 DOM 标签支持的属性 JSX 也支持,部分属性名称有所变化,例如 class 写成 className,之所以改变是因为 React 对 DOM 标签支持的事件重新做了封装。事件属性名称采用驼峰格式。
- 当 JSX 标签是 React 组件类型时,可以自定义标签的属性名。
注释
JSX 的注释需要大括号”{}”将/**/包裹起来。
元素渲染
元素是构成 React 应用的最小砖块。
将一个元素渲染为 DOM
1 | <div id="root"></div> |
1 | const dom = <h1>Hello, Word!</h1>; |
更新已渲染元素
React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的 UI。更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()。
1 | function tick() { |
React 只更新它需要更新的部分
ReactDOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
尽管每一秒都会新建一个描述整个 UI 树的元素,ReactDOM 只会更新实际改变了的内容。
组件
组件的定义
定义组件的两种方式:
- 使用函数(函数组件)
- 使用 ES6 class(类组件)
函数组件
1 | function Funcomponent(props) { |
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
class 类组件
使用 class 定义组件满足的两个条件:
- class 继承自 React.Component。
- class 内部必须自定义 render 方法,render 方法返回代表该组件 UI 的 React 元素。
1 | class Classcomponent extends React.Component { |
组件渲染
1 | const element = <Child name="child" />; |
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。
1 | function Child(props) { |
来看看上述例子都做了什么?
- 调用 ReactDOM.render()函数,并传入< Child name=”函数式组件” />作为参数。
- React 调用 Child 组件,并将{name: ‘函数式组件’}作为 props 传入。
- Child 组件将< h1 >Hello!函数式组件</ h1 >元素作为返回值。
- React DOM 将 DOM 高效地更新为< h1 >Hello!函数式组件</ h1 >。
组合、嵌套组件
将一个组件渲染到某一个节点里的时候,会将这个节点里原有内容覆盖。组件嵌套的方式就是将子组件写入到父组件的模板中去,且 React 没有 Vue 中的内容分发机制(slot),所以我们在一个组件的模板中只能看到父子关系
1 | // 从 react 的包当中引入了 React 和 React.js 的组件父类 Component |
Props(组件的数据挂载方式)
属性 props(只读性)
props 是由外部传进来的,也可以由组件内部设置初始化的 props;组件无论是使用函数式组件还是 class 组件,都不能修改自身的 props。可以通过父组件主动重新渲染的方式来传入新的 props。
1 | import React, { Component, Fragment } from 'react' |
设置组件的默认 props
1 | import React, { Component, Fragment } from 'react' |
props.children
我们知道使用组件的时候,可以嵌套。要在自定义组件的使用嵌套结构,就需要使用 props.children。
1 | import React, { Component, Fragment } from 'react' |
prop-types
React 其实是为了构建大型应用程序而生, 在一个大型应用中,根本不知道别人使用你写的组件的时候会传入什么样的参数,有可能会造成应用程序运行不了,但是不报错。为了解决这个问题,React 提供了一种机制,让写组件的人可以给组件的 props 设定参数检查,需要安装和使用 prop-types:
1 | npm i prop-types -S |
State(状态)
组件的 state 是组件内部的状态,state 的变化最终将反映在组件 UI 的变化上。在构造方法 constructor 中通过 this.state 定义组件的初始状态,并调用 this.setState 方法改变组件的状态。
定义 state
1 | import React, { Component, Fragment } from 'react' |
this.props 和 this.state 是纯 js 对象,在 vue 中,data 属性是利用 Object.defineProperty 处理过的,更改 data 的数据的时候会触发数据的 getter 和 setter,但是 React 中没有做这样的处理,如果直接更改的话,react 是无法得知的,所以,需要使用特殊的更改状态的方法 setState。
setState
1 | import React, { Component, Fragment } from 'react' |
setState 有两个参数:
1 | this.setState = { |
注意的是这个方法接收两个参数,第一个是上一次的 state, 第二个是 props
setState 是异步的,所以想要获取到最新的 state,没有办法获取,就有了第二个参数,这是一个可选的回调函数
1 | this.setState = |
Props 和 State 对比
都是纯 js 对象,都会触发 render 更新,都具有确定性(状态/属性相同,结果相同)
Props | State |
---|---|
能从父组件获取 | 不能 |
可以由父组件修改 | 不可以 |
能在内部设置默认值 | 不能 |
不在组件内部修改 | 要改 |
能设置子组件初始值 | 不可以 |
可以修改子组件的值 | 不可以 |
主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非外部组件主动传入新的 props,否则组件的 props 永远保持不变。 | 主要作用是用于组件保存、控制、修改自己的可变状态。state 在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为 state 是一个局部的、只能被组件自身控制的数据源。state 中状态可以通过 this.setState 方法进行更新,setState 会导致组件的重新渲染。 |
React 组件是通过 props 和 state 两种数据驱动渲染出组件 UI。Props 是组件对外的接口,它是只读的,组件通过 props 接受外部传入的数据;state 是组件对内的接口,它是可变的,组件内部状态通过 state 来反映。
如果搞不清 state 和 props 的使用场景,记住一个简单的规则:尽量少地用 state,多用 props。
没有 state 的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)。因为状态会带来管理的复杂性,我们尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。
状态提升
- 如果有多个组件共享一个数据,把这个数据放到共同的父级组件中来管理
具体操作可以查看 React 文档:状态提升
生命周期
- 第一次渲染
constructor → componentWillMount → render → componentDidMount - props 更新
componentWillReceiveProps → shouldComponentUpdate(如果返回 true) → componentWillUpdate → render → componentDidUpdate - state 更新
shouldComponentUpdate (如果返回 true)→ componentWillUpdate → render → componentDidUpdate - 组件卸载
componentWillUnmount
React 旧版生命周期包含三个过程:
- 挂载过程
constructor()
componentWillMount()
componentDidMount() - 更新过程
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps,nextState)
componentWillUpdate (nextProps,nextState)
render()
componentDidUpdate(prevProps,prevState) - 卸载过程
componentWillUnmount()
其具体作用分别为:
- constructor()
完成了 React 数据的初始化。 - componentWillMount()
组件已经完成初始化数据,但是还未渲染 DOM 时执行的逻辑,主要用于服务端渲染。 - componentDidMount()
组件第一次渲染完成时执行的逻辑,此时 DOM 节点已经生成了。 - componentWillReceiveProps(nextProps)
接收父组件新的 props 时,重新渲染组件执行的逻辑。 - shouldComponentUpdate(nextProps, nextState)
在 setState 以后,state 发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中 return false 可以阻止组件的更新,主要用于性能优化。 - componentWillUpdate(nextProps, nextState)
shouldComponentUpdate 返回 true 以后,组件进入重新渲染的流程时执行的逻辑。 - render()
页面渲染执行的逻辑,render 函数把 jsx 编译为函数并生成虚拟 dom,然后通过其 diff 算法比较更新前后的新旧 DOM 树,并渲染更改后的节点。 - componentDidUpdate(prevProps, prevState)
重新渲染后执行的逻辑。 - componentWillUnmount()
组件的卸载前执行的逻辑,比如进行“清除组件中所有的 setTimeout、setInterval 等计时器”或“移除所有组件中的监听器 removeEventListener”等操作。
所不同的是少了 componentWillMount, componentWillReceiveProps, componentWillUpdate 这三个鸡肋,其实这三个生命周期回调要直到 v17 才会被完全删除,不过现在都被冠以 UNSAFE_的前缀了。
多了两个是 getDerivedStateFromProps 和 getSnapshotBeforeUpdate,其中 getDerivedStateFromProps 在第一次渲染和更新的时候都会被调用,而 getSnapshotBeforeUpdate 只会在更新的时候被调用。
使用 getDerivedStateFromProps(nextProps, prevState)的原因:
旧的 React 中 componentWillReceiveProps 方法是用来判断前后两个 props 是否相同,如果不同,则将新的 props 更新到相应的 state 上去。在这个过程中我们实际上是可以访问到当前 props 的,这样我们可能会对 this.props 做一些奇奇怪怪的操作,很可能会破坏 state 数据的单一数据源,导致组件状态变得不可预测。
而在 getDerivedStateFromProps 中禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问 this.props 并做其他一些让组件自身状态变得更加不可预测的事情。
使用 getSnapshotBeforeUpdate(prevProps, prevState)的原因:
在 React 开启异步渲染模式后,在执行函数时读到的 DOM 元素状态并不总是渲染时相同,这就导致在 componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
而 getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
事件处理
绑定事件
React 元素的事件处理和 DOM 元素很相似,都是采用 on+事件名称的方式绑定事件。但不同的是原生事件采用的是纯小写形式,如:onclick;而 React 的事件使用小驼峰的形式,如:onClick;使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
React 的事件不是原生事件,而是合成事件。
传统 HTML:
1 | <button onclick="clickActive()">BUTTON</button> |
React:
1 | <button onClick="{clickActive}">BUTTON</button> |
事件 handler 的写法
- 直接在 render 里写行内的箭头函数(不推荐)
- 在组件内使用箭头函数定义一个方法(推荐)
- 直接在组件内定义一个非箭头函数的方法,然后在 render 里直接使用 onClick={this.handleClick.bind(this)}(不推荐)
- 直接在组件内定义一个非箭头函数的方法,然后在 constructor 里 bind(this)(推荐)
1 | class Toggle extends React.Component { |
Event 对象
和普通浏览器一样,事件 handler 会被自动传入一个 event 对象,这个对象和普通的浏览器 event 对象所包含的方法和属性都基本一致。不同的是 React 中的 event 对象并不是浏览器提供的,而是它自己内部所构建的。它同样具有 event.stopPropagation、event.preventDefault 这种常用的方法
因此,在 React 中你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault。
1 | <a href="#" onclick="console.log('click'); return false"></a> |
1 | function Active() { |
向事件处理程序传递参数
- 在 render 里调用方法的地方包一层箭头函数
- 在 render 里通过 this.handleEvent.bind(this, 参数)这样的方式来传递
- 通过 event 传递
1 | <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> |
条件渲染
在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。
条件表达式渲染(适用于两个组件二选一的渲染)
1 | render() { |
&& 操作符渲染 (适用于一个组件有无的渲染)
1 | function Mailbox(props) { |
利用变量输出组件渲染 (适用于有多个组件多种条件下的渲染)
1 | render() { |
利用函数方法输出组件或者利用函数式组件进行渲染 (适用于多个子组件需要根据复杂的条件输出的情况)
函数方式
1 | renderButton(){ |
函数式组件
1 | function Greeting(props) { |
列表和 key
当列表数据为多组时,调用数据运行时为提示警告:“Each child in an array or iterator should have a unique ‘ key ’ prop ”说明如果没有给每条数据添加一个“key” 的话,当数据组数过多时,不同组数有相同的数据,调用就可能会出现错误。因此这里的“key”就相当于数据库里的主键,给每组数据调取都有一个参照。
1 | function ListItem(props) { |
- key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
- 虽然列表元素的 key 不能重复,但这个唯一性仅限至于在当前列表中,而不是全局唯一。
- 不推荐使用索引作为 key,因为一旦列表中的数据发生重排,数据的索引也会发生变化,不利于 React 的渲染优化。
表单
受控组件
什么受控组件?简单来说就是如果一个表单元素的值是由 React 来管理的,那它就是一个受控组件。
贴一段 React 官网的解释:
1 | import React, { Component } from "react"; |
用户名和密码两个表单元素的值是从组件的 state 中获取的,当用户更改表单元素的值时,onChange 事件会被触发,对应的 handleChange 处理函数会把变化同步到组件的 state,新的 state 又会触发表单元素重新渲染,从而实现表单元素状态的控制。
总结
本章主要介绍了 React 的主要特性及其用法。React 通过 JSX 语法声明界面 UI,将界面 UI 和它的逻辑封装在同一个 JS 文件中。通过虚拟 DOM 和 diffing 算法来减少与真实 DOM 的交互。组件是 React 的核心,根据组件的外部接口 props 和内部接口 state 完成自身的渲染。使用组件需要理解它的生命周期,借助不同的生命周期方法,组件可以实现复杂逻辑。渲染时,注意 key 的使用,事件处理时,注意事件名称和事件处理函数的写法。最后介绍表单元素的用法,使用方式分受控组价和非受控组件。