feat: manual-update for performance boost

This commit is contained in:
Justineo
2018-08-23 23:19:38 +08:00
parent 020fb5ce73
commit 4d8c68be4c
8 changed files with 209 additions and 86 deletions

View File

@ -1,3 +1,9 @@
3.1.0
* Add `manual-update` prop to handle performance critical scenarios.
* Deprecate `watch-shallow` prop as it was actually not working as expected.
* Fix the computed getters by using `Object.defineProperties` directly instead of Vue's `computed` as it no longer works as expected after Vue 2.0.
* Remove `chart` from `data` to gain a performance boost.
3.0.9 3.0.9
* Update to `resize-detector@0.1.7` to better handle initial resize callback. * Update to `resize-detector@0.1.7` to better handle initial resize callback.

View File

@ -219,9 +219,9 @@ See more examples [here](https://github.com/Justineo/vue-echarts/tree/master/dem
This prop indicates ECharts instance should be resized automatically whenever its root is resized. This prop indicates ECharts instance should be resized automatically whenever its root is resized.
* `watchShallow` (default: `false`) * `manual-update` (default: `false`)
This prop is used to turn off the default deep watch for `options` prop. For charts with large amount of data, you may need to set this prop so that Vue only watches the `options` prop itself instead of watching all its properties inside. To trigger the rerender of the chart, you have to change the root reference to `options` prop itself, or you can manually manage data via the `mergeOptions` method (chart data won't be synchronized with `options` prop when doing this). For performance critical scenarios (having a large dataset) we'd better bypass Vue's reactivity system for `options` prop. By specifying `manual-update` prop with `true` and not providing `options` prop, the dataset won't be watched any more. After doing so, you need to retrieve the component instance with `ref` and manually call `mergeOptions` method to update the chart.
### Computed ### Computed

View File

@ -224,9 +224,9 @@ export default {
这个 prop 用来指定 ECharts 实例在组件根元素尺寸变化时是否需要自动进行重绘。 这个 prop 用来指定 ECharts 实例在组件根元素尺寸变化时是否需要自动进行重绘。
* `watchShallow` (默认值:`false` * `manual-update` (默认值:`false`
这个 prop 可以用来关闭默认的对 `options` prop 的深度监听。对于有大量数据的图表,你可能会需要开启这个选项,来让 Vue 仅监听 `options` prop 本身的变化而忽略内部属性的变化。此时在需要重绘图表时,你需要重新设置 `options` prop 的直接引用,或者调用 `mergeOptions` 方法来手动管理图表内的数据(此时 `options` prop 的数据将不和图表内数据同步) 在性能敏感(数据量很大)的场景下,我们最好对于 `options` prop 绕过 Vue 的响应式系统。当将 `manual-update` prop 指定为 `true` 且不传入 `options` prop 时,数据将不会被监听。然后,你需要用 `ref` 获取组件实例以后手动调用 `mergeOptions` 方法来更新图表
### 计算属性 ### 计算属性

View File

@ -151,7 +151,7 @@
</section> </section>
<h2 id="connect"> <h2 id="connect">
<a href="connect">Connectable charts</a> <a href="#connect">Connectable charts</a>
<button :class="{ <button :class="{
round: true, round: true,
expand: expand.connect expand: expand.connect
@ -187,6 +187,25 @@
</p> </p>
</section> </section>
<h2 id="flight">
<a href="#flight">Manual Updates</a>
<button :class="{
round: true,
expand: expand.flight
}" @click="expand.flight = !expand.flight" aria-label="toggle"></button>
</h2>
<section v-if="expand.flight">
<p><small>You may use <code>manual-update</code> prop for performance critical use cases.</small></p>
<figure style="background-color: #003;">
<chart
ref="flight"
:init-options="initOptions"
manual-update
auto-resize
/>
</figure>
</section>
<footer> <footer>
<a href="//github.com/Justineo">@Justineo</a>|<a href="//github.com/Justineo/vue-echarts/blob/master/LICENSE">MIT License</a>|<a href="//github.com/Justineo/vue-echarts">View on GitHub</a> <a href="//github.com/Justineo">@Justineo</a>|<a href="//github.com/Justineo/vue-echarts/blob/master/LICENSE">MIT License</a>|<a href="//github.com/Justineo/vue-echarts">View on GitHub</a>
</footer> </footer>
@ -234,8 +253,10 @@ import 'echarts/lib/component/legend'
import 'echarts/lib/component/title' import 'echarts/lib/component/title'
import 'echarts/lib/component/visualMap' import 'echarts/lib/component/visualMap'
import 'echarts/lib/component/dataset' import 'echarts/lib/component/dataset'
import 'echarts/map/js/world'
import 'zrender/lib/svg/svg'
import 'echarts-liquidfill' // import 'echarts-liquidfill'
import logo from './data/logo' import logo from './data/logo'
import getBar from './data/bar' import getBar from './data/bar'
import pie from './data/pie' import pie from './data/pie'
@ -284,7 +305,8 @@ export default {
scatter: true, scatter: true,
map: true, map: true,
radar: true, radar: true,
connect: true connect: true,
flight: true
}, },
initOptions: { initOptions: {
renderer: options.renderer || 'canvas' renderer: options.renderer || 'canvas'
@ -328,7 +350,7 @@ export default {
if (this.seconds === 0) { if (this.seconds === 0) {
clearTimeout(timer) clearTimeout(timer)
bar.hideLoading() bar.hideLoading()
bar.mergeOptions(getBar()) this.bar = getBar()
} }
}, 1000) }, 1000)
}, },
@ -394,6 +416,77 @@ export default {
dataIndex dataIndex
}) })
}, 1000) }, 1000)
let { flight } = this.$refs
flight.showLoading({
text: '',
color: '#c23531',
textColor: 'rgba(255, 255, 255, 0.5)',
maskColor: '#003',
zlevel: 0
})
fetch('../static/flight.json')
.then(response => response.json())
.then(data => {
flight.hideLoading()
function getAirportCoord (idx) {
return [data.airports[idx][3], data.airports[idx][4]]
}
let routes = data.routes.map(function (airline) {
return [
getAirportCoord(airline[1]),
getAirportCoord(airline[2])
]
})
flight.mergeOptions({
title: {
text: 'World Flights',
left: 'center',
textStyle: {
color: '#eee'
}
},
backgroundColor: '#003',
tooltip: {
formatter (param) {
let route = data.routes[param.dataIndex]
return data.airports[route[1]][1] + ' > ' + data.airports[route[2]][1]
}
},
geo: {
map: 'world',
left: 0,
right: 0,
silent: true,
itemStyle: {
normal: {
borderColor: '#003',
color: '#005'
}
}
},
series: [
{
type: 'lines',
coordinateSystem: 'geo',
data: routes,
large: true,
largeThreshold: 100,
lineStyle: {
normal: {
opacity: 0.05,
width: 0.5,
curveness: 0.3
}
},
// 设置混合模式为叠加
blendMode: 'lighter'
}
]
})
})
} }
} }
</script> </script>
@ -404,6 +497,9 @@ export default {
*::after *::after
box-sizing border-box box-sizing border-box
html
scroll-behavior smooth
body body
margin 0 margin 0
padding 3em 0 0 padding 3em 0 0

60
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "vue-echarts", "name": "vue-echarts",
"version": "3.0.8", "version": "3.0.9",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1592,7 +1592,8 @@
"dependencies": { "dependencies": {
"lodash": { "lodash": {
"version": "4.17.4", "version": "4.17.4",
"resolved": "", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true "dev": true
} }
} }
@ -1616,7 +1617,8 @@
"dependencies": { "dependencies": {
"lodash": { "lodash": {
"version": "4.17.4", "version": "4.17.4",
"resolved": "", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true "dev": true
} }
} }
@ -1635,7 +1637,8 @@
"dependencies": { "dependencies": {
"lodash": { "lodash": {
"version": "4.17.4", "version": "4.17.4",
"resolved": "", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true "dev": true
} }
} }
@ -4695,28 +4698,32 @@
} }
}, },
"echarts": { "echarts": {
"version": "4.0.4", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-4.0.4.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.1.0.tgz",
"integrity": "sha512-PDWGchRwBvMcNJbg94/thIIDgD8Jw2APtbK6K9rq1X8h6rQIdQ3IFTEvRwGS9U0zsUgJQQwXFLXIw+RJ/EH3fw==", "integrity": "sha512-gP1e1fNnAj9KJpTDLXV21brklbfJlqeINmpQDJCDta9TX3cPoqyQOiDVcEPzbOVHqgBRgTOwNxC5iGwJ89014A==",
"requires": { "requires": {
"zrender": "4.0.3" "zrender": "4.0.4"
},
"dependencies": {
"zrender": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-4.0.3.tgz",
"integrity": "sha512-LdkntRaNogzKAwlICuS0wdZcYaeA94llQ0SWqsgbcd6SPasgkjstaoe6vr5P9Pd2ID/rlhf3UrmIuFzqOLdDuA=="
}
} }
}, },
"echarts-liquidfill": { "echarts-liquidfill": {
"version": "1.1.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/echarts-liquidfill/-/echarts-liquidfill-1.1.1.tgz", "resolved": "https://registry.npmjs.org/echarts-liquidfill/-/echarts-liquidfill-2.0.2.tgz",
"integrity": "sha512-OIOZt4cRrP58pzG1USp+fndcd/bQUQEp6mbmR5JGOpUQEpsaq1xR78kIlMTecI8wvg8HK+zRBe73iv9Ud36tmQ==", "integrity": "sha512-4myPAwexzcQZg8QwpxLKYHqI/1aDhhcxBLRci3WBYHPLIdAd6tx6Qd5BLZwINJNm8e03bbKzTa3PdCLHQ5k1BA==",
"dev": true, "dev": true,
"requires": { "requires": {
"echarts": ">=3.8.2", "echarts": "^4.1.0",
"zrender": ">=3.7.2" "zrender": "^4.0.4"
},
"dependencies": {
"echarts": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-4.1.0.tgz",
"integrity": "sha512-gP1e1fNnAj9KJpTDLXV21brklbfJlqeINmpQDJCDta9TX3cPoqyQOiDVcEPzbOVHqgBRgTOwNxC5iGwJ89014A==",
"dev": true,
"requires": {
"zrender": "4.0.4"
}
}
} }
}, },
"electron-to-chromium": { "electron-to-chromium": {
@ -13985,7 +13992,8 @@
}, },
"fill-range": { "fill-range": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
"integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-number": "^2.1.0", "is-number": "^2.1.0",
@ -14022,7 +14030,8 @@
}, },
"fsevents": { "fsevents": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
"integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -16539,10 +16548,9 @@
} }
}, },
"zrender": { "zrender": {
"version": "3.7.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-3.7.4.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.0.4.tgz",
"integrity": "sha512-5Nz7+L1wIoL0+Pp/iOP56jD6eD017qC9VRSgUBheXBiAHgOBJZ4uh4/g6e83acIwa8RKSyZf/FlceKu5ntUuxQ==", "integrity": "sha512-03Vd/BDl/cPXp8E61f5+Xbgr/a4vDyFA+uUtUc1s+5KgcPbyY2m+78R/9LQwkR6QwFYHG8qk25Q8ESGs/qpkZw=="
"dev": true
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "vue-echarts", "name": "vue-echarts",
"version": "3.0.9", "version": "3.1.0",
"description": "ECharts component for Vue.js.", "description": "ECharts component for Vue.js.",
"main": "dist/vue-echarts.js", "main": "dist/vue-echarts.js",
"scripts": { "scripts": {
@ -16,7 +16,7 @@
"author": "Justineo (justice360@gmail.com)", "author": "Justineo (justice360@gmail.com)",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"echarts": "^4.0.4", "echarts": "^4.1.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"resize-detector": "^0.1.7" "resize-detector": "^0.1.7"
}, },
@ -36,7 +36,7 @@
"connect-history-api-fallback": "^1.3.0", "connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1", "copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"echarts-liquidfill": "^1.1.1", "echarts-liquidfill": "^2.0.2",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7", "eslint-friendly-formatter": "^2.0.7",
@ -76,7 +76,8 @@
"webpack-bundle-analyzer": "^2.2.1", "webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0", "webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0", "webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0" "webpack-merge": "^4.1.0",
"zrender": "^4.0.4"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 4.0.0",

View File

@ -13,7 +13,6 @@
import echarts from 'echarts/lib/echarts' import echarts from 'echarts/lib/echarts'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import { addListener, removeListener } from 'resize-detector' import { addListener, removeListener } from 'resize-detector'
import Vue from 'vue'
// enumerating ECharts events for now // enumerating ECharts events for now
const EVENTS = [ const EVENTS = [
@ -60,43 +59,14 @@ export default {
initOptions: Object, initOptions: Object,
group: String, group: String,
autoResize: Boolean, autoResize: Boolean,
watchShallow: Boolean watchShallow: Boolean,
manualUpdate: Boolean
}, },
data () { data () {
return { return {
// deleted to make this.chart not reactive
lastArea: 0 lastArea: 0
} }
}, },
computed: {
// Only recalculated when accessed from JavaScript.
// Won't update DOM on value change because getters
// don't depend on reactive values
width: {
cache: false,
get () {
return this.delegateGet('width', 'getWidth')
}
},
height: {
cache: false,
get () {
return this.delegateGet('height', 'getHeight')
}
},
isDisposed: {
cache: false,
get () {
return !!this.delegateGet('isDisposed', 'isDisposed')
}
},
computedOptions: {
cache: false,
get () {
return this.delegateGet('computedOptions', 'getOption')
}
}
},
watch: { watch: {
group (group) { group (group) {
this.chart.group = group this.chart.group = group
@ -105,7 +75,15 @@ export default {
methods: { methods: {
// provide a explicit merge option method // provide a explicit merge option method
mergeOptions (options, notMerge, lazyUpdate) { mergeOptions (options, notMerge, lazyUpdate) {
if (this.manualUpdate) {
this.manualOptions = options
}
if (!this.chart) {
this.init()
} else {
this.delegateMethod('setOption', options, notMerge, lazyUpdate) this.delegateMethod('setOption', options, notMerge, lazyUpdate)
}
}, },
// just delegates ECharts methods to Vue component // just delegates ECharts methods to Vue component
// use explicit params to reduce transpiled size for now // use explicit params to reduce transpiled size for now
@ -147,14 +125,13 @@ export default {
}, },
delegateMethod (name, ...args) { delegateMethod (name, ...args) {
if (!this.chart) { if (!this.chart) {
Vue.util.warn(`Cannot call [${name}] before the chart is initialized. Set prop [options] first.`, this) this.init()
return
} }
return this.chart[name](...args) return this.chart[name](...args)
}, },
delegateGet (name, method) { delegateGet (name, method) {
if (!this.chart) { if (!this.chart) {
Vue.util.warn(`Cannot get [${name}] before the chart is initialized. Set prop [options] first.`, this) this.init()
} }
return this.chart[method]() return this.chart[method]()
}, },
@ -172,7 +149,7 @@ export default {
chart.group = this.group chart.group = this.group
} }
chart.setOption(this.options, true) chart.setOption(this.manualOptions || this.options || {}, true)
// expose ECharts events as custom events // expose ECharts events as custom events
EVENTS.forEach(event => { EVENTS.forEach(event => {
@ -188,7 +165,7 @@ export default {
// emulate initial render for initially hidden charts // emulate initial render for initially hidden charts
this.mergeOptions({}, true) this.mergeOptions({}, true)
this.resize() this.resize()
this.mergeOptions(this.options, true) this.mergeOptions(this.options || this.manualOptions || {}, true)
} else { } else {
this.resize() this.resize()
} }
@ -197,6 +174,36 @@ export default {
addListener(this.$el, this.__resizeHandler) addListener(this.$el, this.__resizeHandler)
} }
Object.defineProperties(this, {
// Only recalculated when accessed from JavaScript.
// Won't update DOM on value change because getters
// don't depend on reactive values
width: {
configurable: true,
get: () => {
return this.delegateGet('width', 'getWidth')
}
},
height: {
configurable: true,
get: () => {
return this.delegateGet('height', 'getHeight')
}
},
isDisposed: {
configurable: true,
get: () => {
return !!this.delegateGet('isDisposed', 'isDisposed')
}
},
computedOptions: {
configurable: true,
get: () => {
return this.delegateGet('computedOptions', 'getOption')
}
}
})
this.chart = chart this.chart = chart
}, },
destroy () { destroy () {
@ -207,20 +214,24 @@ export default {
this.chart = null this.chart = null
}, },
refresh () { refresh () {
if (this.chart) {
this.destroy() this.destroy()
this.init() this.init()
} }
}
}, },
created () { created () {
if (!this.manualUpdate) {
this.$watch('options', options => { this.$watch('options', options => {
if (!this.chart && options) { if (!this.chart && options) {
this.init() this.init()
} else { } else {
this.chart.setOption(this.options, true) this.chart.setOption(this.options, true)
} }
}, { deep: !this.watchShallow }) }, { deep: this.watchShallow })
}
let watched = ['theme', 'initOptions', 'autoResize', 'watchShallow'] let watched = ['theme', 'initOptions', 'autoResize', 'manualUpdate', 'watchShallow']
watched.forEach(prop => { watched.forEach(prop => {
this.$watch(prop, () => { this.$watch(prop, () => {
this.refresh() this.refresh()

1
static/flight.json Normal file

File diff suppressed because one or more lines are too long