NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

理解ES6 Promise

| Comments

本文的内容来自于 ES6-Promise-Workshop: https://github.com/benweizhu/es6-promise-workshop

什么是Promise? Promise用来做什么?

延迟操作?网络请求?回调函数?它们统称为“异步操作”。

  • User interaction(mouse, keyboard, etc)
  • AJAX
  • Timers …

为什么大家觉得刚开始写Promise会不太习惯?

因为:

习惯了jQuery的回调

1
2
3
4
5
6
7
8
$.ajax({
  url: '/user',
  data: { id: 1 },
  success: function (data) {
    console.log(data)
  },
  dataType: 'json'
});

习惯同步的Get方法

1
2
3
//Java
User user = userService.getUser(1);
user.getUsername();

当有一天

AngularJS通过Service返回一个Promise的时候,我们仍然将Service命名为UserService,但此时返回是一个Promise,而不是User本身。

1
2
3
var user = userService.getUser(1);
user.username;
// undefined

Promise是一个JavaScript对象

JavaScript对象创建的方法有两个:字面量和new关键字

ES6 Promise语法

通过new关键字创建一个Promise,并传递一个函数作为参数

1
2
3
var promise = new Promise(function (resolve, reject) {
  // 业务代码
});

Promise中业务代码的执行有两个结果:成功(resolve)或者 失败(reject)

成功调用resolve

1
2
3
4
5
6
var promise = new Promise(function (resolve) {
  resolve(42); // pass 42 to then cb
});
promise.then(function (value) {
  console.log(value);
});

失败调用reject

1
2
3
4
5
6
7
8
9
10
11
12
var promise = new Promise(function (resolve, reject) {
  reject(new Error('error')); // pass Error obj to catch cb
});
promise.catch(function (error) {
  console.log(error);
});
var promise = new Promise(function (resolve, reject) {
  reject(new Error('error')); // pass Error obj to catch cb
});
promise.then(resolveCb, function(error){
  console.log(error);
});

这是常见的Promise教程顺其自然的语法讲解,resolve会将传入的参数传递给then的回调函数,reject会传递给catch或者then的第二个参数。

Promise是异步操作

这个时候,我们思考一个问题:我们一直说Promise是解决异步操作的,那么上面的代码中,哪一部分是异步的呢?

先思考下,异步操作中,到底哪一步是异步,比如,ajax调用:代码顺序(同步)执行,发现了一个ajax操作,顺序(同步)执行它,ajax发出一个网络请求,这个网络请求操作交给了浏览器,当网络请求返回,调用对应的callback函数。

真正的异步操作是指这个回调函数,它并没有在JavaScript代码顺序(同步)执行的过程中被调用,而是在晚一些时候才被执行。

那么对于上面的Promise,构造函数传入的函数,是顺序执行的。在这个Promise的传递函数中,没有进行任何的异步操作(比如网络请求),而是顺序执行的,直接调用resolve或者reject将状态设置为成功或者失败。

但是当运行promise.then或者promise.catch,即便当时promise的状态已经是确定的,then和catch里面的函数仍然是异步执行。

Promise实现网络请求

过去,我们都是使用开源的Promise网络请求工具库,比如Fetch,Axios。今天我们来自己通过ES6 Promoise和XHR实现一个Promise网络请求工具。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function fetchData(URL) {
  return new Promise(function (resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', URL, true);
    req.onload = function () {
      if (req.status === 200) {
        resolve(this.responseText);
      } else {
        reject(new Error(req.statusText));
      }
    };
    req.onerror = function () {
      reject(new Error(req.statusText));
    };
    req.send();
  });
}

var promise = fetchData('https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/books.json');

promise.then(function (responseText) {
  document.getElementById('json').innerHTML = responseText;
  console.log(JSON.parse(responseText))
}).catch(function (error) {
  console.log(error)
})

其实非常简单,你只要记得在fetchData执行完之后,你需要一个promise,那么fetchData中就需要通过new关键字创建并返回,剩下的就是将XHR的操作放在传入的构造函数中。

