Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks Immutable #29

Open
Vibing opened this issue Aug 13, 2020 · 0 comments
Open

React Hooks Immutable #29

Vibing opened this issue Aug 13, 2020 · 0 comments

Comments

@Vibing
Copy link
Owner

Vibing commented Aug 13, 2020

默认渲染行为

React 的渲染主要分两种:首次渲染和重渲染。
首次渲染就是第一次渲染,这是无法避免的就不讨论了,重复渲染是指由于状态改变或 props 改变等原因造成的渲染。

React 默认的渲染特性:当父组件渲染时,会递归渲染下面所有的子组件(让人诟病的特性)

如下:

const App = () => {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('Tom');

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  return (
    <>
      <h3>count: {count}</h3>
      <Child name={name} />
    </>
  );
};

function Child(props: { name: string }) {
  console.log('child render', props.name);
  return <div>name: {props.name}</div>;
}

React 对这种行为可以说是不负责任,不管你三七二十八,只要父组件渲染,所有它下面的子组件都会渲染,这种方式简单而粗暴。

既然如此,那么这块只能我们这些负责任的开发者去优化了。

浅比较优化

React 把优化问题抛给开发者,它给我们提供了三个用于性能优化的 API。

React.PurComponent

用于 class 写法的组件,它会对传入组件的 props 进行浅比较,如果比较的结果相同,则不会去渲染组件

React.memo

与 React.PurComponent 一样,用于函数组件

浅比较

在 js 中,数据主要分两种:

  • 基本类型(字符串、数字、undefined...),
  • 引用类型,即对象(JSON、Array、Function、Regex...)

基本类型的数据属于原始值(primitive value),它直接存储在栈内存中,
引用类型的数据存在于堆内存中,通过存在栈内存中的指针来调用它

  • 原始数据是 immutable 的,引用类型(object)一般是 mutable 的
  • 原始数据比较直接通过值比较,而 object 则通过引用比较
var a = 1;
var b = 1;
a === b // true

var a = {};
var b = {};
a === b // false 比较的是引用 所以不相等 

对于对象,不仅有引用比较,也有深比较和浅比较

const a = {num: 1};
const b = {num: 1};
a === b // false 引用不相等
a.num === b.num // true 对象的一级相等

const a = { p: {num: 1}, t: 2 }
const b = { p: {num: 1}, t: 2 }
a === b // false 引用不相等
shallowEqual(a, b) // false a.p === b.p 引用不等 浅比较为false
deepEqual(a, b) // true 深比较相等

如果对象的一级属性中存在引用比较,则不相等。
对象的深比较跳过了引用比较,仅仅是比较相同层级下的属性值。

  • 由于对象存在于堆内存中 通过栈内存中的引用来调用,所以如果对象的值不变,对象的引用变了,就会导致 React 组件缓存失败,造成组件无必要的渲染,进而会造成性能问题
  • 如果对象值变,引用不变,React 则不触发渲染,导致界面与数据不一致

React.memo 可以让 props 在变化时,该组件才会发生有意义的重新渲染
我们将子组件用 React.memo 包起来,只要 props 不变,在父组件更新时,子组件也不会重渲染

const Child = React.memo((props: { name: string }) => {
  console.log('child render', props.name);
  return <div>name: {props.name}</div>;
});

看起来问题解决了,React.memo 将前后的 props 进行浅比较,这基本能解决大多数问题。但如果 props 中含有对象数据,在浅比较时比较的是引用,这种方式就行不通了,好在 React.memo 提供了第二个参数,可以自定义比较前后的 props

浅比较行不通,那么深比较呢

const Child = React.memo((props) => {
  return <div>{props.name}</div>;
}, (prev, next) => {
    // 深比较
  return deepEqual(prev,next)
});

这样的确可以达到效果,但如果是比较复杂的对象,就会存在较大的性能问题,甚至直接挂掉,因此不建议使用深比较去进行性能优化

还有一种方式:如果能保证对象的值相等,再保证对象的引用相等,就可以保证子组件在 props 没变的情况下不会渲染。

React.useRef 的返回值是固定的常量,我们可以使用它来做为对象的引用

const Child = React.memo(({ obj }) => {
  console.log('child render', obj, obj.name);
  return <div>name: {obj.name}</div>;
});

const App = () => {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  const obj = React.useRef({
    name: 'Jack'
  });

  return (
    <>
      <h3>count: {count}</h3>
      <Child obj={obj.current} />
    </>
  );
};

