React三大核心理念
React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架 核心思路:
- 声明式:声明式编程优势直观,便于组合
- 组件化:降低系统间功能的耦合性,提高内部的聚合性
- 单向数据流:数据从父组件到子组件,子组件不能直接修改父组件传递的props。若需要修改,必须通过调用父组件传递的回调函数实现
React使用声明式(JSX)
jsx是JS的语法扩展,结构类似XML,主要用于声明React元素。JSX通过Babel编译,借助@babel/plugin-transform-react-jsx,JSX会被编译为React.createElement(),返回“React Element”的JS对象。
Babel插件如何实现JSX到JS的编译?
通过 babel 的 react 预设包(@babel/preset-react), 我们就可以对 JSX 进行转换: JSX 转为 React.createElement(...)
类组件与函数组件的区别
- 编程方式:类组件是基于面向对象编程,函数是函数式编程
- 类组件有生命周期,函数组件没有。
- 类组件需要继承Class,函数组件不需要。类组件可以获取实例化的this,可以基于this做各种操作,函数组件不行。
- 类组件内部可以定义并维护state,函数组件为无状态组件(可以通过hooks实现)
- 性能优化:类组件依靠shouldComponentUpdate控制组件更新,函数依靠React.Memo控制组件更新
虚拟DOM
定义
虚拟 DOM: 本质上就是一个 JS 对象, 通过一个对象来描述了每个 DOM 节点的特征, 并且通过虚拟 DOM 就能够完整的绘制出对应真实的 DOM。
// React 虚拟DOM节点
const vNode={
key=null,
type='div',//标签名或组件名
props:{
children:[//子元素们(这里表示div下有两个span)
{ type:'span', ... },
{ type:'span', ... }
],
className:'red',//标签上的属性
onclick:()=>{} // 事件
},
ref=null
}
原理
虚拟 DOM 的原理可以分为三个部分:创建虚拟 DOM、比较新旧虚拟 DOM、将差异更新到真实 DOM。
- 创建虚拟 DOM:会将页面上的每一个元素都抽象成一个 JavaScript 对象,包含了这个元素的标签名、属性、样式和子节点等信息
- 比较新旧虚拟 DOM:新旧虚拟 DOM 的比较时,会将新旧虚拟 DOM 树进行深度优先遍历,依次比较每一个节点,找出它们之间的差异。当找到一个节点有差异时,就将这个节点的差异记录下来,并继续比较它们的子节点。
- 主要是diff算法的策略
- 如果节点类型不同,直接替换整个节点;
- 如果节点类型相同,但是节点属性不同,更新节点属性;
- 如果节点类型和属性都相同,但是子节点有变化,递归进行比较子节点。
- 主要是diff算法的策略
- 将差异更新到真实 DOM:将差异更新到真实 DOM 时,会根据 Diff 算法生成的差异对象,依次对真实 DOM 进行操作。对于每一个需要更新的节点,都会先创建一个对应的新节点,然后根据差异对象的操作类型,进行相应的更新操作。
优缺点
- 优点
- 提高渲染性能:虚拟 DOM 可以减少 DOM 操作的次数,从而提高页面的渲染性能。
- 跨平台:虚拟 DOM 不依赖于浏览器,可以运行在各种平台上,包括服务器端和客户端。
- 提高开发效率:通过使用虚拟 DOM,开发者可以更加便捷地更新页面,并且不需要手动操作 DOM。
- 缺点
- 内存消耗:虚拟 DOM 需要额外的内存来存储虚拟 DOM 树和真实 DOM 树之间的关系,这可能会增加内存消耗。
state和props的区别
相同点
- 两者都是JS对象
- 两者都是用来保存信息
- props和state都能触发渲染更新
不同点
- props外部方式传递给组件,state组件内自己管理维护
- props组件内不可修改,state组件内可修改
- state是多变的,可以修改
super()和super(props)的区别
原因
React中不能直接修改state的值。必须通过调用setState来告知React数据发生变化。setState方法是从Component中继承的。
setState
setState发生了什么
- 调用
setState函数之后,React会将传入的参数对象与组件当前状态合并,然后触发调和过程,经过调和过程,React会以相对高效的方式,根据新的状态构建React元素树并重新渲染整个UI界面 - 在
React得到元素树之后,React会自动计算出新旧树节点的差异,根据差异对界面进行最小化重新渲染,React的差异算法能相对精确得知发生改变的节点,保证按需更新
setstate的说明,第一个参数
- 可以多次调用setState(),但是只会调用第一次重新渲染
-
如果后面的一次this.setState()想基于第一个this.setState()的结果做操作,如何实现?
- 使用setState((state,props)=>{})语法
class App extends React.Component{ state = { count:1 } handleClick = () =>{ this.setState((state,props)=>{ return{ count:state.count + 1} // 返回的count的值为2 }) this.setState((state,props)=>{ console.log("state的值:”,state) // 此时count的值为2 return{ count:state.count + 1} // 返回的count的值为3 }) } }
-
setState()的第二个参数
- 场景:在状态更新(页面完成重新渲染)后立即执行某个操作
- 语法:setState(updater,[callback])
this.setState(
(state.props)=>{} // 参数1
()=>{console.log('这个回调函数会在状态更新后立即执行')} //参数2
)
setState是异步还是同步?
- setState只在合成事件和构造函数中是异步的。
- 异步中如果对同一个值进行多次setState,setState批量更新策略会对其进行覆盖,取最后一次的执行。
- 如果是同时setState多个不同的值,在更新时会对其进行合并批量更新
- 在原生事件和setTimeout中是同步的。
合成事件
React是基于浏览器的事件机制自身实现的一套事件机制,包括事件注册、事件合成、事件冒泡、事件派发等。在React中这套事件机制被称为合成事件。
原生事件、合成事件的执行顺序
- React 所有事件都挂载在 document 对象上
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 所以会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
*** 事件名称命名方式不同
// 原生事件绑定方式
<button onclick="handleClick()">按钮命名</button>
// React 合成事件绑定方式
const button = <button onClick={handleClick}>按钮命名</button>
*** 事件处理函数书写不同
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>按钮命名</button>
React构建组件的方式有哪些
函数组件
function HelloComponent(props){
return <div>{props.name}</div>
}
React.createClass
function HelloComponent(props){
return React.createElement('div',null,'hello',props.name)
}
继承React.Component
class Timer extends React.Component{
}
Hook(钩子)
useState(用于在函数组件中添加状态管理)
const [number,setNumber] = useState(initialState)
useState通过传入一个初始值,返回一个数组,数组的第一项是设置的当前组件的值,第二项是修改这个值的方法。
import React,{useState} from 'react'
function Test(){
// 在后续的重新渲染中,useState返回的第一个值始终是更新后最新的state
const [count,setCount] = useState(0) // 初始数据是基本数据类型
const [tc,setTc] = useState({name:'tom',age:12}) // 定义的初始数据是引用类型,使用函数式更新结合扩展运算符
const increment = ()=>{
setCount(count + 1) // 直接调用setState传入更新的数据
// setState(()=>count + 1) // 对于复杂的更新逻辑使用函数式更新
}
const handleClick= ()=>{
setTc(()=>{
return {
...tc,
name:'alan'
}})
}
return(
<div>
<p>{count}</p>
<button onClick={increment}></button>
<button onClick={handleClick}></button>
</div>
)
}
export default Test
useContext(跨级组件之间通信)
useContext 用于在函数组件中访问上下文(Context)的值。它接收一个上下文对象(通过 React.createContext 创建),并返回当前上下文的值。它使得在组件树中深层级的组件中能够方便地使用上下文数据。
useEffect(用于处理副作用和模拟类组件的生命周期方法)
使用场景
- 数据获取、手动更改 DOM、设置订阅等
不同依赖数组的区别
import React, { useState, useEffect } from 'react';
function App(){
const [rightCount, setRightCount] = useState(0);
// 第一种:没有添加依赖数组,它的触发时机有:
- 组件挂载、卸载的时候
- 页面每一次`re-render`的时候,即`leftCount`和`rightCount`更新的时候,也是左按钮和右按钮点击的时候
useEffect(()=>{console.log('112121')})
useEffect(()=>{console.log('112121')},[rightCount])
useEffect(()=>{console.log('112121')},[])
}
//
清除函数的作用
useEffect(()=>{
cosnt timer = setInterval(()=>{
console.log('2323232')
},1000)
return ()=>{
cleatInterval(timer)
}
},[])
useMemo(缓存、记忆计算结果)
useMemo仅在任何依赖项的值发生变化时才会重新计算
useCallback
只有当其中一个依赖项的值发生变化时,回调才会发生变化。
diff算法
diff算法定义
diff算法可以帮助我们计算出Virtual Dom中真正变化的部分,针对该部分进行实际的DOM操作,而非渲染整个页面,保证每次操作后页面的高效渲染。
diff策略
tree diff :Web UI中DOM节点跨层级的移动操作比较少,忽略不计 component diff:相同类的两个组件将会生成相似得树形结构,不同类的两个组件将会生成不同的树形结构 element diff:对于同一层级的一组子节点,可以通过唯一ID进行区分
tree diff(同层级比较)
- React通过使用updateDepth对虚拟DOM树进行层次遍历
- 两棵树对同一层级节点进行比较,只要该节点不存在,那么该节点与其所有子节点会被删除,不比较
- 只需要遍历一次,便完成整个DOM树的比较
component diff(类型组件)
- 同一类型的组件,按照原策略比较virtual DOM tree
- 同类型组件,组件A转化为组件B,如果virtual DOM无变化,通过shouldComponentUpdate()方法 判断是否
- 不同类型的组件,diff算法会把要改变的组件判断为dirty component,替换整个组件的所有节点
element diff
节点属于同一层级时,diff提供3种节点操作,插入,移动,删除
- 插入:新的组件类型不在就集合中,全新的节点,需要对新节点进行插入操作
- 移动:旧集合中有新组件类型,且element是可更新的类型,就需要移动
- 删除:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要删除/
HOC(Higher-Order Components,高阶组件)
定义
高阶组件是一个参数为组件,并且返回值为新组件的函数
*** 定义类组件返回
// 定义一个高阶组件
// 1.高阶组件会接收一个组件作为参数
function hoc(Cpn) {
class NewCpn extends PureComponent {
render(){
return <Cpn/>
}
}
// 2.并且返回一个新的组件
return NewCpn
}
*** 定义函数组件返回
// 定义一个高阶组件
// 1.高阶组件会接收一个组件作为参数
function hoc(Cpn) {
function NewCpn() { }
// 2.并且返回一个新的组件
return NewCpn
}
实现高阶组件的方式
- 属性代理
- 反向继承
运用场景
- props增强
- 渲染判定鉴权
- 日志打点、表单校验
组件之间传参方法
父子之间通信(props)
格式:
父组件---传入数据
function EmailInput(){
return (
<label>Email:<input value={props.email}></label>
)
}
<子组件 自定义属性1={值1} 自定义属性={值2}。。。/>
const element = <EmailInput email="1213@163.com"/>
子组件 ---类组件---接收数据
// 接收数据: class 组件需要通过 this.props 来获取
class 子组件 extends Component {
console.log('从父组件中传入的自定义属性被收集在对象':this.props)
render(){return (<div>子组件的内容</div>)}
}
子传父
- 父组件提供函数,传递给子组件
- 子组件接收函数并且调用
//父组件提供函数,传递给子组件
class Parent extends React.Component {
state:{
num:11
}
fn = (num)=>{
console.log('接收到子组件数据',num)
}
render(){
return (
<div>
<Child fn = {this.fn}/>
</div>
)
}
}
// 子组件接收函数并且调用
class Child extends React.Component {
handleClick = ()=>{
// 调用父组件传入的props,并传入参数
this.props.fn(11)
}
return (
<button onClick={this.handleClick}>按钮</button>
)
}
兄弟之间通信
思路:状态提升,将共享状态提升到最近的公共组件中,由公共父组件管理这个状态
// Parent.js
任意组件之间通信 context
React-router的原理
React Router 路由的基础实现原理分为两种,
- 如果是切换 Hash 的方式,那么依靠浏览器 Hash 变化即可;
- 如果是切换网址中的 Path,就要用到 HTML5 History API 中的 pushState、replaceState 等。在使用这个方式时,还需要在服务端完成 historyApiFallback 配置。
在 React Router 内部主要依靠 history 库完成,这是由 React Router 自己封装的库,为了实现跨平台运行的特性,内部提供两套基础 history,一套是直接使用浏览器的 History API,用于支持 react-router-dom;另一套是基于内存实现的版本,这是自己做的一个数组,用于支持 react-router-native。
React Router 的工作方式可以分为设计模式与关键模块两个部分。从设计模式的角度出发,在架构上通过 Monorepo 进行库的管理。Monorepo 具有团队间透明、迭代便利的优点。其次在整体的数据通信上使用了 Context API 完成上下文传递。 在关键模块上,主要分为三类组件:第一类是 Context 容器,比如 Router 与 MemoryRouter;第二类是消费者组件,用以匹配路由,主要有 Route、Redirect、Switch 等;第三类是与平台关联的功能组件,比如 Link、NavLink、DeepLinking 等。
redux
定义
Redux 是一个独立的 JS 状态管理库。提供可预测化的状态管理。
Redux 核心概念及工作流程:
Store:存储状态的容器,JS 对象
View:视图,HTML 页面
Actions:对象,描述对状态进行怎样的操作
Reducer:函数,操作状态并返回新的状态
Redux 核心 API
1:创建 Store 状态容器
const store = Redux.createStore(reducer)
2:创建用于处理状态的 reducer
function reducer(state = onotoalState,action){}
3:获取状态
store.getState()
4:订阅状态
store.subscribe(function(){})
5:触发 action
store.dispatch({type:'description...'})
React状态管理有哪些?
Flux
Mobx
Mobx实现观察者模式(发布-订阅模式)
Mobx 提供了类似observable和的装饰器computed来定义可观察的状态和反应函数。用action修饰的动作用于修改状态,确保跟踪所有更改。
Mobx三个概念
- State(状态):能被Mobx追踪到的响应式状态数据,状态发生变化时,相关的依赖就会自动更新
- Conputed(计算属性):只有在其依赖的状态发生变化时,计算属性才会重新计算
- Action:用来改变状态
如何访问Mobx状态下的变量?
使用装饰器 observable 将变量定义为可观察来访问状态中的变量。
import { observable, computed } from 'mobx';
class MyStore {
@observable myVariable = 'Hello Mobx';
@computed get capitalizedVariable() {
return this.myVariable.toUpperCase();
}
}
const store = new MyStore();
console.log(store.capitalizedVariable); // Output: HELLO MOBX
store.myVariable = 'Hi Mobx';
console.log(store.capitalizedVariable); // Output: HI MOBX
Redux 工作流程
1:组件通过 dispatch 方法触发 Action
2:Store 接收 Action 并将 Action 分发给 Reducer
3:Reducer 根据 Action 类型对状态进行更改并将更改后的状态返回给 Store
4:组件订阅 Store 中的状态,Store 中的状态更新会同步到组件
Redux和Mobx有什么区别?
-
Redux 是一种更简单、更有主见的状态管理库,它遵循严格的单向数据流,并提倡不变性。它需要更多的模板代码和显式更新,但与 React 的集成度很高。
-
Mobx 提供的 API 更灵活、更直观,模板代码更少。它允许你直接修改状态,并自动跟踪变化以获得更好的性能。在 Redux 和 Mobx 之间做出选择取决于您的具体需求和偏好。
class组件中,事件中的this为啥是undefined
标准函数中,this引用的是把函数当成方法调用的上下文对象。 箭头函数中,this引用的是定义箭头函数的上下文。 bind()方法会创建一个新的函数实例,this值会被绑定到传给bind()的对象。apply()和call()都会以指定的this调用函数.
class Test extends React.Component{
constructor(props) {
super(props)
this.state = {count:0}
}
handleClick = ()=>{
console.log('箭头函数this',this)// 有内容,不为undefined
}
handle(){
console.log('标准函数 this',this) // undefined
}
render(){
return (
<div><button onClick={this.handleClick}>箭头函数</button>
<button onClick={this.handle}>标准函数</button>
</div>
)
}
}