版本v16.13.0
单独看react不是mvvm也不是mvc,只是一个view层(但是引入了redux等,并且把控制视图的逻辑写在组件内部,就是mvvm了)。 vue也不是mvvm和mvc,vue只是view层(但是如果引入vuex等,就是mvvm了)。mvvm和mvc都是一个架构模式。vm指的是数据驱动视图(也可以理解为组件,一个拥有状态也需要管理状态的视图),比如vue和react都是单向数据流(vue用v-modal实现双向绑定,react即使是非受控组件也不是双向绑定)
一、React简介
React 是一个用于构建用户界面的 JavaScript 库。React 是单向数据流
React 从诞生之初就是可被逐步采用的,因而你可以按需引入或多或少的 React 特性
script直接引入react,如下
// 通过 CDN 的方式引入 React,建议设置 crossorigin 属性
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
ReactDOM.render()函数:用来在指定的节点上面渲染对应的react元素,简单说就是用来更新 UI
// 把<h1>Hello, world!</h1>,绑定到后面获取到的root元素上面。root元素是在
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
二、JSX 简介
ReactElement就是react元素,或者说就是react中的虚拟dom。React 通过读取这些react元素,然后使用它们来构建 DOM 以及保持随时更新
(1)JSX,是一个 JavaScript 的语法扩展,jsx也就是react中的一个标签语法(React.createElement()的语法糖),也是react中的一个表达式,用来创建ReactElement;语法样式就是,在js中直接写的类似于html的样式。如下:就是一个jsx语法
// 下面标签语法既不是字符串也不是 HTML,而是jsx
// 由于jsx也是一个表达式,所以可以赋值给变量
const element = <h1>Hello, world!</h1>;
(2)jsx中用
{}
(大括号)的形式来嵌入 JavaScript 表达式。属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。还一个重要的是 如果大括号里面为一个数组,则数组里面的每一项都会被渲染为一个reactElement,并且展示出来。注意这种遍历的情况,需要在每一项上面加上key属性(这个key的默认值为数组的索引)
import React from 'react'
import { Router, Route, Link } from 'react-router'
import ReactDOM from 'react-dom'
const name = 'Josh Perez';
const element = <h1>Hello, {name}<img src={user.avatarUrl}></img></h1>; // 后面一段为在属性中插入js表达式,直接用大括号就好了。不需要加上引号
ReactDOM.render(
element,
document.getElementById('root')
);
// 大括号里面为一个数组,数组里面的每一项都会被渲染处理
function myComponent() {
const arr = [11.22];
return (
<div>
{
arr.map(value => {return <p key={value}>{value}</p>}) // 这里return出来的一个数组,里面的每一项都会被渲染为一个reactElement.而且,每一项是需要有一个key值的
}
</div>
);
}
(3)jsx里面属性的命名更加类似于js,如class 会写为className
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
(4)React.createElement(component, props, …children)创建ReactElement: 这个实际上是react原生的创建ReactElement的语法。上面的JSX也会被babel-loader或者ts-loader解析成这个函数,然后来创建ReactElement。
// 上面(3)里的代码,和下面这段等效
const element = React.createElement(
'h1', // 标签
{className: 'greeting'}, // 属性
'Hello, world!' // 子元素
);
// 创建的ReactElement的样式为下,注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
三、React 元素渲染
元素是构成 React 应用的最小砖块。元素描述了你在屏幕上想看到的内容
与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM这个插件会负责更新 DOM 来与 React 元素保持一致。可以理解为react dom 就是虚拟dom,而React 元素就是虚拟dom中的每个dom节点
React DOM 在更新的时候,会将元素和它的子元素与它们之前的状态进行比较,并只更新和之前状态有差别的部分
四、组件 & Props
props参数:在react中,自定义的组件会将jsx中接收的属性以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”
组件:react中,会将以小写字母开头的标签视为原生 DOM 标签,而大写字母开头标签的则视作一个自定义组件。组件都会返回(render)用于描述当前组件内容的 React 元素。
props和state的区别之一:一般情况下props是不可变数据是从外面传入进来的,而state可以任意修改是组件自己内部的。
定义组件: react中的组件有两种定义方式,一种是定义为函数组件,另外一种是定义为类组件
1、无状态组件(函数组件)
- 无状态组件(函数组件):这个组件的设计目的是,只做为展示性组件,即根据props展示组件,不涉及到要state状态的操作。函数组件接收一个props参数,返回一个reactElement。并且函数组件还有下面几个特点
- <1>组件不会被实例化,整体渲染性能得到提升
- <2>组件不能访问this对象。无状态组件由于没有实例化过程,所以无法访问组件this中的对象,所以也没有下面这些this上面的东西了(注,props是直接传入的,所以可以使用props)
- <3>没有state
- <4>没有context
- <5>没有refs
- <6>没有生命周期。
- <7>如果函数组件中用了hooks,就也可以使用上面的this、state、context、refs和生命周期。
- <8>性能。目前React还是会把函数组件在内部转换成类组件,所以使用函数组件和使用类组件在性能上并无大的差异
- 函数组件有一个特点,就是在每次重新渲染的时候,函数组件会把整个组件里面的所有东西都重新渲染,即把所有的函数那些都重新创建,所以可能会导致React.memo的优化失败
// 函数组件
// test.js文件
function Test(props) {
const showMessage = (ev) => {
alert('Followed ' + props.user, ev, this); // ev值为'aaa', this值为undefined
};
const handleClick = () => {
setTimeout(showMessage, 3000); // 这里都是没有用this,而是直接访问的showMessage
};
return (
<div onClick={()=>{showMessage('aaaa')}}>show me about function component2222222222233355!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!</div>
);
}
export default Test;
// welcome.tsx文件
import Test from '@/pages/test/test'
export default (): React.ReactNode => (
<PageHeaderWrapper>
<Test /> // 使用上面的函数组件,还是和类组件一样的方法
</PageHeaderWrapper>
);
2、类组件
类组件:类组件中的props存放在this中,这一点和VUE中的props类似,但是Vue可以直接使用this后跟属性名,但是React中还需要this.props后跟相对应的属性名
// 类组件
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={()=>{this.handleClick()}}>Follow</button>;
}
}
export default ProfilePage;
3、父子组件传值
- 父组件给子组件参数传递,props:父组件给子组件传递参数依然是用 props(“props” 是 “properties” 简写);父组件中,写在子组件标签上面的所有属性,都为传到子组件中的参数,然后传递进来的参数(如果是类组件)在render方法里面,通过this.props来调用。this.props上面的属性名字,就为传进来的名字,比如上面的
this.props.name
- Props 的只读性: 组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props
- JSX 组件标签中的所有内容都会作为一个 prop属性上的children 传递给对应的组件。this.props.children: 这个东西很好用,相当于vue中的router-view,用来替换写在父组件中的子组件,router 会帮我们找到这个 children
// BasicLayout.tsx
// 下面的this.props.children就是在路由BasicLayout下面的子路由的位置
<Layout>
<Header></Header>
<Content style={
{height: this.state.contentHeight,padding: '15px'}}>
{this.props.children}
</Content>
</Layout>
- 子组件给父组件传递参数,触发自定义事件:在react中,也是通过触发自定义事件来使子组件给父组件传递参数。不过好像是不能用$emit这种来触发父组件中的自定义事件的。react中传参如下:
- 父组件中:在子组件的标签中绑定一个自定义事件,事件里面绑定一个写在父组件中和render同级的事件处理函数。在 React中,有一个命名规范,通常会将自定义事件命名为 on[Event],将事件处理函数命名为 handle[Event] 这样的格式
- 子组件中:子组件中直接在this.props上面调用自定义事件就好了,而不是用this.$emit触发了。
- 注:由于在react中,父组件调用子组件的时候,写在子组件标签上面的属性都会放在子组件的props中,所以在子组件触发自定义事件的时候,就相当于是父组件调用了那个函数(因为函数调用是调用的定义的那个位置的),所以就可以向父组件传值了
// 子组件
class Square extends React.Component<any,any> {
render() {
return (
<button className="square" onClick={() => this.props.onSquareClick(this.props.index)}> // 子组件上面调用自定义事件,并且传值
{this.props.value}
</button>
);
}
}
// 父组件
class Board extends React.Component<any,any> {
handleSquareClick(index: any) { // 父组件中定义的自定义事件的处理函数
console.log(index);
}
renderSquare(i: any) {
return <Square index={i} value={this.state.squares[i]} onSquareClick={(index:any) => this.handleSquareClick(index)}/>; // 父组件写给子组件的props
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
</div>
</div>
);
}
}
五、State & 生命周期
state和生命周期都是只有class组件才有,当然this.setState()也是只有class组件才有的
1、state
State 与 vue中的data 类似,可以通过改变这个值来改变界面;state 是私有的,并且完全受控于当前组件
- 组件的state
- 组件内部数据state: 每个组件中可以有state,用来存当前组件的数据,且应该被视为一个组件的私有属性。在组件的构造函数中初始化,其余地方用this.state来访问。
- this.setState(updater, callback): 这个为修改组件中的state,
this.setState()
,传入的值为要修改的属性的一个对象,或者传入一个函数。如果传入对象的话,传入的对象会和state上面的值进行合并;传入函数的话。函数的返回值会和state上面的值合并。如果不用这个函数改变state,而是直接给this.state赋值,则不会有效果,但是如果直接给this.state里面的属性赋值,那么会改变对应的属性,但是不会同步更新到dom里面- constructor: 每次调用这个组件的时候,都会先调用这个组件的constructor方法。初始化state就必须添加一个constructor。每次定义子类的构造函数时,都需要调用 super 方法,所以在所有含有构造函数的的 React 组件中,构造函数必须以 super(props) 开头。
class Square extends React.Component<any,any> {
constructor(props: any) {
super(props);
this.state = {value2: props.value};
}
render() {
return (
<button className="square" onClick={() => this.setState({value2: 'x'})}>
{this.state.value2}
</button>
);
}
}
2、生命周期
和vue一样,react也有一个生命周期。生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。详情请见,
零、React API
章节的React.Component
// 挂载阶段: 当组件实例被创建并插入 DOM 中时
constructor(props)
static getDerivedStateFromProps(nextProps, prevState)
render()
componentDidMount()
// 更新阶段: 当组件的 props 或 state 发生变化时会触发更新
static getDerivedStateFromProps(nextProps, prevState)
shouldComponentUpdate(nextProps, nextState)
render()
getSnapshotBeforeUpdate(prevProps, prevState)
componentDidUpdate(prevProps, prevState, snapshot)
// 卸载阶段: 当组件从 DOM 中移除时会调用如下方法
componentWillUnmount()
六、事件处理
React 自定义事件命名为 on[Event],将事件处理函数命名为 handle[Event] 这样的格式。小驼峰式(camelCase),而不是纯小写
- <1>保证事件处理函数中this可使用。有下面三种方法
- (1)直接在构造函数中给函数绑定this。这种方法是指,普通方法传参的时候,直接在构造函数中绑定,这方法太麻烦了,不用这个
- (2)使用实验阶段的 public class fields 语法,即在定义函数的时候,把函数定义为箭头函数,那么this就自动是整个class组件了。这个方法最好,不过就是在实验阶段。如果不生效,可能就需要安装插件了。
npm install --save-dev babel-plugin-transform-class-properties
,配置为{"plugins": [["babel-plugin-transform-class-properties", { "spec": true }]]}
。配置了也不一定有效,所以还是函数组件好- (3)调用函数的时候使用箭头函数,这个方法还行,不过会导致PureComponent的优化无效(原因是调用的时候的箭头函数,创建的是匿名函数,而匿名函数的地址每次都不一样,所以nextProps和preProps就会不一样),如下
- <2>向事件处理函数中对应的传参
- (1)使用bind进行传参。由于bind是把参数传入,并且是返回一个函数的,所以不会影响事件处理函数
- (2)使用箭头函数进行传参
// 使用箭头函数来让事件处理函数中this可使用
class LoggingButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick1.bind(this); // 法1,直接在构造函数里面绑定,这时候,函数里面的this就是class了,如果不绑定,则this为undefined
}
handleClick1() { // 法一的函数
console.log('this is:', this);
}
handleClick2 = () => { // 法二的函数,直接这样定义就好了。里面的this就是整个组件了。一般都是这样用
console.log('this is:', this);
}
handleClick3() { // 法三的函数
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick1}> // 法一,直接在构造函数里面绑定
Click me1
</button>
<button onClick={this.handleClick2}> // 法二,定义的函数为箭头函数。一般都是用这个
Click me2
</button>
<button onClick={(e) => this.handleClick(e)}> // 法三,在调用的时候使用箭头函数
Click me3
</button>
);
}
}
// 使用bind进行传参。这样把对应的this也改变了,就可以用上面那个法二的语法了。一般都是用这个
<div onClick={this.func.bind(this, 'shommmmmmmmmmmmm')}>1111111</div>
// 使用箭头函数进行传参
<div onClick={(ev) => {this.func(ev, '111111')}}>1111111</div>
七、条件渲染
就是相当于实现vue种的v-if
- 用运算符来实现vue中的v-if这种条件渲染效果
- 用与运算符
&&
来实现。这个一般是用来实现那种只有一种结果的情况- 用三目运算符
? :
来实现。这个就可以实现两种结果了。
- 如果组件里面的render 方法返回 null,则不进行任何渲染。但是不会影响组件的生命周期,组件的生命周期函数依然会被调用
// 下面为用与运算符 ``&&``来实现的。unreadMessages.length > 0为true的时候,就返回后面的
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
// 下面为用三目运算符 ``? :``来实现
return (
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
);
// 下面这个组件,return为null,不进行任何渲染
function myComponent(props) {
return null;
}
八、列表 & Key
就是相当于实现vue中的v-for,就是一个遍历
- 用map来实现vue中的v-for这种遍历效果:实际上在react中的做法是,给数组使用map,然后把最后生成好了的完整数组dom结构,插入到render里面去。这里的原理是在jsx的大括号里面如果包裹数组,那么这个数组的每一项都会被渲染为一个reactElement
- key:key 帮助 React 识别哪些元素改变了,比如被添加或删除。所以如果是在jsx中使用数组进行遍历的情况,需要给数组的每一项一个key值(这个key的默认值为数组的索引)。
- 数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的
- 这里这个key,最主要的目的就算性能优化;因为在diff算法中,新旧节点树的同一位置的兄弟节点中,如果有相同key的节点,有可能是只需要移动位置,而不需要创建
- 还有一个重要的点是,就算在组件上, 指定了key值,但是在这个组件的props属性中,依然是无法读取出key的,即无法访问props.key
// 创建一个变量,然后使用
function NumberList(props) {
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
// 或者用下面方法,直接写在大括号里面
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =><ListItem key={number.toString()} value={number} />)}
</ul>
);
}
九、表单
就是相当于实现vue中的双向绑定。
- 受控组件与非受控组件:它们都是基于react的form表单组件元素的
- 受控组件:就是为某个form表单组件添加了value属性。而一旦设置了一个value值(这个值是存在state里面的),则不管用户在界面输入什么东西,值都不会被改变,所以想要改变这个value就需要我们自己来调用事件来改变(setState),所以称为受控;如:
<input type="text" value="Hello!" />
- 受控组件如果不需要默认值,而是需要把组件的placeholder展示出来,则需要把value设置为undefined或者null
- 非受控组件:就是没有添加value属性的表单组件元素。这时候,不管用户输入什么(值是存在DOM 节点上面的),都是会显示到界面上的。如
<input type="text" />
或者<input type="file" />
当type等于file的时候,永远不可能绑定value,所以这个肯定就是非受控组件了。
- 非受控组件的使用方法为给组件绑定ref,然后或者这个ref,就可以获取到组件的值了。具体使用请看进阶部分
- 通过自己绑定state来实现vue中的双向绑定效果:具体做法就是给表单的value绑定为state,然后给表单绑定change事件,事件中用setState()来改变state
- 下面绑定在jsx上面的这个onChange实际上是js中的input事件,因为这里的onChange是在value改变的时候就立即触发,而不是等到失去焦点的时候才触发。在jsx里面使用onInput会报错
- label标签上面的for,在react中应该被写为htmlFor。如:
<label htmlFor="namedInput">Name:</label>
- 关于textarea标签:在html中textarea标签上面是没有value属性的,但是可以通过js中的textarea的dom元素来读取value值(读取的value值为textarea里面的文本值)。而在 React 中, 标签拥有了value属性,来表示内容
- 关于select标签的选项选中:在react中,在select 标签上使用 value 属性,来对应option 的value值,来进行选中
class Board extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {value: '',value2: 'lime'};
this.myRef = React.createRef();
}
handleChange(ev: any) {this.setState({value: ev.target.value});}
handleChange2(ev: any) {this.setState({value2: ev.target.value});}
show() {this.myRef.current.value;} // 这里为使用非受控组件,直接获取对应ref上面的值
render() {
return (
<input type="text" value={this.state.value} onChange={(ev) => {this.handleChange(ev)}} />
<select value={this.state.value2} onChange={(ev) => {this.handleChange2(ev)}}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
</select>
<input type="text" value={undefined} placeholder='gogogo' /> // 这里把value设置为了undefined,所以可以显示出对应的placeholder
<input type="text" detaultValue={'11'} ref={this.myRef} /> // 这里为使用非受控组件,直接给组件绑定ref
);
}
}
十、状态提升
状态提升,就是把子组件中相同的数据或者方法,放到父组件中去。使用context应该也是一种状态提升的方法。
十一、组合 vs 继承
组合即在组件中传入一些内容,然后使用props.children来获取。或者使用render props来在属性中传入jsx
react中的组件没有继承这一需求。一般使用组合就好了
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">Welcome</h1>
</FancyBorder>
);
}