4. 虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么
虚拟DOM相对原生的DOM不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量DOM时,由于多了一层虚拟DOM的计算,虚拟DOM也会比innerHTML插入慢。它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
5. React 与 Vue 的 diff 算法有何不同?
diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。
- 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
- 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
- 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。
以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。
九、其他
1. React组件命名推荐的方式是哪个?
通过引用而不是使用来命名组件displayName。
使用displayName命名组件:
export default React.createClass({ displayName: 'TodoApp', // ...}) 复制代码
React推荐的方法:
export default class TodoApp extends React.Component { // ...} 复制代码
2. react 最新版本解决了什么问题,增加了哪些东西
React 16.x的三大新特性 Time Slicing、Suspense、 hooks
- Time Slicing(解决CPU速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得react能在性能极其差的机器跑时,仍然保持有良好的性能
- Suspense (解决网络IO问题) 和lazy配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T异步但是同步的写法,这是最好的解决异步问题的方式
- 提供了一个内置函数componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。
(1)React16.8加入hooks,让React函数式组件更加灵活,hooks之前,React存在很多问题:
- 在组件间复用状态逻辑很难
- 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
- class组件的this指向问题
- 难以记忆的生命周期
hooks很好的解决了上述问题,hooks提供了很多方法
- useState 返回有状态值,以及更新这个状态值的函数
- useEffect 接受包含命令式,可能有副作用代码的函数。
- useContext 接受上下文对象(从 React.createContext返回的值)并返回当前上下文值,
- useReducer useState 的替代方案。接受类型为 (state,action)=> newState的reducer,并返回与dispatch方法配对的当前状态。
- useCalLback 返回一个回忆的memoized版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的ref对象,其Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。
- useImperativeMethods 自定义使用ref时公开给父组件的实例值
- useMutationEffect 更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发
- useLayoutEffect DOM改变后同步触发。使用它来从DOM读取布局并同步重新渲染
(2)React16.9
- 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
- 废弃 javascrip:形式的 URL。以javascript:开头的URL 非常容易遭受攻击,造成安全漏洞。
- 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。
- act()也支持异步函数,并且你可以在调用它时使用 await。
- 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便
(3)React16.13.0
- 支持在渲染期间调用setState,但仅适用于同一组件
- 可检测冲突的样式规则并记录警告
- 废弃 unstable_createPortal,使用CreatePortal
- 将组件堆栈添加到其开发警告中,使开发人员能够隔离bug并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
3. react 实现一个全局的 dialog
import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import ReactDOM from 'react-dom'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import './dialog.css'; let defaultState = { alertStatus:false, alertTip:"提示", closeDialog:function(){}, childs:'' } class Dialog extends Component{ state = { ...defaultState }; // css动画组件设置为目标组件 FirstChild = props => { const childrenArray = React.Children.toArray(props.children); return childrenArray[0] || null; } //打开弹窗 open =(options)=>{ options = options || {}; options.alertStatus = true; var props = options.props || {}; var childs = this.renderChildren(props,options.childrens) || ''; console.log(childs); this.setState({ ...defaultState, ...options, childs }) } //关闭弹窗 close(){ this.state.closeDialog(); this.setState({ ...defaultState }) } renderChildren(props,childrens) { //遍历所有子组件 var childs = []; childrens = childrens || []; var ps = { ...props, //给子组件绑定props _close:this.close //给子组件也绑定一个关闭弹窗的事件 }; childrens.forEach((currentItem,index) => { childs.push(React.createElement( currentItem, { ...ps, key:index } )); }) return childs; } shouldComponentUpdate(nextProps, nextState){ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } render(){ return ( <ReactCSSTransitionGroup component={this.FirstChild} transitionName='hide' transitionEnterTimeout={300} transitionLeaveTimeout={300}> <div className="dialog-con" style={this.state.alertStatus? {display:'block'}:{display:'none'}}> {this.state.childs} </div> </ReactCSSTransitionGroup> ); } } let div = document.createElement('div'); let props = { }; document.body.appendChild(div); let Box = ReactD 复制代码
子类:
//子类jsx import React, { Component } from 'react'; class Child extends Component { constructor(props){ super(props); this.state = {date: new Date()}; } showValue=()=>{ this.props.showValue && this.props.showValue() } render() { return ( <div className="Child"> <div className="content"> Child <button onClick={this.showValue}>调用父的方法</button> </div> </div> ); } } export default Child; 复制代码
css:
.dialog-con{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); } 复制代码
4. React 数据持久化有什么实践吗?
封装数据持久化组件:
】let storage={ // 增加 set(key, value){ localStorage.setItem(key, JSON.stringify(value)); }, // 获取 get(key){ return JSON.parse(localStorage.getItem(key)); }, // 删除 remove(key){ localStorage.removeItem(key); } }; export default Storage; 复制代码
在React项目中,通过redux存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过redux存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是localStorage,localStorage是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。
但是在已经使用redux来管理和存储全局数据的基础上,再去使用localStorage来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合redux来达到持久数据存储功能的框架呢?当然,它就是redux-persist。redux-persist会将redux的store中的数据缓存到浏览器的localStorage中。其使用步骤如下:
(1)首先要安装redux-persist:
npm i redux-persist 复制代码
(2)对于reducer和action的处理不变,只需修改store的生成代码,修改如下:
import {createStore} from 'redux' import reducers from '../reducers/index' import {persistStore, persistReducer} from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; const persistConfig = { key: 'root', storage: storage, stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况 }; const myPersistReducer = persistReducer(persistConfig, reducers) const store = createStore(myPersistReducer) export const persistor = persistStore(store) export default store 复制代码
(3)在index.js中,将PersistGate标签作为网页内容的父标签:
import React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux' import store from './redux/store/store' import {persistor} from './redux/store/store' import {PersistGate} from 'redux-persist/lib/integration/react'; ReactDOM.render(<Provider store={store}> <PersistGate loading={null} persistor={persistor}> {/*网页内容*/} </PersistGate> </Provider>, document.getElementById('root')); 复制代码
这就完成了通过redux-persist实现React持久化本地数据存储的简单应用。
5. 对 React 和 Vue 的理解,它们的异同
相似之处:
- 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库
- 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板。
- 都使用了Virtual DOM(虚拟DOM)提高重绘性能
- 都有props的概念,允许组件间的数据传递
- 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性
不同之处:
1)数据流
Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM
Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
- Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
- 对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化。
3)组件化
React与Vue最大的不同是模板的编写。
- Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性。
- React推荐你所有的模板通用JavaScript的语法扩展——JSX书写。
具体来讲:React中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。
4)监听数据变化的实现原理不同
- Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
- React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
5)高阶组件
react可以通过高阶组件(Higher Order Components-- HOC)来扩展,而vue需要通过mixins来扩展。
原因高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不采用HOC来实现。
6)构建工具
两者都有自己的构建工具
- React ==> Create React APP
- Vue ==> vue-cli
7)跨平台
- React ==> React Native
- Vue ==> Weex
6. 可以使用TypeScript写React应用吗?怎么操作?
(1)如果还未创建 Create React App 项目
- 直接创建一个具有 typescript 的 Create React App 项目:
npx create-react-app demo --typescript 复制代码
(2)如果已经创建了 Create React App 项目,需要将 typescript 引入到已有项目中
- 通过命令将 typescript 引入项目:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest 复制代码
- 将项目中任何 后缀名为 ‘.js’ 的 JavaScript 文件重命名为 TypeScript 文件即后缀名为 ‘.tsx’(例如 src/index.js 重命名为 src/index.tsx )
7. React 设计思路,它的理念是什么?
(1)编写简单直观的代码
React最大的价值不是高性能的虚拟DOM、封装的事件机制、服务器端渲染,而是声明式的直观的编码方式。react文档第一条就是声明式,React 使创建交互式 UI 变得轻而易举。为应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。 以声明式编写 UI,可以让代码更加可靠,且方便调试。
(2)简化可复用的组件
React框架里面使用了简化的组件模型,但更彻底地使用了组件化的概念。React将整个UI上的每一个功能模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成更大的组件。React的组件具有如下的特性∶
- 可组合:简单组件可以组合为复杂的组件
- 可重用:每个组件都是独立的,可以被多个组件使用
- 可维护:和组件相关的逻辑和UI都封装在了组件的内部,方便维护
- 可测试:因为组件的独立性,测试组件就变得方便很多。
(3) Virtual DOM
真实页面对应一个 DOM 树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作 DOM 来进行更新。 DOM 操作非常昂贵。在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难 以维护。
React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM,每次数据更新后,重新计算 Virtual DOM,并和上一次生成的 Virtual DOM 做对比,对发生变化的部分做批量更新。React 也提供了直观的 shouldComponentUpdate 生命周期回调,来减少数据变化后不必要的 Virtual DOM 对比过程,以保证性能。
(4)函数式编程
React 把过去不断重复构建 UI 的过程抽象成了组件,且在给定参数的情况下约定渲染对应的 UI 界面。React 能充分利用很多函数式方法去减少冗余代码。此外,由于它本身就是简单函数,所以易于测试。
(5)一次学习,随处编写
无论现在正在使用什么技术栈,都可以随时引入 React来开发新特性,而不需要重写现有代码。
React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。因为 React 组件可以映射为对应的原生控件。在输出的时候,是输出 Web DOM,还是 Android 控件,还是 iOS 控件,就由平台本身决定了。所以,react很方便和其他平台集成
8. React中props.children和React.Children的区别
在React中,当涉及组件嵌套,在父组件中使用props.children
把所有子组件显示出来。如下:
function ParentComponent(props){ return ( <div> {props.children} </div> ) } 复制代码
如果想把父组件中的属性传给所有的子组件,需要使用React.Children
方法。
比如,把几个Radio组合起来,合成一个RadioGroup,这就要求所有的Radio具有同样的name属性值。可以这样:把Radio看做子组件,RadioGroup看做父组件,name的属性值在RadioGroup这个父组件中设置。
首先是子组件:
//子组件 function RadioOption(props) { return ( <label> <input type="radio" value={props.value} name={props.name} /> {props.label} </label> ) } 复制代码
然后是父组件,不仅需要把它所有的子组件显示出来,还需要为每个子组件赋上name属性和值:
//父组件用,props是指父组件的props function renderChildren(props) { //遍历所有子组件 return React.Children.map(props.children, child => { if (child.type === RadioOption) return React.cloneElement(child, { //把父组件的props.name赋值给每个子组件 name: props.name }) else return child }) } //父组件 function RadioGroup(props) { return ( <div> {renderChildren(props)} </div> ) } function App() { return ( <RadioGroup name="hello"> <RadioOption label="选项一" value="1" /> <RadioOption label="选项二" value="2" /> <RadioOption label="选项三" value="3" /> </RadioGroup> ) } export default App; 复制代码
以上,React.Children.map
让我们对父组件的所有子组件又更灵活的控制。