React笔记3

Posted demystify

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React笔记3相关的知识,希望对你有一定的参考价值。

一、虚拟DOM

1.产生背景

  • 真实DOM: 就是指我们平时所说的DOM,它是对结构化文本的抽象表达;
  • 传统的前端开发,通过浏览器提供的API直接对DOM执行增删改查的操作。DOM操作效率很低,尤其是对DOM进行增删改操作,每一次对DOM的修改都会引起浏览器对网页的重新布局和重新渲染,这会很耗时;
    为了解决操作操作的效率问题,通过引入虚拟DOM这个抽象层,建立在真实DOM之上,对真实DOM的抽象,来提高性能;
    虚拟DOM技术并非React所独有的,它是一个独立的技术,只不过React使用了这项技术来提高自身性能;

2.虚拟DOM

  • 虚拟DOM使用普通的javascript对象来描述DOM元素;
  • 虚拟DOM是普通的JavaScript对象;
  • React元素本身就是一个虚拟DOM节点;

3.Diff算法

  • React采用声明式的API描述UI结构,每次组件状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象;
  • React通过比较前后两次虚拟DOM结构的变化找出差异部分,再将差异部分更新到真实DOM上;
    这一过程称为React的调和过程(Reconciliation),其中比较两个树形结构的差异算法是Diff算法;

注意: 在Diff算法中,比较的是两方是新的虚拟DOM和旧的虚拟DOM,Diff的结果才会更新到真实DOM上

  • 正常情况下,比较两个树形结构差异的算法的时间复杂度是O(N^3), 这个效率对DOM来说是无法接受的;
  • React总结了DOM的实际使用场景,提出了两个在绝大多数实践场景下都成立的假设,基于这两个假设,实现了在O(N)时间复杂度内完成两棵虚拟DOM树的比较,这两个假设是:
    • 如果两个元素的类型不同,那么它们将生成两棵不同的树。
    • 为列表中的元素设置key属性,用key标识对应的元素在多次render过程中是否发生变化;

React具体如何比较两棵树的差异:

  • (1)、当根节点是不同类型时:

    • 根节点类型变化,React会认为新的树和旧的树完全不同,不会再继续比较其他属性和子节点,而是把整棵树拆掉重建(包括虚拟DOM树和真实DOM树);
    • 在旧的虚拟DOM树被拆除的过程中,旧的DOM元素类型的节点会被销毁,旧的React组件实例的componentWillUnmount会被调用;
    • 在重建的过程中,新的DOM元素会被插入新的虚拟DOM树中,新的组件实例的componentWillMount和componentDidMount方法会被调用。
    • 重建后的新的虚拟DOM树又会被整体更新到真实DOM树中
  • (2)、当根节点是相同的DOM元素类型时:

    • 如果两个根节点是相同类型的DOM元素,React会保留根节点,而比较根节点的属性,然后只更新那些变化了的属性(只更新虚拟DOM树和真实DOM树中对应节点的变化属性)。
  • (3)、当根节点是相同的组件类型时:

    • 如果两个根节点是相同类型的组件,对应的组件实例不会销毁,只是会执行更新操作,同步变化的属性到虚拟DOM树上,这一过程组件实例的componentWillReceiveProps()和componentWillUpdate()会被调用,需要再组件更新并且render方法执行完成后,根据render返回的虚拟DOM结构决定如何更新真实DOM树。

    比较完根节点后,React会以同样的原则继续递归比较子节点,直到比较完两棵树上的所有节点,计算得到最终的差异,再更新到真实DOM中。

4. 性能优化

(1)、使用生产环境版本的库

开发环境使用开发环境版本的库; 生产环境使用生产环境版本的库;(process.env.NODE_ENV)

