Does defining variables inside of a computed property have any impact on the perfomance of Vue components?
Background: I built a table component which generates a HTML table generically from the passed data and has different filters per column, filter for the whole table, sort keys, etc., so I'm defining a lot of local variables inside the computed property.
Imagine having an array of objects:
let data = [
{ id: "y", a: 1, b: 2, c: 3 },
{ id: "z", a: 11, b: 22, c: 33 }
]
..which is used by a Vue component to display the data:
<template>
<div>
<input type="text" v-model="filterKey" />
</div>
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>C</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredData" :key="item.id">
<td v-for="(value, key) in item" :key="key">
{{ value }}
</td>
</tr>
</tbody>
</table>
</template>
The data gets filtered via input:
<script>
export default {
props: {
passedData: Array,
},
data() {
return {
filterKey: null,
};
},
computed: {
filteredData() {
// defining local scope variables
let data = this.passedData;
let filterKey = this.filterKey;
data = data.filter((e) => {
// filter by filterKey or this.filterKey
});
return data;
},
},
};
</script>
My question refers to let data = .. and let filterKey = .. as filteredData() gets triggered from any change of the filterKey (defined in data()) so the local variable gets updated too, although they're not "reactive" in a Vue way.
Is there any impact on the performance when defining local variables inside a computed property? Should you use the reactive variables from data() (e. g. this.filterKey) directly inside of the computed property?
The best way to test if something affects performance, is to actually test it.
According to my tests below, it is consistency more than 1000% slower to use this.passedData instead of adding a variable on top of the function. (869ms vs 29ms)
Make sure you run your benchmarks on the target browsers you write your application for the best results.
function time(name, cb) {
var t0 = performance.now();
const res = cb();
if(res !== 20000000) {
throw new Error('wrong result: ' + res);
}
var t1 = performance.now();
document.write("Call to "+name+" took " + (t1 - t0) + " milliseconds.<br>")
}
function withoutLocalVar() {
const vue = new Vue({
computed: {
hi() {
return 1;
},
hi2() {
return 1;
},
test() {
let sum = 0;
for(let i = 0; i < 10000000; i++) { // 10 000 000
sum += this.hi + this.hi2;
}
return sum;
},
}
})
return vue.test;
}
function withLocalVar() {
const vue = new Vue({
computed: {
hi() {
return 1;
},
hi2() {
return 1;
},
test() {
let sum = 0;
const hi = this.hi;
const hi2 = this.hi2;
for(let i = 0; i < 10000000; i++) { // 10 000 000
sum += hi + hi2;
}
return sum;
},
}
})
return vue.test;
}
function benchmark() {
const vue = new Vue({
computed: {
hi() {
return 1;
},
hi2() {
return 1;
},
test() {
let sum = 0;
const hi = 1;
const hi2 = 1;
for(let i = 0; i < 10000000; i++) { // 10 000 000
sum += hi + hi2;
}
return sum;
},
}
})
return vue.test;
}
time('withoutLocalVar - init', withoutLocalVar);
time('withLocalVar - init', withLocalVar);
time('benchmark - init', benchmark);
time('withoutLocalVar - run1', withoutLocalVar);
time('withLocalVar - run1', withLocalVar);
time('benchmark - run1', benchmark);
time('withoutLocalVar - run2', withoutLocalVar);
time('withLocalVar - run2', withLocalVar);
time('benchmark - run2', benchmark);
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
Related
Intended outcome:
We did a pagination implementation, following the apollo docs (https://www.apollographql.com/docs/react/pagination/offset-based/). Using a read/merge function
We expect that the read function is called with the new variables, once we call the fetchMore function and update the variables manually.
Actual outcome:
We do not manage to get the pagination working, the data is fetched correctly but is not displayed properly. The bad display is caused by the variables that are not updated. The "read" function always receives the variables of the original query.
useEffect(() => {
if (isDefined(fetchMore)) {
fetchMore({
variables: {
offset:
currentPage === 1
? (currentPage - 1) * total
: (currentPage - 1) * total + 1,
first: total,
searchString: searchString
}
})
} else {
getData({
variables: {
offset: 0,
first: total,
searchString: searchString
}
})
} }, [currentPage, fetchMore, searchString, getData])
We have also tried to manually update our variables, since we do not use useQuery, but instead useLazyQuery.
client
.watchQuery({
query: GET_REPORTS,
variables: {
offset: 0,
first: total,
searchString: searchString
}
})
.setVariables({
offset:
currentPage === 1
? (currentPage - 1) * total
: (currentPage - 1) * total + 1,
first: 25,
searchString
})
Our typePolicy:
typePolicies: {
Query: {
fields: {
REPORT: {
keyArgs: false,
read(existing, {args}) {
**//here we would expect for our args to change, the moment we try fetch our
//second page**
return existing && existing.slice(args.offset, args.offset + args.first)
},
// The keyArgs list and merge function are the same as above.
merge(existing, incoming, { args: { offset = 0 } }) {
const merged = existing ? existing.slice(0) : []
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i]
}
return merged
}
}
}
}
}
I had the same issue earlier, turns out since you are using useLazyQuery, you have to query the function first, be using fetchMore.
Example
const [getList,{loading, fetchMore}] = useLazyQuery (GET_LIST)
Then implement a condition like so;
if(! fetchMore){ getList(....passargs) }else{ fetchMore (...passargs) }
This is because fetchMore will be undefined without calling getList.
vue component won't wait for data from controller using axios get, it prompt error:
index.vue?d4c7:200 Uncaught (in promise) TypeError: Cannot read property 'ftth' of undefined
my code are below:
<template>
<div class="dashboard-editor-container">
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData"/>
</el-row>
</div>
</template>
<script>
import LineChart from './components/LineChart';
import axios from 'axios';
const lineChartData = {
all: {
FTTHData: [],
VDSLData: [],
ADSLData: [],
},
};
export default {
name: 'Dashboard',
components: {
LineChart,
},
data() {
return {
lineChartData: lineChartData.all,
};
},
created() {
this.getData();
},
methods: {
handleSetLineChartData(type) {
this.lineChartData = lineChartData[type];
},
async getData() {
axios
.get('/api/data_graphs')
.then(response => {
console.log(response.data);
var data = response.data;
var i = 0;
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
});
},
},
};
</script>
Do I have to use watch method?
First, because you have such a nested data structure you'll want a computed property to return whether the data is loaded or not. Normally, you could do this check in the template.
computed: {
isDataLoaded() {
const nestedLoaded = Object.keys(this.lineChartData).map(key => this.lineChartData[key].length !== 0)
return this.lineChartData && nestedLoaded.length !== 0
}
}
You can use v-if="isDataLoaded" to hide the element until the data has been loaded.
It is not exactly clear how response.data looks like, but because you're using Object.keys I'm assuming it's an object.
If you need to loop over the keys then when using numeric indexes you most likely won't get an object. So you need to get the key and index i and use that value to access the object. Change this:
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
to this:
const keys = Object.keys(data)
for (i = keys.length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[keys[i]]['ftth']);
lineChartData.all.VDSLData.push(data[keys[i]]['vdsl']);
lineChartData.all.ADSLData.push(data[keys[i]]['adsl']);
}
But for looping over object's keys is easier to use this:
for (let key in data) {
lineChartData.all.FTTHData.push(data[key]['ftth']);
lineChartData.all.VDSLData.push(data[key]['vdsl']);
lineChartData.all.ADSLData.push(data[key]['adsl']);
}
The alternative syntax will feed you keys and in my opinion is easier to read.
For the mean time:
Use set Axios Timeout 5000ms
axios
.get('/api/data_graphs', { timeout: 5000 })
.then(response => {
console.log(response.data);
var data = response.data;
var i = 0;
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
this.lineChartIsLoaded = true;
});
Use v-if in vue component
<line-chart v-if="lineChartIsLoaded" :chart-data="lineChartData" :date-data="dateData" />
Set lineChartIsLoaded to false at default
const lineChartIsLoaded = false;
You can write dummy data in your data properties before real ones are loading
all: {
FTTHData: ["Loading..."],
VDSLData: ["Loading..."],
ADSLData: ["Loading..."],
},
I am using ag-grid with angular 4.
I am using infinite scrolling as the rowModelType. But since my data is huge, we want to first call just 100 records in the first ajax call and when the scroll reaches the end, the next ajax call needs to be made with the next 100 records? How can i do this using ag-grid in angular 4.
This is my current code
table-component.ts
export class AssaysTableComponent implements OnInit{
//private rowData;
private gridApi;
private gridColumnApi;
private columnDefs;
private rowModelType;
private paginationPageSize;
private components;
private rowData: any[];
private cacheBlockSize;
private infiniteInitialRowCount;
allTableData : any[];
constructor(private http:HttpClient, private appServices:AppServices) {
this.columnDefs = [
{
headerName: "Date/Time",
field: "createdDate",
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
checkboxSelection: true,
width: 250,
cellRenderer: "loadingRenderer"
},
{headerName: 'Assay Name', field: 'assayName', width: 200},
{headerName: 'Samples', field: 'sampleCount', width: 100}
];
this.components = {
loadingRenderer: function(params) {
if (params.value !== undefined) {
return params.value;
} else {
return '<img src="../images/loading.gif">';
}
}
};
this.rowModelType = "infinite";
//this.paginationPageSize = 10;
this.cacheBlockSize = 10;
this.infiniteInitialRowCount = 1;
//this.rowData = this.appServices.assayData;
}
ngOnInit(){
}
onGridReady(params) {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
//const allTableData:string[] = [];
//const apiCount = 0;
//apiCount++;
console.log("assayApiCall>>",this.appServices.assayApiCall);
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "50";
if(this.appServices.assayApiCall>0){
console.log("this.allTableData >> ",this.allTableData);
assaysObj.startEvalulationKey = {
}
}
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
var dataSource = {
rowCount: null,
getRows: function (params) {
console.log("asking for " + params.startRow + " to " + params.endRow);
setTimeout(function () {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
this.allTableData = this.allTableData.concat(response);
}
var rowsThisPage = response.slice(params.startRow, params.endRow);
var lastRow = -1;
if (response.length <= params.endRow) {
lastRow = response.length;
}
params.successCallback(rowsThisPage, lastRow);
}, 500);
}
}
params.api.setDatasource(dataSource);
this.appServices.setIsAssaysAvailable(true);
this.appServices.assayApiCall +=1;
}
else{
this.appServices.setIsAssaysAvailable(false)
}
}
)
}
}
I will need to call this.appServices.downloadAssayFiles(assaysObj) at the end of 100 rows again to get the next set of 100 rows.
Please suggest a method of doing this.
Edit 1:
private getRowData(startRow: number, endRow: number): Observable<any[]> {
var rowData =[];
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "10";
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
rowData = response;
this.allTableData = this.allTableData.concat(response);
}
this.appServices.setIsAssaysAvailable(true);
}
else{
this.appServices.setIsAssaysAvailable(false)
}
console.log("rowdata>>",rowData);
});
return Observable.of(rowData);
}
onGridReady(params: any) {
console.log("onGridReady");
var dataSource = {
getRows: (params: IGetRowsParams) => {
this.info = "Getting datasource rows, start: " + params.startRow + ", end: " + params.endRow;
console.log(this.info);
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(dataSource);
}
Result 1 : The table is not loaded with the data. Also for some reason the service call this.appServices.downloadAssayFiles is being made thrice . Is there something wrong with my logic here.
There's an example of doing exactly this on the ag-grid site: https://www.ag-grid.com/javascript-grid-infinite-scrolling/.
How does your code currently act? It looks like you're modeling yours from the ag-grid docs page, but that you're getting all the data at once instead of getting only the chunks that you need.
Here's a stackblitz that I think does what you need. https://stackblitz.com/edit/ag-grid-infinite-scroll-example?file=src/app/app.component.ts
In general you want to make sure you have a service method that can retrieve just the correct chunk of your data. You seem to be setting the correct range of data to the grid in your code, but the issue is that you've already spent the effort of getting all of it.
Here's the relevant code from that stackblitz. getRowData is the service call that returns an observable of the records that the grid asks for. Then in your subscribe method for that observable, you supply that data to the grid.
private getRowData(startRow: number, endRow: number): Observable<any[]> {
// This is acting as a service call that will return just the
// data range that you're asking for. In your case, you'd probably
// call your http-based service which would also return an observable
// of your data.
var rowdata = [];
for (var i = startRow; i <= endRow; i++) {
rowdata.push({ one: "hello", two: "world", three: "Item " + i });
}
return Observable.of(rowdata);
}
onGridReady(params: any) {
console.log("onGridReady");
var datasource = {
getRows: (params: IGetRowsParams) => {
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(datasource);
}
I have dynamic products list to create an invoice. Now I want to search the product from select->option list. I found a possible solution like Vue-select in vuejs but I could not understand how to convert my existing code to get benefit from Vue-select. Would someone help me please, how should I write code in 'select' such that I can search product at a time from the list?
My existing code is -
<td>
<select id="orderproductId" ref="selectOrderProduct" class="form-control input-sm" #change="setOrderProducts($event)">
<option>Choose Product ...</option>
<option :value="product.id + '_' + product.product_name" v-for="product in invProducts">#{{ product.product_name }}</option>
</select>
</td>
And I want to convert it something like -
<v-select :options="options"></v-select>
So that, I can search products also if I have many products. And My script file is -
<script>
Vue.component('v-select', VueSelect.VueSelect);
var app = new Vue({
el: '#poOrder',
data: {
orderEntry: {
id: 1,
product_name: '',
quantity: 1,
price: 0,
total: 0,
},
orderDetail: [],
grandTotal: 0,
invProducts: [],
invProducts: [
#foreach ($productRecords as $invProduct)
{
id:{{ $invProduct['id'] }},
product_name:'{{ $invProduct['product_name'] }}',
},
#endforeach
],
},
methods: {
setOrderProducts: function(event) {
//alert('fired');
var self = this;
var valueArr = event.target.value.split('_');
var selectProductId = valueArr[0];
var selectProductName = valueArr[1];
self.orderEntry.id = selectProductId;
self.orderEntry.product_name = selectProductName;
$('#invQuantity').select();
},
addMoreOrderFields:function(orderEntry) {
var self = this;
if(orderEntry.product_name && orderEntry.quantity && orderEntry.price > 0) {
self.orderDetail.push({
id: orderEntry.id,
product_name: orderEntry.product_name,
quantity: orderEntry.quantity,
price: orderEntry.price,
total: orderEntry.total,
});
self.orderEntry = {
id: 1,
product_name:'',
productId: 0,
quantity: 1,
price: 0,
total: 0,
}
$('#orderproductId').focus();
self.calculateGrandTotal();
} else {
$('#warningModal').modal();
}
this.$refs.selectOrderProduct.focus();
},
removeOrderField:function(removeOrderDetail) {
var self = this;
var index = self.orderDetail.indexOf(removeOrderDetail);
self.orderDetail.splice(index, 1);
self.calculateGrandTotal();
},
calculateGrandTotal:function() {
var self = this;
self.grandTotal = 0;
self.totalPrice = 0;
self.totalQuantity = 0;
self.orderDetail.map(function(order){
self.totalQuantity += parseInt(order.quantity);
self.totalPrice += parseInt(order.price);
self.grandTotal += parseInt(order.total);
});
},
setTotalPrice:function(event){
var self = this;
//self.netTotalPrice();
self.netTotalPrice;
}
},
computed: {
netTotalPrice: function(){
var self = this;
var netTotalPriceValue = self.orderEntry.quantity * self.orderEntry.price;
var netTotalPriceInDecimal = netTotalPriceValue.toFixed(2);
self.orderEntry.total = netTotalPriceInDecimal;
return netTotalPriceInDecimal;
}
}
});
Assuming that invProducts is an array of product objects and each product object has a product_name property, try this snippet.
<v-select #input="selectChange()" :label="product_name" :options="invProducts" v-model="selectedProduct">
</v-select>
Create a new data property called selectedProduct and bind it to the vue-select component. So, whenever the selection in the vue-select changes, the value of selectedProduct also changes. In addition to this, #input event can be used to trigger a method in your component. You can get the selected product in that method and do further actions within that event listener.
methods: {
selectChange : function(){
console.log(this.selectedProduct);
//do futher processing
}
I have this computed property:
computed: {
filteredCars: function() {
var self = this
return self.carros.filter(function(carro) {
return carro.nome.indexOf(self.busca) !== -1
})
},
},
and i'm using v-for like this:
<tr v-for="carro in filteredCars">
<td>{{carro.nome}}</td>
<td>{{carro.marca}}</td>
<td>{{carro.categoria}}</td>
<td>{{carro.motor}}</td>
<td>{{carro.cambio}}</td>
<td>{{carro.preco}}</td>
</tr>
but I need to create another computed property to limit my data quantity, how i call it inside the same v-for?
I'm trying to use filteredCars + another filter, in this case something like 'limit' filter from vue 1.x. I've done an example using Vue 1.x but i need to do using Vue 2.x.
Vue.filter('limit', function (value, amount) {
return value.filter(function(val, index, arr){
return index < amount;
});
<tr v-for="carro in carros | limit upperLimit>
...
</tr>
Just use Array.prototype.slice (Array.prototype.splice should work too) in the computed property.
data: {
carros: [...],
upperLimit: 30
},
computed: {
filteredCars: function() {
const arr = this.carros.filter(function(carro) {
return carro.nome.indexOf(self.busca) !== -1
});
if (arr.length > this.upperLimit) {
return arr.slice(0, this.upperLimit + 1);
}
return arr;
},
}