Promise Chain

先看代码:

1
2
3
4
5
6
7
8
9
function increment(value) { return value + 1; }
function output(value) { console.log(value); }
/**  1 + 1 = 2 **/

var promise = Promise.resolve(1);

promise
  .then(increment)
  .then(output);

Promise.resolve(1)是Promise提供的快速创建一个Promise的方法。

这里,我们通过代码反向推导,promise可以调用then或者catch方法,当我们看到then方法后面可以继续调用then方法时,就可以明白,then方法也返回了一个promise,这个promise的then方法中的函数接收到的参数是上一个then方法中的函数return的结果。

假设现在我们来实现( 1 + 1 ) * 2 = 4

1
2
3
4
5
6
7
8
9
10
11
function doubleUp(value) { return value * 2; }
function increment(value) { return value + 1; }
function output(value) { console.log(value); }
/** ( 1 + 1 ) * 2 = 4 **/

var promise = Promise.resolve(1);

promise
  .then(increment)
  .then(doubleUp)
  .then(output);

那么,这里很容混淆的时候,以前你可能会认为.then方法之所以可以chain,是因为then的函数中返回了一个promise,但其实不是这个原因。

那么,如果真的返回了一个promise,结果是什么呢?答案是:

如果你返回类似于promise的内容,下一个then()则会等待,并仅在promise产生结果(成功/失败)时调用

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function resolveAfterTime(num, time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(num);
    }, time)
  });
}

resolveAfterTime(10, 1000).then(function (value) {
  console.log(value)
  return resolveAfterTime(value + 10, 5000);
}).then(function (value) {
  console.log(value)
});

第一个log在1秒后打印,第二个log在5秒后打印。

终极作业

链式调用请求书列表中每本书的详细内容,并返回JSON数据

https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/books.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
  {
    "id": 1,
    "name": "《重构 改善既有代码的设计》",
    "price": 100,
    "url": "https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/refactoring.json"
  },
  {
    "id": 2,
    "name": "《JavaScript编程精粹》",
    "price": 100,
    "url": "https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/javascript-the-good-parts.json"
  }
]

这里我们会用到Promise.all

Promise.all: 接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用.then方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function fetchData(URL) {
  return new Promise(function (resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', URL, true);
    req.onload = function () {
      if (req.status === 200) {
        resolve(req.responseText);
      } else {
        reject(new Error(req.statusText));
      }
    };
    req.onerror = function () {
      reject(new Error(req.statusText));
    };
    req.send();
  });
}

fetchData("https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/books.json")
  .then(function (data) {
    var books = JSON.parse(data);
    var booksPromise = books.map(function (book) {
      return fetchData(book.url);
    });
    return Promise.all(booksPromise);
  })
  .then(function (bookDetailsList) {
    bookDetailsList.forEach(function (bookDetails) {
      var img = document.createElement("img");
      img.src = JSON.parse(bookDetails).imageUrl;
      document.body.appendChild(img);
    });
  })
  .catch(function (error) {
    console.error(error);
  });

React应用在产品环境下的性能优化

| Comments

只有10%~20%的最终用户响应时间花在了下载HTML文档上,其余的80%~90%时间花在了下载页面中的 所有组件 上。 - 性能黄金法则,Steve Souders

Steve Souders在2007年提出这样的“性能黄金法则”,我猜测当他看到React这样一项技术之后,一定会觉得自己的这个法则居然如此的准确,可能甚至觉得这个比例不够极致。(虽然此组件非React组件,但是我还是忍不住想笑)

所以,今天我们就来聊聊,React应用在产品环境下的性能优化问题。

Bundle大小分析

在开始做任何的优化之前,你需要知道痛点在什么地方?既然Steve说80%~90%时间花在了下载页面中的 所有组件 上,那么就从了解项目的模块组成开始。

1.Webpack运行时的输出