(2)、避免不必要的组件渲染

  • 在一些情况下,组件是没有必要重新调用render方法的。例如,父组件的每一次render调用都会触发子组件的componentWillReceiveProps的调用,进而子组件的render方法的调用;如果这时传递给子组件的props没有发生变化,那么子组件的render方法就没必要执行;
  • React组件生命周期方法中的shouldComponentUpdate方法,如果这个方法返回false,组件此次的更新就会停止,后续的componentWillUpdate、render方法将不会执行。利用这个方法,结合业务逻辑判断是返回true还是false,来避免组件不必要的渲染;
    -其中比较nextProps与当前props的差异,最精确的方式是遍历对象的每一层级的属性分别比较(也就是深比较);
  • 当对象层级很深时,深比较对性能影响也较大。一种折中的方法是,只比较对象的第一层级属性(也就是浅比较);
  • React提供了一个PureComponent组件,这个组件会使用浅比较来比较新旧props和state。

(3)、使用key

React会根据key索引元素,在render前后,拥有相同key值的元素是同一个元素。key的使用可以减少DOM操作,提高DOM更新效率。

5.性能检测工具

  • React Developer Tools fot Chrome

    Chrome插件

  • Chrome Performance Tab
  • why-did-you-update

二、高阶组件

高阶组件主要用来实现组件逻辑的抽象和复用,在很多第三方库中都被使用到。

1.基本概念

  • 高阶函数:在JavaScript中,高阶函数是以函数为参数,并且返回值也是函数的函数。
  • 高阶组件:简称HOC,高阶组件接收React组件作为参数,并且返回一个新的React组件。(高阶组件的本质也是一个函数);
  • 高阶组件的函数形式如下:
    const EnhancedComponent = higherOrderComponent(WrappedComponent);
  • 高阶组件的这种实现方式本质上是装饰者设计模式;

2.使用场景

高阶组件主要有四种使用场景:

(1)、操纵props

在被包装组件接收props前,高阶组件可以先拦截到props,对props执行增加、删除或修改的操作,然后将处理后的props再传递给被包装组件。例如:

import React,  Component  from 'react'
function withPersistentData(WrappedComponent) 
   return class extends Component 
       componentWillMount() 
            let data = localStorage.getItem('data');
            this.setState(data);
       
       render() 
            // 通过...this.props把传递给当前组件的属性继续传递给被包装的组件
            return  <WrappedComponent data=this.state.data  ...this.props />
       
   

class MyComponent extends Component 
    render() 
         return <div>this.props.data</div>
    

const MyComponentWithPersistentData = withPersistentData(MyComponent)

(2)、通过ref访问组件实例

高阶组件可以通过ref获取被包装组件实例的引用,然后高阶组件就具备了直接操作被包装组件的属性或方法的能力。例如:

function withRef(wrappedComponent) 
    return class extends React.Component 
        constructor(props) 
            super(props);
            this.someMethod = this.someMethod.bind(this);
        
        someMethod()  
            this.wrappedInstance.someMethodInWrappedComponent();
        
        render() 
             // 为被包装组件添加ref属性,从而获取该组件实例并赋值给this.wrappedInstance
             return <WrappedComponent ref = (instance) => this.wrappedInstance ...this.props />
        
    

(3).组件状态提升

无状态组件更容易被复用。高阶组件可以通过将被包装组件的状态及相应的状态处理方法提升到高阶组件自身内部,实现被包装组件的无状态化。

(4).用其他元素包装组件

可以在高阶组件渲染被包装组件是添加额外的元素,这种情况通常用于为WrappedComponent增加布局或修改样式;例如:

function withRedBackground(WrappedComponent) 
   return class extends React.Component 
       render() 
            return (
                <div style=backgroundColor: 'red'>
                    <WrappedComponent ...this.props />
                </div>
            )
       
   

3.参数传递

  • 高阶组件的参数并非只能是一个组件,还可以接受其他参数;
  • 通用的高阶组件传参形式:
    HOC (…params) (WrappedComponent)
    例如:
