为什么要写这个主题,是因为首先就是因为周会立了一个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有些相似的问题
-
mixins造成了隐式的依赖 假设,你有一个组件有一个状态count = 1 ,然后有一个同事创建了一个mixins,是一个通用的mixins,去读取了本地状态里面的count 处理一些复用逻辑,过了几个月之后,你希望做一些状态共享的业务,把count也传递给别人,那么做了状态提升,把count拉高到父组件,这时候这个mixins就会爆炸了。并且mixins之间可以相互依赖,移除其中一个,有可能会造成另外一个的爆炸。在这种情况下,很难准确的描述mixins之间的依赖关系
-
mixins会有命名冲突的问题
-
类似于class 继承的问题
所以综合以上的种种问题,在React里面,甚至是facebook这么多优秀工程师的团队,都会出现以上的问题,并且无法很好的组织,重构,维护诸如此类的复杂问题。那明显,mixins是一个bad design 。
所以,对于逻辑复用,组件复用,随着时间的推移,经验的推进,就出现了我们最熟悉的higher order component
了。
Higher Order Component
完美的解决了以上的种种问题,对于逻辑复用,组件复用能够很好的处理,正是因为HOC是以组合的形式出现的。hoc就不需要过多的介绍了。但是hoc依然出现了一些问题:
- 冗余的嵌套高阶组件,比方说Reach Router里面大量使用了hoc。一个最基本的路由显示,就出现了7层的hoc。
- 多个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,我自己个人来说,是以解决业务,解决问题的想法去保持热情从而学习他们的。
- 在历史中发掘真理,
- 在过程中迭代方案,
- 在业务中尝试它们