在没有任何外部力量帮助的情况下,我们可以直接阅读Webpack的输出 react ouput

1
webpack --display-chunks

可以查看模块在哪个分块中出现,帮助我们查找分块中重复的依赖。

2.bundle-size-analyzer

webpack-bundle-size-analyzer是我个人比较喜欢的模块大小分析工具,使用起来非常简单,输出也非常清晰。

webpack-analyzer

3.webpack-bundle-analyzer

webpack-bundle-analyzer在github上star人数更多,功能也相对更加齐全(fancy)。 webpack-bundle-analyzer.gif

代码分离(Code Splitting)

1.Vendor代码分离

代码分离是Webpack核心功能之一,典型的做法是将第三方依赖代码从应用代码中抽离出来,这样可以利用浏览器的缓存来提高性能(减少下载次数)。

Webpack官方文档有非常详细的介绍: code-splitting-libraries,我就不在这里赘述,下面是一个简单代码样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var webpack = require('webpack');
var path = require('path');

module.exports = function(env) {
    return {
        entry: {
            main: './index.js',
            vendor: 'react', 'react-dom', 'react-redux', 'babel-polyfill']
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                name: 'vendor' // Specify the common bundle's name.
            })
        ]
    }
}

2.CSS代码

也许你还会想要的就是将CSS文件分离,原因是一样的。官方文档也给出了非常详细的介绍:code-splitting-css,所以同样也不赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
const extractCSS = new ExtractTextPlugin('styles.css');
module: {
  rules: [
    {
      test: /\.scss$/,
      use: extractCSS.extract(['css-loader', 'postcss-loader', 'sass-loader'])
    }
  ]
},
plugins: [
  extractCSS
]

3.React-Router按需分离

当应用逐渐变得复杂后,你会发现,仅仅将代码分离为vendor和app两个bundle,远远是不够的,要么vendor.js文件特别大,要么app.js文件特别大,这个时候你一定会想到,要按需加载(异步加载)。

ES2015 Loader spec中定义了一个import()方法来在运行时动态加载ES2015的模块,代码如下:

1
2
3
4
5
6
7
8
function determineDate() {
  import('moment').then(function(moment) {
    console.log(moment().format());
  }).catch(function(err) {
    console.log('Failed to load moment', err);
  });
}
determineDate();

Webpack会将import()方法看做一个“代码分离点”,将被加载的模块放在一个单独的文件块中。

那么,如果你的应用采用了React-Router,我们就可以根据路由,按需加载所使用的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function errorLoading(error) {
  throw new Error(`Dynamic page loading failed: ${error}`);
}

function loadRoute(cb) {
  return module => cb(null, module.default);
}

<Router history={history} queryKey="false">
  <Route path="/user" name="UserPage" getComponent={(location, cb) => {
    System.import('./components/UserPage').then(loadRoute(cb, false)).catch(errorLoading)}}
  />
  <Route path="/data" name="DataPage" getComponent={(location, cb) => {
    System.import('./components/DataPage').then(loadRoute(cb, false)).catch(errorLoading)}}
  />
  <Route path="/about" name="AboutPage" getComponent={(location, cb) => {
    System.import('./components/AboutPage').then(loadRoute(cb, false)).catch(errorLoading)}}
  />
</Router>

Webpack会根据分离点生成对应的JS文件

4.大文件异步加载

笔者遇到过这样的需求,采用了某图表库来做地图绘制,但是地图库JS文件或者JSON文件特别的大,即便压缩之后也有400+KB。所以,我将该依赖放在应用(SPA)的首页,并采用了异步加载,这样,首屏加载速度不会依赖于它,而用户从首页到需要使用该地图的部分还存在一些操作过程,所以留存了一些时间来异步加载地图数据。

1
2
System.import('./map/china.js').then().catch(errorLoading);
System.import('./map/world.js').then().catch(errorLoading);

当首页需要的JS加载完成之后,才开始加载:

运行webpack -p