import React,  Component  from 'react'
function withPersistentData = (key) => (WrappedComponent) => 
     return class extends Component 
         componentWillMount() 
             let data = localStorage.getItem(key);
             this.setState(data);
         
         render() 
             // 通过... this.props 把传递给当前组件的属性继续传递给被包装的组件
             return <WrappedComponent data=this.state.data ...this.props />
         
     

class MyComponent extends Component 
     render() 
         return <div>this.props.data</div>
     

// 获取key='data'的数据
const MyComponent1WithPersistentData = withPersistentData('data')(MyComponent);
const MyComponent2WithPersistentData = withPersistentData('name')(MyComponent);

4.继承方式实现高阶组件

  • 高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,这种实现高阶组件的方式称为“属性代理”。
  • 除了属性代理,还可以通过继承方式实现高阶组件,通过继承被包装组件实现逻辑的复用。

例如:当用户处理登录状态时,允许组件渲染,否则渲染一个空组件:

function withAuth(WrappedComponent) 
    return class extends WrappedComponent 
        render() 
            if (this.props.loggedIn)
                 return super.render();
             else 
                 return null;
            
        
    

5.使用高阶组件的注意事项

  • 为高阶组件的显示名称做自定义处理
  • 这样做的目的是为了在开发和调试阶段更好地区别包装了不同组件的高阶组件。
  • 常用的处理方法是:把被包装组件的显示名称也包到高阶组件的显示名称中。例如:
function withPersistentData(WrappedComponent) 
   return class extends Component 
       // 结合被包装组件的名称,自定义高阶组件的名称
       static displayName = `HOC($getDisplayName(WrappedComponent))`;
       render() 
          // ....
       
   


function getDisplayName(WrappedComponent) 
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';

  • 不要在组件的render方法中、尽量不要在组件的生命周期方法中使用高阶组件
  • 因为调用高阶组件,每次都会返回一个新的组件,于是每次render,前一次高阶组件创建的组件都会被卸载,然后重新挂载本次创建的新组件,既影响效率又丢失了组件及其子组件的状态。例如:
render() 
    // 每次render, enhance都会创建一个新的组件,尽管被包装的可能组件没有变化
    const EnhancedComponent = enhance(MyComponent);
    // 因为是新组件,所以会经历旧组件的卸载和新组件的重新挂载
    return <EnhancedComponent />;

  • 高阶组件最适合使用在组件定义的外部。
  • 高阶组件返回的新组件不会包含被包装组件的静态方法

因此,如果需要使用被包装组件的静态方法,那么必须手动复制这些静态方法。

// WrappedComponent组件定义了一个静态方法staticMethod
WrappedComponent.staticMethod = function() 
   // ....


function withHOC(WrappedComponent) 
     class Enhance extends React.Component 
         // ....
     
     // 手动复制静态方法到Enhance上
     Enhance.staticMethod = WrappedComponent.staticMethod;
     return Enhance;

  • Refs不会被传递给被包装组件

如果在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部包装的组件。
如果希望获取被包装组件的引用,那么可以自定义一个属性,属性的值是一个函数,传递给被包装组件的ref。例如:

function FocusInput(  inputRef, ...rest) 
    // 使用高阶组件传递的inputRef作为ref的值
    return  <input ref=inputRef ...rest  />;

// enhance是一个高阶组件
const EnhanceInput = enhance(FocusInput);
// 在一个组件的render方法中,自定义属性inputRef代替ref,
// 保证inputRef可以传递给被包装组件
return (<EnhanceInput 
    inputRef=(input) => 
         this.input = input
    
 />)
// 组件内,让FocusInput自动获取焦点
this.input.focus();
  • 高阶组件与父组件的区别。

高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。

以上是关于React笔记3的主要内容,如果未能解决你的问题,请参考以下文章

React 学习笔记

初学react笔记

React学习笔记1

React.js学习笔记之事件系统

一个用react+nodejs实现的笔记本小应用

React-router学习笔记