React相关问题
框架好处
- 组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。
- 天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP还是MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。
- 生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。
- 开发效率: 现代前端框架都默认自动更新DOM,而非我们手动操作,解放了开发者的手动DOM成本,提高开发效率,从根本上解决了UI 与状态同步问题。
常见框架:Angular React Vue Svelte
React vs Vue
React 与 Vue 存在很多共同点,例如他们都是 JavaScript 的 UI 框架,专注于创造前端的富应用。
不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。
React组件倾向于使用
jsx
语法,all in js,将html与css全都融入javaScript,jsx语法相对来说更加灵活。vue仍然是拥抱经典的**html(结构)+css(表现)+js(行为)**的形式,vue鼓励开发者使用
template
模板,并提供指令供开发者使用如v-if、v-show、v-for等指令,因此在开发vue应用的时候会有一种在写经典web应用(结构、表现、行为分离)的感觉。React 整体是函数式的思想,在 React 中是单向数据流,推崇结合 immutable 来实现数据不可变。
而 Vue 的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立 Watcher 来监听,当属性变化的时候,响应式的更新对应的虚拟 DOM。
如上,所以 React 的性能优化需要手动去做。
React优势
- 灵活的结构和可扩展性。
- 丰富的JavaScript库。
- 发展: React得到了
Facebook
专业开发人员的支持,他们不断寻找改进方法。 - Web或移动平台: React提供
React Native
平台,可通过相同的React组件模型为iOS
和Android
开发本机呈现的应用程序。
react
在中后台项目中由于在处理复杂的业务逻辑或组件的复用问题比vue
优雅而被人认可,但这种优雅是要有成本代价的,它更需要团队技术整体比较给力,领头大佬的设计与把关能力要更优秀,因此开发成本更大。
vue
更友好更易上手的写法著称,渐进式的框架、更友好的api、更亲民的设计让开发成本大大下降而效率大大提升。
vue
与react
在发展长河中越发成熟,深思熟虑后觉得两者不管在移动端或大型中后台都是非常可行的,其实框架本无好坏之分,我们更应该思考的是团队想要用什么技术栈、自己喜欢与擅长什么技术栈。
JSX
JSX 是JavaScript XML 的简写。是 React 使用的一种文件,它利用 JavaScript 的表现力和类似 HTML 的模板语法。这使得 HTML 文件非常容易理解。此文件能使应用非常可靠,并能够提高其性能。
React不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息。
实际上,JSX 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖。
虚拟DOM
操作DOM会引起页面的回流或者重绘。相比起来,通过多一些预先计算来减少DOM的操作要划算的多。
但是,“使用虚拟DOM会更快”这句话并不一定适用于所有场景。例如:一个页面就有一个按钮,点击一下,数字加一,那肯定是直接操作DOM更快。使用虚拟DOM无非白白增加了计算量和代码量。即使是复杂情况,浏览器也会对我们的DOM操作进行优化,大部分浏览器会根据我们操作的时间和次数进行批量处理,所以直接操作DOM也未必很慢。
使用虚拟DOM可以提高代码的性能下限,并极大的优化大量操作DOM时产生的性能损耗。同时这些框架也保证了,即使在少数虚拟DOM不太给力的场景下,性能也在我们接受的范围内。
UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调。
这种方式赋予了 React 声明式的 API:您告诉 React 希望让 UI 是什么状态,React 就确保 DOM 匹配该状态。这使您可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。
实现原理
Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。
- 每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。
- 然后计算之前 DOM 表示与新表示的之间的差异。
- 完成计算后,将只用实际更改的内容更新 real DOM。
React Fiber
Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。
React Fiber的思想: React 渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
diff 算法/协调
在某一时间节点调用 React 的 render()
方法,会创建一棵由 React 元素组成的树。在下一次 state
或 props
更新时,相同的 render()
方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。
此算法有一些通用的解决方案,即将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n^3 )
,其中 n 是树中元素的数量。
于是 React 在以下两个假设的基础之上提出了一套 O(n)
的启发式算法:
- 两个不同类型的元素会产生出不同的树;
- 开发者可以通过设置
key
属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;
Keys
当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素,从而减少不必要的元素重新渲染。
虚拟DOM中key的作用:
简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。
详细的说: 当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM, 随后React进行新虚拟DOM与旧虚拟DOM的diff比较,比较规则如下:
旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM中内容没变, 直接使用之前的真实DOM
- 若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
旧虚拟DOM中未找到与新虚拟DOM相同的key
- 根据数据创建新的真实DOM,随后渲染到到页面
用index作为key可能会引发的问题:
若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
开发中如何选择key?:
最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
如果确定只是简单的展示数据,用index也是可以的。
两种组件
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
函数式
1 | //1.创建函数式组件 |
执行了ReactDOM.render(<MyComponent/>.......)
之后,发生了什么?
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
类式
1 | //1.创建类式组件 |
执行了ReactDOM.render(<MyComponent/>.......)
之后,发生了什么?
React解析组件标签,找到了MyComponent组件。
发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。
类的方法默认开启了局部严格模式
组件中的render方法中的this为组件实例对象
但组件自定义方法中this为undefined
,如何解决?
a) 强制绑定this:通过函数对象的bind()
1 | //1.创建组件 |
b) 箭头函数推荐
1 | //1.创建组件 |
事件处理
- 通过
onXxx
属性指定事件处理函数(注意大小写)- React使用的是自定义(合成)事件,而不是使用的原生DOM事件—-为了更好的兼容性
- React中的事件是通过事件委托的方式处理的(委托给组件最外层的元素)—-为了更高效
- 通过
event.target
得到发生事件的DOM元素对象 —–不要过度使用ref
- React的所有事件都挂载在
document
中,当真实dom触发后冒泡到document后才会对react事件进行处理 - 所以原生的事件会先执行,然后执行react合成事件,最后执行真正在document上挂载的事件。
State
关于 setState()
你应该了解三件事:
不要直接修改 State
而是应该使用 setState()
:
State更新可能异步
出于性能考虑,React 可能会把多个 setState()
调用合并成一个调用。
因为 this.props
和 this.state
可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
要解决这个问题,可以让 setState()
接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
1 | // Correct |
上面使用了箭头函数,不过使用普通的函数也同样可以:
1 | // Correct |
setState更新状态的2种写法
setState(stateChange, [callback])
——对象式的setState- stateChange为状态改变对象(该对象可以体现出状态的更改)
- callback是可选的回调函数, 它在状态更新完毕(状态更新是异步的)、界面也更新后(render调用后)才被调用
setState(updater, [callback])
——函数式的setState- updater为返回stateChange对象的函数。
- updater可以接收到state和props。
- callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
- 对象式的setState是函数式的setState的简写方式(
语法糖
) - 使用原则:
- 如果新状态不依赖于原状态 => 使用对象方式
- 如果新状态依赖于原状态 => 使用函数方式
- 如果需要在setState()执行后获取最新的状态数据, 要在第二个callback函数中读取
setState只在合成事件和 hook() 中是“异步”的,在 原生事件和 setTimeout 中都是同步的。
State更新会合并
当你调用 setState()
的时候,React 会把你提供的对象合并到当前的 state。
这里的合并是浅合并。
Refs
在典型的React数据流理念中,父组件跟子组件的交互都是通过传递props
实现的。
如果父组件需要修改子组件,只需要将新的属性传递给子组件,由子组件来实现具体的绘制逻辑。
在特殊的情况下,如果你需要命令式(imperatively)的修改子组件,React也提供了应急的处理办法–Ref。
Ref 既支持修改DOM元素,也支持修改自定义的组件。
声明式编程
React有2个基石设计理念:一个是声明式编程,一个是函数式编程。
声明式编程的特点体现在2方面:
- 组件定义的时候,所有的实现逻辑都封装在组件的内部,通过state管理,对外只暴露属性。
- 组件使用的时候,组件调用者通过传入不同属性的值来达到展现不同内容的效果。一切效果都是事先定义好的,至于效果是怎么实现的,组件调用者不需要关心。
因此,在使用React的时候,一般很少需要用到Ref。
Ref使用场景
简单理解就是,控制一些DOM原生的效果,如输入框的聚焦效果和选中效果等;触发一些命令式的动画;集成第三方的DOM库。最后还补了一句:如果要实现的功能可以通过声明式的方式实现,就不要借助Ref。
refs是React组件中非常特殊的props, 可以附加到任何一个组件上,从字面意思上看,ref即reference,组件被调用时会创建一个该组件的实例,而ref就会指向这个实例。
Ref用法
如果作用在原生的DOM元素上,通过Ref获取的是DOM元素,可以直接操作DOM的API。
如果作用在自定义组件,Ref获取的是组件的实例,可以直接操作组件内的任意方法。
创建 Refs
Refs 是使用 React.createRef()
创建的,并通过 ref
属性附加到 React 元素。
在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
1 | class MyComponent extends React.Component { |
访问 Refs
当 ref 被传递给 render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。
1 | const node = this.myRef.current; |
ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 你不能在函数组件上使用
ref
属性,因为他们没有实例。
Refs 与函数组件
默认情况下,你不能在函数组件上使用 ref
属性,因为它们没有实例。
如果要在函数组件中使用 ref
,你可以使用 forwardRef
(可与 useImperativeHandle
结合使用)。
但你可以在函数组件内部使用 ref
属性,只要它指向一个 DOM 元素或 class 组件。
React组件通信如何实现?
由于
React
是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值。因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中。
React组件间通信方式:
- 父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
- 子组件向父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中。
- 兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
- 跨层级通信:
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context
通信再适合不过。 - 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信。
- 全局状态管理工具: 借助
Redux
或者Mobx
等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态。
比较好的搭配方式
- 父子组件:props
- 兄弟组件:消息订阅-发布、集中式管理
- 祖孙组件(跨级组件):消息订阅-发布、集中式管理、Context(开发用的少,封装插件用的多)
React.memo
React.PureComponent
React.Component
是使用ES6 classes方式定义 React 组件的基类。
React.PureComponent
与 React.Component
很相似。
两者的区别在于 React.Component
并未实现 shouldComponentUpdate()
,而 React.PureComponent
中以浅层对比 prop
和 state
的方式来实现了该函数。
如果赋予 React 组件相同的 props 和 state,render()
函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent
可提高性能。
React.memo
1 | const MyComponent = React.memo(function MyComponent(props) { |
React.memo
为高阶组件。
如果你的组件在相同 props
的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。
这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅检查 props 变更。
如果函数组件被 React.memo
包裹,且其实现中拥有 useState
,useReducer
或 useContext
的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。