# 特性
React 的三大特性:
- 数据驱动 -> 单向数据流
- 在React中,一切皆数据。要想改变界面元素或更改DOM节点,只需修改数据即可。但不要轻易操作DOM节点。
- 所有的数据都用state来管理,分为组件state和全局state。
- 数据只能从State流向DOM,不能逆向更改。
- 函数式编程(声明式编程) = 组件化 + JSX
- 纯函数:函数的输出不受外部环境影响,同时也不影响外部环境。
- 非纯函数:输入相同,输出不同的函数。
- 函数的柯里化:将一个低阶函数转换为高阶函数的过程。
- 组件化开发:把数据组织起来的表现形式。
() => 'My Component';
- JSX语法:在JavaScript中可以编辑HTML片段。
() => <div>我也是一个组件</div>;
- 虚拟 DOM -> 跨平台
- 服务端渲染:在服务端提前渲染成静态HTML页面。
- 性能:每次数据更新后,重新计算Virtual DOM并和上次做对比,对发生变化的部分做批量更新。还提供了shouldComponentUpdate生命周期函数,减少数据变化后不必要的对比过程,保证性能。
- 虚拟节点(DOM):是在DOM的基础上建立一个抽象层,其实质是一个JavaScript对象,当数据和状态发生变化,都会被自动高效的同步到虚拟DOM中,最后再将仅变化的部分同步到真实DOM中。
- 差异化算法:将虚拟DOM转化为真实DOM的算法,分为三级。Tree、Component、Element
# JSX
JSX是一个JavaScript的语法扩展,可以很好地描述UI应该呈现出它应有交互的本质形式。JSX可能会使人联想到模板语言,但它具有JavaScript的全部功能。
**React将JSX映射为虚拟元素,并通过创建与更新虚拟元素来管理整个Virtual DOM系统。**JSX只是为React.createElement(component, props, ...children)提供的一种语法糖。React.createElement会构建一个JavaScript对象来描述HTML结构的信息,包括标签名、属性、还有子元素等。
每个DOM元素的结构都可以用JavaScript的对象来表示,一个DOM元素包含的信息其实只有三个:
- 标签名
tagName
- 属性
props
- 子元素
children
# 在 JSX 中嵌入表达式
在JSX语法中,可以在大括号内放置任何有效的JavaScript表达式。
const name = 'Josh Perez';const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
2
3
4
5
# JSX 也是一个表达式
在编译之后,JSX表达式会被转为普通JavaScript函数调用,并且对其取值后得到JavaScript对象。
也就是说,可以在if语句和for循环的代码块中使用JSX,将JSX赋值给变量,把JSX当作参数传入,以及从函数中返回JSX。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>; }
return <h1>Hello, Stranger.</h1>;}
2
3
4
# JSX 中指定属性
可以使用引号,来将属性值指定为字符串字面量,也可以使用大括号,来在属性值中插入一个JavaScript表达式(不要在大括号外面加上引号)。
注意: 仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。 因为JSX语法上更接近JavaScript而不是HTML,所以React DOM使用camelCase(小驼峰命名)来定义属性的名称,而不使用HTML属性名称的命名约定。
const element = <div tabIndex="0"></div>; //引号,将属性值指定为字符串字面量
const element = <img src={user.avatarUrl}></img>; // 大括号,在属性值中插入JS表达式
2
# 使用 JSX 指定子元素
假如一个标签里面没有内容,可以使用 /> 来闭合标签,就像XML语法一样。JSX标签里能够包含很多子元素。
const element = <img src={user.avatarUrl} />;
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
2
3
4
5
6
7
# JSX 防止注入攻击
React DOM在渲染所有输入内容之前,默认会进行转义。它可以确保在应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。可以有效地防止XSS(cross-site-scripting, 跨站脚本)攻击。
const title = response.potentiallyMaliciousInput; //可安全地在JSX中插入用户输入内容
const element = <h1>{title}</h1>; //直接使用是安全的
2
# JSX 表示对象
Babel会把JSX转译成一个名为React.createElement()函数调用。
// 以下两种示例代码完全等效
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
2
3
4
5
6
7
8
9
10
11
React.createElement()
会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象。这些对象被称为React元素,它们描述了你希望在屏幕上看到的内容。React通过读取这些对象,然后使用它们来构建DOM以及保持随时更新。
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
2
3
4
5
6
7
8
# 元素渲染
元素是构成React应用的最小砖块。元素描述了你在屏幕上想看到的内容。与浏览器的DOM元素不同,React元素是创建开销极小的普通对象。React DOM会负责更新DOM来与React元素保持一致。
# 将一个元素渲染为 DOM
假设你的HTML文件某处有一个
<div>
,将其称为根DOM节点,因为该节点内的所有内容都将由React DOM管理。仅使用React构建的应用通常只有单一的根DOM节点。如果你在将React集成进一个已有应用,那么你可以在应用中包含任意多的独立根DOM节点。
想要将一个React元素渲染到根DOM节点中,只需把它们一起传入ReactDOM.render():
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
2
# 更新已渲染的元素
React元素是不可变对象。一旦被创建,就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的UI。根据我们已有的知识,更新UI唯一的方式是创建一个全新的元素,并将其传入ReactDOM.render()。
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));}
setInterval(tick, 1000);
2
3
4
5
6
7
8
9
# React 只更新它需要更新的部分
React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。
# 组件化
React组件的封装思路就是面向对象思想。组件,从概念上类似于JavaScript函数。它接受任意的入参(即props),并返回用于描述页面展示内容的React元素。
# Web Component
四个组成部分:
- HTML Templates 定义模版
- Custom Elements 定义组件展示形式
- Shadow DOM 定义组件的作用域范围、可以囊括样式
- HTML Imports 提出新的引入方式
# 组件类型
React组件基本由三个部分组成:属性Props、状态State和生命周期钩子函数。组件类型:
- 无状态组件:最基础的组件形式,由于没有状态的影响所以就是纯静态展示的作用,这种组件的复用性也最强。
- 有状态组件:组件内部包含状态state且状态随着事件或者外部的消息而发生改变的,通常会带有生命周期,用以在不同的时刻触发状态的更新。
- 容器组件:将数据获取以及处理的逻辑放在容器组件中,使得组件的耦合性进一步地降低。
容器组件关心数据(包括数据的格式和数据的来源等),UI组件关心组件展示出来是什么样子
- 高阶组件:和高阶函数的概念类似,就是一个会返回组件的组件,更确切地说,它其实是一个会返回组件的函数。
- 渲染回调组件:在组件中使用渲染回调的方式,将组件中的渲染逻辑委托给其子组件。
# 函数组件与 class 组件
定义组件最简单的方式就是编写JavaScript函数,接收唯一带有数据的 props(代表属性)对象与并返回一个React元素。这类组件被称为函数组件,因为它本质上就是JavaScript函数。
还可以使用ES6的class来定义组件。
// 以下两个组件在 React 里是等效的
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
2
3
4
5
6
7
8
9
# 渲染组件
当React元素为用户自定义组件时,它会将JSX所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称为props。
组件名称必须以大写字母开头。React会将以小写字母开头的组件视为原生DOM标签。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
const element = <Welcome name="Sara" />;ReactDOM.render(
element,
document.getElementById('root')
);
2
3
4
5
# 组合组件
组件可以在其输出中引用其他组件,这就可以让我们用同一组件来抽象出任意层次的细节。
# 提取组件
将组件拆分为更小的组件。提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。
# Props
Props是一个从外部传入组件的参数,主要用于从父组件向子组件传递数据。它具有只读性和不可变性,组件内部无法控制也无法修改。除非外部组件主动传入新的Props来更新,否则组件的Props永远保持不变。
# 只读性/不可变性
组件无论是使用函数声明还是通过class声明,都不能修改自身的props。所有的React组件都必须像纯函数一样使用它们的props,保护props不被更改。
如果Props渲染过程中可以被修改,那么就会导致这个组件显示形态和行为变得不可预测,这样会可能会给组件使用者带来困惑。但不意味着Props决定的显示形态不能被修改。组件的使用者可以主动地通过重新渲染的方式把新的Props传入组件中,这样这个组件中由Props决定的显示形态也会得到相应的改变。
# State
React把用户界面当作简单状态机。通过与用户的交互,实现不同状态,然后渲染UI,让用户界面和数据保持一致。只需要更新组件的state
,然后根据新state
重新渲染用户界面。
State的主要作用是用于组件保存、控制、修改自身的可变状态。State在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。可以认为State是一个局部的、只能被组件自身控制的数据源。State中的状态可以通过setState方法进行更新,数据的更新会导致组件的重新渲染。
⚠️ 并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖一个状态时,一般的做法是状态提升,将这个状态放到这几个组件的公共父组件中。
# 不可变对象
React官方建议把State当做不可变对象(Immutable),State中包含的所有状态都应该是不可变对象,当State中的某个状态发生变化,我们应该重新创建这个状态对象,而不是直接修改原来的状态。
State根据状态类型可以大致分为三种:
基本数据类型:
Number、String、Boolean、Null、Undefined 是不可变类型,由于其本身就是不可变的,要修改状态时,直接赋新值即可
this.setState({ number: 1, string: 'hello', boolean: true, });
1
2
3
4
5
数组类型:
数组类型是可变类型。假如有一个数组类型的State需要新增一个数组元素,应使用数组的
concat
方法或ES6的数组扩展语法// 方法一:将 state 先赋值给另外的变量,然后使用 concat 创建新数组 let students = this.state.students; this.setState({ students: students.concat(['xiaoming']), }); // 方法二:使用 prevState、concat 创建新数组 this.setState(preState => { students: preState.books.concat(['xiaoming']); }); // 方法三:ES6 扩展语法 this.setState(preState => { students: [...preState.students, 'xiaoming']; });
1
2
3
4
5
6
7
8
9
10
11
12
13从数组中截取部分作为新状态时,应使用
slice()
方法;从数组中过滤部分元素后作为新状态时,使用filter()
方法不应该使用
push()
、pop()
、shift()
、unshift()
、splice()
等数组的突变方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改的
对象类型:
对象是可变类型,修改对象类型的
state
时,应该保证不会修改原来的state
。可以使用ES6的Object.assign
方法或者对象扩展语法// Obejct.assign() 方法 this.setState(preState => { school: Obejct.assign({}, preState.school, {classNum: 10}) }) // 对象扩展语法 let school = this.state.school this.setState({ school: { ...school, {classNum: 10} } })
1
2
3
4
5
6
7
8
9
10
11
12创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。也可以使用一些不可变对象的JavaScript库
# 无状态组件
没有State的组件叫无状态组件,设置了State的叫有状态组件。因为状态会带来管理的复杂性,我们尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。
当对象组件状态都是不可变对象时,在组件的
shouldComponentUpdate
方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的render()
调用。当使用React提供的PureComponent
时,更要保证组件状态是不可变对象,否则在组件的shouldComponentUpdate
方法中,状态比较就可能出现错误,因为PureComponent
执行的是浅比较(也就是比较对象的引用)。
# 生命周期
状态组件主要通过3个生命周期阶段来管理,分别是装载阶段(MOUNTING
),更新阶段(UPDATING
)和卸载阶段(UNMOUNT
)。
从纵向划分,可以划分为Render阶段和Commit阶段。
- Render 阶段:纯净且不包含副作用,可能会被React暂停、中止或重新启动
- Commit 阶段:可以使用DOM,运行副作用,安排更新
# 装载阶段
- 组件的渲染并且构造DOM元素插入到页面的过程称为组件的装载。
- 装载阶段执行的函数会在组件实例被创建和插入DOM中时被触发,这个过程主要实现组件状态的初始化。
# 更新阶段
- 属性Props或状态State的改变会触发一次更新阶段,但是组件未必会重新渲染,这取决于
shouldComponentUpdate
。
# 卸载阶段
- 发生在组件从DOM中移除时,在这个阶段,会调用一些特定的生命周期方法,以便组件可以进行必要的清理工作。
# 组件挂载器
React的声明式渲染机制把复杂的DOM操作抽象为简单的State和Props的操作,因此避免了很多直接的DOM操作。不过,仍然有一些DOM操作是React无法避免或者正在努力避免的。
# ReactDOM API
ReactDOM.render(element, container [, callback])
顶层组件用于将VirtualDOM渲染到浏览器的DOM中。(将React元素VirtualDOM渲染到指定的DOM容器中)ReactDOM.findDOMNode(component)
获取当前组件的DOM元素节点引用。(返回与React组件关联的DOM元素)ReactDOM.unmountComponentAtNode(container)
从DOM树中卸载已装载的React组件并清空事件监听和状态。hydrate
如果在客户端和服务器端都使用React,并且希望利用SSR的优势,那么hydrate
是你需要的。ReactDOM.createPortal(child, container)
创建一个Portal,这是一个子组件,可以在DOM树中的任何地方渲染,而不仅仅是其父组件的DOM结构中。
# render
通常一个组件要发挥作用,总是要渲染内容,render
函数并不往DOM树上渲染或者装载内容,它只是返回一个JSX描述的结构,最终由React来操作渲染过程。而React肯定是把所有组件返回的结果综合起来,才能知道该如何产生对应的DOM修改。render
函数应该是一个纯函数,完全根据 this.state
和 this.props
来决定返回的结果,而且不要产生任何副作用。
控制传进来的容器节点里的内容。第一次被调用时,内部所有已经存在的DOM元素都会被替换掉。之后的调用会使用React的DOM比较算法进行高效的更新。
不会修改容器节点(只修改容器的子项)。可以在不覆盖已有子节点的情况下添加一个组件到已有的DOM节点中去。
目前会返回一个引用,指向ReactComponent的根实例。但这个返回值是历史遗留,应避免使用。因为未来版本的React可能会在某些情况下进行异步渲染。如果真的需要一个指向ReactComponent的根实例的引用,推荐的方法是添加一个callback到根元素上。
# 事件处理
React元素的事件处理和DOM元素的很相似,但是有一点语法上的不同:
- React事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用JSX语法时需要传入一个函数作为事件处理函数,而不是一个字符串。
- 不能通过返回false的方式阻止默认行为,必须显式使用preventDefault。