React发展历程中找到问题

为什么要写这个主题,是因为首先就是因为周会立了一个flag要说分享,但是我又不想去分享一些什么类库了,因为一些别的类库对于我们业务来说,并没有什么特别好的帮助,其次之前跟同事聊天里面,他问了我一个问题“你们是怎么做到对新库,新API保持这种持续学习的热情的”,我是这么回答他的“因为业务”,哈哈,大家会觉得我在扯淡,但其实里面是有道理的,一些新的api,新的库其实就是为了解决一些历史问题,当你在当前环境下无法解决一些复杂问题而头疼,用一些绕来绕去的方式解决的时候,发现来了一个新的模式去解决,你就会保持兴奋和激动。那这次就带着思考的角度去讲React的编年史,也希望各位能够从中复习或者有些小伙伴从没接触过一些 react历史问题,也同样看看为什么新的React会产生这些新的API来帮助开发者们。

Earlier than 0.14.x (2015年)

早于0.14x 的时候,ES6还没有普及,所以大家创建一个React的类的时候,都是以函数调用的形式创建的,传入对应的键值对函数执行相应的lifeCycle, state, 和 props。

一个普通最基本,带有props 以及 state的组件是以这样的形式组织的

var Counter = React.createClass({
	getInitialState: function() {
		return {
			count: 0
		}
	},
	getDefaultProps: function() {
	    return {
	    	name: 'Mary'
	    };
	},
	componentDidMount: function() {
		this.setState({
			count: this.state.count + 1
		})
	},
	handleClick: function() {
this.setState(function(preState) {
			return {
				count: preState.count + 1
			}
		})
	},
	render: function() {
		return (
			<div 
			  onClick={this.handleClick}
			>
				{this.state.count}
			</div>
		)
	}
})
复制代码

JSX

Jsx和ES6版本是基本上没有差异性的

State

state的初始值是以initialState的函数调用返回值来创建的

handler Method

不用ES6: 方法调用和ES6版本有一个最大的区别,对于ES6来说,我们都知道它必须手动bind this。但是对于React.CreateClass来说,是不需要的,原因其实是在旧版本的reactClass这个对象里面,在初始化的时候,会遍历所有的传入到createClass的key value,默认的内置lifCycle都会走默认的行为,但是那些所有不是lifeCycle的,都统统会经过一个判断循环,源码不贴了,大家可以自己翻, [github.com/facebook/re…]

if (this.__reactAutoBindMap) {
  	bindAutoBindMethods(this);
}

function bindAutoBindMethods(component) {
  for (var autoBindKey in component.__reactAutoBindMap) {
    if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
      var method = component.__reactAutoBindMap[autoBindKey];
      component[autoBindKey] = bindAutoBindMethod(
        component,
        method
      );
    }
  }
}
复制代码

上面的this,就是整个ReactClass的上下文环境,至此,对于handler的autoBind就是这样实现的。

Es6: JS丢失this问题

var obj = {
a: 123,
	test: function() {
console.log(this.a)
	}
}
function render(func) {

    func()
}

render(obj.test)
复制代码

阶段一总结:仅仅是比较新旧语法创建的React实例,就已经可以探讨到2个小知识了

  • ES5React.createClass 的方法体为什么不需要bind this
  • ES6Class extends React.Component this 指针的问题

Class Extends

对于class OOP来说,最基本的就是符合里氏替换原则,里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。也就是,打个比方,有一个BaseHeader,其中一个子类集成了BaseHeader,叫做HeaderWithAvatar,那么所有BaseHeader出现的地方都能够替换成HeaderWithAvatar,这就称为同一类型的组件。

首先可以肯定的是,用继承来实现逻辑复用没有问题,但是有局限性

先看一下代码

export class BaseHeaderInh extends React.Component {
  state = {
    classname: 'base'
  }
  componentDidMount() {
    console.log('shit')
  }
  render() {
    return (
      <div className={this.state.classname}>HeaderInh base</div>
    )
  }
}

export class HeaderInh extends BaseHeaderInh {
  componentDidMount() {
    console.log('bull shit')
  }
  componentWillReceiveProps(nextProps) {
    // code for base components/*  */
    console.log('base componentWillReceiveProps')
  }
  state = {
    classname: `${this.state.classname} red`
  }
}

export class HeaderReadAvatar extends HeaderInh {
  componentWillReceiveProps(nextProps) {
    super.componentWillReceiveProps()
    console.log('i want change somethin in domain props')
  }
  render() {
    return (
      <div>
        {super.render()}
        icon
      </div>
    )
  }
}
复制代码
  • lifeCycle或者method的override会导致组件不符合预期运行
  • 严重耦合子类和父类的关系
  • 如果不仔细阅读基类,完全没法放心地实现子类特有的应用场景。
  • 必须小心翼翼地写代码

阶段二总结:基于React组织方式的继承逻辑复用有些什么问题

对于React的哲学思想来说,希望开发者对于每个组件都承担着对应自己的单一职责,进行解耦,比方说:HOC,render props 模式。

继承耦合度高,在React组件模式下更难用好,这是一个显而易见的问题。

组合各个组件是分离的,所以组合更加符合单一责任原则,并且组合的情况能够更好地利用好children, state, props。

说到底,继承是一种多态工具,而不是一种代码复用工具,当使用组合来实现代码复用的时候,是不会产生继承关系的。过度使用继承的话,如果修改了父类,会损坏所有的子类。这是因为子类和父类的紧耦合关系是在编译期产生的。

Mixins

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};


var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the mixin
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});
复制代码
  • Mixins核心源码基本上和autoBind的机制差不多,不过多赘述了。
  • Mixins的问题其实跟class extends有些相似的问题
  1. mixins造成了隐式的依赖 假设,你有一个组件有一个状态count = 1 ,然后有一个同事创建了一个mixins,是一个通用的mixins,去读取了本地状态里面的count 处理一些复用逻辑,过了几个月之后,你希望做一些状态共享的业务,把count也传递给别人,那么做了状态提升,把count拉高到父组件,这时候这个mixins就会爆炸了。并且mixins之间可以相互依赖,移除其中一个,有可能会造成另外一个的爆炸。在这种情况下,很难准确的描述mixins之间的依赖关系

  2. mixins会有命名冲突的问题

  3. 类似于class 继承的问题

所以综合以上的种种问题,在React里面,甚至是facebook这么多优秀工程师的团队,都会出现以上的问题,并且无法很好的组织,重构,维护诸如此类的复杂问题。那明显,mixins是一个bad design 。

所以,对于逻辑复用,组件复用,随着时间的推移,经验的推进,就出现了我们最熟悉的higher order component了。

Higher Order Component

完美的解决了以上的种种问题,对于逻辑复用,组件复用能够很好的处理,正是因为HOC是以组合的形式出现的。hoc就不需要过多的介绍了。但是hoc依然出现了一些问题:

  1. 冗余的嵌套高阶组件,比方说Reach Router里面大量使用了hoc。一个最基本的路由显示,就出现了7层的hoc。
  2. 多个hoc组合之后,相同的props命名无法区分到底是来自哪个hoc 所以就出现了render props

Render Props

Render props 和 hoc都是解决同样的事情的,就是逻辑复用,所有的hoc都能够通过render props 重写,render props 剔除了所有上面hoc的问题,写法会相对优雅一点,但是这是需要分场景的,我个人觉得并没有说所有的逻辑复用的hoc都用render props去重写。有一些场景,的确没必要去用render props,比方说一些权限问题,套一个render props是真的麻烦。因为render props终究是一个jsx,不能从外部解决问题,而是在render函数内解决问题。

function isAuth(Component) {
  return class Auth extends React.Component {
    state = {
      auth: false
    }
    componentDidMount() {
      setTimeout(() => {
        this.setState({ auth: true})
      }, 300)
    }
    render() {
      return(
        <div>
          {this.state.auth ? <Component {...props} /> : '无权查看'}
        </div>
      )
    }
  }
}
@isAuth
class Demo extends React.Component {

}

// render props
class Auth extends React.Component {
  state = {
    auth: false
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({ auth: true})
    }, 300)
  }
  renderError = () => {
    return(
      <div>Error component</div>
    )
  }
  render() {
    return (this.props.children({
      auth: this.state.auth,
      renderError: this.render
    }))
  }
}


class RenderProps extends React.Component{
  render() {
    return(
      <div>
        <Auth>
          {({auth, renderError}) => {
            return <div>
              {auth ? Component : renderError}
            </div>
          }}
        </Auth>
      </div>
    )
  }
}
复制代码

对于Hoc来说基本没有对JSX的入侵性,只需要套一个decorator或者套一个函数返回组件 但是对于render props就必然对JSX 有一个入侵性,也就是说,无论如何,你都需要有一个合理的JSX结构来组织。

但其实,Render props依然还有一些问题,就是call backhell了,比方说,最底下的一个div需要用到多个createContext的props,那就需要写成这样。

class Demo extends React.Component {
  render() {
    return(
      <Auth>
        {({ auth, renderError }) => (
          <Game>
            {(props) => (
              <Dude>
                {(props) => (
                  <Shit>
                    {props => (
                      <div>213</div>
                    )}
                  </Shit>
                )}
              </Dude>
            )}
          </Game>
        )}
      </Auth>
    )
  }
}
复制代码

Hooks api 除了基本上能够解决现阶段所有的问题,还解决了一些额外的问题

  • 编译后代码量
  • 看起来更加FP一点(仅仅是看起来)
  • 降低智障代码出错率
  • 让更多的组件易于测试

阶段总结一下:

  • Mixins(0.14x<): 逻辑复用初代目,虽然解决了逻辑复用,但是本质和Class inheritance有类似问题(工程协作会造成困扰)

  • Class Inheritance:是一种多态工具,而不是一种代码复用工具,(需要非常完整的OOP能力,但也没法解决耦合问题)

  • Higher Order Component(0.14x-15.6):逻辑代码复用以组合的形式出现,颗粒度适中,具备完整的state , props形态,符合React核心的单一职责原则(但是会造成冗余嵌套组件的问题)

  • Render Props(16.x):优雅地处理hoc剩余问题,但依然可能会出现(Call BackHell)

  • Hooks (16.7 alpha):基本是现阶段逻辑复用的解决方案

0.14.x – 16.6(2016年-2017、8年) 这个里面出现了很多的api更新,Reconciler的架构不断地改进,以及拆包,例如像ReactDom , React.CSSTransitionGroup,React.CreateClass,React.PropTypes等,包括开始加入fiber架构在之后的代码增进,ComponentDidCatch,getDerivedStateFromProps,getSnapShotBeforeUpdate等等等等

16.x RoadMap

除了一些我们所已经接触到的还有以下两个(这两个细节其实都可以从官网和iceland dan的演讲视频中找到)

  • 16.8: concurrent mode
  • 16.9: suspense for Data fetching

最后总结一下

对于这么多日新月异的框架和API,我自己个人来说,是以解决业务,解决问题的想法去保持热情从而学习他们的。

  1. 在历史中发掘真理,
  2. 在过程中迭代方案,
  3. 在业务中尝试它们

猜你喜欢

转载自juejin.im/post/5c05e6d6e51d4567a30eb2e3