这样还是存在一个严重的问题:如果 name 改变了,但 obj 没有变,导致子组件不会重新渲染,数据与UI界面不一致。 看来 useRef 只能用于常量

那我们只要保证 name 不变的时候 obj 和上次一样, name 才让子组件更新就可以了。没错,就是 useMemo。

useMemo 的特性就是保证依项不变时,对应的对象也不会变,只有依赖项变化时,对应的对象才会变

const App = () => {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('');

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  // 只有当 name 变化时,obj才会变化
  const obj = React.useMemo(
    () => ({
      name
    }),
    [name]
  );

  return (
    <>
      <h3>count: {count}</h3>
      <input
        type="text"
        value={name}
        onChange={e => {
          setName(e.target.value);
        }}
      />
      <Child obj={obj} />
    </>
  );
};

immutable

上面的方式算是一种解决方案,现在我们来看看其他的东西。

我们在 class 组件中更新状态,只需要一个 setState 就行,不管你传入什么 state,组件都会刷新

// class 的 setState 传什么都会更新组件
this.setState({
    name: 'Jack'
})

但在 hooks 里, 如果前后两次的引用相等,就不会更新组件

const [state, setState] = useState({})

// 同一个引用,不会更新
setState(s => {
    s.name = 'Tom'
    return s
})

// 生成新应用,可以更新
setState(s => {
    const newState = {
        ...s,
        name: 'Tom'
    };
    return newState;
})

在 hooks 中,如果你想修改状态对象,必须保证前后修改的对象引用不等。这就要求我们不能直接更新老的 state,而是要保持老的 state 不变,去生成一个新的 state,也就是 immutable 方式。
老的 state 保持不变,也就意味着该 state 应该是 immutable obj

const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

state[0].name = 'Alen'

// hooks中需要如下写法
const newState = [
    {
        ...state[0],
        name: 'Alen'
    },
    ...state
]

immutable 的写法过于繁琐,这不是我们想要的

其实,综上来说,我们的需求很简单:

  • 需要改变状态
  • 改变状态后和之前的状态引用不相等

第一个冲上脑门的答案就是:先深拷贝,然后做 mutable 修改就可以了

const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

const newState = deepClone(state);
newState[0].name = 'Alen'

深拷贝有两个缺点:

  • 拷贝的性能问题
  • 对于循环引用的处理
    虽然市面上有些库支持高性能的深拷贝,但会对参考物对等(reference equality )造成了破坏
const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

const newState = lodash.cloneDeep(state)

state === newState // false
state[0] === newState[0] // false

可以发现,所有对象的结构都被破坏,在 React 中使用这种方式,即使对象属性没有任何变化,也会导致没有意义的重新渲染,仍会导致比较严重的性能问题

深拷贝是非常糟糕的

这么看来,更新状态还要其他的需求。
我们将 oldState 视为一个属性树,改变其中某节点时,能返回一个新的对象

  • 当前节点及其组件节点的引用在新老 state 中不相等,这样能保证UI组件即时刷新
  • 非当前节点及其祖先节点的引用在新老 state 中保持引用相等,这能保证状态不变时组件不重渲染

遗憾的是,JavaScript 并没有内置这种对 immutable 数据的支持,更不用说对 immutable 数据更新了,但可以使用一些三方库来解决这个问题,比如:immer 和 immutablejs

import Immutable from 'immutable'

var state = Immutable.Map({
    a: 1,
    b: 2
})

var newState = state.set('a', 3) 

Immutable 数据使用结构共享的方式,只更新修改了子节点的引用,不会去修改未更改的子节点引用,达到我们想要的需求

总结

  • 默认情况下,React 组件更新会触发其下面所有的子组件递归渲染
  • 通过 React.memo 的浅比较 props,来保证 props 不变的情况下,组件不会刷新
  • 浅比较只对基本类型(primitive)生效,对于对象无效,即使对象中的值不变,也会引发重渲染
  • 通过使用 useRef 和 useMemo 来缓存对象,在对象值没变时不用引发重渲染
  • 不能通过深拷贝的方式修改 state,不仅导致性能问题,还会引起即使 state 内的值没有变化,但引用发生变化,从而造成无意义渲染,引发严重性能问题
  • 这要求我们使用 immutable 的方式更新 state,来保证引用和缓存
  • 可以通过第三方库:immer、immutablejs 来简化 immutable 的 state 更新写法
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant