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 Render 相关 #45

Closed
yuxino opened this issue Feb 18, 2018 · 3 comments
Closed

React Render 相关 #45

yuxino opened this issue Feb 18, 2018 · 3 comments
Labels

Comments

@yuxino
Copy link
Owner

yuxino commented Feb 18, 2018

The render() method is required.

render函数是必须要有的。

When called, it should examine this.props and this.state and return one of the following types:

当render函数被调用的时候。函数应该检查this.propsthis.state然后返回下列类型中的其中一种:

React elements. Typically created via JSX. An element can either be a representation of a native DOM component (

), or a user-defined composite component ().

React elements。典型的创建方式是通过JSX。一个element可以被代表称一个原生的组件(例如

),或者是用户自定义个组件(例如)。

String and numbers. These are rendered as text nodes in the DOM.

String或者numbers。通常是在渲染text节点在DOM里面的时候。

Portals. Created with ReactDOM.createPortal.

Portals。通过ReactDOM.createPortal创建的元素。 PS: 这个还蛮有用的。看这个例子

null. Renders nothing.

null。什么都不渲染的时候。

Booleans. Render nothing. (Mostly exists to support return test && pattern, where test is boolean.)

Booleans。什么都不渲染的时候。(这个情况大多会发生在return test && 这种写法时候。)

When returning null or false, ReactDOM.findDOMNode(this) will return null.

不管是null或则和false。ReactDOM.findDOMNode(this)函数都会返回null。

The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead. Keeping render() pure makes components easier to think about.

render函数应该是(pure)的,也就是说我们不应该在render函数里面修改组建的state。render函数应该每次执行的时候都返回相同的结果。它不直接与浏览器交互。如果你想与浏览器交互你应该在componentDidMount或者其他的生命周期里面进行。保持render函数的纯度可以让组件更容易理解。

Note
render() will not be invoked if shouldComponentUpdate() returns false.

非常需要注意的一点是。当shouldComponentUpdate方法返回false的时候render函数不会被执行。

Fragments

React支持片段。片段指的是没有被容器包裹的元素。通常直接写的话会报错。但是这样写就不会了

render() {
  return [
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}

但是要注意需要添加key噢 不然会有警告。

Since React 16.2.0, the same can also be accomplished using fragments, which don’t require keys for static items:

不过 React16.2.0以后可以不写key了。但是你需要改变一下写法.

render() {
  return (
    <React.Fragment>
      <li>First item</li>
      <li>Second item</li>
      <li>Third item</li>
    </React.Fragment>
  );
}

参考资料

TL;DR

实际上我写这篇的目的是为了理解Redux为何会频繁触发component。Reselect是如何解决这个问题的。后面发现触发更新的其实是react-redux。但是很显然上面的官方文档中我没有找到特别有用能帮助到我的东西。为了让大家所谓的性能问题。我这里写个例子。

组件部分

App.js

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Demo from "./demo"

export default Demo;

demo/index.js

import {Provider} from 'react-redux';
import React from 'react';
import store from './store';
import Posts from './Posts';
import Counter from './Counter';
import './index.css';

const initial = store.getState();

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Reselect Redux</h1>
        <Posts />
        <Counter />
      </div>
    );
  }
}

export default () => <Provider store={store}><App /></Provider>;

demo/Post.js

import React from 'react';
import {connect} from 'react-redux';

let count = 0;

class Posts extends React.Component {
  render() {
    console.log(`Posts render ${++count}`);
    return (
      <div>
        <h3>Posts</h3>
        <ul>
          {this.props.posts.map(x =>
            <li key={x.id}>
              {`${x.title} - ${x.user.first} ${x.user.last}`}
            </li>
          )}
        </ul>
      </div>
    );
  }
}

const mapState = (state) => {
  const posts = state.postsById
  const users = state.usersById
  const listing = state.postListing
  return {
    posts: listing.map(id => {
      const post = posts[id];
      return {...post, user: users[post.author]}
    })
  }
};

export default connect(mapState)(Posts);

demo/Counter.js

import React from 'react';
import {connect} from 'react-redux';

class Counter extends React.Component {
  componentDidMount() {
    setInterval(() => {
      this.props.increment();
    }, 500);
  }
  render() {
    return (
      <div>
        <h3>Count: {this.props.count}</h3>
      </div>
    );
  }
}

const mapState = (state) => ({count: state.count});
const mapDispatch = {
  increment: () => ({type: 'INCREMENT'}),
};


export default connect(mapState, mapDispatch)(Counter);

Redux部分

demo/store.js

import {createStore} from 'redux';
import reducer from './reducers';
import {getPosts} from './fixtures';

const store = createStore(reducer);

store.dispatch({type: 'RECEIVE_DATA', payload: getPosts()});

export default store;

demo/reducers.js

import {combineReducers} from 'redux';

export const usersByIdReducer = (state = {}, action) => {
  switch (action.type) {
    case 'RECEIVE_DATA':
      const newState = {...state};
      action.payload.users.forEach((user) => {
        newState[user.id] = user;
      });
      return newState;
    default: return state
  }
};

export const postsByIdReducer = (state = {}, action) => {
  switch (action.type) {
    case 'RECEIVE_DATA':
      const newState = {...state};
      action.payload.posts.forEach((post) => {
        newState[post.id] = post;
      });
      return newState;
    default: return state
  }
};

export const postListingReducer = (state = [], action) => {
  switch (action.type) {
    case 'RECEIVE_DATA':
      return action.payload.posts.map(x => x.id);
    default: return state
  }
};

export const counterReducer = (state = 1, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default: return state;
  }
}

export default combineReducers({
  usersById: usersByIdReducer,
  postsById: postsByIdReducer,
  postListing: postListingReducer,
  count: counterReducer,
});

这个demo是视频里面的小哥提供的。我把它重现了出来。复制粘贴一下就好了。因为我不想单独开个仓库存代码所以贴在上面。如果你喜欢视频教程你可以直接去看小哥哥的视频。里面讲的很清楚。

这个代码很简单就是每500毫秒在Counter组件里面调用一下increment。触发Redux的状态变化。让counter状态不停的加1。另外在Post的render函数里面写了个console.log来确认是Post函数被调用了。

然后运行的结果是。明明Post里面没有用到counter变量却被更新了。打开控制台可以看见。不停的在输出。换句话说不停的在render。这很显然如果比较复杂的组件会带来性能问题。

运行结果

image

Post.js 内心OS: MMP 为什么我在疯狂更新,管我卵事。

那好啊。为什么会这样咧。就是我要探究的问题。到底为什么render函数触发更新了,又为什么用了Reselect修改后的版本就不会触发更新了。Reselect对Redux做了什么,期间有什么py交易。以及Render函数运行的条件是我接下来关心的重点。

Render 函数运行的条件

首先最重要的是这个。在官方文档中我们知道当shouldComponentUpdate方法返回false的时候render函数不会被执行。那么是不是因为这个呢。我粗略的扫了一下redux和reselect的源码,源码里面都没有用到shouldComponentUpdate来控制组件的更新。于是看了下react-redux。ok很快就找了shouldComponentUpdate相关的代码。很明显react-redux通过shouldComponentUpdate做了什么,那我们粗略的看一下源码吧。

PS: 吃饭的时候想了一下傻逼了为什么去看redux的源码。redux本身和react无关。

首先看项目结构。我只看src目录这里用了学长写的工具(可能他已经忘掉了这个工具了)。一开始用的是cmder自带的那个tree,可是我发现体验极差。投奔学长。

- components
    connectAdvanced.js 
    Provider.js

- connect
    connect.js
    mapDispatchToProps.js
    mapStateToProps.js
    mergeProps.js
    selectorFactory.js
    verifySubselectors.js
    wrapMapToProps.js
  index.js

- utils
    PropTypes.js
    shallowEqual.js
    Subscription.js
    verifyPlainObject.js
    warning.js
    wrapActionCreators.js

index.js

东西很少。三个目录compoent,connect,utils。component下面放的是组件,connect是核心的逻辑,utils是会用到的工具类。index.js统一暴露所有主要的接口。

先看看index.js

import Provider, { createProvider } from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import connect from './connect/connect'

export { Provider, createProvider, connectAdvanced, connect }

Ok 那就先看Provider吧。平时我们都是用包裹组件的。来看看里面写了什么。

import { Component, Children } from 'react'
import PropTypes from 'prop-types'
import { storeShape, subscriptionShape } from '../utils/PropTypes'
import warning from '../utils/warning'

let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
  if (didWarnAboutReceivingStore) {
    return  
  }
  didWarnAboutReceivingStore = true

  warning(
    '<Provider> does not support changing `store` on the fly. ' +
    'It is most likely that you see this error because you updated to ' +
    'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
    'automatically. See https://github.com/reactjs/react-redux/releases/' +
    'tag/v2.0.0 for the migration instructions.'
  )
}

