React
1. react 中 dom
虚拟dom
虚拟dom相当于在js和真实dom之间加了个缓存,使用虚拟 DOM + Diffing 算法,尽量减少与真实 DOM 的交互。
虚拟dom和real dom区别 性能差异
减少DOM的操作:虚拟dom可以将多次操作合并为一次操作,减少DOM操作的次数
真实 dom | 虚拟 dom |
---|---|
更新慢 | 更新快 |
可以直接更新 html | 无法直接更新 html |
如果元素更新,则创建新的DOM | 如果元素更新,则更新JSX |
DOM操作代价高 | DOM操作简单 |
消耗内存多 | 消耗内存少 |
2. 组件之间通信
- 父组件向子组件通信:父组件更新状态,通过
props
传给子组件,子组件得到后更新 - 子组件向父组件通信:通过
props回调函数
父组件把回调函数放在props,传给子组件,子组件就利用这个回调函数将信息传递给父组件 - 兄弟组件:找到它们的共同父节点,结合上面两种方式通信
- 跨级组件之间通信:通过props一层一层传,或通过
context
提供一个全局态的store
。 - 非嵌套组件: 发布订阅模式:。发布者发布事件,订阅者监听事件并做出反应,通过引入
event
模块进行通信 - 全局状态管理工具:借助
redux
或Mobx
等全局状态管理工具,进行通信。这种工具会维护一个全局状态中心store,并根据不同的事件产生新的状态。
3. redux 原理
作用: 集中式管理 react 应用中多个组件共享的状态
数据如何通过 redux 流动?
- 首先,用户(View)发出 Action,发出方式就用 dispatch
- 之后,Store 自动调用 Reducer,并且传入两个参数:当前state 和 收到的 Action, Reducer会返回新的State
- State一旦有变化,Store就会调用监听函数,来更新View。
二,核心概念
Store:保存程序的状态,并提供一些方法来访问状态、调度操作和注册侦听器。
在 redux 里面只有一个Store,整个应用需要管理的数据都在这个Store里面。这个Store我们不能直接去改变,我们只能通过返回一个新的Store去更改它。redux提供了一个createStore来创建state
import { createStore } from 'redux'
const store = createStore(reducer) //创建state
action 是视图层发起的一个操作,告诉Store 我们需要改变。比如用户点击了按钮,我们就要去请求列表,列表的数据就会变更。每个 action 必须有一个 type 属性,这表示 action 的名称,然后还可以有一个 payload 属性,这个属性可以带一些参数,用作 Store 变更:
const action = {
type: 'ADD_ITEM',
payload: 'new item', // 可选属性
}
Reducer
在上面定义了一个Action,但是Action不会自己主动发出变更操作到Store,所以这里我们需要一个叫dispatch的东西,它专门用来发出action,这个dispatch不需要我们自己定义和实现,redux已经帮我们写好了,在redux里面,store.dispatch()是 View发出 Action 的唯一方法。
store.dispatch({
type: 'ADD_ITEM',
payload: 'new item', // 可选属性
})
当 dispatch 发起了一个 action 之后,会到达 reducer,那么这个 reducer 用来干什么呢?
reducers 通过接受先前的状态和 action 来工作,然后它返回一个新的状态。它根据操作的类型确定需要执行哪种更新,然后返回新的值。如果不需要完成任务,它会返回原来的状态。
const reducer = function(prevState, action) {
...
return newState;
};
我们在创建store的时候,我们在createStore里面传入了一个reducer参数,在这里,我们就是为了,每次store.dispatch发送一个新的action,redux都会自动调用reducer,返回新的state。
Redux 由以下组件组成:
Action – 这是一个用来描述发生了什么事情的对象。
Reducer – 这是一个确定状态将如何变化的地方。
Store – 整个程序的状态/对象树保存在Store中。
View – 只显示 Store 提供的数据。
redux遵循的三个原则?
- 单一事实来源:Redux 使用 “Store” 将程序的整个状态存储在同一个地方。因此所有组件的状态都存储在 Store 中,并且它们从 Store 本身接收更新。
单一状态树可以更容易地跟踪随时间的变化,并调试或检查程序。 - 状态是只读的:改变状态的唯一方法是去触发一个动作。
- 使用纯函数进行更改:为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数
redux 优点
- 结果的可预测性:由于总是存在于一个真实来源store,所以不存在当前状态和其他部分同步的问题
- 可维护性
- 服务器端渲染:只需要将服务器的store传到客户端即可,对初始渲染非常有用,可以优化应用性能
- 易于测试,对开发人员非常方便:从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
react-rudex
4. react生命周期各个阶段
生命周期分为三个阶段:挂载阶段、更新阶段、卸载阶段。
- 初始渲染阶段:这是组件即将开始并进入dom的阶段
- 更新阶段:一旦组件被添加到dom中,只有在prop或状态发生改变时才能更新和重新渲染
- 卸载阶段:组件被销毁且从dom中删除
详细解释 React 组件的生命周期方法
1.挂载阶段
- constructor:构造函数,最先被执行,初始化state对象或者绑定this
- getDerivedStateFromProps:当我们接收到新的属性想去修改state,可以使用
- render:返回需要渲染的东西。可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
- componentDidMount
更新阶段:
- getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用
- shouldComponentUpdate:返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true
- render: 更新阶段也会触发此生命周期
- getSnapshotBeforeUpdate:这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用
- componentDidUpdate:该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。
卸载阶段:
- componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作
错误处理
当渲染过程,生命周期或子组件的构造函数中抛出错误时,会调用如下方法:
static getDerivedStateFromProps()
componentDidCatch()
React 16之后有三个生命周期被废弃(但并未删除)
componentWillMount
componentWillReceiveProps
componentWillUpdate
官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,目的是为了向下兼容
5. route 路由
React 路由是一个构建在 React 之上的路由库,有助于向应用程序添加新的屏幕和流。这使 URL 与网页上显示的数据保持同步。
Router用于定义多个路由,当用户定义特定的URL时,如果此URL和Router内定义的任何’路由’相匹配,则用户将重定向到该路由。
所以基本上我们在自己的应用中添加一个Router库,允许创建多个路由,每个路由都会向我们提供不同的视图
switch:switch会按顺序将已定义的URL与已定义的路由进行匹配,找到第一个匹配项后,将渲染指定的路径,从而绕过其他路线。
React Router 的优点
- 可以将 Router 可视化为单个根组件(),其中我们将特定的子路由()包起来。
- 不需要手动设置历史值,我们要做的就是将路由包装在组件中
- 包是分开的:共有三个包,分别用于 Web、Native 和 Core。这使我们应用更加紧凑。基于类似的编码风格很容易进行切换。
HashRouter 和 HistoryRouter 区别与原理
单页应用 是在移动互联时代诞生的,目标是不刷新浏览器,而通过感知地址栏中的变化来决定内容区域显示什么内容。要达成这个目标,我们要用到前端路由技术,具体来说有两种方式来实现:hash模式和history模式。hash模式是通过监听hashChange事件来实现的,history模式是通过pushState方法+popstate事件来实现的。
它们俩是前端路由的两种模式
HashRouter —— 即地址栏 URL 中的 # 符号
比如这个 URL:http://www.aaa.com/#/hello
,hash 的值为 #/hello
。
特点:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
HistoryRouter —— 利用了 HTML5 中新增的 pushState()
和 replaceState()
方法。这两个方法应用于浏览器的历史记录栈
,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
特点:丢掉了丑陋的#,但也有个问题:不怕前进,不怕后退,就怕刷新,因为刷新是实实在在地去请求服务器的
在hash模式下,前端路由修改的是#中的信息,而浏览器请求时不会将 # 后面的数据发送到后台,所以没有问题。但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,则会刷新出来404页面。
6. hooks 的优缺点
优点:
- 更容易复用代码:它通过自定义hooks来复用状态
- 给函数组件加上了state,让它拥有自己的状态和生命周期
- 一个React组件就是一个JS函数,更容易复用代码和实现逻辑复用
- 不需要老是纠结this指向
缺点:
- class组件的三个生命周期函数合并在一个生命周期函数内
- hooks只能在React函数组件中调用,不能在普通JS函数中调用,且不能在循环,条件判断调用hooks
7. setState 是同步还是异步
setState 本身是同步的,但是因为react的批处理下体现为异步。
所以在react的生命周期函数和合成时间中为异步,在原生环境下为同步
(在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新)
state 和 props 区别
都是普通的JS对象
- state 是组件自己管理数据,控制自己的状态,可变
- props 是外部传入的数据参数,不可变
- 没有state叫做 无状态参数,有 state叫有状态参数
- 多用 props,少用state,也就是多写无状态组件
8. refs
refs是用来访问DOM节点的方式
一般情况,react很少操作真实DOM,而是在render里编写页面结构,再由react组织真实DOM更新。
但是少数情况需要我们对页面真实DOM直接进行操作,这就要求我们有直接访问真实DOM的能力,就是refs
官方文档对Refs的描述是:Refs提供了一种方式,允许我们访问DOM节点,或在render方法中创建的React元素。
何时使用?
1)管理焦点,文本选择,媒体播放
2)触发强制动画
3)集成第三方DOM库
即:在React无法控制局面的时候,就需要直接操作refs
refs 有哪些使用方式?
1)字符串形式的refs:不推荐
2)回调形式的refs
3)使用React.createRef()创建,并通过ref属性附加到React元素
class Demo extends Component{
constructor(props){
super(props)
// React.creatRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点
this.myRef=React.createRef()
}
showData=()=>{
console.log(this.myRef)//输出myRef容器
console.log(this.myRef.current)//输出input节点
console.log(this.myRef.current.value)//输出input中的值
}
render(){
return(
<div>
<input ref={this.myRef} type="text" placeholder='点击按钮提示数据'></input>
</div>
) }}
9. react 事件绑定
React 中事件绑定都不是绑定在对应的真实DOM上,而是统一绑定
在document
上,采用事件冒泡
的形式向上传递,当事件发生并且冒泡至root
处时,react将事件内容封装并交由真正的处理函数运行。
采用合成事件的原因:
① 兼容所有的浏览器和实现跨平台开发 ;
② 统一挂载,减少内存消耗,方便在组件挂载/卸载时,统一订阅和移除事件 ;
③ 方便事件统一管理
另外,冒泡到root上的事件也不是原生浏览器事件,而是react自己实现的合成事件。因此如果我们不想要事件冒泡的话,调用event.stopPropagation是无效的,而应该调用
event.preventDefault
10. diff 算法
diff 算法作用:渲染真实DOM开销太大,,有时候修改某个数据会引起整个DOM树的修改,但是我们只希望更新我们修改的那部分,而不是整个dom,diff算法就实现这点
diff算法本质:找到两个对象之间的差异,尽可能做到节点复用
diff算法本质是对比,对比旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点
改变,找到这个虚拟节点,并只更新这个虚拟节点所对应的真实节点
,而不用更新其他,这就做到了精准更新DOM
- Tree diff:对树分层比较,两棵树只对
同一层次节点
比较,如果该节点不存在,则该节点及其子节点会被完全删除,不会再进行比较
(当出现节点跨层级移动时,并不会出现想象中的移动操作,而是会进行删除,重新创建的动作,这是一种很影响React性能的操作。因此,官方建议不要进行DOM节点跨层级操作,可以通过css隐藏、显示节点,而不是真正地移除、添加DOM节点) - Component diff:拥有相同类的两个组件 生成相似的树形结构,拥有不同类的两个组件 生成不同的树形结构。
(1)同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可。
(2)同一类型的两个组件,组件A变化为组件B时,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变),可节省大量计算时间,所以用户可以通过 shouldComponentUpdate() 来判断是否需要判断计算。
(3)不同类型的组件,将一个(将被改变的)组件判断为dirtycomponent(脏组件),从而替换整个组件的所有节点。
注意:如果组件D和G的结构相似,但是React判断是不同类型的组件,则不会比较其结构,而是删除组件D及其子节点,创建G及其子节点。 - Element diff:当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
例如:组件D已经在集合(A、B、C、D)里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A、D、B、C),则(对同一层级的同组子节点)添加唯一的key进行区分,移动即可
11. 简述一下 React 的源码实现
- React 的实现主要分为Component和Element;
- Component属于 React实例,在创建实例的过程中会在实例中注册state和props属性,还会依次调用内置的生命周期函数;
- Component中有一个render函数,render函数要求返回一个Element对象(或null);
- Element对象分为原生Element对象和组件式对象,原生Element+ 组件式对象会被一起解析成虚拟 DOM 树,并且内部使用的state和props也以 AST 的形式注入到这棵虚拟 DOM 树之中;
- 在渲染虚拟 DOM 树的前后,会触发 React Component 的一些生命周期钩子函数,比如componentWillMount和componentDidMount,在虚拟 DOM 树解析完成后将被渲染成真实 DOM 树;
- 调用setState时,会调用更新函数更新state,并且触发内部的一个updater,调用render生成新的虚拟 DOM 树,利用 diff 算法与旧的虚拟 DOM 树进行比对,比对以后利用最优的方案进行 DOM 节点的更新,这也是 React 单向数据流的原理(与 Vue 的 MVVM 不同之处)。
12. 框架的好处和弊端
优势:
- 组件化:高度的组件化有利于维护,有利于组合扩展
- 天然分层:现代框架不管是MVC、MVVM模式都可以帮我们进行分层,代码解耦更易于读写;
- 生态:现在的主流框架都自带生态,无论是数据流管理架构或UI都有成熟方案
- 开发效率:现在框架都默认自动更新DOM,无需手动
缺点:
- 兼容性问题,SEO不行
- 有场景要求,开发自由度低
- 框架本身也有出错风险
13. 模块化、组件化、工程化
模块化:一个模块就是一个实现特定功能的文件,有了模块就更好使用别人的代码,用什么功能就加载什么模块
js模块化方案很多有AMD、CommonJS、UMD、ES6 Module等。css模块化开发大多数是在less、sass、stylus等预处理器的import、minxin特性支持下实现。
模块化优势:避免变量污染,命名冲突
提高代码复用率
提高维护性
依赖关系的管理
组件化
页面上每个独立的,可视/可交互区域视为一个组件。
每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护。
由于组件具有独立性,所以组件与组件之间可以自由组合
页面不过是组件的容器,负责组合组件形成功能完善的界面
工程化
工程化是将前端项目当成一项系统工程进行分析、组织和构建从而达到项目结构清晰、分工明确、团队配合默契、开发效率提高的目的。
14. dva 数据流流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过dispatch发起一个 action,如果是同步行为会直接通过Reducers改变State,如果是 异步行为(副作用)会先触发Effects然后流向Reducers最终改变State
Models
(1) State---------------State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
(2)Action---------------Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。
(3)dispatch 函数------------dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
(4)Reducer-------------Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值,在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用.
(5)Effect------------dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。
15. 防抖 节流
防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。多次触发但触发只生效最后一次的场景
思路:每次触发事件时都取消之前的延时调用方法。
使用的本质:不允许某一行为触发。
应用场景:search搜索联想,用户在不断输入值时,用防抖来节约请求资源
设计思路:事件触发后开启一个定时器,如果事件在这个定时器限定的时间内再次触发,则清除定时器,在写一个定时器,定时时间到则触发。
function debounce(fn, delay){
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(()=> {
fn.apply(this, arguments);
}, delay)
}}
节流: 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。
思路:每次触发事件时都判断当前是否有等待执行的延时函数。
应用:鼠标不断点击发送,但单位时间只触发一次
使用的本质:允许某一行为触发,但是触发的频率不能太高。
设计思路:我们可以设计一种类似控制阀门一样定期开放的函数,事件触发时让函数执行一次,然后关闭这个阀门,过了一段时间后再将这个阀门打开,再次触发事件。
function throttle(fn, delay){
let valid = true;
return function(){
if(valid) { //如果阀门已经打开,就继续往下
setTimeout(()=> {
fn.apply(this, arguments);//定时器结束后执行
valid = true;//执行完成后打开阀门
}, delay)
valid = false;//关闭阀门
}
}
}
// 刚开始valid为true,然后将valid重置为false,进入了定时器,在定时器的时间期限之后,才会将valid重置为true,valid为true之后,之后的点击才会生效
// 在定时器的时间期限内,valid还没有重置为true,会一直进入return,就实现了在N秒内多次点击只会执行一次的效果
//用法:
function fn(value){
console.log(value);
}
var throttleFunc = throttle(fn,2000);//节流函数
//事件处理函数,按钮点击事件
btn.addEventListener("click",function(){
throttleFunc(Math.random());// 给节流函数传参
})