阅读 462
immutable data 是如何优化 React 性能的

immutable data 是如何优化 React 性能的

今天我们解决以下几个问题,什么是immutable datamutable data带来了哪些问题,immutable data优化了哪些性能?

mutable data 数据的可变性

数据的可变性用一段代码就可以描述清楚

const a = [{ todo: 'Learn js'}, {todo: 'Learn react'}];
const b = [...a];
b[1].todo = 'Learn vue';
console.log(a[1].todo); //Learn vue
复制代码

其实可以一眼看出这是浅copy导致的问题。内层的对象指向堆内存地址相同,所以修改b数组中的对象,a数组也会发生变化。平时大伙在项目中操作比较复杂的数据结构时,都习惯性deepCopy,否则就会出现一些不易察觉的bug。

那我们只要遇到需要操作多层数据结构的情况使用deepCopy不就解决了么。
然而事实是随着数据层数的增加,deepCopy性能的消耗还是十分明显的,并且在React中deepCopy会对渲染造成很大的开销,这与react的渲染机制有关。

immutable数据优化了哪些性能

首先我们要看一下React 是如何渲染的。

React 渲染机制解析

graph LR
setState或者props改变 --> shouldComponentUpdate --true --> 递归render 
递归render --> componentDidUpdate 

在React中,render函数返回虚拟dom树,并经过Diff算法计算出与上次虚拟dom的区别,针对差异的部分做更新,渲染出真实dom。

递归render的过程是性能消耗的大头,如果shouldComponentUpdate返回false,更新的过程就会被打住,所以我们要好好的利用这个shouldComponentUpdate。

shouldComponentUpdate

这是一个组件的子树。每个节点中,SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停,红色代表shouldComponentUpdate返回true,进行render,绿色代表返回false,不进行render。

image.png

c1是红色节点,shouldComponentUpdate返回 true,进入diff算法比对新旧VDom树,如果新旧VDom树中节点类型不同,则全部替换,包括下面子组件,图中展示的是节点类型相同的情况,则递归子组件

//什么是节点类型不同
<A>
  <C/>
</A>
// A与B是不同节点类型
<B>
  <C/>
</B>
React会直接删掉A节点(包括它所有的子节点),然后新建一个B节点插入。
复制代码

节点 C2 的 shouldComponentUpdate 返回了 false,React 因而不会调用 C2 的 render,也因此 C4 和 C5 的 shouldComponentUpdate 不会被调用到。

C3,shouldComponentUpdate 返回了 true,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate 返回了 true,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。

最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。

显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。

类组件React.PureComponent与函数组件memo

通过shouldComponentUpdate可以避免不必要的渲染过程,从而达到性能上的优化。但是如果需要我们挨个对比props和state中的每个属性的话就太麻烦了,React提供了两种方式自动帮我们完成shouldComponentUpdate中的工作,类组件只要继承React.PureComponent就可以,函数组件提供了memo方法。

//三种方式
class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}


class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}


const CounterButton = props => {
  const [count, setCount] = useState(1);

  return (
    <button color={props.color} onClick={() => setCount(count => count + 1)}>
      Count: {count}
    </button>
  );
};

export default React.memo(CounterButton);

复制代码

注意:无论是React.PureComponent还是memo都只会进行浅比较,一旦属性值为引用类型,浅比较会失效,原因为上面解释的数据可变性。如果使用deepCopy,那所有shouldComponentUpdate都会返回true,都进入了diff运算,就没有了意义。

那有没有一种方式,使用浅比较就可以得出哪部分是改变的数据节点呢?

immutable 数据结构

我们还是通过一小段代码来认识什么是immutable数据,这里使用的是Immer这个库。

  import produce from 'immer';
  
  const a = [{ todo: 'Learn js' }, { todo: 'Learn react' }];
  const b = produce(a, draftState => {
    draftState[1].todo = 'Learn vue';
  });

  console.log(a === b); //false
  console.log(a[0] === b[0]); //true
  console.log(a[1] === b[1]); //false
  console.log(a[1].todo === b[1].todo); //false

复制代码

这里可以看到未改变的引用类型内存地址未发生改变,保证了旧节点的可用且不变,而改变了的节点,它和与它相关的所有上级节点都更新。如图所示:

5518628-61c587b3466654e9.webp

这样就避免了深拷贝带来的极大的性能开销问题,并且更新后返回了一个全新的引用,即使是浅比对也能感知到哪一部分数据需要更新。

immer应用示例

const [state, setState] = useState({
    id: 14,
    email: "stewie@familyguy.com",
    profile: {
      name: "Stewie Griffin",
      bio: "You know, the... the novel you've been working on",
      age:1
    }
  });

function changeBio(newBio) {
    setState(current => ({
      ...current,
      profile: {
        ...current.profile,
        bio: newBio
      }
    }));
  }



//使用 immer
import { useImmer } from 'use-immer';

const [state, setState] = useImmer({
    id: 14,
    email: "stewie@familyguy.com",
    profile: {
      name: "Stewie Griffin",
      bio: "You know, the... the novel you've been working on",
      age:1
    }
 });

function changeBio(newBio) {
   setState(draft => {
      draft.profile.bio = newBio;
    });
  }
复制代码

减少了解构语法是不是清爽了很多,当然随着数据结构进一步复杂,immer优势也会进一步体现。

感谢大家的阅读。

文章分类
前端
文章标签