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
* 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.
* `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

View File

@ -224,9 +224,9 @@ export default {
这个 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>
<h2 id="connect">
<a href="connect">Connectable charts</a>
<a href="#connect">Connectable charts</a>
<button :class="{
round: true,
expand: expand.connect
@ -187,6 +187,25 @@
</p>
</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>
<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>
@ -234,8 +253,10 @@ import 'echarts/lib/component/legend'
import 'echarts/lib/component/title'
import 'echarts/lib/component/visualMap'
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 getBar from './data/bar'
import pie from './data/pie'
@ -284,7 +305,8 @@ export default {
scatter: true,
map: true,
radar: true,
connect: true
connect: true,
flight: true
},
initOptions: {
renderer: options.renderer || 'canvas'
@ -328,7 +350,7 @@ export default {
if (this.seconds === 0) {
clearTimeout(timer)
bar.hideLoading()
bar.mergeOptions(getBar())
this.bar = getBar()
}
}, 1000)
},
@ -394,6 +416,77 @@ export default {
dataIndex
})
}, 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>
@ -404,6 +497,9 @@ export default {
*::after
box-sizing border-box
html
scroll-behavior smooth
body
margin 0
padding 3em 0 0

60
package-lock.json generated
View File

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

View File

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

View File

@ -13,7 +13,6 @@
import echarts from 'echarts/lib/echarts'
import debounce from 'lodash/debounce'
import { addListener, removeListener } from 'resize-detector'
import Vue from 'vue'
// enumerating ECharts events for now
const EVENTS = [
@ -60,43 +59,14 @@ export default {
initOptions: Object,
group: String,
autoResize: Boolean,
watchShallow: Boolean
watchShallow: Boolean,
manualUpdate: Boolean
},
data () {
return {
// deleted to make this.chart not reactive
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: {
group (group) {
this.chart.group = group
@ -105,7 +75,15 @@ export default {
methods: {
// provide a explicit merge option method
mergeOptions (options, notMerge, lazyUpdate) {
this.delegateMethod('setOption', options, notMerge, lazyUpdate)
if (this.manualUpdate) {
this.manualOptions = options
}
if (!this.chart) {
this.init()
} else {
this.delegateMethod('setOption', options, notMerge, lazyUpdate)
}
},
// just delegates ECharts methods to Vue component
// use explicit params to reduce transpiled size for now
@ -147,14 +125,13 @@ export default {
},
delegateMethod (name, ...args) {
if (!this.chart) {
Vue.util.warn(`Cannot call [${name}] before the chart is initialized. Set prop [options] first.`, this)
return
this.init()
}
return this.chart[name](...args)
},
delegateGet (name, method) {
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]()
},
@ -172,7 +149,7 @@ export default {
chart.group = this.group
}
chart.setOption(this.options, true)
chart.setOption(this.manualOptions || this.options || {}, true)
// expose ECharts events as custom events
EVENTS.forEach(event => {
@ -188,7 +165,7 @@ export default {
// emulate initial render for initially hidden charts
this.mergeOptions({}, true)
this.resize()
this.mergeOptions(this.options, true)
this.mergeOptions(this.options || this.manualOptions || {}, true)
} else {
this.resize()
}
@ -197,6 +174,36 @@ export default {
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
},
destroy () {
@ -207,20 +214,24 @@ export default {
this.chart = null
},
refresh () {
this.destroy()
this.init()
if (this.chart) {
this.destroy()
this.init()
}
}
},
created () {
this.$watch('options', options => {
if (!this.chart && options) {
this.init()
} else {
this.chart.setOption(this.options, true)
}
}, { deep: !this.watchShallow })
if (!this.manualUpdate) {
this.$watch('options', options => {
if (!this.chart && options) {
this.init()
} else {
this.chart.setOption(this.options, true)
}
}, { deep: this.watchShallow })
}
let watched = ['theme', 'initOptions', 'autoResize', 'watchShallow']
let watched = ['theme', 'initOptions', 'autoResize', 'manualUpdate', 'watchShallow']
watched.forEach(prop => {
this.$watch(prop, () => {
this.refresh()

1
static/flight.json Normal file

File diff suppressed because one or more lines are too long