Webpack的官方文档有详细的说明,对于产品环境的构建应该运行webpack -p:production-build

1
webpack --optimize-minimize --define process.env.NODE_ENV="'production'"

此时,Webpack会做几件事情,我们也需要根据这些事情做相关的配置:

1.对JS代码进行压缩

–optimize-minimize 标签会对JS代码用UglifyJsPlugin做压缩,并根据Webpack中配置的devtool配置SourceMap

2.SourceMap

即便在产品环境下,仍然建议使用SourceMap,方便产品环境的bug定位,但是对于开发环境和产品环境,我们需要使用不同力度的SourceMap,才能既方便开发也兼容产品环境性能。

1
2
3
4
const config = {
  // webpack config
  devtool: isProd ? 'cheap-source-map' : 'cheap-module-inline-source-map',
}

官方提供了7种Devtool,而且有更详细的关于devtool的配置,请详见 devtool3.Node环境变量production

将redux的中间件和开发环境使用的devtool通过变量分离 –define process.env.NODE_ENV=“‘production’” 标签会以下面的方式使用DefinePlugin:

1
2
3
4
5
6
7
8
9
const webpack = require('webpack');
module.exports = {
  /*...*/
  plugins:[
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

这个时候,就可以在产品代码里面获取到此环境变量。这个时候我们要做的就是根据环境变量的不同,来进行不同的配置,比如:这样写log

1
if (process.env.NODE_ENV !== 'production') console.log('...')

对于产品环境就等价于:

1
if (false) console.log('...')

此时UglifyJS插件就会将它去除掉。

又比如:在react-redux开发中,我们一般都会配置开发插件DevTool,或者log中间件,但其实,在产品环境中,我们不需要,这个时候就需要根据环境变量来动态配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { applyMiddleware, compose, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import rootReducer from './reducers';
import promiseMiddleware from 'redux-promise-middleware';

export default () => {
  let finalCreateStore;
  if (process.env.NODE_ENV === 'production') {
    finalCreateStore = compose(
      applyMiddleware(promiseMiddleware(), thunkMiddleware)
    )(createStore);
  } else {
    finalCreateStore = compose(
      applyMiddleware(promiseMiddleware(), thunkMiddleware, createLogger()),
      window.devToolsExtension ? window.devToolsExtension() : f => f
    )(createStore);
  }
  return finalCreateStore(rootReducer);
};

Tree Shaking

Webpack TreeShake

清理无用的JS代码,真实导入有用的模块。配置起来非常简单:

1
"presets": [["es2015", {"modules": false}], "react", "stage-0"]

tree-shake.png

Babel对React代码的优化

除了从产品环境模块架构上优化,Babel也在编译阶段优化React应用性能作出了巨大贡献。

1.transform-react-constant-elements插件

transform-react-constant-elements,自从React0.14版本,我们可以将React元素以及他们的属性对象当做普通的值对象看待。这时候我们就可以重用那些输入是immutable的React元素。

1
2
3
const Hr = () => {
  return <hr className="hr" />;
};
1
2
3
4
5
const _ref = <hr className="hr" />;

const Hr = () => {
  return _ref;
};

从而减少对React.createElement的调用。

2.transform-react-inline-elements插件

transform-react-inline-elements,自从React0.14版本,可以将React元素内联为对象,Babel将React.createElement方法替换成babelHelpers.jsx来转换元素为对象。

1
<Baz foo="bar" key="1"></Baz>;
1
2
3
4
5
6
babelHelpers.jsx(Baz, {
  foo: "bar"
}, "1");

output:
{type: Baz,props:{foo:"bar"},key:"1"}

3.其他开源Babel插件

除了以上两个官方插件,在开源世界还有许多其他Babel插件可以优化代码,而且非常实用,这里留给大家自己去探索: transform-react-remove-prop-typestransform-react-pure-class-to-functionbabel-react-optimize(综合所有优化的插件,此处应该有掌声)

代码本身的优化

除了利用工具和构建,以及模块按需加载,来提高产品环境下的代码性能,最最基础的还是开发在平时写代码的需要注意的一些基础原则

1.只导入需要的包

以Lodash为例,比如:如果你只用到isEqual,那么就不要把整个lodash都引入

1
import isEqual from 'lodash/isEqual';

通过babel-plugin-lodashlodash-webpack-plugin来缩小应用所需要的lodash的模块

用高阶函数Flow来代替Chain,以避免将整个ladash都加载。

2.使用ESLint

合理的使用ESLint,除了帮助团队指导代码风格,也可以告诉你如何正确的写React应,比如,当组件是纯presentational组件时,就应该使用PureComponent或者纯函数组件,这些eslint都会告诉你。

3.利用React官方的Perf工具

1
2
3
Perf.start()
// ...
Perf.stop()

服务器端优化

使用Gzip压缩倒不是React应用才有的性能优化策略,但还是要提一下,因为确实有用。

1.Nginx服务器端配置

我猜测大部分的情况下,都会用Nginx来部署静态资源,斗胆提供一个nginx的gzip配置。

1
2
3
4
gzip on; //仅仅配置这一行是不会起作用的
gzip_types  text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
gzip_proxied    no-cache no-store private expired auth;
gzip_min_length 1000;

2.手动压缩

另外一种方式,就是我们自己手动压缩Gzip,这样可以减少Nginx编码带来的性能消耗,Webpack插件 compression-webpack-plugin可以做到。

还有什么别的提高性能的办法呢?

1.服务器端渲染如何

有人会说,服务器端渲染如何? 这个要看情况。服务器端渲染一般主要用来处理首屏渲染性能(注意是首次加载)和搜索引擎爬虫问题。如果你的JS文件特别大,那么服务器端渲染能够,让用户在加载完HTML和CSS之后立刻看到页面。如果不是首次加载,那么其实JS是可以缓存在客户端的,所以即便不用服务器端渲染,之后也不会很慢。

相对的缺点是:配置起来比较麻烦,但如果是一劳永逸的事情,还是值得一做的。

更多关于是否应该进行服务器端渲染服务器端渲染的好处 以及如何进行服务器端渲染?请查看相关文章。

2.ServiceWork

渐进式 Web 应用程序思想(PWA)最近可火了,2017年ThoughtWorks技术雷达“Progressive Web Applications”放在了试验阶段。

简单介绍什么是service worker:

在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app拥有与native app相同的离线体验、消息推送体验。 service worker是一段脚本,与web worker一样,也是在后台运行。 作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。

Github: Google sw-toolbox, sw-precache-webpack-pluginoffline-plugin

3.Preload

Preload 作为一个新的web标准,旨在提高性能和为web开发人员提供更细粒度的加载控制。Preload使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。

1
<link rel=“preload”>

作为新的标准,浏览器兼容性是你有必要考虑的一个方面:

preload

github: preload-webpack-plugin

最后

文章内容有点长,但我相信这些都是干货是值得一读的,前端产品环境性能优化确实是一个说不完的话题,前端技术更新迭代也没有多少其他计算机技术能够匹敌的,这也对前端开发工程师(全栈开发工程师)的技术敏感度和追求新技术的态度有很高的要求。

作者:Benwei,ThoughtWorks高级咨询师,全栈开发工程师,《实战Gradle》译者

转载原文地址: http://benweizhu.github.io/blog/2017/05/12/react-redux-production-optimisation/

参考文献:
1.https://hackernoon.com/optimising-your-application-bundle-size-with-webpack-e85b00bab579
2.https://brotzky.co/blog/code-splitting-react-router-webpack-2/
3.http://www.jianshu.com/p/f4054b2dcc6e
4.http://2ality.com/2015/12/webpack-tree-shaking.html
5.https://hackernoon.com/how-i-built-a-super-fast-uber-clone-for-mobile-web-863680d2100f
6.http://andrewhfarmer.com/server-side-render/