React学习笔记二

301 阅读16分钟

一  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节点出现了跨层级操作:只有创建节点和删除节点的操作) 

  1. React通过updateDepth对Virtual DOM树进行层级控制。
  2. 对树分层比较,两棵树只对同一层级节点进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较;
  3. 只需遍历一次,就能完成整棵DOM树的比较;

component diff: React对不同的组件间的比较,有三种策略:

  1. 同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可;
  2. 同一类型的两个组件,组件A变化为组件B,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变), 可节省大量计算时间,所以用户可以通过shouldComponentUpdate()来判断该组件是否需要进行diff;
  3. 不同类型的组件,将一个(将被改变的)组件判断为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的区别

  1. react-router: 实现了路由的核心功能

  2. react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如: Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由

  3. 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来实现;

相同点: 

  1. 都支持服务端渲染,nuxt.js(vue服务端渲染框架)和next.js(react服务端渲染框架); 
  2. 都使用虚拟DOM来实现;都是组件化开发,通过props参数进行父子组件数据的传递; 
  3. 只有框架的骨架,其他功能如路由,状态管理等是框架分离的组件; 
  4. 都是javaScript的UI框架,数据驱动视图,专注于创造前端的富应用; 
  5. 都有支持native的方案,react有react native,vue有weex; 
  6. 都要状态管理工具,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参数就是应用中数据的保存者; 

不同点: 

  1. React严格上只针对MVC的view层,而Vue则是mvvm模式; 
  2. Virtual DOM不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树; 
  3. 对react而言,每当应用的状态被改变时,全部组件树都会重新渲染,所以react会需要shouldComponentUpdate这个生命周期函数方法来控制渲染; 
  4. 组件写法不一样,react推荐的是JSX + inline style,也就是html和css全都写进了javascript,即all in js; 
  5. vue推荐的做法是webpack + vue-loader的单文件组件格式,即html,css, js写在同一个文件; 
  6. 数据绑定:vue实现了数据的双向绑定,react数据流动是单向的; 
  7. state对象在react中是不可变的,需要setState()去更新状态; 
  8. 在vue中,state对象不是必须的,数据由data属性在vue对象中管理; 
  9. 事件机制不同;vue原生事件使用标准的web事件,组件自定义事件机制是父子通信的基础;react原生事件被包装,采用事件代理的方式,所有事件都冒泡到document 对象上监听,然后在这里合成事件下发;react组件上无事件,父子通信使用props;