export function createProvider(storeKey = 'store', subKey) {
    const subscriptionKey = subKey || `${storeKey}Subscription`

    class Provider extends Component {
        getChildContext() {
          return { [storeKey]: this[storeKey], [subscriptionKey]: null }
        }

        constructor(props, context) {
          super(props, context)
          this[storeKey] = props.store;
        }

        render() {
          return Children.only(this.props.children)
        }
    }

    if (process.env.NODE_ENV !== 'production') {
      Provider.prototype.componentWillReceiveProps = function (nextProps) {
        if (this[storeKey] !== nextProps.store) {
          warnAboutReceivingStore()
        }
      }
    }

    Provider.propTypes = {
        store: storeShape.isRequired,
        children: PropTypes.element.isRequired,
    }
    Provider.childContextTypes = {
        [storeKey]: storeShape.isRequired,
        [subscriptionKey]: subscriptionShape,
    }

    return Provider
}

export default createProvider()

很快我们注意到了Provider里面用了context api。prop接收stroe和children两个参数。children就是根元素啦。就是平时用的this.props.children。有一个warnAboutReceivingStore。会在改变store的时候在非生产模式的时候提供警告。Children.only方法是验证是否只有一个child用的。如果不是会报错。如果是的话就会渲染。Provider是createProvider默认参数的实现。

那createProvider也不用看了。是可以给用户自定义的Provider(并没有用过)。createProvider有两个参数storeKey和subKey。不过第二个参数文档里面没有讲怎么用 算了 当作不知道。

最主要的部分应该是这两个部分。

getChildContext() {
    return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}

constructor(props, context) {
    super(props, context)
    this[storeKey] = props.store;
}

现在只知道Provider组件把props.store记录到了this[storeKey]里面。还有getChildContext里面访问[store]的话可以读取到props.store。另外一个subscriptionKey总是为空。

这些东西接下来应该会用到。所以看第三个connectAdvanced吧。connectAdvanced也是组件。更加的复杂。其实是注释好多 因为好长 直接戳进去看吧

嘛 首先看下依赖。hoist-non-react-statics和invariant都是我不知道的东西。好呀既然不知道那就查查看。一下就找到了这个是hoist-non-react-statis。嗯他叫我这个傻逼去看官方的解释。好吧我没有好好看文档。那就看看。这个是做什么的。

简单的说就是有时候给组件加静态方法超爽der。但是如果你用了hoc的话静态方法会丢失。虽然你可以手动将hoc的静态方法指向原组件的静态方法。但是那样太麻烦了。所以你可以用hoist-non-react-statis解决这个问题。简单而又足够高效。

好hoist-non-react-statics知道了那就看看invariant。手动戳进invariant的源码。发现很短有一行这样的注释。

/**
 * Use invariant() to assert state which your program assumes to be true.
 *
 * Provide sprintf-style format (only %s is supported) and arguments
 * to provide information about what broke and what you were
 * expecting.
 *
 * The invariant message will be stripped in production, but the invariant
 * will remain to ensure logic does not differ in production.
 */

嗯大概是用来处理抛出异常时候的一个函数。用法看起来也比较像所以不关心这个东西。看到别人有个copy版本。额 他的描述更清晰了。

A way to provide descriptive errors in development but generic errors in production.

剩下的两个工具类其中PropTypes没什么好说的提取了可以复用的PropType。Subscription的话比较重要。用来订阅Redux的状态的。因为我觉得这个比较重要点开来看看好了。

// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendants

Emmmm . 英语废表示完全不理解。那就直接看代码吧。嗯 就是普通的订阅者模式。

通过createListenerCollection创建订阅者。createListenerCollection提供了一个闭包函数。有notify,subscribe之类常用的功能。current和next都是数组。每次notify的时候会更新一下current并执行队列里面的回调。OK 继续回到connectAdvanced。

很好往下看发现根本无法理解嘛。那我们就看Connect吧。看看Connect是怎么调用connectAdvanced的。Connect暴露的connect也就是我们平时用的connect方法。

嗯一下子就找到了。connectAdvanced是作为一个Hoc来使用的。也就是说只要我们用了connect。react-redux都会将原组件封装成hoc再暴露出来。这样就使得组件有能力访问redux。

connect的文档可以读一下。虽然平时我们只要state和disptch。

抛开这些东西现在。我们知道reselect是把selector传给第二个参数的,第二个参数是mapDispatchToProps。那么一切都将围绕着它。我们只关心和这个参数挂钩的地方就好了。

