react --(2) React 核心概念(上):JSX、元素渲染、组件props、state生命周期、事件处理
2019-11-14:
学习内容: React 应用程序的组成部分:元素和组件
学习材料:https://zh-hans.reactjs.org/docs/introducing-jsx.html
如果您最近几年没有使用JavaScript,那么这三点应该给您足够的知识,让您轻松阅读React文档:
- 我们使用
let
和const
语句定义变量。就React文档而言,您可以将它们视为与等效var
。 - 我们使用
class
关键字定义JavaScript类。关于它们,有两件事值得记住。首先,不同于对象,你不要需要把类的方法定义之间的逗号。其次,与许多其他带有类的语言不同,在JavaScript中this
,方法的值取决于调用方法的方式。 - 有时我们
=>
用来定义“箭头功能”。它们就像常规函数,但更短。例如,x => x * 2
大致等于function(x) { return x * 2; }
。重要的是,箭头函数没有自己的this
值,因此当您希望this
从外部方法定义中保留值时,它们非常方便。
补充:
(1)JS的自动分好插入陷阱https://zhuanlan.zhihu.com/p/33504239,所以我们应该多手动换行
(2)关于setState()更改state:
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。
一、JSX(JS + XML)简介:
const element = <h1>Hello, world!</h1>;
JSX 可以生成 React “元素”:Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。
React 不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息。
(1)在JSX中嵌入表达式:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。例如,2 + 2
,user.firstName
或 formatName(user)
都是有效的 JavaScript 表达式。
function formatName(user) { return user.firstName + ' ' + user.lastName; } const user = { firstName: 'Harper', lastName: 'Perez' }; const element = ( <h1> Hello, {formatName(user)}! </h1> );
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)JSX特定属性:
举例:添加tabindex属性
(tabindex 属性用于管理键盘焦点,决定元素是否能被选中,以及按下 tab
键过程中被选中的顺序,使用得当能够极大的提高应用的易用性。当 tabindex
设置为 0
时,元素的 tab 键序与其在源码中的顺序一致。默认情况下,如果元素本身是可获取焦点的就没有必要设置 tabindex
属性。但,如果你想让一个不能获取焦点的元素,比如 <span>
或 <div>
,也被包含在 tab
键序列表中,那么设置 tabindex = 0
就可以使这些元素按其在源码中的顺序出现在 tab 键序中。当 tabindex
被设置为像 -1
一样的负数时,该元素就变成可由代码获取焦点,但其本身并不在 tab 键序列表中。也就是说,在按下 tab
键时,该元素不能获取到焦点,但是可以通过代码来获取到焦点。)
两种方法:
i、通过使用引号,来将属性值指定为字符串字面量:
const element = <div tabIndex="0"></div>;
ii、也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
const element = <img src={user.avatarUrl}></img>;
⚠️注意:在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。
假如一个标签里面没有内容,你可以使用 />
来闭合标签(对比上一行代码)
const element = <img src={user.avatarUrl} />;
JSX 标签里能够包含很多子元素:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
(3)JSX自带防注入攻击:
const title = response.potentiallyMaliciousInput; // 直接使用是安全的: const element = <h1>{title}</h1>;
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
三、元素渲染:
元素是构成 React 应用的最小砖块。
(1)元素渲染
举例:假设你的 HTML 文件某处有一个 <div>,想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入
ReactDOM.render()
:
// html: <div id="root"></div> // js: const element = <h1>Hello, world</h1>; ReactDOM.render(element, document.getElementById('root'));
由于:React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。所以要更新一个已渲染的元素,只能创建一个全新的元素并传入。
在实践中,大多数 React 应用只会调用一次 ReactDOM.render()
。在下一个章节,我们将学习如何将这些代码封装到有状态组件中。
(2)React 只更新它需要更新的部分
React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
尽管每一秒我们都会新建一个描述整个 UI 树的元素,React DOM 只会更新实际改变了的内容。
三、组件与Props:
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
(1)函数组件:
本质就是一个js函数,接受唯一带有数据的“props”对象并返回一个React元素。
函数组件的两种定义方法:
i、js的方式:
ii、ES6的class方式:
(3)渲染组件:
渲染上面定义的函数组件,welcome:
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)转换为单个对象传递给组件,这个对象被称之为 “props”。
(4)完整流程:
让我们来回顾一下这个例子中发生了什么:
- 我们调用
ReactDOM.render()
函数,并传入<Welcome name="Sara" />
作为参数。 - React 调用
Welcome
组件,并将{name: 'Sara'}
作为 props 传入。 Welcome
组件将<h1>Hello, Sara</h1>
元素作为返回值。- React DOM 将 DOM 高效地更新为
<h1>Hello, Sara</h1>
。
注意: 组件名称必须以大写字母开头。
React 会将以小写字母开头的组件视为原生 DOM 标签。例如,
<div />
代表 HTML 的 div 标签,而<Welcome />
则代表一个组件,并且需在作用域内使用Welcome
。
(5)组合组件:
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。
例子:app 组件内多次渲染Welcome 组件
通常来说,每个新的 React 应用程序的顶层组件都是 App
组件。但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像 Button
这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。
(6)提取组件(建议多用):
提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(Button
,Panel
,Avatar
),或者组件本身就足够复杂(App
,FeedStory
,Comment
),那么它就是一个可复用组件的候选项。
(7)props的只读性:
规则:所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
相对的,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
四、state和生命周期:设置自己的计时器并每秒更新一次。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
上一章元素渲染的Clock组件是获取实时的系统时间,而非定时每秒更新一次:
此时Clock 组件已经被提取出来
(1)首先,将Clock 函数组件转换成class组件:
-
创建一个同名的 ES6 class,并且继承于
React.Component
。 -
添加一个空的
render()
方法。 -
将函数体移动到
render()
方法之中。 -
在
render()
方法中使用this.props
替换props
。 -
删除剩余的空函数声明。
这样做的目的是,每次组件更新时 render
方法都会被调用,但只要在相同的 DOM 节点中渲染 <Clock />
,就仅有一个 Clock
组件的 class 实例被创建使用。这就使得我们可以使用如 state 或生命周期方法等很多其他特性。
(2)向class组件中添加局部的state:
i、把 render()
方法中的 this.props.date
替换成 this.state.date
ii、添加一个 class 构造函数,然后在该函数中为 this.state
赋初值。super(props)是为了吧props传递到父类的构造函数中(Class 组件应该始终使用 props
参数来调用父类的构造函数。)
iii、移除 <Clock />
元素中的 date
属性:
(3)将生命周期方法添加到Class中:
组件的生命周期可分成三个状态:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
全部生命周期方法:https://reactjs.org/docs/react-component.html#lifecycle-methods
当 Clock
组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。
同时,当 DOM 中 Clock
组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。
我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法。这些方法叫做“生命周期方法”。
componentDidMount()
方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器(挂载):
componentWillUnmount()
生命周期方法中清除计时器(卸载):
tick()
的方法,Clock
组件每秒都会调用它。使用 this.setState()
来时刻更新组件 state:
setInterval() 方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。
总结:
-
当
<Clock />
被传给ReactDOM.render()
的时候,React 会调用Clock
组件的构造函数。因为Clock
需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化this.state
。我们会在之后更新 state。 -
之后 React 会调用组件的
render()
方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配Clock
渲染的输出。 -
当
Clock
的输出被插入到 DOM 中后,React 就会调用ComponentDidMount()
生命周期方法。在这个方法中,Clock
组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()
方法。 -
浏览器每秒都会调用一次
tick()
方法。 在这方法之中,Clock
组件会通过调用setState()
来计划进行一次 UI 更新。得益于setState()
的调用,React 能够知道 state 已经改变了,然后会重新调用render()
方法来确定页面上该显示什么。这一次,render()
方法中的this.state.date
就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 -
一旦
Clock
组件从 DOM 中被移除,React 就会调用componentWillUnmount()
生命周期方法,这样计时器就停止了。
(4)不要直接修改state:
不用setState是不会引起重新渲染组件的,另外setState同步异步问题请见置顶的补充。
(5)State 的更新可能是异步的:
出于性能考虑,React 可能会把多个 setState()
调用合并成一个调用。因为 this.props
和 this.state
可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
由于state可能异步更新,setState()不应该接受一个对象,而应该是一个函数。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
(6)state 的更新会被合并:
例如,你的 state 包含几个独立的变量,然后你可以分别调用 setState()
来单独地更新它们,当你调用 setState()
的时候,React 会把你提供的对象合并到当前的 state。这里的合并是浅合并(单层的合并),即完全替换同名的部分,其余部分不动。
(7)数据是向下流动的:
不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的 state 作为 props 向下传递到它的子组件中:屏蔽了来源
这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
五、事件处理:
(1)React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
(2)在 React 中另一个不同点是你不能通过返回 false
的方式阻止默认行为。你必须显式的使用 preventDefault
。
⚠️:在这里,e
是一个合成事件。React 根据 W3C 规范来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。
另外,很喜欢对onclick事件的处理单独写成一个处理函数
🌟(3)为了在回调中使用 `this`,绑定this到回调是必不可少的:
你必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。这个跟JS语法保持一致
不这么绑定也有方法的:(原理就是利用箭头函数的this从外部方法定义中保留值,不是在事件处理函数,就是在回调中用箭头自动绑定this)
一、public clss fields 语法:
二、可以在回调中使用箭头函数:(不推荐,因为:如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。)
🌟(4)向事件处理程序传递参数:
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。