Appearance
React
主流的思想:不在直接去操作DOM,而是改为 “数据驱动思想”
操作DOM思想:
- 操作DOM比较消耗性能广主要原因就是:可能会导致DOM重排(回流)/重绘
- 操作起来麻烦一些
数据驱动思想:
- 不去直接操作DOM
- 直接去操作数据(修改了数据,框架会按照相关的数据,让页面重新渲染)
- 框架底层实现视图的渲染,也是基于操作DOM完成的
- 构建了一套 虚拟DOM->真实DOM 的渲染体系
- 有效避免了DOM的重排/重绘
- 开发效率较高,最后的性能相对较好
MVC
React框架采用的是MVC体系,Vue框架采用的是MVVM体系 MVC:"M" 代表数据模型(Model),"V" 代表视图(View), "C" 代表控制器(Controller)
- 我们需要按照专业的语法去构建视图(页面): react 中是基于jsx语法来构建视图的
- 构建数据层:但凡在视图中,需要“动态”处理的(需要变化的,不论样式还是内容),都要有对应的数据模型
- 控制层:当我们在视图中(或者根据业务需求)进行某些操作的时候,都是去修改相关的数据,然后React框架会按照最新的数据,重新渲染 视图,以此让用户看到最新的效果 数据驱动视图的渲染!
视图中的表单内容改变,想要修改数据,需要自己写代码实现
- 单向驱动
MVVM:"M" 代表数据模型,"V" 代表视图,"VM" 代表视图模型(ViewModel)
- 数据驱动视图的渲染:修改数据,视图会跟着更新
- 视图驱动数据的更改,监听页面中表单元素内容的改变,自动去修改相关的数据
- 双向驱动(双向数据绑定)
版本之间区别
16版本:一些项目用的最多的
17版本:最大的升级就是看不出升级(语法没变啥,只是底层处理机制上的升级)
18版本:机制和语法都有区别使用creat-react-app 脚手架
json
{
"scripts": {
// 开发环境:在本地启动web服务器,预览打包内容
"start": "react-scripts start",
// 生产环境:打包部署,打包的内容输出到 build 目录中
"build": "react-scripts build",
// 单元测试
"test": "react-scripts test",
// 暴露webpack的配置规则,因为想修改默认的打包规则
"eject": "react-scripts eject"
},
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
// 性能检测工具
"web-vitals": "^2.1.4"
},
}HTML中简单使用
React 18以前(使用的js,没用jsx)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Hello World</title>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/javascript">
const VDOM = React.createElement("h1", {id: 'title'}, 'Hello React')
ReactDOM.render(VDOM, document.getElementById('root'))
</script>
</body>
</html>jsx
html
<body>
<div id="root"></div>
<script type="text/babel">
const VDOM = <h1 id="title">Hello,React</h1>
ReactDOM.render(VDOM, document.getElementById('root'))
</script>
</body>React 18
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Hello World</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const VDOM = <h1 id="title">Hello,React</h1>
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(VDOM)
</script>
</body>
</html>TIP
关于虚拟DOM:
- 本质是Object类型的对象(一般对象)
- 虚拟DOM身上的属性少,真实DOM身上的属性多,因为虚拟DOM是React内部在用,无需真实DOM身上那么多的属性
- 虚拟DOM最终会被React渲染成真实DOM
绑定变量
jsx
const myId = 'tItLe'
const content = 'HeLLo,ReAct!'
const VDOM = (
<h2 id={myId.toLowerCase()}>
<span>{content.toLowerCase()}</span>
</h2>
)
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(VDOM)JSX的语法规则:
多层结构的时候,使用小括号包裹
定义虚拟DOM时,不要写引号
标签中混入JS表达式时要用
{}{}胡子语法中嵌入不同的值,所呈现的也不同- number/string 值是什么,就渲染出什么
- boolean/null/undefined/Symbol/BigInt 渲染的内容是空
- 除数组对象外,其余对象一般都不支持在
{}中进行渲染,但也有特殊情况- JSX虚拟DOM对象
- 个元素style行内样式,要求必须写成一个对象格式
- 数组对象,将数组中的每一项分别拿出来渲染(并不是变成字符串渲染,中间没有逗号)
- 函数对象,不支持在
{}中渲染,但是可以作为函数组件,用<Component />方式渲染
样式的类名指定不用
class,使用className内联样式要用
jsx<div style={{color: 'red'}}></div>虚拟DOM必须只有一个根标签
标签必须闭合
标签名首字母大写:react就去渲染对应的组件,若组件没有定义,则报错
标签名首字母小写:则将标签转为html中同名的元素,若html中无该标签对应的同名元素,则会报错
JSX中使用循环
循环创建的元素一定设置key属性,属性值是本次循环中的唯一值,用于优化DOM-DIFF
jsx
const list = ['Angular', 'Vue', 'React']
const VDOM = (
<>
<h2> 前端框架列表 </h2>
<ul>
{
list.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
</>
)
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(VDOM)JSX条件渲染
jsx
const flag = false
root.render(
// 类似于Vue中的v-show
<button style={{display:flag?'block':'none'}}>按钮1</button>
// 类似于Vue中的v-if
{flag ? <button>按钮2</button> : null}
{flag && <button >按钮3</button>}
)普通class类名和动态class类名的绑定
可以使用 classnames 这个js库,可以非常方便的通过条件动态控制class类名的显示
jsx
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span
key={item.type}
onClick={() => handleTabChange(item.type)}
className={`nav-item ${ type === item.type && 'active'}`}
className={classNames('nav-item', { active: type === item.type })}> //[!code ++]
{item.text}
</span>)}
</li>JSX底层处理机制
第一步:把我们编写的JSX语法,编译为虚拟DOM对象 virtualDOM, 虚拟DOM对象:框架自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征!!
- 基于
babel-preset-react-app把 JSX 编译为React.createElement(...)(legacy API) 这种格式!! - 只要是元素节点,必然会基于createElement进行处理!
- React.createElement(ele,props,..children)
- ele: 元素标签名(或组件)
- props: 元素的属性集合(对象),如果没有设置过任何的属性,则此值是null
- children: 第三个及以后的参数,都是当前元素的子节点
虚拟DOM中的内容
js
virtualDOM = {
$$typeof: Symbol('react.element'),
key: null,
props:{
'元素的相关属性',
children: '子节点信息 (没有子节点则没有这个属性、属性值可能是一个值、也可能是一个数组)'
},
ref:null,
type: '标签名 (或组件)'
}第二步:把构建的 virtualDOM 渲染为真实DOM 真实DOM:浏览器页面中,最后渲染出来,让用户看见的DOM元素!!
补充说明:第一次染页面是直接从 virtualDOM -> 真实DM;但是后期视图更新的时候,需要经过一个 DOM-DIFF 的对比, 计算出补丁包 PATCH (两次视图差异的部分),把 PATCH 补丁包进行染!!

组件分类
函数式组件
jsx
function Component() {
console.log(this) // undefined
// 此处的this是 undefined 是因为通过 `babel` 编译后开启了严格模式
return <h1>Hello</h1>
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Component/>)root.render()之后,发生了什么
- React解析组件标签,找到了 Component 组件。
- 发现组件是使用函数定义的,调用该函数,将返回的虚拟DOM转为真实的DOM,随后呈现在页面中。
类式组件
js
class Person {
constructor(name, age) {
// 构造器中的this是谁,- 类的实例对象
this.name = name
this.age = age
}
// 一般方法
say() {
// say方法放在了哪里 - 类的原型对象上,供实例使用
// 通过Person实例调用say时,say中的this就是Person实例
console.log(`我是${this.name},今年${this.age}岁`)
}
}
const p1 = new Person('张三', 18)
p1.say()
class Student extends Person {
constructor(name, age, grade) {
super(name, age)
this.grade = grade
}
say() {
console.log(`我是${this.name},今年${this.age}岁,我在读${this.grade}`)
}
}
const s1 = new Student('李四', 19, '一年级')
s1.say()总结
- 类中的构造器不是必须填写的,要对实例进行一些初始化的操作,如添加指定属性时才写
- 如果B类继承了A类,且B类中有构造器,那么B类中的构造器的super是必须被调用的
- 类中的方法都是放在了类的原型对象上,供实例去使用
使用
jsx
class MyComponent extends React.Component {
render() {
// render是放在哪里的 - MyComponent的原型对象上,供实例使用
// render中的this 指向MyComponent的实例对象(MyComponent组件实例对象)
return <h2>hhhh</h2>
}
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<MyComponent/>)root.render()之后,发生了什么
- React解析组件标签,找到了 Component 组件。
- 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用 原型上的render方法
- 将render返回的虚拟DOM转为真实的DOM,随后呈现在页面中。
组件实例的三大核心属性
state
state 是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)
组件被称为“状态机”,通过更新组件的state来更新对应的页面显示(重新渲染组件)
在class中使用
jsx
class Weather extends React.Component {
constructor(props) {
super(props)
this.state = {
isHot: true
}
this.change = this.change.bind(this)
}
render() {
return (
// 这里使用change 而不是change(),是因为change()返回的是函数的返回值
<h2 id="title" onClick={this.change}>今天的天气很{this.state.isHot ? '炎热' : '凉爽'}</h2>
)
}
change() {
console.log('this', this);
// 严重注意:状态(state)不能直接修改,要借助内置API去更改,下面就是直接更改
// this.state.isHot = !this.state.isHot // 这是错误的写法
this.setState({
isHot: !this.state.isHot
});
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Weather/>)WARNING
change是作为onClick的回调,所以不是通过实例调用的,是直接调用类中的方法默认开启了局部的严格模式,所以change中的this为undefined
使用bind解决this指向问题,在constructor中使用
this.change = this.change.bind(this);state 不可以直接修改,必须通过
setState修改,而且更新是合并不是替换construct 只有被实例化的时候被调用一次
render 调用1+n次,1是初始化的那次,n是状态更新的次数
简写:
jsx
class Weather extends React.Component {
// 初始化状态
state = {isHot: true}
render() {
return (
<h2 id="title" onClick={this.change}>今天的天气很{this.state.isHot ? '炎热' : '凉爽'}</h2>
)
}
// 自定义方法,—— 要用赋值语句的形式 + 箭头函数
change = () => {
console.log('this', this);
this.setState({
isHot: !this.state.isHot
});
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Weather/>)props
props是只读的(单向数据流)
jsx
class Person extends React.Component {
render() {
console.log('this', this)
const { name, age, sex } = this.props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
</>
)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Person name="Tom" sex="男" age={19} />);解构传递一个对象
jsx
class Person extends React.Component {
render() {
console.log('this', this)
const { name, age, sex } = this.props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
</>
)
}
}
const obj = {
name:'Tom',
sex:'男',
age:19
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Person {...obj} />);PropTypes(prop 传递的参数类型)
引用
html
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>jsx
class Person extends React.Component {
render() {
console.log('this', this)
const { name, age, sex } = this.props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
</>
)
}
}
Person.propTypes = {
name:PropTypes.string.isRequired, // name是字符串且是必填项
sex:PropTypes.string
}
// 默认值
Person.defaultProps = {
sex:'不男不女'
}
const obj = {
name:'Tom',
sex:'男',
age:19
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Person {...obj} />);WARNING
在propTypes中定义一个函数的时候,要写成 speak:PropTypes.func的形式
这些都已经过时了,TS是更好的选择
简写
jsx
class Person extends React.Component {
static propTypes = {
name:PropTypes.string.isRequired, // name是字符串且是必填项
sex:PropTypes.string
}
static defaultProps = {
sex:'不男不女'
}
render() {
console.log('this', this)
const { name, age, sex } = this.props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
</>
)
}
}
const obj = {
name:'Tom',
sex:'男',
age:19
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Person {...obj} />);如果写了
constructor,就必须在super中传props,不然会造成this.props为undefined
jsx
constructor(props) {
super(props)
console.log(this.props);
}在函数式组件中使用props
函数式组件中只能使用props,因为函数可以接受参数
jsx
function Person(props) {
const { name, age, sex } = props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>性别:{sex}</li>
</ul>
</>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
const obj = {
name: '张三',
age: 18,
sex: '男'
}
root.render(<Person {...obj} />)函数式组件也可以使用 PropTypes 来限制类型
jsx
function Person(props) {
const { name, age, sex } = props
return (
<>
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>性别:{sex}</li>
</ul>
</>
)
}
Person.propTypes = {
name:PropTypes.string.isRequired, // name是字符串且是必填项
sex:PropTypes.string,
age:PropTypes.number
}
Person.defaultProps = {
sex:'不男不女'
}
const root = ReactDOM.createRoot(document.getElementById('root'));
const obj = {
name: '张三',
age: 18,
sex: '男'
}
root.render(<Person {...obj} />)refs
使用 ref 来管理React中的 DOM 元素
字符串形式的ref
jsx
class Com extends React.Component {
render() {
return (
<>
<div>
<input ref="input1" type="text" placeholder="点击按钮显示数据" />
<button onClick={this.show}>点我显示左侧数据</button>
<input ref='input2' onBlur={this.blur} type="text" placeholder="失去焦点显示数据" />
</div>
</>
)
}
show = () => {
console.log('this', this);
console.log('input1', this.refs.input1.value);
}
blur = () => {
const {input2} = this.refs
console.log(input2.value);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Com />);WARNING
这种方法已经过时了,而且有着效率问题,不推荐使用
回调函数形式的ref
在ref中使用回调函数,该函数会自动执行,currentNode 就是当前标签所在的节点
jsx
class Com extends React.Component {
render() {
return (
<>
<div>
<input
ref={(currentNode) => {this.input1 = currentNode}}
type="text"
placeholder="点击按钮显示数据" />
<button onClick={this.show}>点我显示左侧数据</button>
<input
onBlur={this.blur}
ref={currentNode => this.input2 = currentNode}
type="text"
placeholder="失去焦点显示数据" />
</div>
</>
)
}
show = () => {
console.log(this.input1.value);
}
blur = () => {
const { input2 } = this
console.log(input2.value);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Com bool />);如果 ref 回调函数是一内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM元素。
这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式 可以避免上述问题,但是大多数情况下,他是无关紧要的。
class的ref绑定函数
jsx
class Com extends React.Component {
render() {
return (
<>
<div>
{/* <input
ref={(currentNode) => { this.input1 = currentNode }}
type="text"
placeholder="点击按钮显示数据"
/> */}
<input
ref={this.saveInput}
type="text"
placeholder="点击按钮显示数据" />
<button onClick={this.show}>点我显示左侧数据</button>
<input onBlur={this.blur}
ref={currentNode => this.input2 = currentNode}
type="text"
placeholder="失去焦点显示数据" />
</div>
</>
)
}
saveInput = (currentNode) => {
this.input1 = currentNode
console.log(this);
console.log('#',currentNode);
}
show = () => {
console.log(this.input1.value);
}
blur = () => {
const { input2 } = this
console.log(input2.value);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Com bool />);TIP
注释的时候需要使用 /* */ 来包裹,然后用大括号包裹
jsx
{/*<h2>hello</h2>*/}createRef
React.createRef 调用后可以返回一个容器,该容器可以存储被ref所标识的节点,该容器是“专人专用的”(只能存一个)
createRef 返回一个对象,该对象只有一个属性:
- current:初始值为 null,你可以稍后设置为其他内容。如果你把 ref 对象作为 JSX 节点的 ref 属性传递给 React,React 将设置其 current 属性。
jsx
class Com extends React.Component {
myRef = React.createRef()
render() {
return (
<>
<input type="text" ref={this.myRef} />
<button onClick={this.show}>点我</button>
</>
)
}
show = () => {
console.log(this.myRef.current.value);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Com />)React中的事件处理
- 通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件,而不是使用的原生DOM事件 ———— 为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素(冒泡)) ———— 为了高效
- 通过
event.target得到发生事件的DOM元素对象,不要过多的使用ref
当发生事件的元素,正好是要操作的元素就可以省略ref
jsx
class Com extends React.Component {
myRef = React.createRef()
render() {
return (
<>
<input type="text" ref={this.myRef} />
<button onClick={this.show}>点我</button>
<input onBlur={this.blur} type="text" /> //[!code ++]
</>
)
}
show = () => {
console.log(this.myRef.current.value);
}
blur = (event) => {
console.log(event.target.value);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Com />)非受控组件
非受控组件是指表单元素的值和状态并不由React组件的状态控制的一类组件。换句话说,表单元素的值不会受到React组件的状态变化影响,而是直接从DOM中读取。 这意味着在处理非受控组件时,你需要直接操作DOM来获取和设置表单元素的值。非受控组件通常比较适用于简单的场景,或者与第三方库集成时。
页面中所有输入类DOM,是现用现取,就是非受控组件
jsx
class Login extends React.Component {
render() {
return (
<>
<form onSubmit={this.submit}>
用户名:<input
ref={c=>this.username=c}
type="text"
name="username" />
密码:<input
ref={c=>this.password=c}
type="password"
name="password" />
<button>login</button>
</form>
</>
)
}
submit = (event) => {
// 阻止默认事件
event.preventDefault();
const { username, password } = this
console.log(`登录的用户名是${username.value},密码是${password.value}`)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Login />);在这里,ref 被用于获取输入框的引用,并且在提交表单时,直接通过 this.username.value 和 this.password.value 来获取输入框的值,这是非受控组件的典型做法。
受控组件
受控组件是指表单元素的值和状态完全由React组件的状态来控制的一类组件。 在受控组件中,表单元素的值通过React的state来管理,同时通过事件处理函数来更新这些状态。 每当用户与表单元素交互时,都会触发React组件的状态更新,从而保持React组件与表单元素的状态同步。
jsx
class Login extends React.Component {
state = {
username: '',
password: ''
}
render() {
return (
<>
<form onSubmit={this.submit}>
用户名:<input
onChange={this.nameChange}
type="text"
name="username" />
密码:<input
onChange={this.passwordChange}
type="password"
name="password" />
<button>login</button>
</form>
</>
)
}
submit = (event) => {
// 阻止默认事件
event.preventDefault();
const { username, password } = this.state
console.log(`登录的用户名是${username},密码是${password}`)
}
nameChange = (event) => {
this.setState({
username: event.target.value
})
}
passwordChange = (event) => {
this.setState({
password: event.target.value
})
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Login />);高阶函数
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数
- 若A函数,接收的参数是一个函数,那么A就可以称为高阶函数
- 若A函数,调用的返回值依然是一个函数,那么A就可以成为高阶函数
常见的高阶函数:Promise、setTimeout、arr.map()
函数柯里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
jsx
class Login extends React.Component {
state = {
username: '',
password: ''
}
render() {
return (
<>
<form onSubmit={this.submit}>
用户名:<input
onChange={this.nameChange}
onChange={this.formChange('username')}
type="text" name="username" />
密码:<input
onChange={this.formChange('password')}
type="password" name="password" />
<button>login</button>
</form>
</>
)
}
submit = (event) => {
// 阻止默认事件
event.preventDefault();
const { username, password } = this.state
console.log(`登录的用户名是${username},密码是${password}`)
}
formChange = (type) => {
return (event) => this.setState({
[type]: event.target.value
})
}
nameChange = (event) => {
this.setState({
username: event.target.value
})
}
passwordChange = (event) => {
this.setState({
password: event.target.value
})
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Login />);也可以不使用函数柯里化来解决
jsx
class Login extends React.Component {
render() {
return (
<form>
密码:<input
onChange={(event) => {this.change('password', event)}}
type="password" name="password"/>
<button>login</button>
</form>
)
}
change = (type, event) => {
this.setState({
[type]: event.target.value
})
}
}