和这个挂钩的只有initMapDispatchToProps。

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) => {
    throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`)
  }
}

const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')

match函数的作用是迭代工厂方法,最终返回结果。Emmmmm 目的不明。暂时不影响阅读。接下去看有一个

return connectHOC(selectorFactory, {
  // used in error messages
  methodName: 'connect',

    // used to compute Connect's displayName from the wrapped component's displayName.
  getDisplayName: name => `Connect(${name})`,

  // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
  shouldHandleStateChanges: Boolean(mapStateToProps),

  // passed through to selectorFactory
  initMapStateToProps,
  initMapDispatchToProps,
  initMergeProps,
  pure,
  areStatesEqual,
  areOwnPropsEqual,
  areStatePropsEqual,
  areMergedPropsEqual,

  // any extra options args can override defaults of connect or connectAdvanced
  ...extraOptions
})

戳进去看就到了connectAdvanced。反正目的是建一个HOC组件Connect。然后这个Connect组件实现了shouldComponentUpdate方法。

shouldComponentUpdate() {
  return this.selector.shouldComponentUpdate
}

shouldComponentUpdate方法围绕着selector依赖着selector的shouldComponentUpdate方法。selector的shouldComponentUpdate长这样子。

function makeSelectorStateful(sourceSelector, store) {
  // wrap the selector in an object that tracks its results between runs.
  const selector = {
    run: function runComponentSelector(props) {
      try {
        const nextProps = sourceSelector(store.getState(), props)
        if (nextProps !== selector.props || selector.error) {
          selector.shouldComponentUpdate = true 
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

  return selector
}

最关键部分在那个if判断里面nextProps !== selector.props || selector.error。如果上次传的prop不一致就会把shouldComponentUpdate设为true。如果一致的话就会是false。虽然没认真找但应该有这么一段。

所以总结一下就是react-redux使用高阶组件Connect来链接Redux。而Connect组件实现了shouldComponentUpdate方法,目的应该是想做优化的。使用Connect链接的组件在state更新的时候都会触发mapState方法。但是因为mapState方法每次返回的对象不一致导致props不一致。而组件被动的更新了。

const mapState = (state) => {
  const posts = state.postsById
  const users = state.usersById
  const listing = state.postListing
  return {
    posts: listing.map(id => {
      const post = posts[id];
      return {...post, user: users[post.author]}
    })
  }
};

使用reselect以后,reselect会在selector返回结果不一致的情况下调用tranfrom函数。但是如果一致的话会返回上次计算的结果。所以组件不会被更新。

@glitchboyl
Copy link

Post.js 中的state是和Redux store.getState()已经connect关系挂钩了。所以当store发生变化, Post组件中的state也会发生变化。(不涉及原理, 个人观点.)

React的底层 Virtual Dom 机制就是当组件状态的state发生变化的时候就会调用组件的render(), 但是这时候还没有进行 DOM 的重绘, 真正进行重绘是在 React 的 Virtual Dom diff算法比较后。

深度剖析:如何实现一个 Virtual DOM 算法
看知乎看到的一个, 感觉写的很容易懂, 上班闲的时候也有试着做一个小demo来研究玩。

shouldComponentUpdate() Will Short-Circuit An Entire Subtree Of Components In ReactJS
然后是这个, 顶级组件中的shouldComponentUpdate()会对子组件重新计算它们的 Virtual Dom
节点造成影响。就是俗称的 短路(short circuit)

最后我的想法是, shouldComponentUpdate()阻止调用render()肯定是对于 React 应用来说是一个非常好的优化方式, 然后还有别的解决方案, 例如 pureComponent, 它好像是使用浅比较判断组件是否需要重绘。
我很懒, 优化这类事情我真的很少做......

你的文章写得超级棒, 给你八个大拇指。 👍👍👍👍👍👍👍👍
我永远也写不出来你这样的文章, 说这些可能是在班门弄斧, 也只是想发表一下我的看法。
Hope you won't mind.

非常希望能向你请教问题。

@yuxino
Copy link
Owner Author

yuxino commented Feb 21, 2018

love you @LonelyLiar

@yuxino
Copy link
Owner Author

yuxino commented Feb 22, 2018

其实写了那么多很多都是废话。 简单的总结一下就是。

因为react-redux会在state更新的时候触发mapState。

react-redux里面组件更新的依据是selector的props是否一致,不一致就将shouldComponentUpdate的返回值设为true,然后组件就更新了。

这样子做很合理思路也是对的。问题出在了我们自己的写法上面。因为我们写的mapState每次都会返回一个新的对象。所以会一直导致更新。

所以我们需要用到reselect。

那么reselect做了什么咧。reselect只是简单的把我们的mapState做了一遍缓存,保证同样的state的情况下每次返回一样的对象仅此而已。

这样一来就不会触发组件的更新了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants