React 相关知识点
- React 渲染 DOM 的整体流程(从初始加载和后续更新的两个角度进行阐述)
- JSX 及其特点
- 函数组件的特点及其使用场景
- 类组件的特点及其使用场景
- props 和 state 的区别
- 类组件中的数据声明方式及其各自的使用场景
- state 中的数据如何修改
- setState 是异步还是同步
- setState中如何获取修改后的最新值
- forceUpdate 的作用
- React 条件渲染的方式
- 渲染列表时,为什么要使用 key
- 受控组件和不受控组件的区别及其各自的使用场景
- React 中的传值方式
- 发布订阅模式的原理及其使用方法
- diff算法及其优势
- 虚拟 DOM 的优点
- shouldComponentUpdate 的作用
- React 对于引用类型的数据如何进行渲染优化
- Immutable 及其作用
- 组件树之间如何传值
- Fragments 的作用
- PureComponent 类的作用
- 常用的生命周期钩子以及各自的使用场景
- react-router 路由传值的方法及组件如何获取这些传递的参数
- 组件懒加载的使用方法及其优势
- React 中如何配置环境变量
- React 中如何对路由进行登录权限控制
- React 中高阶组件的概念及其使用场景
- 路由组件和普通子组件的使用场景区别
- HashRouter 和 BrowserRouter 的区别
- Redux 的工作流程
- Redux 三大原则
- 什么情况下建议使用 Redux
- Redux 如何处理异步 Action
- 组件中如何获取 Redux 中的数据以及如何修改数据
- React 中如何解决类名作用域冲突的问题
- Less 和 Sass 语法的不同之处
- React 如何解决跨域问题
React 渲染 DOM 的整体流程(从初始加载和后续更新的两个角度进行阐述)
- 使用 JSX 声明 React 元素结构,这个结构就是 React 生成真实 DOM 的模板;
- 这些 JSX 模板被 babel 编译成 React.createElement();
- 由 React.createElement() 将这些 React 元素渲染为虚拟 DOM;
- render() 函数,将这些虚拟 DOM 渲染为真实 DOM,将虚拟 DOM 节点插入到 body 中。
- 后续当数据发生变化时,React 利用diff算法,与上次的 DOM 对象进行同层对比,针对变化的元素,重新 render() 渲染。
JSX 及其特点
- JSX 是 JavaScript 的一种扩展,JSX 可以生成 React 元素,可以更简洁的构建 UI 结构。
- 本质上,它只是 React.createElement(component, props, …children) 函数的语法糖,写代码更加方便快捷;JSX 所描述的代码结构,最终还是会被编译成 React.createElement(component, props, …children) 的形式;因为 JSX 语法上更接近 JS 而不是 HTML,所以 React DOM 使用 cameCase (小驼峰命名) 来定义属性的名义,而不使用 HTML 属性名称的命名约定。
函数组件的特点及其使用场景
- 函数组件不能声明自己的私有数据,它可以通过形参 props 来接收从外部组件传给当前组件的 prop 数据;函数组件没有组件生命周期。
- 函数组件一般用于封装静态的(没有动态数据),不需要私有数据的组件,比如:导航条、footer、侧边栏等。
类组件的特点及其使用场景
- 类组件可以声明自己的私有数据,也可以通过 props 接收外部传递的数据;类组件有组件生命周期(创建 - 运行 - 销毁),且有一个声明周期函数 render() 是必须实现的。
- 类组件一般用于封装动态的,需要私有数据的组件。
props 和 state 的区别
- props :是组件配置;接收外部传递给当前组件的数据,一般用于父组件给子组件传值;且 props 具有只读性;使用时可直接通过 “props.属性” 调用即可。
- state :是数据结构;声明类组件的私有数据;可读可写:以通过 “this.state” 访问当前类组件的私有属性;这个 state 仅限当前类组件访问,其他组件不能访问;如果一个组件需要访问另一个组件的数据,只能通过 props 传值。
类组件中的数据声明方式及其各自的使用场景
-
直接声明:当属性不参与页面渲染的时候,只参与逻辑运算,可以将其设置为类属性,直接声明。
-
在 constructor 中声明 this.state:在 constructor 构造器中,声明组件私有数据,只要有 constructor构造器,就必须要有 super() 调用,也可以不使用constructor,直接声明state数据。
props :(单向数据流)一般用于父子组件,自上而下,父组件向子组件传值。
state :用于声明修改数据。
context :主要是应用在组件树之间的传值,外层的父组件负责Context对象提供数据,它内部的嵌套组件都可以使用Context取值,避免props层层传递的现象。创建context实例,使用provider包裹组件树,并提供数据,被它包裹的组件树就可以使用Provider提供的数据,达到共享数据的目的。
refs:是 React 提供给我们安全的访问 DOM 元素或者某个组件实例的句柄, 它是组件对象this的一个属性, 可以在组件模板中的标签添加ref属性, 然后组件中使用this.refs即可访问对应的DOM元素
state 中的数据如何修改
- 使用 setState() 修改数据,页面会自动根据最新的 state 进行重新渲染。
state属性也是一种类属性,react中规定了使用this.setState修改数据,可以触发render函数,进行重新渲染页面。不使用this.setState,使用this.state…修改属性,这就是普通的class属性的修改,当然是可以的,而且这时拿到的数据还是最新的数据,就是不会渲染页面了。如果在一次执行中,只要有一次this.setState,那么就可以实现整个页面的渲染
setState 是异步还是同步
- 在 React 控制的事件中,setState() 是异步的,无法立刻获得最新数据,比如:onClick、componentDidMount() 等。
- 在一些不受 React 控制的事件中,setState() 是同步的,可以立刻获得最新数据,比如:原生事件、定时器、延时器、addEventListener() 等。
setState中如何获取修改后的最新值
- 异步调用中想要立刻获取更新后的数据,可以在 setState 中设置第二个参数-回调函数,即**
setState({}, () => {})
**,React 会在数据更新后,立即执行回调函数。 - 以函数形式,如
setState(state => { return {}})
,或this.setState((state)=>{})
此时回调函数中的参数 state 也是最新数据。
forceUpdate 的作用
- 强制渲染页面,该方法一般应用于页面无法正常渲染的情况。
React 条件渲染的方式
- 逻辑与 (&&) 运算
- 三目运算符
- if 语句
渲染列表时,为什么要使用 key
- 主要是对 DOM 渲染进行优化。当列表元素顺序发生改变时,如果不加 key,不管数据是否发生变化,所有列表元素都会重新渲染。当列表元素顺序没有发生改变时,只是在末尾产生一个新的元素,此时可以不加 key,React只会渲染最后一个元素。
- 注意:key 值要求唯一且不会变化;不建议使用索引作为 key 值,因为索引是会动态变化的,会造成所有列表元素重新渲染。
受控组件和不受控组件的区别及其各自的使用场景
- 受控组件:在 React 中,表单元素 (input、select、textarea等) 被 React 以 setState 的方式驱动试图变化的元素就被称为受控组件。每一个受控组件都维护自己的 state 数据,当输入内容发生变化时,对应的 state 也会随之更新。受控组件需要绑定 value 和 onChange 属性。
- 非受控组件:是 React 提供的用于操作 DOM 的方法,在某些情况下,操作 DOM 可能会更加方便快捷。React 不鼓励操作真实 DOM,但特殊情况除外。
React 中的传值方式
- 父传子:父组件通过 prop 属性向子组件传值,这个 prop 属性设置在子组件的开始标签内部。子组件通过 props 形参或者 this.props 属性获取传值。当父组件的数据发生变化时 (调用setState) ,父组件会触发 render(),此时子组件也会被重新编译生成新的虚拟 DOM,同时也将最新的数据传给子组件了,子组件的数据也会随之更新。
- 子传父:父组件提前声明函数,准备接收子组件传值;父组件通过 prop 将这个函数传递到子组件中;子组件在触发点击事件的时候,通过 this.props.xxx 调用父组件传过来的函数,实现传值;父组件收到传值后,调用 setState 将数据保存到 state 中,父组件就可以用这个数据了。
- 父传孙:利用 context 传值。
- 兄弟组件:1. 发布订阅模式;2. Redux(Mobx);3. 找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。
发布订阅模式的原理及其使用方法
- 准备事件中心:它主要负责协调发布者和订阅者。它内部主要是存储订阅者注册 (subscribe) 的各种事件,而发布者则会通过事件中心调用 (publish) 这些事件,从而让订阅者收到消息。
- 创建订阅者:订阅者一定先往事件中心注册事件,然后再发布。
- 发布消息。
// 基本使用
// 1. 准备事件中心:它主要负责协调发布者和订阅者。它内部主要是存储订阅者注册的各种事件,而发布者则会通过事件中心调用这些事件,从而让订阅者收到消息。
let events = []; // 存储订阅者事件的数组
let eventsCenter = {
subscribe(func) {
events.push(func);
}, // subscribe 它是让订阅者注册事件的方法。
publish(info) {
// info 就是发布者提供的信息
events.forEach(event => event(info));
}, // publish 让发布者调用的。
};
// 2. 创建订阅者:订阅者一定先往事件中心注册事件,然后再发布。
eventsCenter.subscribe(data => {
console.log("订阅者1", data);
});
eventsCenter.subscribe(data => {
console.log("订阅者2", data);
});
eventsCenter.subscribe(data => {
console.log("订阅者3", data);
});
// 3. 发布消息
eventsCenter.publish("下课了");
diff算法及其优势
- diff算法, 是 Virtual DOM 产生的一个概念, 用来计算出Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面,从而提高了页面渲染效率。
- 优势:同层对比,分为三种类型对比(虚拟DOM树的对比、组件的对比、列表元素的对比)
- 虚拟 DOM 树 的diff算法对比:
- 同层对比标签名称、标签属性、标签内容是否一致;不会跨层级对比。
- 如果虚拟 DOM 出现跨层级移动,React 直接将旧层级删除,在新的节点下创建新的层级元素。不会复用旧的 DOM 层级。- 组件 的diff算法对比 (结合动态组件):
- 如果在重复 render() 生成虚拟 DOM 时,组件没有发生变化,此时组件就相当于固定的虚拟 DOM 树了,只是封装了一个组件而已。参照虚拟 DOM 树的diff算法对比规则。
- 如果组件是动态切换的,React 直接放弃对比,哪怕两个组件的结构是一样的,直接将消失的组件 DOM 移除,出现的组件再创建新的 DOM 节点。(谁出现就创建谁,谁消失就销毁谁)。- 列表元素 的diff算法对比 (结合 key):
- 没有 key 属性的时候,React 对比新旧虚拟 DOM 时,会按照元素的顺序对比元素内容是否发生变化,变化了就重新生成 DOM。
- 有 key 属性的时候,React 对比新旧虚拟 DOM 时,会将两个 key 值相同的元素进行对比,看看元素内容是否变化,变化了就重新生成 DOM。
虚拟 DOM 的优点
虚拟 DOM 会进行 diff算法 的对比,从而发现哪些 DOM 更新了,根据变化了的虚拟 DOM 局部更新真实 DOM,避免频繁的操作真实 DOM,极大的提升了页面渲染的性能。
shouldComponentUpdate 的作用
-
虚拟 DOM 本质上就是react提供的一个 JS 对象而已,React 通过对象来描述元素结构,这就是虚拟 DOM。它是 React 中唯一的一个性能优化钩子函数,如果项目性能没有问题,页面加载速度也比较快,这个钩子就不用使用了。
-
它这个钩子要求返回布尔值,返回 true,就会调用当前组件的 render(),生成虚拟 DOM;返回 false,就不会调用当前组件的 render(),减少一些 JS 的计算过程。
-
shouldComponentUpdate() 是在对新旧数据进行比较,发现数据改变,从而触发 render,组件渲染更新。
1. 什么情况下子组件需要 render,即 return true? 当子组件接收了父组件传递的 prop 数据时,父组件数据更新时,子组件也需要 render 更新。 对于组件本身的 state 来说,当调用 setState 的时候,数据发生变化,此时组件自身应该 render。 2. 什么情况下子组件不需要 render,即 return false? 当子组件没有接收任何父组件传递的 prop 数据时,当父组件数据更新时,子组件不需要 render。 对于组件本身的 state 来说,当调用 setState 的时候,数据并没有发生变化,此时组件自身也不应该render。
注意:PureComponent ,它是默认实现了 shouldComponentUpdate() 钩子,内部会根据 props 和 state 是否发生变化,做出浅层对比,从而让当前组件或子组件进行渲染优化,提升性能
React 对于引用类型的数据如何进行渲染优化
对于基本数据类型 string/boolean/number,shouldComponentUpdate 可以很好的识别;对于引用数据类型 array/object,shouldComponentUpdate 识别不了对象内部属性或元素的变化,因为引用类型指向的都是指向的同一个对象的内存地址。解决 shouldComponentUpdate 正确识别对象/数组内部属性变化的唯一方式就是:基于旧对象生成新对象。
基于旧对象生成新对象的方式:
1. {...}展开运算符和 JSON 对象两个方法。弊端是对于内容复杂的对象来说,每次都将所有节点复制生成新节点,比较浪费内存和CPU性能。
2. 使用 Immutable不可变对象 生成新对象。
Immutable 及其作用
- Immutable 不可变对象,表示原始对象不可修改变化,一旦你对原始不可变对象产生了修改了行为,它会返回一个新的经过深拷贝之后的对象。
- 一方面它会基于旧对象生成新对象;同时会对没有发生变化的节点数据进行内存共享,减少重复性的节点复制操作,减少内存浪费和CPU计算的消耗。
组件树之间如何传值
Context:它主要是应用在组件树之间的传值,外层的父组件负责 Context 对象提供数据,它内部的嵌套组件都可以使用 Context 取值,避免 props 层层传递的现象。
使用方式:
1. 创建上下文对象Context;
2. 利用 <Context.Provider> 包裹组件树,并提供数据,被它包裹的组件树就可以使用 Provider 提供的数据。
3. 通过 Class.contextType(contextType 该属性是固定名称)这个类属性,获取上下文对象 Context 提供的数据。此属性可以让你使用 this.context 来获取最近 Context 上绑定的值。
4. 通过 this.context 这个属性获取值(context属性是固定的)。
Fragments 的作用
- React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点
- <React.Fragment> 把它作为空标签使用,既不会额外的生成标签,又可以作为组件的根元素使用,起到包裹的作用。简写:<></>
PureComponent 类的作用
PureComponent ,它是默认实现了 shouldComponentUpdate() 钩子,内部会根据 props 和 state 是否发生变化,做出浅层对比,从而让当前组件或子组件进行渲染优化,提升性能。
常用的生命周期钩子以及各自的使用场景
-
组件挂载阶段(初始渲染阶段):
1. constructor : 在执行组件初始化时执行,可以给组件初始化state,或者给点击事件bind绑定this;只会触发一次; 2. render : 根据组件模板编译成虚拟DOM,此时页面还没有渲染; 不要在 render 内调用 this.setState(),容易形成死循环; render 由于会触发多次,只做数据渲染操作,比如结构 props、state。不要在此处发送请求或执行一些渲染无关的操作; 3. componentDidMount : 组件已经挂载完毕,此时已经可以看到页面了,虚拟DOM已被转化为真实DOM了。可以在该函数操作DOM。只执行一次。 组件初始化请求,放在 componentDidMount 钩子中即可。虽然 constructor 也可以发送请求,且执行时机比 componentDidMount 更早,但是react对于初始化请求的发送,显然是等到组件挂载完毕之后才正式发送请求的异步任务的。因此初始化请求不会放在constructor中。
-
组件更新阶段:
1. render : 三种情况会触发render函数的调用,a.父组件传递了新的Prop;b.子组件调用了setState()修改私有数据;c.forceUpdate()强制渲染。 2. componentDidUpdate : 新DOM更新完毕,此时可以获取最新的DOM结构。 以上两个钩子都是频繁执行的,只要props或state更新,它两就会执行。
-
组件销毁阶段:
componentWillUnmount : 做一些清理组件内容的操作,比如定时器、添加的订阅,监听的事件(addEventListener)。
react-router 路由传值的方法及组件如何获取这些传递的参数
-
使用** url 添加“?”拼接字符串**形式传值,目标组件使用 this.props.location.search 接收参数。
-
使用** url 动态传值**(路由配置中使用“:”设置参数类型),目标组件使用 this.props.location.params 接收参数。
-
使用** query 对象**传值,目标组件使用 this.props.location.query 接收参数。
-
使用** state 对象**传值,目标组件使用 this.props.location.state 接收参数。
-
还可以使用编程式导航跳转路由,使用 this.props.history.push() 传值。
注意:使用对象传值及编程式导航传值时,如果页面刷新,那么传递的值就会消失。
组件懒加载的使用方法及其优势
- 使用方法:
- 导入 Suspense 组件,import React, { Suspense } from “react”;;
- 修改组件导入语句,例如:import Test from “./pages/Test”;,改为:const Test = React.lazy(() => import("./pages/Test "));;
- App.js 中,使用 Suspense 包裹全部路由组件,另外,Suspense 需要加 fallback 属性,提供加载时的过渡画面,如
<Suspense fallback={ <div className="centered"> <LoadingSpinner /> </div> } > 路由组件 </Suspense>
- 优势:使网页首次加载速度变快,使用懒加载后,用户访问页面,根据访问到的路径实时按需加载相应的代码。这样,用户不需要一开始就下载大量的代码,因为其中一些代码可能不需要用到。
React 中如何配置环境变量
-
暴露配置:
yarn eject
-
安装 cross-env:
yarn add cross-env
-
在项目根目录下创建 .env ,.env.development ,.env.production ,.env.uat ,.env.sit:
-
.env
REACT_APP_ENV=development
-
.env.development
REACT_APP_ENV=development
-
.env.production
REACT_APP_ENV=production
-
.env.uat
REACT_APP_ENV=uat
-
.env.sit
REACT_APP_ENV=sit
-
-
配置 package.json :
"scripts": {
"serve": "cross-env REACT_APP_ENV=development node scripts/start.js",
"build": "cross-env REACT_APP_ENV=production node scripts/build.js",
"uat": "cross-env REACT_APP_ENV=uat node scripts/build.js",
"sit": "cross-env REACT_APP_ENV=sit node scripts/build.js"
},
-
项目中获取当前环境变量:
console.log(process.env.REACT_APP_ENV)
React 中如何对路由进行登录权限控制
- 路由中添加配置:
- 封装判断是否登录的工具:
// auth.js
import { Navigate } from "react-router-dom";
export default function AuthConfig(props) {
let token = localStorage.getItem("token") || "";
if (token) {
// 登录了,给一级路由的element属性,传递目标组件
return props.target;
} else {
// 没有登录,给一级路由的element属性,传递Navigate
return <Navigate to={`/login?redirect=${props.targetURL}`} replace={true}></Navigate>;
}
}
- 使用工具包裹组件:
import AuthConfig from "../utils/auth";
class RouterMap extends Component {
render() {
return (
<Routes>
{routes.map((route) => {
return (
<Route
key={route.name}
path={route.path}
element={
// route.component
route.meta.auth ? (
<AuthConfig
target={route.component}
targetURL={route.path}
></AuthConfig>
) : (
route.component
)
}
>
{route.children &&
route.children.map((subroute) =>
subroute.path ? (
<Route
key={subroute.name}
path={subroute.path}
element={subroute.component}
></Route>
) : (
<Route
key={subroute.name}
index
element={subroute.component}
></Route>
)
)}
</Route>
);
})}
</Routes>
);
}
}
React 中高阶组件的概念及其使用场景
- 高阶组件:是 React 中用于重用组件逻辑的高级技术,它本身不是 react 中的组件,而是一个函数, 这个函数接受一个 reac 组件作为参数,并返回一个新组件,实现了对原有组件的增强和优化,可以对原有组件中的 state,props 和逻辑执行增删改操作。
- 使用场景:权限控制、页面复用、组件渲染性能追踪等。
- 属性代理。高阶组件通过被包裹的React组件来操作props, 可以增强组件模板和props
- 反向继承。高阶组件继承于被包裹的React组件 ,可以更新state
路由组件和普通子组件的使用场景区别
- 每一个路径就代表着一个组件,每一个路由组件都是一个单独的页面。
- 页面需要跳转时,使用路由组件。
- 页面根据不同的状态展示不同的内容时,使用一般组件。
HashRouter 和 BrowserRouter 的区别
-
底层原理不一样:
BrowserRoute 使用的是 H5 的 history API,不兼容 IE9 及以下版本; HashRouter 使用的是 URL 的哈希值。
-
path 表现形式不一样:
BrowserRouter 的路径中没有 # ,例如:localhost:3000/demo/test; HashRouter 的路径包含 # ,例如:localhost:3000/#/demo/test。
-
刷新后对路由state参数的影响:
BrowserRouter 没有任何影响,因为 state 保存在 history 对象中; HashRouter 刷新后会导致路由 state 参数的丢失。
-
HashRouter 可以用于解决一些路径错误相关的问题。
Redux 的工作流程
- View 在 redux 中会派发 action 方法;
- action 通过 store 的 dispatch 方法会派发给 store;
- store 接收 action,连同之前的 state,一起传递给 reducer;
- reducer 返回新的数据给 store;
- store 去改变自己的 state。
Redux 三大原则
- 唯一数据源(整个应用的 state 状态数据被储存在一个状态树(对象)中,单一状态树更容易跟踪数据的变化, 方便调试检查应用程序)
- State 状态是只读的, 想要更改数据必须经过派发action事件,通过接收action参数修改
- reducer必须是纯函数(一个输入必须对应着唯一的输出, 返回值取决于参数)
什么情况下建议使用 Redux
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
Redux 如何处理异步 Action
- 异步 action:这个 action不再返回一个对象了,内部返回一个函数(返回一个箭头函数),函数里面可以封装 ajax 异步请求任务,将这个请求的结果,保存到 Redux 中,供其他组件使用。此时 dispatch 分发的不再是一个对象,而是一个函数,此时需要借助于 redux-thunk 这个包处理这种方法。dispatch 默认分发同步 action ,且里面是一个对象
- 在组件中异步回调中操作同步action,从而实现异步action的操作
- 直接写一个函数类型的action,在函数体内进行一些异步操作,回调中操作同步action,从而实现异步action的操作。
- 在store 中引入redux-thunk 中间件来处理异步action
异步 action 并不能修改 redux 数据,只是可以让我们发送额外的请求而已,最终请求结果的保存还是要依靠 dispatch({type: “”})的同步 action 完成。
组件中如何获取 Redux 中的数据以及如何修改数据
- 获取数据: store.getState()
- 修改数据:store.dispatch(action)
- 类组件中, 需要 使用connect 来包裹当前组件,props会返回redux的数据 ,而且还会返回一个dispatch函数, 调用dispatch方法,可以修改数据
- 在函数组件中 ,使用useSelector 接收store的值,useDispatch() 接收 dispatch
React 中如何解决类名作用域冲突的问题
- 行内样式
- 类名私有化(CSS Modules):
- 改样式文件名:从 xx.scss -> xx.module.scss ;
- 组件中导入该样式文件:
import styles from './index.module.scss'
- 通过 styles 对象访问对象中的样式名来设置样式:
<div className={styles.css类名}></div>
- less 或 sass 语法
Less 和 Sass 语法的不同之处
- 变量符不一样,Less 是 @,而 Sass 是$。
- Less 在 JS 上运行,Sass 在 Ruby 上使用。
- Sass 支持 if 语句,Less 不支持。
React 如何解决跨域问题
- 通过 package.json 配置代理
{
// 其他配置项......
"proxy": "http://localhost:5000"
}
- 配置多个代理
多个代理的配置,需要在 src 文件夹中,建立 setupProxy.js 文件,这个文件中使用的是 CommonJS 规范。配置文件如下:
const proxy = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
proxy('/api1', { // 当发起以 /api1 为前缀的请求时,触发该代理
target: 'http://localhost:5000', /* 要转发的地址 */
changeOrigin: true, /* 控制服务器接收的 Host 字段的值 */
// 将 /api1 前缀置为空
pathRewrite: {'^/api1': ''}
}),
// 第2、3、....、n个代理
)
}