https://fd.svsva.cn/wjd
React的理解 有哪些特性
一、什么是虚拟Dom,实现原理,区别,优缺点?
Diff算法说明
Fiber
二、函数组件与类组件区别
三、state和props的区别
super和super(props)的区别
类组件生命周期函数
四、setState执行机制
五、react事件机制
六、react事件绑定方式class
八、React组件通信
九、React中refs是什么
十、React中的key作用
11、受控组件、非受控组件
12、什么是高阶组件
13、什么是 JSX
14、不能直接更新state数据
15、什么是React Hooks
16、react中引入css方式
17、redux理解,以及工作原理
React中间件
18、React中router的原理、解释、传参方式?
20、immutable 不变的
21、类组件render方法原理
22、React组件复用
23、获取页面滚动距离
22、性能优化、避免不必要的render渲染
23、怎样理解三次握手,四次挥手
JS基础部分
JS高级
作用域的理解
手写Promise
面试题总结
JS面试常问
项目亮点
React是一个构建用户界面的 Javasript 库,只提供了UI层面的解决方案
主要使用组件设计模式、声明式编程和函数式编程。
声明式编程:他只关心你要做什么 而不是怎么做
component:react中一切皆为组件 把整个应用逻辑拆分成小部分,每个小的部分称为组件
优点:可复用、可组合、易维护
使用jsx语法, 它具备了js的能力,可以将HTML结构嵌入JS代码中但是不能直接被浏览器识别,最终会被 babel
编译为合法的 JS
语句。
遵循单向数据流原则。比双向数据绑定更安全速度更快
使用虚拟DOM来有效的操作DOM,。虚拟Dom原理…
虚拟Dom是真实Dom在内存中的表示
本质上是以 JavaScript
对象形式存在,是对 真实DOM
的描述
数据状态更新,会记录新树与旧树的差异,把差异更新到真实Dom中
区别在于虚拟Dom不会进行重排重绘,而真实Dom会频繁进行重排重绘
文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实DOM
真实Dom优势在于:易用,
缺点:效率低:解析速度慢,内存占用量高, 性能差:频繁操作Dom导致重排重绘
虚拟Dom优点:简单方便,避免的真实Dom的频繁更新,减少多次重绘重排,提高了性能
跨平台使用:借助虚拟Dom,一套代码多端使用
缺点:首次渲染大量虚拟Dom,由于多了一些计算,加载会较慢,无法满足针对性能要求极高的应用
底层是一套diff算法
tree diff -----将整棵树分层,对同层级的节点进行比较,说对比两个标签 判断是否相同,如果不同直接删除重建
conponent diff------对比同层级节点的类型,如果有不同 则直接删除节点,重新创建新的节点 比如说conponent A 换成了 conponent B 虽然长得很像,但是也会删除A 再创建B
element diff 比较同层级中子节点
默认按照下标对比,如果设置了key,通过key属性指定唯一标识进行对比
如果key是相同的只需要把旧集合节点更新到新的节点
key是唯一表示,如果渲染列表数据时,
JS引擎和页面渲染引擎两个线程是互斥的,其中一个线程执行的时候,另一个只能挂起,如果JS线程长时间占用着主线程,那么页面渲染引擎就不得不原地等待,时间明显超过16ms了,浏览器就会阻塞页面的渲染,界面长时间不更新 会导致页面响应速度变差,用户会觉得卡顿 掉帧。当js引擎渲染组件时,从开始到渲染完成是一气呵成的无法中断,采用遍历递归来对比虚拟Dom树,找出需要变动的节点,然后同步的更新他们,如果组件较大,那么js线程会一直执行 等到dom树计算完成后,才会把控制权交给渲染线程,如果递归花费100ms,那么这100ms浏览器是无法相应的,代码执行时间越长 卡顿越明显,CPU的瓶颈
为了解决主线程长时间占用的问题,react16引入了Fiber数据结构
一个 fiber 就是一个 JavaScript 对象
**核心:**Fiber 架构的核心即是可中断、可恢复、优先级
利用 时间切片 解决的问题就是,划分优先级 把长任务分拆到每一帧中,拆分成一个个小的任务块,将同步的更新变为可中断的异步更新。
通过ConCurrent Mode,通过合理的调用机制来调控时间,指定任务执行时间 从而降低页面卡顿的概率** 同时必须结合event loop
的机制来操作,如果一个小任务执行js解析 页面绘制 没有超过16.67ms 就会执行 requestIdleCallback 中注册的任务
类组件与函数组件的区别在于,组件是否有状态,
类组件
- 类组件通过ES6 类的编写形式编写代码 此类继承 React.Component方法
- 如果想访问父组件的数据必须通过props接收,通过this.props访问l
- 通过state在组件内部维护数据
- 直接说 state和props的区别…+生命周期…
函数组件
- 在使用类组件时 需要实例化,比较消耗性能
- 在没有hooks之前只能使用这种方法维护状态,
- 从react16.8之后有了hooks的出现…
- 函数组件使用时直接取函数的返回值即可,在能满足需求的情况下,多使用函数组件
- 而且目前比较推崇函数式编程
类组件:
//状态管理不同
类组件可以通过this.setState 来管理状态信息
//生命周期:
render //渲染DOM结构
componentDidMount //组件挂载到真实DOM节点后执行
componentDidIpdate //组件更新结束后触发
componentWillUnmount //组件卸载时触发
函数组件:
/状态管理不同
函数组件通过useState声明管理状态信息
通过useEffect模拟类组件声明周期函数
核心:组件的显示形态可以由数据状态和外部参数 所决定,数据状态就是state
总结:
- state和props都是js对象
- 两者都保存着影响页面渲染结果的信息
- props是外部组件传递过来的数据,数据状态无法改变
- state是在组件内部,数据状态由组件本身进行管理,数据状态可以改变
- 类组件通过setState修改,函数组件结合hooks进行修改
在React中,类组件是基于ES6,所以在constructor中必须用super
在调用super中,无论是否传入props,React内部都会将props赋值给组件实例props属性中
传入props原因是 在子构造函数中能够使用this.props来获取传入的值
实例过程中自动调用的方法,在方法内通过super
来获取父组件的 props
初始化组件内部state数据
无论state、props是否变化都会执行的方法,参数1 为即将更新的的props数据,参数2 上一个state状态,可以在其中做逻辑处理,防止无用的state更新
类组件中必须使用的方法 渲染DOM的方法 可以使用state和props数据
注意不能再render中调用setState 会造成死循环
组件挂载完毕后执行的方法 可以做一些事件监听、发送ajax请求
告诉组件 基于本身props和state是否需要重新渲染组件,默认情况返回true
参数1 NextPorps 参数2 NextState 可以做逻辑处理判断组件是否需要更新
返回一个 Snapshot值 可以获取组件更新前的一些信息 作为cmponentDiUpdate的第三个参数传入
组件更新结束后执行,可以根据state和props的变化做处理
在组件卸载前触发 可以卸载事件 关闭定时,取消订阅的网络请求器,一旦组件实例被卸载,不会再次被挂载,而只会重新创建
第一种:通过onclick事件,执行
this.setState
方法,进行更新state状态,重新执行render
函数从而导致视图更新第二种:setstate异步状态,,在组件中使用
setstate
时为异步状态,如果想拿到最新的状态,需要传递第二个参数,setstate在原生Dom中或setTimeout
中呈现同步状态第三种:批量更新状态,对同一个值进行多次
setState
时会产生覆盖,更新策略只会取最后一次的执行结果,而在setTimeout
和原生dom中不会出现覆盖
为了解决跨浏览器兼容性问题React
基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等
组件注册的事件最终会绑定在document
这个 DOM
上,而不是 React
组件对应的 DOM
,从而节省内存开销,它拥有和浏览器原生事件相同的接口,包含preventdefault()
render方法中使用bind
render方法中使用箭头函数
constructor中bind
定义阶段使用箭头函数绑定
区别:方法1和方法2简单,
方法3.该属性值传递给组件时,会导致渲染,有性能浪费
方法4.使用箭头函只会生成一个实例,所以方法4对于开发更友好
分类:
- 父组件向子组件传递
- 子组件向父组件传递
- 兄弟组件之间的通信
- 父组件向后代组件传递
- 非关系组件传递
总结:React遵循单项数据流,组件自身不会改变接收到的状态,当状态发生变化时候,组件使用新的值,而不是修改之前的值。数据存储的位置都是在上级组件中
核心:允许访问Dom节点和React元素的一种方法,,如果是渲染组件则返回的是组件实例,如果为dom则返回具体的dom节点,
类组件:通过React.createRef创建, 函数组件通过useRef()创建
可以给组件添加ref属性来使用 使用ref对象中的currect属性来访问react元素,
该属性的值是一个回调函数,接收作为第一个参数的底层Dom元素或组件挂载实例
不能在函数组件上使用ref
属性,因为他们并没有实例
但是过多使用refs会造成节点暴露,违反组件封装原则
使用场景:操作Dom元素,获取聚焦、内容选择、内容设置、媒体播放等等
key的作用
key 是 React 的虚拟 DOM 对象的唯一标识,在页面发生更新渲染时,判断是新创建的元素还是修改的元素,从而避免不必要的渲染
即当状态数据发生变化时,React 会根据 新的数据
生成 新的虚拟DOM
,随后再进行新虚拟DOM
与旧虚拟DOM
之间的差异比对(diff)。
在 diff 算法中,存在三大策略,分别是:
总结
流程图:
受控组件:标签、
的值通常是根据用户输入进行更新,但在react中可变状态通常保存在组件状态属性中,并且通过setState进行更新状态,通过 onChange 事件获取当前输入内容,将当前输入内容作为 value 传入,此时就成为受控组件。
顾名思义就是受我们控制的组件,
例如input
由value属性来控制输入框内容,输在用户输入内容时,发现内容并没有变化内容,因为此时value值已经被state或porps数据所控制,用户在输入内容是并不会直接修改到state的状态,如果想接触可以使用onChange方法触发 修改state的方法 setSate 从而实现更新。
非受控组件:表单数据由DOM本身处理。即不受setState()
的控制,一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态
应用场景:
高阶组件(HOC)是接受一个组件并返回一个新组件的函数。基本上,这是一个模式,是从 React 的组合特性中衍生出来的,称其为纯组件,因为它们可以接受任何动态提供的子组件,但不会修改或复制输入组件中的任何行为。
jsx是javascript的一种扩展语法跟模板语法接近,它具备了js的能力,将原始html模板嵌入到js代码中,而且jsx中的标签可以是单标签也可以是双标签,单标签必须保证闭合
类组件:如果直接更新state数据,不会重新渲染组件,需要使用setState(),方法更新state,它会调度组件state的更新,当state改变时,组件通过重新渲染来响应更新
函数组件:直接用useState进行数据更新
核心:hooks是react16.8新增的特性,可以在不编写class组件和state的情况下使用React
特性,主要功能就是给无状态的函数组件提供状态,使函数组件可以有自己的状态
usestate返回一个数组,参数1为当前state,第二个参数是更新state状态的函数
useState优点:useState 使用起来更为简洁,减少了this
指向不明确的情况
副作用包括axios请求等等
useEffect(()=>{})=== 代替componentDidMount和componentDidUpdate两个生命周期函数中执行回调
useEffect(()=> return)=== return一个回调函数并且第二个参数为空数组时,组建卸载时触发,代替componentDidMount+componentWillUnmount
第二个参数为空数组时 只在首次加载页面时候执行
useEffect(()=>[])=== 代替 componentDidMount
state
第二个参数数组中有依赖项时候 在依赖项发生变化时 触发
useEffect(()=>[依赖项])
componentDidMount+componentDidIpdate
这种方式存在不好的地方在于样式是全局生效,样式之间会互相影响
1.module.css作为模块引入,模块中的css样式旨在当前组件中有效,不影响后代组件1
2.注意点:引用的类名不能使用连接符-
3.所以的classname必须使用{style.样式类名}
4.如果不想全部hash类名 可以在样式模块前添加 :global 在:global写的样式不会随机类名 也不会影响后代组件的样式
第一种:在组件中大量写样式,容易编写混乱
第二种:符合日常编写,但是存在全局样式问题
第三种:能够解决全局作用域的问题,但是不方便样式修改
整个应用中会存在很多个组件,每个组件的state
是由自身进行管理,包括组件定义自身的state
、组件之间的通信通过props
传递、如果让每个组件都存储自身相关的状态,理论上来讲不会影响应用的运行,但在开发及后续维护阶段,我们将花费大量精力去查询状态的变化过程
可以把redux理解为是一个仓库,可以把一些组件共用的数据统一状态存在redux中,将所有状态进行集中管理,当需要更新状态的时候,仅需要对管理集中处理,而不用去关心状态是如何分发到每一个组件内部的。
单一数据源
state为只读状态
使用纯函数进行数据的修改
不能在reducer中使用Date.now()或者 Math.random()等不纯的方法,
redux不仅可以在react使用,其他框架包括原生Dom也可以使用
redux
要求我们把数据都放在 store
公共存储空间
只要store
里的数据发生改变,其他组件就能感知到变化,再来取数据,从而间接的实现数据传递的功能
原理:把数据全部放在store公共储存空间中,
通过dispatch类派发action给reducer
通过接收action传递过来的type匹配相应的状态处理
创建数据的公共存储区域(管理员)
import { createStore } from 'redux' // 引入一个第三方的方法
const store = createStore()
更多情况下使用react-redux语法使用redux
- useSelector用于获取state的状态
- useDispatch用于调用action中修改state的方法
遵顼三个原则:
Action:可以理解为他是一个专家,专门分发任务给reducer,
reducer:可以理解为员工,专门处理action分发过来的任务,根据action传递过来的数据和指示进行state数据的修改,注意:不能直接修改state数据,需要基于原来的state数据进行修改
store:可以理解为老板,管理action和reducer,
通过:redux中的 createStore进行创建store store 参数1为 reducer,参数二为一些中间件
通过applyMiddewares来注册一些中间件。
需要想让组件都能使用redux中的数据 需要在应用的跟组件中 使用react-redux 中的 Provider 将跟组件进行包裹,传入store属性 属性值为创建好的store 创建完成后组件就可以订阅redux中的数据 根据state的变化 进行组件更新渲染
下面可以继续说组件中
获取store原生的方法
使用react-redux中的hooks 更方便使用store
useSelstcer获取数据 useDispatch派发action
当action
发出之后,reducer
立即算出state
,整个过程是一个同步的操作
那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件
Redux
中,中间件就是放在就是在dispatch
过程,在分发action
进行拦截处理,
通过applyMiddlewares
进行注册
const store = createStore(reducer,applyMiddleware(thunk, logger));
判断传入的是不是一个函数,如果是函数则传入dispatch、getstate两个参数
dispatch用于再次派发action getstate 用于获取之前的一些state状态
用于获取操作日志 打印在控制台
react-router
:路由核心
原理:可以实现无刷新的条件下切换显示不同的页面,在URL发生变化时,页面结果可以根据URL的变化而改变,可以实现单页(SPA)应用
react-router-dom
基于ract-router实现的一些配置功能
提供了常用的Api
BrowserRouter:基于history模式
HashRouter:基于URL地址的哈希值
两者作为最顶层组件包裹其他组件
Route用于匹配URL路径进行组件渲染
通过path属性指定路径地址
通过component属性指定匹配成功后渲染的组件
通过exact属性 是否开启严格匹配 严格匹配就是url地址必须和path属性值完全一致才会渲染对应的组件
Link和NavLink 最终会渲染成a标签用于跳转 两者区别在于 NavLink添加组件被选中时的高亮效果
react-router-dom
BrowserRouter、HashRouter
使用两者作为最顶层组件包裹其他组件
Route
Link,NavLink
Switch
Redirect
- BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。 HashRouter使用的是URL的哈希值。
- path表现形式不一样 BrowserRouter的路径中没有#,例如: HashRouter的路径包含# 9.l;
- 刷新后对路由state参数的影响 (1).BrowserRouter没有任何影响,因为state保存在history对象中。 (2).HashRouter刷新后会导致路由state参数的丢失
- 备注:HashRouter可以用于解决一些路径错误相关的问题。
Route
用于路径的匹配- path属性用于匹配路径
- component 匹配路径后用于指定渲染的组件
- exact精确匹配,开启后只有路由完全一致才会渲染组件
通常路径跳转使用Link组件,link组件最终会被渲染成a标签属性to
指定需要跳转的路径。
NavLink、Link区别在于
NavLink在Link基础上增加了一些样式属性,如果想让组件被选中由样式变化可以使用Navlink
用于路由的重定向,当访问路径匹配到from属性中的路径时,就会执行跳转到对应的to
属性中的路径
匹配访问路径 从第一个开始匹配如果匹配到了就渲染相应组件中,匹配不到可以在最后匹配以恶搞404页面
组件通过接收props 可以获取到路由对象 对象中包含
useHistory:无需通过props,直接访问history对象,
useLocation:获取当前路径,和跳转时传递的参数
useParams:用于获取动态路由中 传递的一些参数
比如商品列表 点击后向后端发送该商品的id 获取详情 可以通过useParams拿到当前动态路由传递的id
immutable是一种持久化数据。一旦被创建就不会被修改
触发时机:
在类组件中只要调用 setState 修改状态,一定会触发render
函数组件中:通过useEffect,[state] state改变则触发重新渲染
一个特殊的props传值,传递的是一个函数 通过函数的返回值定制结构,为什么传函数 为了支持接收内部的参数
缺点:
render-props的一种 工作中常见
本质是一个函数 增强组件功能 传入一个组件返回一个增强后的新组件
缺点:多层嵌套、外层数据丢失
IE兼容:
document.documentElement.scrollTop ||document.body.scrollTop
最新:
windown.pageXoffset
windown.pageYoffset
父组件渲染,必然会重新渲染子组件,但是渲染只会渲染当前组建的子树
进行的是浅层对比,只会比较基本数据类型-比较地址
跟`shouldComponentUpdate`原理基本一致,通过对 `props` 和 `state`的浅比较结果来实现`shouldComponentUpdate`
React.memo
进行的是检测对比 简单类型比较值 复杂比较地址
有依赖数据,如果更新组件时非常的消耗性能,能不更新 就不更新
缺点:如果父组件给子组件传递一个函数(提高警惕),每次只要父组件更新,函数就会重新声明,传给子组件的函数地址变化,React.memo就会无效
用来缓存函数-复杂数据类型,配合React.memo实现性能优化,参数1是回调函数,参数2依赖项
记忆任意数据类型-参数1是回调函数,参数2依赖项
作用:确定双方的接收、发送能力是否正常,指定自己的初始化序列号,为后续可靠传输做准备
刚开客户端处于Closed状态,服务端处于Listen状态
第一次握手:客户端向服务端发送SYN报文,并指明客户端初始化序列号ISN©,此时客户端进入SYN_send状态/等待
第二次握手:服务端收到SYN报文后,会以自己的SYN作为应答,并指明自己的初始化序列号ISN,同时将客户端的ISN+1作为Ack的值,表示收到客户端的SYN,同时进入SYN_RCVD状态/接收
第三次:客户端收到SYN后,发送一个ACK报文,也是将服务端的ISN+1作为ACK的值,表示收到服务端的SYN报文。此时客户端处于established状态,服务端收到ACK后,也处于established状态,此时双方成功建立连接
答案:不是固定的,三次握手的一个重要功能就是交换双方的ISN,以便让对方知道接收到的输入如何按序列号进行组装,如果ISN是固定的,攻击者很容易推断出后续的确认号,所以ISN是动态生成的
服务端第一次收到SYN后,就会处于SYN_RCVD,此时双方还没完全建立连接,服务端会把这种状态放在一个队列中,我们把这这种队列称为半连接队列,等三次握手完毕后,建立的连接就会放在全连接队列中,如果队列满了就会产生丢包的情况。
第三次握手时可以携带参数,第一次和第二次不能携带,举例:
第一次就可以携带参数的话,假如有人恶意攻击服务器,攻击者根本不在乎服务器的接收、发送能力是否正常,如果每次都在第一次握手中的SYN报文中携带大量数据,重复的发SYN报文的话,那么服务器会花费大量时间、内存空间接收这些报文。所以得出的结论,如果在第一次握手中携带数据,那么服务器更容易收到攻击
对于在第三次携带数据的话,此时客户端已经处于established状态,对于客户端来说已经建立连接,并且确定服务端的发送和接收能力时正常的,所以携带参数完全没问题。
第一次挥手:客户端发送一个FIN报文,报文中指定一个序列号,此时客户端进入FIN_WAIT_1状态,表示停止在发送数据
第二次挥手:服务端收到FIN报文后,会回应一个ACK报文,把客户端的序列号+1作为ACK的值,此时服务端处于Close_WAIT状态,表示服务端同意关闭请求,客户端进入FIN_WAIT2状态 等待服务端发出连接释放报文
第三次挥手:如果服务端也想断开连接了,和第一次挥手一样发出FIN报文,此时服务端处于LAST_ACK状态,表示发出连接释放报文,等待客户端确认
第四次挥手:客户端收到FIN报文后,一样发送一个ACK报文,且把服务器的序列号+1作为ACK的值。此时客户端进入TIME_WAIT状态,需要经过等待计时器的时间2MSL后仍然没有回复,才会进入closed状态,而服务端收到ACK报文后直接进入Closed状态
通俗理解:
第一次挥手:小明和小红闹分手,小红说我不爱你了分手吧,
第二次挥手:小明说我知道了,让我考虑一下。
第三次挥手:小明说我考虑好了分手吧
第四次挥手:小红说那就这样吧,再见!这时候小明看到微信直接给小红拉黑了,代表服务端已经关闭了连接,但是这时候小红有点不甘心,又等了一会看看小明会不会回复自己,等了一会没有回复 那么小红也给小明拉黑了,这时候四次挥手完成
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
unshift()在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
pop()
方法用于删除数组的最后一项,同时减少数组的length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
shift()
方法用于删除数组的第一项,同时减少数组的length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组
slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow1
即修改原来数组的内容,常用splice
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组
concat
slice() substr() substring()
函数可以有属性, 每个函数都有一个特殊的属性叫作原型prototype
,也被称为原型对象
prototype
原型对象里的constructor
指向构造函数本身
对象都有–proto-- 属性,指向当前对象的原型对象,原型对象也是对象也有–proto–属性,指向原型对象的原型对象,着用一层一层的链式关系叫做原型链
通过构造函数Person new出来的是实例对象p,p对象有–proto–属性,指向原型对象Person.rototype,原型对象也有–proto–属性,指向原型对象Object.prototype,他也有–proto–属性指向null
作用:可以使子类具有父类的各种属性和方法,而不需要再次编写相同的代码,在继承父类时可以重新定义某些属性,覆盖父类原有的属性和方法,使他获得和父类不同的功能
原型链继承:子原型的父类型的一个实例对象
函数都有prototype属性,属性的值的是个对象.被称为原型,对象都有proto属性指向它的原型对象…
Student.prototype = new Person()
// 子类型的原型为父类型的一个实例对象
// 来自原型对象的所有属性被所有实例共享
// 如果需要重新子类属性 一定放在替换原型的语句之后
// 父类新增的属性/方法 子类都可以访问
// 无法向父类构造函数传参
// 在子类型构造函数中通用call()调用父类型构造函数
// 只能继承父类的属性和方法,不能继承父类原型的属性和方法
// 创建实例时 可向父类传参
function Parent() {
this.name = 'parent1'
this.getplay = function () {
console.log(121)
}
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this)
this.type = 'child'
}
let child = new Child()
console.log(child) // 没问题
console.log(child.getName()) // 会报错
// 保留继承父类属性和传参的优点,通过将父类实例作为子类原型
// 可以继承实例属性/方法,也可以继承原型属性/方法
// 缺点:调用了两次父类构造函数,生成了两份实例
通过 #Object.create()浅拷贝已有的对象 创建新的对象,同时还不必因此创建自定义类型。
用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了一个可以随意添增属性的实例或对象。
缺点:多个实例的引用类型属性指向相同的内存,存在篡改的可能
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
借助解决普通对象的继承问题的Object.create 方法
在前面几种继承方式的优缺点基础上进行改造
class可以通过 extends 关键字,只是原型的语法糖,仍然是基于原型实现的。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
[https://juejin.cn/post/6889815642099335181]:
分为函数作用域、全局作用域、块级作用域
任何不在函数内或者大括号内生命的变量都是全局作用域,他声明的变量可以让全局访问
函数作用域 :就是在函数内部声明的变量 只在当前函数内部有效,外部不可以访问这些变量
块级作用域 比如说if 或者大括号内 都算块级作用域、
作用域链就是 在js中声明一个变量 他会在当前作用域中查找 如果找不到就会向上层作用域查找 直到找到全局作用域 如果还是找不到那么控制台回直接报错
Promise内部提供两个方法
成功时调用 resolve() 失败调用reject()
三个状态:
pending 等待中 fulfilled 成功状态 rejected 失败状态
#初始状态pending
调用resolve() 会将当前状态修改为# fulfilled fao非o的
调用reject() 会将当前状态修改为 #rejected
.then 接收成功后的结果
.catch 接收失败时的结果
.finally 无论成功失败都会执行
1.创建Promise类=>构造函数
2.创建new Promise实例对象 传入一个函数=>需要传递两个参数
3.遵循promiseA+ constructor 传入 executor AK在KT 并且立即执行
4.executor中需要两个函数 成功/失败
5.在实例中调用resolve() 执行成功信息 / 反之
调用resolve或reject时候可传实参,函数本身作为形参接收
成功时 修改this.state的状态为 fulfilled
并且记录修改后的值this.value = 接收的形参
失败时 修改this.state的状态为 rejected
并且记录失败原因this.reason = 接受的形参
6.Promise的状态一旦发生变化就会凝固 不能再改变
只有pending的时候才能改变状态 修改判断状态是否为pending
7.此时用.then拿结果是拿不到的 还没有.then方法
给原型对象添加.then方法
.then语法:传入两个函数
参数1 onFulfilled 成功的回调
参数2 onrejected 失败后的回调
区别:onrejected只会捕获 当前实例对象p的失败,而.catch会捕获全部错误 包括onFulfilled 全局捕获
8.原型上的.then接收两个参数
成功的onFulfilled 失败onRejected
判断当前状态 是否为Fulfilled
如果是 则调用onFulfilled
如果不是 则调用onRejected
将成功的值/失败的值返回出去
9.如果下面的.then想拿到成功或失败的参数
需要将当前 this.value给到onFulfilled this.reason给onRejected
10.为了代码健壮性 如果执行executor(resolve,reject)报错
直接调用reject()
'异步处理'
11.此时调用resovle或reject为异步
由于调用是异步的 而.then并不知道你是异步的 所以then先执行
而状态永远是pending 所以永远没有返回东西出来
.解决方案
定义两个数组
onResolveCallback 用于存放成功时需要执行的函数
onRejectCallback 用于存放失败时需要执行的函数
如果当前状态等于 pending的情况下 将成功/失败要做的事 全部存入相应数组
当调用resovle或reject时 将对应数组中的函数全部 .遍历 执行
同步代码:
class Promise {
// 遵循prmoiseA+ 传入imd 并且立即执行
constructor(imd) {
//记录初始状态
this.state = 'pending'
//记录成功初始值 给将来.then获取成功结果使用的
this.value = undefined
//记录失败的原因/值 给将来.catch获取失败原因使用的
this.reason = undefined
// 成功函数,成功时 修改this.state的状态为 fulfilled
let resolve = (value) => {
// 如果当前不是pendind状态 就不能修改
if (this.state !== 'pending') return
// 更新后的状态
this.state = 'fulfilled'
//更新成功初始值
this.value = value
// console.log('成功信息')
}
// 失败函数。失败时 修改this.state的状态为 rejected
let reject = (err) => {
if (this.state !== 'pending') return
// 更新后的状态
this.state = 'rejected'
// 记录失败原因
this.reason = err
// console.log('失败信息')
}
// imd需要传递两个函数 成功/失败
// 如果执行imd报错 直接返回错误
try {
imd(resolve, reject)
} catch (err) {
reject()
}
}
// 如果此时不加then方法 下面获取不到成功信息
// onFu1Fi1led成功时候被调用
// onRejected 失败时调用
then(onFu1Fi1led, onRejected) {
// 判断状态 如果 fulfilled 状态 是调用 onFu1Fi1led
// 将成功的值/失败的值返回出去
if (this.state === 'fulfilled') {
onFu1Fi1led(this.value)
}
// 否则 onRejected
if (this.state === 'rejected') {
onRejected(this.reason)
}
}
}
const p1 = new Promise((resolve, reject) => {
// console.log(resolve, reject)
// console.log('立即执行')
// 调用执行成功信息
// resolve('你对了')
// 调用执行失败信息
reject('你错了')
})
p1.then(
(res) => {
console.log(res, '成功的回调')
},
(err) => {
console.log(err, '失败的回调')
}
)
console.log(p1)
处理异步后
class Promise {
// 遵循prmoiseA+ 传入imd 并且立即执行
constructor(imd) {
//记录初始状态
this.state = 'pending'
//记录成功初始值 给将来.then获取成功结果使用的
this.value = undefined
//记录失败的原因/值 给将来.catch获取失败原因使用的
this.reason = undefined
// 处理异步
//用于存放成功时候需要执行的函数
this.onResolveCallback = []
// 用于存放失败时候需要执行的函数
this.onRejectCallback = []
// 成功函数,成功时 修改this.state的状态为 fulfilled
let resolve = (value) => {
// 如果当前不是pendind状态 就不能修改
if (this.state !== 'pending') return
// 更新后的状态
this.state = 'fulfilled'
//更新成功初始值
this.value = value
// console.log('成功信息')
// 处理异步
// 遍历成功数组中的每个函数 并执行
this.onResolveCallback.forEach((fn) => fn())
console.log('resolve,执行了')
}
// 失败函数。失败时 修改this.state的状态为 rejected
let reject = (err) => {
if (this.state !== 'pending') return
// 更新后的状态
this.state = 'rejected'
// 记录失败原因
this.reason = err
// console.log('失败信息')
this.onRejectCallback.forEach((fn) => fn())
}
// imd需要传递两个函数 成功/失败
// 如果执行imd报错 直接返回错误
try {
imd(resolve, reject)
} catch (err) {
reject(err)
}
}
// 如果此时不加then方法 下面获取不到成功信息
// onFu1Fi1led成功时候被调用
// onRejected 失败时调用
then(onFu1Fi1led, onRejected) {
// 判断状态 如果 fulfilled 状态 是调用 onFu1Fi1led
// 将成功的值/失败的值返回出去
if (this.state === 'fulfilled') {
onFu1Fi1led(this.value)
}
// 否则 onRejected
if (this.state === 'rejected') {
onRejected(this.reason)
}
// 处理异步 如果当前状态时pending状态
// 在成功的数组中 添加对应的函数
if ((this.state = 'pending')) {
// 成功的
this.onResolveCallback.push(() => {
onFu1Fi1led(this.value)
})
// 失败的
this.onRejectCallback.push(() => {
onFu1Fi1led(this.reason)
})
}
console.log('then', '执行了')
}
}
const p1 = new Promise((resolve, reject) => {
// console.log(resolve, reject)
// console.log('立即执行')
// 调用执行成功信息
setTimeout(() => {
resolve('你对了')
// reject('你错了')
}, 3000)
// 调用执行失败信息
// reject('你错了')
})
// 此时函数并没有满足调用onFulfilled的条件 所以没有结果
// 应该等条件满足时调用 只有状态等于 fu1Fi1led 在调用onFulfilled
p1.then((res) => {
console.log(res, '1')
})
p1.then((res) => {
console.log(res, '2')
})
p1.then((res) => {
console.log(res, '3')
})
p1.then((res) => {
console.log(res, '4')
})
p1.then((res) => {
console.log(res, '5')
})
console.log(p1)
//直接任意判断类型
Object.prototype.toString.call
可以在不编写类组件的情况下 使组件容易自己的状态和可以使用react的特性
- 更容易解决状态相关的重用的问题
- 通过自定义hook能够更好的封装组件功能,能解决开发中的大多数问题,拥有代码复用机制
- 可以完全避免使用生命周期方法,编写起来代码整体风格优雅
let obj = {}
// 判断是不是一个空对象
console.log(JSON.stringify(obj) === '{}') //true
// 对象转数组 判断数组长度
console.log(Object.keys(obj).length === 0) //true
let arr = []
// 判断是不是数组类型
console.log(Object.prototype.toString.call(arr)) //[object Object]
// jQuery判断类型
console.log($.type(arr)) //Array
// 判断是不是函数/数组类型
function fn() {}
console.log(typeof fn) //function
console.log(arr instanceof Array) //Array
// 数组去重
let arr1 = [1, 3, 4, 5, 5, 5, 4, 4, 4, 4, 7, 8, 0]
let res = new Set(arr1)
console.log(...res)
// 数组降维 1 concat配合扩展运算符
let arr2 = [1, 3, 4, 5, 5, [5, 6, 9], 0]
console.log([].concat(...arr2))
// 数组降维 2 里面包裹几层就flat传几
let arr3 = [1, 3, 4, 5, 5, [5, 3, 5, 8, 4, 2, [4, 7], 6, 9], 0]
let res2 = arr3.flat(2)
// 无感刷新回答:
// 首先发送请求 如果后台返回401
那么响应拦截器中处理 => 判断是否是有token
// 如果有 说明token过期了 => 尝试用refresh_token
如果成功 把获取到的新token存仓库 并重新发送第一次请求
如果失败,说明refresh_token 也过期了
则跳转到登录页 情况仓库中油管token的信息
// 优化:可以在跳转时记录被拦截前的路径 等待用户登录后实现页面回跳
原理:在进入需要token请求的页面时,通过封装的路由组件进行一次判断,
原理:
#接收children传参 component标签传参 ...rest
1.判断本都是否有token,如果有
return children 或者 component传入的路由信息
2.如果没有token 做路由重定向 定向到登录页面 并且传入被拦截前的路径信息 用于登录回跳
import { getToken } from '@/utils/tokenApi'
import { Redirect, Route, RouteProps, useLocation } from 'react-router-dom'
interface PrivateRouteType extends RouteProps {
component: any
}
// children传入组件 component标签传组件
function PrivateRoute({
children,
component: Component,
...rest
}: PrivateRouteType) {
const location = useLocation()
return (
{
// 查看本地是否有token 获取token
const token = getToken()
// 如果有 就去传递过来的组件
if (token.token) {
return children ? children :
} else {
// 没有就去login state/from 记录拦截下来的地址 用于回调
return (
)
}
}}
/>
)
}
export default PrivateRoute
防抖核心: 延时执行
当用户输入关键字时,不会立即触发请求
开启延时器 在固定时间后发送请求,如果在这期间继续输入了内容,则关闭延时器 重新开启新的延时器
原生实现:
const timer = useRef(0)
//关闭延时器
clearTimeout(timer.current)
// 开启延时器
timer.current = window.setTimeout(() => {
console.log('输入内容,可以发请求了')
}, 500)
ahooks实现:useDebounceFn自定义hooks
使用ahooks中的 #useDebounceFn
const { run }=useDebounceFn(()=>{
//执行逻辑
},{
options 配置防抖 时间...等等
})
立即执行,指定时间范围内 只会触发一次
const { run } = useThrottleFn(
() => {
setcount((count) => {
return count + 1
})
},
{
wait: 2000,
}
)
将Antd文件上传组件进行二次封装,保留原有的属性,
把用户上传的文件存入数组 组件通过props接收数据 进行文件类型 文件大小进行判断 最后决定是否上传
分片上传:就是将整个大的文件拆分成多个小的数据块进行分片上传,上传完成之后再由服务器进行数据块组装,整合成原始的文件
1.将需要上传的文件按照一定的分割规则,拆分成大小相同的数据块
2.创建一个分片上传任务 返回本次分片上传的唯一标识
3.串行或者并行的策略发送每个分片数据块
4.发送完成过 服务器判断是否上传完整 如果完整则将所有数据块进行组装合成 合成完整的文件
使用:
函数:const changePhoto = (e: React.ChangeEvent) => {
// 获取选择的文件
const file = e.target.files![0]
// 需要上传这张图片
const fd = new FormData()
fd.append('photo', file)
// 发送请求
dispatch(.....)
}
白屏时的loading动画
路由懒加载-React.lazy
异步组件React.Suspense- 骨架屏
前端请求改为服务器内网请求
比如用户信息这类接口本来是前端请求完后拿到用户信息,再拿着用户信息去请求与用户相关的页面数据,但是有些网络不稳定的地方接口串行很容易慢,如果一个超时了还得再请求一遍,所以这类移到服务端去做,直接变成内网调用接口,不受客户端网络环境影响。
部分接口数据做localstorage缓存
一进页面先去localstorage中拿数据渲染,然后再动态更新。
服务端渲染首屏 不然你这样还是要等js返回才开始渲染页面
使用 CDN 加载资源、OSS对象存储
App webview只是一个可视化的组件
前端代码打包优化
webpack是一个打包的工具它可以解决浏览器兼容问题模块化整合代码的能力
//资源打包分析
webpack-bundle-analyzer
//反向代理
proxy
//首先需要一个中间服务器,webpack中提供服务器的工具为
webpack-dev-server
//压缩代码
webpack -p
//html压缩
html-webpack-plugin
//对图片进行压缩和优化
image-webpack-loader
//删除无用的CSS样式
purgecss-webpack-plugin
// 开启Tree Shaking
// 按需加载&动态加载
plugin-syntax-dynamic-import
const Login = React.lazy(() => import('./pages/Login'))
const Layout = React.lazy(() => import('./pages/Layout'))
const NotFound = React.lazy(() => import('./pages/NotFound'))
优化:
// 原理:
// 首先不会直接给img设置图片路径,因为直接设置会进入页面就加载
// 通过data-img设置图片路径,使用 原生 IntersectionObserver的Api进行监听,当图片进入可视区域后在将data-src图片地址赋值给img的src属性
observe 开启监听
unobserve 结束特定监听
disconnect()组件卸载时 触发全 部停止监听
原理:
客户端与服务器建立连接,成功后,服务器会确认连接建立成功。
客户端与服务器建立 WebSocket 长连接
适用场景:聊天室、实时推送消息、实时图表、股票
这时候服务器就可以主动发请求给客户端。
通过yarn add socket.io-client
包
//提供了API
// 和服务器建立了链接
const client = io('地址', {
query: {
token: 用户token
},
transports: ['websocket']
})
#使用中Api
//当和服务器建立连接成功触发
client.on('connect', () => {
在这里可以做提示用户 你好......
})
//接收到服务器的消息触发
client.on('message', (data) => {
data中是#机器人\客服接口 返回来的消息
})
// 主动给服务器发送消息
client.emit('message', 值=>传递类型由后端定制)
// 和服务器断开链接,就会触发disconnect
client.on('disconnect', () => {
//可以做提示是等。。。。
})
// 主动关闭和服务器的链接
client.close()
它是由三部分组成:头信息(header), 消息体(payload)和签名(signature)。
用户在前端输入账号信息请求到后端服务器、当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证,后续访问会根据这个令牌判断用户时候有权限进行访问
包含alg 表示算法HS256 和 typ 默认就是JWT 两个属性
因为JWT是字符串 所以进行需要base64编码 编码后就是token
这里存放的是用户的信息,账号,密码,id等等
认情况下也会携带令牌的签发时间同样进行Base64编码后
对前两个的结果进行HMACSHA25
算法
早期实现:单系统登录的核心是cookie用cookie携带会话id
在浏览器与服务器之前维护会话状态,但是cookie有限制,
早期实现方案
核心是通过cookie实现单系统登录
但是cookie存在局限性
1.cookie存在域的限制
http请求时 只会卸载与该域名匹配的cookie 而不是所有的cookie
2.只能在相同顶级域名下 才能实现cookie共享
3.应用群系统使用的技术必须一致 最起码需要在统一个服务器环境下
4.共享cookie无法实现跨语言技术平台的登录
5.共享cookie本身不安全
所以有了sso技术,相对于单系统登录,
说明:多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
用途:用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了
sso需要一个独立的认证中心,只有认证中心能接收到用户的账号密码等信息,子系统接受认证中心的间接受令牌授权,sso认证中心验证用户账号密码没问题,就会生成令牌,在跳转过程中把令牌作为参数发送给每个子系统,子系统拿到令牌后,即得到了授权,可以通过令牌来创建会话,
面试回答:
1.sso需要一个独立的认证中心 只有sso认证中心能接收用户的账号密码信息
2.sso中心收到的账号密码没问题 就会生成令牌 创建全局会话
3.在跳转过程中将令牌作为参数分配个每个子系统
4.子系统拿到令牌即授权成功,可以通过令牌建立与用户之间的局部会话
5.当局部会话创建后 后续访问子系统将不在通过sso认证中心
注意:、
局部会话存在 那么全局会话一定存在
全局会话存在 局部会话不一定存在
全局会话销毁 局部会话必须销毁
将后端传递过来的html代码通过属性差异解析成合法的html语句
属性差异dangerouslySetInnerHTML
// 类似原生的 innerHTLI
dangerouslySetInnerHTML={{
_html: highLight(item),
}}
攻击方案:写html或者script脚本被innerHMTL脚本解析
解决方案: 详解
yarn add dompurify
yarn add @types/dompurify --安装类型说明文件
import DOMPurify from 'dompurify'
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
// ``
info.content
),
}}
var params = new URLSearchParams(location.search)
console.log(params.get('keyword'))
通过:Tabs.Tab 组件 只能解决当前页暂存
forceRender 被隐藏时是否渲染 `DOM` 结构 `boolean``false`
import { Route, RouteProps } from 'react-router-dom'
import styles from './index.module.scss'
type Props = RouteProps & {
activePath: string
}
// 示例:
// 1 需要保留页面的路由地址 activePath: '/home',浏览器地址栏(当前路由)pathname: '/home'
// 1 需要保留页面的路由地址 activePath: '/home',浏览器地址栏(当前路由)pathname: '/home/index'
// 2 需要保留页面的路由地址 activePath: '/home',浏览器地址栏(当前路由)pathname: '/login'
// 使用方法:
const KeepAlive = ({ activePath, children, ...rest }: Props) => {
return (
{
const {
location: { pathname },
} = props
const isMatch = pathname.startsWith(activePath)
return (
{children}
)
}}
/>
)
}
export defaulet KeepAlive
yarn add highlight.js
import hljs from 'highlight.js'
import 'highlight.js/styles/vs2015.css'
useEffect(() => {
// 配置 highlight.js
hljs.configure({
// 忽略未经转义的 HTML 字符
ignoreUnescapedHTML: true,
})
// 获取到内容中所有的code标签
const codes = document.querySelectorAll('.dg-html pre code')
codes.forEach((el) => {
// 让code进行高亮
hljs.highlightElement(el as HTMLElement)
})
}, [])