转载

响应式react:构建高效易用的react应用

这篇文章翻译自mobx作者 Michel Weststrate 的一篇博客,虽然我也想做到信达雅,奈何英语水平有限……所以我写的内容自动屏蔽了一些前因后果没营养的话,只翻译我觉得重点的内容。我的原则是,捞干的说,不BB~

想了解细节的话可移步 原文

使用react开发应用有啥好处,我想是个前端都能数出个十条八条来。但是如果你的项目需要在浏览器中绘制成千上万的对象,而且这些对象之前还有大量的耦合关系,那维护这些对象也够喝一壶的……mobx作者在发明mobx的时候就面临了这样一个项目,一个对象的值可能被其他对象引用,任何变化都可能引起大量的ui更新和重绘。在一些特殊场景,比如拖拽操作,这些动作还必须在40毫秒响应……

于是,mobx被开发出来了。简单来说,他使用了函数响应式编程的概念 Observables 来解决上述的问题,其实 mobx 并不是第一个使用这一概念的前端库,ember、knockout还有vue其实都使用了这一概念,我个人感觉,mobx就好像把vue和knockout的数据绑定那部分抽出来然后揉到一起一样……真的能发现很多有相似的地方……使用 Observables 的好处就在于,你可以很容易实现自动更新关联数据和sideways data loading,从而解放生产力,提升应用的性能。后面测试数据一章可以看到具体的对比结果。

下面我们以一个简单的购物应用为例:

响应式react:构建高效易用的react应用

里面包含商品列表,购物车,结算等部分。完整的例子可以再 jsfiddle 上看到(需要翻墙)。

数据模型

首先,我们先看下数据模型。商品列表(Articles)里的每个商品,有名称(name)和价格(price)属性,购物车(Cart)有已加入购物车列表(Entries)和总价(Total)属性。已加入购物车列表中的每个商品除了名字和价格属性以外,还有数量属性(amount)、根据数量和单价计算出的总价。数据之间的关系如下图所示。

响应式react:构建高效易用的react应用

从上图中我们可以看出,如果对一个数据进行修改,那么就会带动其他的数据修改,同时还得修改ui。

大概列一下数据联动的逻辑:

  • 如果商品列表里的商品价格发生改变,购物车里的商品价格也需要更新。
  • ……购物车的总价也得更新。
  • 如果购物车中商品数量变化,总价也得跟着更新。
  • 如果商品列表里的商品被重命名了,商品列表的界面得更新。
  • ……购物车里对应的商品也得更新。
  • 如果添加新文章合计购物车……

响应式react:构建高效易用的react应用

作为一个程序员,就是这么闹心……你不得不写大量的代码来处理各种可能的状态,处理速度还得快,让用户老爷等着急了你担待得起吗……

那么,让我们来看看怎么用mobx来日翻这些问题:

functionArticle(name, price){
    mobx.extendObservable(this, {
        name: name,
        price: price
    });
}

functionShoppingCartEntry(article){
    mobx.extendObservable(this, {
        article: article,
        amount: 1,
        price: function(){
            return this.article ? this.article.price * this.amount : 0;
        }
    });
}

functionShoppingCart(){
    mobx.extendObservable(this, {
        entries: [],
        total: function(){
            return this.entries.reduce(function(sum, entry){
                return sum + entry.price;
            }, 0);
        }
    });
}

使用工具函数包装之后,通过构造函数创建的对象都是可观测的,就是说当对象的某一个属性改变,跟他跟他相关联的属性都会自动更新。这样状态变化就不用我们手动维护了,比如添加商品到购物车时总价的变化、物品的价格发生变化时其他关联项的变化等等。

界面

模型建好了,该搭界面了。下面的列了一段购物车组件的代码,包含了显示购物车里的商品列表和总价。其他组件请自行脑补,都差不离的。

var CartView = React.createClass({
    render: function(){
        functionrenderEntry(entry){
            return (<CartEntryView entry={entry} cart={this.props.cart} key={entry.id} />);
        }
        return (<div>
            <ul id="cart">{this.props.cart.entries.map(renderEntry)}</ul>
            <div><b>Total: <span id="total">{this.props.cart.total}</span></b></div>
        </div>)
    }
});

var CartEntryView = React.createClass({
    render: function() {
        return (<li>
            <button onClick={this.removeArticle}>«</button>
            <span>{this.props.entry.article.name}</span>
            <span>{this.props.entry.amount}</span>
        </li>);
    },

    removeArticle: function() {
        if (--this.props.entry.amount < 1)
            this.props.cart.entries.splice(this.props.cart.entries.indexOf(this.props.entry), 1);      
    }
});

组件写完了,但是没跟模型关联还跑不起来,需要用mobx-react库里的 mobxReact.observer 方法把组件都包装装一下,这就算关联上了:

var CartEntryView = mobxReact.observer(React.createClass({
    render: function(){
        return (<li>
            // etc...

然后,就没有然后……是的,这就算齐活了。让我们看下 jsfiddle 上的演示。

这里 observer 函数为我们做了两件事。首先,它将组件的 render 函数变成一个可观察到的函数。然后,把组件注册到观察者函数,所以每次 render 需要更新了他都会自动更新。 mobxReact.observer (如果你使用ES6的话是 @observer )确保每当数据改变,只更新UI的相关部分,就是刚才上面提到的sideways data loading。你可以自己点点试试,注意看下方的日志面板,看看UI更新的数据,你会发现,每次操作,组件的重绘数量都是最低的。

另外,由于每个组件都只跟踪自己的依赖,通常不需要重新渲染子组件。比如,如果购物车的总价重新渲染了,在不必要的情况下就不会重新渲染购物车里的商品列表。

测试数据

东西好不好,主要看疗效。 这里 你可以找到一个一毛一样的应用,但没有使用mobx,而是简单的使用每次替换数据的方法构建的。只有几个商品,不会感觉到有任何区别,但一旦商品数量上升,就会提现出真正意义上的性能差异。

响应式react:构建高效易用的react应用 响应式react:构建高效易用的react应用

创建大量的数据和组件的时候,基本没啥差别。但是在修改数据时,强弱立分高下力判。如果在有10000个元素的列表中更新其中10个元素的数据,速度大约快了十倍。2.5秒下降到250毫秒……那么这种差别是从哪里来的呢?让我们来瞅瞅不适用mobx时React的渲染报告:

响应式react:构建高效易用的react应用

可以看到,ArticleViews 和 CartEntryViews一共渲染了20000次。2433毫秒的渲染时间中,2145毫秒的渲染时间是被浪费的(Wasted time)。Wasted time的意思是:花费在执行渲染函数上的时间,实际上并没有更新任何一个DOM元素。这有力地说明了,无脑更新是一件很浪费cup资源的,组件越多浪费的时间就越多。

相较之下,这是使用mobx的报告:

响应式react:构建高效易用的react应用

重绘的只有31个组件,完全没有一点浪费。就是说每个重新渲染的组件都是确实需要修改的。这正是我们想要实现的效果!

然而,这样你就满足了吗?我们还可以再进一步优化!

从报告中我们还可以看出,267毫秒的总体渲染时间里,大部分剩余的渲染时间消耗在CartView的渲染上(243毫秒)。那是在更新购物车的总价属性。值得注意的是,要重新渲染CartView,也就意味着要检查购物车中一万个商品是否有修改,是否要更新CartEntryView。而这就浪费了大部分时间。我们可以把总价再单独做一个控件,CartTotalView。通过这个简单的处理,如果只是总价的变化,就可以跳过CartView的重新渲染。这使得渲染时间进一步下降到约60毫秒(见上图中的灰色那一条),这比没有使用Mobx的React应用大约快了40倍!

总结

好的,通过上面的例子我们看到了使用mobx和不使用mobx在性能上的区别。这里还需要强调的一点是,使用了mobx还有一个优势,就是不会影响代码的可维护性,对于程序员来说,这点很重要,就算有万般好处,如果代码写出来像屎一样难看,也不会有人想用对吧?在jsfiddle里面可以看到两个例子的完整程序。两段代码基本没啥太大的区别……┑( ̄Д  ̄)┍

那么,我们可以用其他技术达到相同的效果吗?也许吧。例如,使用ImmutableJS也能做到sideways data loading。然而,就像我刚才说的,有可能你会收获一坨是一样的代码……毕竟,恕我直言,相对于不可变对象,可变类使用起来会更方便一些。此外,不可变的数据结构不能帮助你保持计算属性。如果使用不可变数据,改变商品的名字ArticleView会重新渲染的很快,但是CartEntryView中引用的相同商品实例就会失效。

另一种优化React应用的方案是为每一个可能发生的操作创建事件,然后在管理这些事件,在恰当的时机恰当的地点(组件)触发(注销)它们。但这将导致编写大量的样板式代码,维护起来相当困难。我不知道人啊,反正我是懒得弄这些……(哥你是不是不会断句……这段看的好纠结啊……)

顺便说一下,我强烈建议使用action来抽象对模型的更新,这样能有效的做到表现和行为分离。

最后,在大型项目中使用mbox配合React是非常好用的。有时我看到数据变化时,界面上某个角落也跟着更新了,我自己都惊呆了……而且还没有任何性能问题,你说气人不……( ̄▽ ̄)”……心动不如行动,让我们把繁重的维护工作都丢给React和Mobx,敬请享受更轻松有趣的coding吧~!

原文  http://brooch.me/2016/12/23/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/
正文到此结束
Loading...