mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2025-08-16 04:31:22 +08:00
feat: manual-update for performance boost
This commit is contained in:
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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` 方法来更新图表。
|
||||
|
||||
### 计算属性
|
||||
|
||||
|
104
demo/Demo.vue
104
demo/Demo.vue
@ -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
60
package-lock.json
generated
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
1
static/flight.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user