一 Virtual DOM
1.1 什么是虚拟DOM
JavaScript中,虚拟DOM就是一个Object对象,并且至少包含标签名(tag),属性(props)和子元素(children)这三个属性(不同的框架命名可能会有差异)。
<div id ="app">
<p class="text">hello stoney!</p>
</div>
上述html转换为虚拟DOM如下:
{
tag: 'div',
props: {
id: 'app'
},
children: [
{
tag: 'p',
props: {
className: 'text'
},
children: [
'hello stoney!'
]
}
]
}
1.2 虚拟DOM实现原理
- 本质上是JavaScript对象,是对真实dom树的抽象
- 状态变更时,记录新旧树的差异
- 最后把差异更新到真正的dom树中;
二 diff 算法
2.1 传统的diff算法
传统的diff算法的时间复杂度是O(n^3),会将两个树的节点两两比较(O(n^2),还需要编辑(O(n)),因此复杂度是O(n^3);
2.2 react diff原理
react中对diff算法进行了优化,基于以下三个策略进行优化
- WEB UI中DOM节点跨层级的移动操作特别少,可以忽略不计。(tree diff)
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。(component diff)
- 对于同一层级的一组子节点,它们可以通过唯一id进行区分。(element diff)
tree diff(如果dom节点出现了跨层级操作:只有创建节点和删除节点的操作)
- React通过updateDepth对Virtual DOM树进行层级控制。
- 对树分层比较,两棵树只对同一层级节点进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较;
- 只需遍历一次,就能完成整棵DOM树的比较;
component diff: React对不同的组件间的比较,有三种策略:
- 同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可;
- 同一类型的两个组件,组件A变化为组件B,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变), 可节省大量计算时间,所以用户可以通过shouldComponentUpdate()来判断该组件是否需要进行diff;
- 不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件), 从而替换整个组件的所有节点; 注意:如果组件A和组件B的结构相似,但是React判断是不同类型的组件,则不会比较其结构,而是删除组件A及其子节点,创建 组件B及其子节点。
element diff 当节点处于同一层级时,diff提供三种节点操作:删除,插入,移动;
2.3 减少diff损耗
基于tree diff
不使用跨层级节点移动的操作,对于条件渲染多个子节点时,可以通过css隐藏或显示节点,而不是移动或添加节点;
基于 component diff
注意使用shouldComponentUpdate来减少组件不必要的更新,对于类似的结构应尽量封装成组件;
基于element diff
对于列表结构,应该避免将最后一个节点移动到列表首部的操作
三 高阶组件
3.1 高阶组件的定义
高阶组件(Higher-Order Components)就是一个函数,该函数接收一个组件作为参数,并返回一个新的组件;
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props;
if (visible === false) return null;
return <WrappedComponent {...props} />;
}
}
}
高阶组件可以解决以下问题:
- 抽取重复代码,实现组件复用;
- 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制
- 捕获/劫持处理组件的生命周期;
3.2 实现方式
3.2.1 属性代理
该方式在render中返回要包裹的组件,这样我们可以代理所有传入的props,并且决定如何渲染。此方式生成的高阶组件就是传入组件的父组件,以下代码实现了属性代理方式;
function proxyHOC(WrappedComponent) {
return class extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
该方式可以实现:
- 可操作所有传入的props
- 可操作组件的生命周期
- 可操作组件的static方法
- 可获取refs
3.2.2 反向继承
返回的新组件继承原组件,在render中调用原组件的render。由于继承了原组件,能通过this访问到原组件的生命周期,props,state,render等,以下是反向继承的代码实现;
function inheritHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
该方式可以实现:
- 可操作所有传入的props
- 可操作组件的生命周期
- 可操作组件的static方法
- 可获取refs
- 可操作state
- 可以渲染劫持
3.3 高阶组件的使用方式
compose
logger(visible(style(Input)))
可以手动封装一个函数组合工具
const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
compose(logger,visible,style)(Input);
Decorators
@logger
@visible
@style
class Input extends Component {
// ...
}
3.4 使用实例
下面代码实现了通过反向继承实现了渲染劫持
export default function pageInit(SourceComponent) {
return class HOC extends SourceComponent {
render() {
if (client_env === 'PC') {
return super.renderPC()
}
return super.render();
}
};
}
四 React中获取数据
4.1 生命周期中获取数据
在componentDidMount()中第一次渲染时获取数据,在componentDidUpdate()中props更新时重新获取数据;
4.2 Hooks中获取数据
4.3 suspense中获取数据
Suspense提供了一种声明性方法来异步获取react中的数据,采用包裹进行异步操作的组件
import React, { Suspense } from "react";
import EmployeesList from "./EmployeesList";
function EmployeesPage({ resource }) {
return (
<Suspense fallback={<h1>Fetching employees....</h1>}>
<EmployeesFetch resource={resource} />
</Suspense>
);
}
function EmployeesFetch({ resource }) {
const employees = resource.employees.read();
return <EmployeesList employees={employees} />;
}
数据获取时,Suspense将显示fallback中的内容,获取完数据后,Suspense将使用获取到的数据渲染里面的组件;Suspense以声明性和同步的方式处理异步操作。组件内没有复杂的数据获取逻辑,组件内没有生命周期,没有Hooks;
五 React中的优化
5.1 PureComponent,React.memo
react中,如果父组件发生更新,即使父组件传给子组件的所有props都没有修改,也会引起子组件重新render过程。从react声明式设计理念来看,子组件的props和state都没有改变,其生成的dom结构和副作用也不应该改变。当子组件符合声明式设计理念时,就可以忽略组件本次的render过程。PureComponent是对类组件的props和state进行浅比较,React.memo是针对函数组件的props进行浅比较。但要配合不可变值使用;
5.2 shouldComponentUpdate
shouldComponentUpdate,这个生命周期通过判断props和state是否变化来判断是否需要执行render;
例如要往数组中添加一项数据时,如果使用state.push(item),而不是 const newState=[...state, item]时,就需要在shouldComponentUpdate中来深比较props,只有在props有修改时才执行组件的render过程。但如今数据不可变性和函数组件的流行,这样的场景出现的较少;
第二种情况下,如果开发过程中给子组件传递一个大对象作为props(有些属性用不到)。当大对象中某个子组件未用到的属性发生了更新,子组件也会重新触发render。这种情况下,通过实现子组件的shouldComponentUpdate方法,仅在子组件使用的属性值发生改变时才返回true,能避免子组件重新render;但这种场景有两个弊端:
-
如果存在很多子孙组件,找出所有子孙组件使用的属性,会增加很多工作量,也容易漏掉导致bug,开发过程中,最好做到只传递需要的属性,避免使用{...props}进行传递;
-
以下代码实例进行说明,B组件的shouldComponentUpdate中只比较了data.a和 data.b,目前没有任何问题。之后开发者在C组件中使用了data.c,假设data.a和data.c是一起更新的,也没有问题。但如果某次代码修改导致data.a和data.c不一起更新了,那么代码就会出bug,实际中从B到C组件还有若干中间组件,这时就很难想到是shouldComponentUpdate引起的问题了;
<A data="{data}"> {/* B 组件只使用了 data.a 和 data.b */} <B data="{data}"> {/* C 组件只使用了 data.a */} <C data="{data}"></C> </B> </A>
5.3 合理拆分子组件
基于react的工作流,只要父组件更新,就会导致所有的子组件更新。虽然可以使用shouldComponentUpdate和PureComponent,React.memo来避免重新渲染。但也要做到合理拆分子组件,把一些比较独立的数据变化分配给子组件,这样子组件数据变化导致的刷新不会引起其他组件的刷新;
5.4 事件绑定的正确写法
1,构造器中绑定this(也是官方推荐的写法)
export default class Test extends Component {
constructor(props){
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick(){
console.log('点击了',this)
}
render(){
return (
<div onClick={ this.handleClick }>click btn</div>
)
}
}
2,属性初始化器语法(class fields)绑定回调函数(需要Babel转译)
export default class Test extends Component {
handleClick=()=>{
console.log('点击了',this)
}
render(){
return (
<div onClick={ this.handleClick }>click btn</div>
)
}
}
3,bind绑定
export default class Test extends Component {
handleClick(){
console.log('点击了',this)
}
render(){
return (
<div onClick={ this.handleClick.bind(this) }>click btn</div>
)
}
}
4,箭头函数
export default class Test extends Component {
handleClick(){
console.log('点击了',this)
}
render(){
return (
<div onClick={ ()=> this.handleClick() }>click btn</div>
)
}
}
第2,3,4种方法的好处就是在不需要state的时候,不用额外的多写constructor函数。但是这个方法的问题在于每次渲染这个组件的时候,都会创造不同的回调函数,会对性能产生影响,如果作为props传入给子组件,子组件会进行额外的重新渲染,因为每一次都是新的方法实例作为新的属性在传递;官方推荐第一种和第二种方法来绑定this;
5.5 SetState优化
批量更新setState时,多次执行setState只会触发一次render过程。非批量更新时(setTimeout/promise回调)每次执行setState都会触发一次render过程,此时应该减少setState的触发次数,常见的业务场景即处理接口回调时,保证最后一次调用setState。
5.6 合理使用函数式组件
函数式组件又叫无状态组件,没有状态,方法,生命周期,只负责纯展示;主要通过减少继承Component而来的生命周期函数达到性能优化的效果;当一个组件只用来纯展示,内部不需要自己的状态,主要通过父组件传递的props进行展示时可以考虑只要函数式组件;
5.7 ComponentWillUnmount
react的生命周期函数,在组件卸载时触发,该生命周期函数中需要清除事件监听和timeout事件;
5.8 懒加载
在单页应用中,懒加载一般用于从一个路由跳转到另一个路由。还可以用于用户操作后才展示的复杂组件,比如点击按钮后展示的弹窗模块。懒加载是通过webpack的动态导入和React.lazy方法;
import { lazy, Suspense, Component } from "react"
// 对加载失败进行容错处理
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return <h1>这里处理出错场景</h1>
}
return this.props.children
}
}
const Comp = lazy(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
reject(new Error("模拟网络出错"))
} else {
resolve(import("./Component")) // Component是懒加载组件
}
}, 2000)
})
})
export default function App() {
return (
<div className="App">
<div style={{ marginBottom: 20 }}>
实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。
</div>
<ErrorBoundary>
<Suspense fallback="Loading...">
<Comp />
</Suspense>
</ErrorBoundary>
</div>
)
}
六 面试遇到的问题
6.1 项目中入口文件的作用
src/index.js
src文件存放的是这个项目的核心内容,也就是我们的主要工作区域。其中index.js文件是和index.html文件进行关联的文件的唯一接口。内容如下:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render()的作用是将的内容渲染到根"root"中去。document.getElementById('root')中的"root"便是index.html中的"root"了,便是引用页面内容了。在这里,也可以写一些内容(结构,样式,逻辑)是整个项目的根组件。
6.2 react和react-dom的区别
React在v0.14之前是没有react-dom的,所有功能都包含在react里面。从v0.14开始,react才被拆分成react和react-dom。之所以拆开,是因为有了react-native; react只包含了Web和Mobile通用的核心部分,即react包是抽象逻辑, 只包含React的主干逻辑,例如组件实现,更新调度等;负责DOM的操作分到react-dom中,负责Mobile的包含在react-native中。react-dom顾名思义就是一种针对web dom的平台实现,主要用在web端进行渲染;ReactDOM只做和浏览器或者dom相关的操作,例如ReactDOM.render()和 ReactDOM.findDOMNode()。如果是服务端渲染,可以ReactDOM.renderToString()。除这些以外其他的都是react做的;
都需要import React from 'react';
而Web应用还要import REactDOM from 'react-dom';
Mobile应用还要import {Text, View} from 'react-native';
React.js: 提供React.js核心功能,如: 虚拟dom,组件
- React.createElement(type, props, children);
ReactDOM 提供了与浏览器交互的DOM功能,如: dom渲染
- ReactDOM.render(element, container[, callback])
-element: 要渲染的内容
-container: 要渲染的内容存放器
-callback: 渲染后的回调函数
6.3 react-router与react-router-dom的区别
-
react-router: 实现了路由的核心功能
-
react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如: Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由
-
react-router-dom依赖react-router,所以我们使用npm安装依赖的时候,只需要安装相应环境下的库即可,不用再显式安装react-router。基于浏览器环境的开发,只需要安装react-router-dom
6.4 项目中yarn.lock文件的作用
一句话概括,就是锁定安装时的包的版本号,并且需要上传到git,以保证其他人在yarn install时,大家的依赖能保证一致。并且会避免由于开发人员意外更改或者更新版本,导致不兼容的情况;
七 Children
组件标签对之间的内容会被当作一个特殊的属性 props.children 传入组件内容
可以自定义结构的组件的常用形式,下面以弹窗组件示例进行说明
- children
- 传递函数
- 传递子组件
7.1 children 示例
传递标签
import React, { PureComponent} from 'react';
import Alert from './alert';
class App extends PureComponent {
render() {
return <div >
<Alert title = "开课吧" >
<p> 这是第一条信息 < /p>
<p>这是第二条信息</p >
<p> 这是第三条信息 < /p>
</Alert>
< /div>
}
}
export default App;
alert.js如下:
import React, { PureComponent } from 'react';
import "./alert.css";
class Alert extends PureComponent {
render() {
let { title, children } = this.props;
return <div id = "alert" >
<header className = "alert-header" >{title} </header>
{children}
</div >
}
}
export default Alert;
效果:
传递react 组件
import React, { PureComponent } from 'react';
import Alert from './alert';
class Btn extends PureComponent {
render() {
return <button>关闭</button>
}
}
class App extends PureComponent {
render() {
return <div>
<Alert title="开课吧">
<Btn />
</Alert>
</div >
}
}
export default App;
alert.js
import React, { PureComponent } from 'react';
import "./alert.css";
class Alert extends PureComponent {
state = {
show: true
}
hide = () => {
this.setState({
show: false
})
}
render() {
let { show } = this.state;
let { title, children } = this.props;
return <div id = "alert" style = {{ display: show ? 'block' : 'none' }}>
<header className = "alert-header">{title}< /header>
{children}
</div >
}
}
export default Alert;
效果
7.2 传递component
import React, { PureComponent } from 'react';
import Alert from './alert';
class Btn extends PureComponent {
render() {
return <button>关闭</button>
}
}
class App extends PureComponent {
render() {
return <div>
<Alert title='hello Stoney' component={<Btn />} />
</div>
}
}
export default App;
alert.js
import React, { PureComponent } from 'react';
import "./alert.css";
class Alert extends PureComponent {
state={show: true}
hide = () => {
this.setState({show: false})
}
render() {
let {show} = this.state;
let { title } = this.props;
return <div id="alert" style={{display: show ? 'block' : 'none'}}>
<header className="alert-header">{title}</header>
{this.props.component}
</div>
}
}
export default Alert;
7.3 传递子组件
import React, { PureComponent } from 'react';
import Alert from './alert';
class Btn extends PureComponent {
render() {
return <button onClick={() => {this.props.hide()}}>关闭</button>
}
}
class App extends PureComponent {
render() {
return <div>
<Alert title='hello Stoney' Child={Btn} />
</div>
}
}
export default App;
alert.js
import React, { PureComponent } from 'react';
import "./alert.css";
class Alert extends PureComponent {
state={show: true}
hide = () => {
this.setState({show: false})
}
render() {
let {show} = this.state;
let { title, Child } = this.props;
return <div id="alert" style={{display: show ? 'block' : 'none'}}>
<header className="alert-header">{title}</header>
<Child hide={this.hide} />
</div>
}
}
export default Alert;
7.4 传递函数
render是定义的函数名,也可以定义成cb的形式
import React, { PureComponent } from 'react';
import Alert from './alert';
class Btn extends PureComponent {
render() {
return <button onClick={() => {this.props.hide()}}>关闭</button>
}
}
class App extends PureComponent {
render() {
return <div>
<Alert title='hello Stoney' render={(props) => {
return <Btn {...props} />
}} />
</div>
}
}
export default App;
alert.js
import React, { PureComponent } from 'react';
import "./alert.css";
class Alert extends PureComponent {
state={show: true}
hide = () => {
this.setState({show: false})
}
render() {
let {show} = this.state;
let { title, render } = this.props;
return <div id="alert" style={{display: show ? 'block' : 'none'}}>
<header className="alert-header">{title}</header>
{render({hide: this.hide})}
</div>
}
}
export default Alert;
八 vue和react对比
在渲染界面的时候,DOM的操作都是昂贵的,我们可以做的就是尽量减少DOM的操作。VUE和React都采用虚拟DOM来实现;
相同点:
- 都支持服务端渲染,nuxt.js(vue服务端渲染框架)和next.js(react服务端渲染框架);
- 都使用虚拟DOM来实现;都是组件化开发,通过props参数进行父子组件数据的传递;
- 只有框架的骨架,其他功能如路由,状态管理等是框架分离的组件;
- 都是javaScript的UI框架,数据驱动视图,专注于创造前端的富应用;
- 都有支持native的方案,react有react native,vue有weex;
- 都要状态管理工具,react有redux,vue有vuex;
在React中,所有的组件的渲染功能使用的都是JSX。JSX是使用XML语法编写的javaScript的一种语法糖,它可以使用完整的编程语言 JavaScript来构建视图页面,工具对JSX的支持相比对于现有的可用的其他vue模版还是比较先进的,在Vue中我们采用的Web技术在其 上面扩展,使得Template在写模版的过程中,样式风格已确定和更少的涉及业务逻辑,一个模版总是被声明的,模版中任何html都是有效的, 相比之下,React功能没有Vue模版系统的丰富。需要从组件文件中分离出html代码。
在性能方面,当我们考虑重新渲染功能。当组件的状态发生变化时,React的机制会触发整个组件树的重新渲染,并且由于React有大量的检查记住, 能让他提供许多有用的警告和错误提示信息,但可能需要额外的属性来避免不必要的重新渲染子组件。虽然vue的重新渲染功能是开箱即用的, 但vue提供了优化的重新渲染,其中系统在渲染过程中跟踪依赖关系并相应的工作; 在react中应用中的状态是关键概念。也有一些配套框架被设计为管理一个大的state对象,如redux。此外,state对象在react应用中是不可变 的,意味着它不能被直接改变(但是=赋值的时候可以改变state,但是不会重新渲染页面)。在react中你需要使用setState()方法去更新状态; 在vue中,state对象不是必须的,数据由data属性在vue对象中进行管理,而在Vue中,则不需要使用如setState()之类的方法去改变它的状态,在 vue对象中,data参数就是应用中数据的保存者;
不同点:
- React严格上只针对MVC的view层,而Vue则是mvvm模式;
- Virtual DOM不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树;
- 对react而言,每当应用的状态被改变时,全部组件树都会重新渲染,所以react会需要shouldComponentUpdate这个生命周期函数方法来控制渲染;
- 组件写法不一样,react推荐的是JSX + inline style,也就是html和css全都写进了javascript,即all in js;
- vue推荐的做法是webpack + vue-loader的单文件组件格式,即html,css, js写在同一个文件;
- 数据绑定:vue实现了数据的双向绑定,react数据流动是单向的;
- state对象在react中是不可变的,需要setState()去更新状态;
- 在vue中,state对象不是必须的,数据由data属性在vue对象中管理;
- 事件机制不同;vue原生事件使用标准的web事件,组件自定义事件机制是父子通信的基础;react原生事件被包装,采用事件代理的方式,所有事件都冒泡到document 对象上监听,然后在这里合成事件下发;react组件上无事件,父子通信使用props;