踩坑场景
- 父子组件传值为对象,就会出现违背了vue单向流逻辑的现象,修改组件props的值后,父组件相对应的值也会被修改。因为引用了相同的对象地址,所以修改了父组件原值。vue中⽗⼦组件通信最常⽤的⽅式是props和$emit,通常来说,⽗级props 的更新会向下流动到⼦组件中,但是反过来则不⾏,是为了防⽌⼦组件意外改变⽗级组件的状态,导致你的应⽤的数据流向难以理解。但是当⽗组件的传值是数组或者对象时,⼦组件中不仅能够直接修改props,还不会报错,在⼦组件中改变这个对象或数组本⾝,将会影响到⽗组件的状态。
- 切换路由后,原页面未返回数据的请求还处于pending状态,消耗资源。父组件根据条件去请求数据,然后传值给子组件去渲染,在这种情况下会出现延时长的请求数据会覆盖延时低的数据,当我先请求条件为A的数据,因数据量大可能接口返回为20s,在等待返回的过程中,我又把条件切成了B,B数据量少,2s内就返回了数据,此时页面渲染的是条件B的数据,但是当20s后,条件A的数据返回,就会在条件B的页面渲染了条件A的数据,很难判断哪个接口返回的是哪个条件的数据。
总结就是三种情况:
切换路由,之前的未返回的请求还处于pending,接口还在请求数据。
不切换路由,切换组件,之前的未返回的请求还处于pending,接口还在请求数据。
不切换路由,不切换组件,切换请求参数,之前的未返回的请求还处于pending,接口还在请求数据。
场景一
原因
浅拷贝与深拷贝:只是针对复杂数据类型(Object,Array)的复制问题。浅拷贝与深拷贝都可以实现在已有对象上再⽣出⼀份的作⽤, 但是对象的实例是存储在堆内存中然后通过⼀个引⽤值(指针)去操作对象,针由此拷贝的时候就存在两种情况了:拷贝引⽤和拷贝实例,这也是浅拷贝和深拷贝的区别
浅拷贝:拷贝引⽤。将原对象或者原数组的引用地址直接赋值给新对象,新数组或新对象只是原数据的一个引用。拷贝后的对象都是指向同⼀个对象的实例,彼此之间的操作会互相影响。
//拷贝原对象的引用
/**
* 对象的浅拷贝
*/
var obj1 = {
key: 'level',
name: "等级"
}
var obj2 = obj1;
obj2['value'] = 'INFO';
console.log(obj1); //Object {key: "level", name: 等级, value: "INFO"}
console.log(obj2); //Object {key: "level", name: 等级, value: "INFO"}
/**
* 数组的浅拷贝
*/
var arr1 = [1, 2, 3, '4'];
var arr2 = arr1;
arr2[1] = "test";
console.log(arr1); // [1, "test", 3, "4"]
console.log(arr2); // [1, "test", 3, "4"]
//源对象拷贝实例,其属性对象拷贝引⽤:
//这种情况,外层源对象是拷贝实例,如果其属性元素为复杂数据类型(Object、Array)时,内层元素拷贝引⽤。
//对源对象直接操作,不影响另外⼀个对象,但是对其属性操作时候,会改变另外⼀个对象的属性的值。
/**
* 对象的浅拷贝
* ES6的 Object.assign() 和对象扩展运算符...
*/
var obj1 = {
name:'zcy',
age: 22,
social: {
blog: 'www.zcy.com'
},
skills: ['js', 'html', 'css', 'python']
}
var obj2 = Object.assign({},obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出true,说明对于Object类型的属性是拷贝引⽤
console.log(obj1.skills === obj2.skills) // 输出true,说明对于Array类型的属性是拷贝引⽤
var obj3 = {...obj1};
console.log(obj1 === obj3) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.skills === obj3.skills) // 输出true,说明对于Array类型的属性是拷贝引⽤
console.log(obj1.skills === obj3.skills) // 输出true,说明对于Array类型的属性是拷贝引⽤
/**
* 数组的浅拷贝
* Array.prototype.slice()
*/
var arr1 = [{name: "zcy"}, {name: "ops"}];
var arr2 = arr1.slice(0);
console.log(arr1 === arr2); // 输出false,说明外层数组拷贝的是实例
console.log(arr1[0] === arr2[0]); // 输出true,说明其元素拷贝的是引⽤
/**
* 数组的浅拷贝
* Array.prototype.concat()
*/
var arr1 = [{name: "zcy"}, {name: "ops"}];
var arr2 = arr1.concat();
console.log(arr1 === arr2); // 输出false,说明外层数组拷贝的是实例
console.log(arr1[0] === arr2[0]); // 输出true,说明其元素拷贝的是引⽤
深拷贝:创建一个新的对象和数组,将原对象的各种属性的值赋值过来,是一个新的对象。在堆中重新分配内存,把原对象所有属性都新建拷贝。
/**
* 对象的深拷贝
* JSON.stringify()和JSON.parse()
* 这种深拷贝最简单,但有其局限性
*/
var obj1 = {
name:'zcy',
age: 22,
social: {
blog: 'www.zcy.com'
},
skills: ['js', 'html', 'css', 'python']
}
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出false,说明对于Object类型的属性也是拷贝实例
console.log(obj1.skills === obj2.skills) // 输出false,说明对于Array类型的属性也是拷贝实例
/**
* 对象的深拷贝
* lodash的_.cloneDeep
*/
var obj1 = {
name:'zcy',
age: 22,
social: {
blog: 'www.zcy.com'
},
skills: ['js', 'html', 'css', 'python']
}
var obj2 = _.cloneDeep(obj1);
console.log(obj1 === obj2) // 输出false,说明外层数组拷贝的是实例
console.log(obj1.social === obj2.social) // 输出false,说明对于Object类型的属性也是拷贝实例
console.log(obj1.skills === obj2.skills) // 输出false,说明对于Array类型的属性也是拷贝实例
对于js中的变量,值类型存放在栈中,引用类型的地址存放在栈中,对应的值存放在堆中。
当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针。
传值:String、Number、Boolean
传引用:Array、Object
function setName(obj) {
obj.name = 'zcy';
obj = new Object();
obj.name = 'ops';
}
var person = new Object();
setName(person);
alert(person.name); // zcy
// setName传的是person的地址,setName里面new的是另一个对象的地址
// 函数的参数其实就是函数作用域内部的变量,函数执行完之后就会销毁。
function change(a, b, c) {
a = a * 10;
b.item = "changed";
c = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
change(num, obj1, obj2);
console.log(num); // 10
console.log(obj1.item); // changed
console.log(obj2.item); // unchanged
解决
- Object.assgin() ,它只能深拷贝第⼀层,深层的还是浅拷贝。如果对象不是多层嵌套,可以使用此方法。
- 对象扩展运算符...,var obj2 = {...obj1} 。与Object.assgin() 功能以及弊端一致。
- 子组件使用JSON.parse(JSON.stringify(obj)),修改原来的引用。这种方式有局限性,当值为 undefined、function、symbol 会在转换过程中被忽略。对象值有这三种的话⽤这种⽅法会导致属性丢失。
- lodash的_.cloneDeep,完全深拷贝。需要引入包
import cloneDeep from 'lodash.clonedeep'
const routerMap = cloneDeep(asyncRouterMap)
- 父组件传值的时候不要直接原数据,可以定义新变量,传递新变量。
💡 props禁止修改,直接修改会报异常(除了引用数据类型)
场景二
原因
经常会遇到当前页面未加载完毕时跳转路由或者返回操作, 但是通过network会发现, 若请求数据量大或者网络环境较差的情况下, 会长时间处于pending状态, 切换路由后在network中添加新的请求但是已经存在pending的请求依然存在。当我在项目中做了拓扑图展示的时候,因数据量大有时候会30s返回数据, 等待不耐烦后可能会切换参数或者切换路由的操作, 但是此刻旧的请求还是会一直在加载中, 直到该请求加载成功或超时才肯罢休。最终返回了一些不必要的结果,同时也对web性能造成一定的影响。
我们项目中使用的是axios的异步ajax框架。Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中,axios时目前最流行的ajax封装库之一,很方便地实现ajax请求的发送。
Promise是一个构造函数,自身有all、reject、resolve这几个方法,原型上有then、catch等方法。Promise处理方法的异步执行,Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected,then就跟我们平时的回调函数一个意思,异步任务执行完成之后被执行。简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
💡 参考文章,axios中文网。 www.axios-js.com/zh-cn/docs/
💡 参考文章,www.cnblogs.com/lvdabao/p/e…
使用cancel token取消请求
单个请求取消
- 使用 CancelToken.source 工厂方法创建 cancel token
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
- 通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token。(我使用的)
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// cancel the request
cancel();
多个请求取消:单个请求方式取消也可以应用到多个请求,但每次请求都需要调用cancelToken方法和cancel方法,这种公用配置可以在axios拦截器中实现。
axios拦截器:就是函数。两种类型:
请求拦截器:用于拦截请求,自定义做一个逻辑后再把请求发送,可以用于配置公用的逻辑,就不用每个请求都配一遍。发送请求前,请求被请求拦截器拦截了,并且请求拦截器返回了一个非Promise实例的对象config。
响应拦截器:用于拦截响应,做一些处理后再出发响应回调。
- 针对pending的请求,请求拦截器将其保存在一个全局的列表里,切换路由或者组件后,取消相同的或者是全部的请求。
axios.interceptors.request.use(
config => {
config.cancelToken = new axios.CancelToken(function (cancel) {
store.push(cancel)
})
return config
}
)
- 针对pending的请求,请求拦截器将其保存在一个全局的列表里,发出请求前取消相同的pending请求,这种方法有一个严重的缺点就是:针对同url且同参数的请求,由于传参不一致,所以无法阻止上个请求还在pending。本次我的场景没有使用到。
/**
* 添加pending请求
*
* **/
const addPending = (config) => {
config.cancelToken = config.cancelToken || new CancelToken(c => {
if (!pending.has(url)) {
pending.set(url, c);
}
})
}
/**
* 取消单个pending请求
* **/
const cancelPending = (config) => {
if (pending.has(url)) {
const cancel = pending.get(url);
cancel('cancel request');
pending.delete(url);
}
}
/**
* 清空pending请求(退出页面时)
* **/
const clearAllPending = () => {
for (const [url, cancel] of pending) {
cancel('cancel request');
}
pending.clear();
}
service.interceptors.request.use(
config => {
cancelPending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
cancelPending(response) // 在请求结束后,移除本次请求
},
error => {
return Promise.reject(error)
}
)
解决
- 不切换路由,不切换组件,切换请求参数,取消处于pending请求。
data () {
return {
cancelAjax: null,
}
handleGetSpanDependency () {
// 再次使用这个接口的时候,cancelAjax方法就会执行cancel
if (typeof this.cancelAjax === 'function') {
this.cancelAjax()
}
// 将cancelAjax 传输到axios,用this
SpanDependency(params, this).then(res => {
// ***
})
},
/**
API接口处,引入axios,定义CancelToken,写new CancelToken
注意这块传参为两个参数,第一个是业务正常的请求参数,第二个是发起请求页面的this
*/
export function SpanDependency (parameter, that) {
// 手动取消请求
const CancelToken = Axios.CancelToken
let cancelFun = null
return axios({
url: '***',
method: 'get',
params: parameter,
cancelToken: new CancelToken(function executor(c) {
that.cancelAjax = c;
})
// 下面的代码主要用在我下面介绍的批量取消时,可以为某个接口制定白名单
//cancelToken: new CancelToken(function executor (c) {
// that.cancelAjax = c
// cancelFun = c
//}),
//cancelFun: cancelFun
})
}
- 切换路由,取消处于pending请求。
// axios请求头设置
service.interceptors.request.use(config => {
const whiteList = ['aa', 'bb'] // 白名单接口
if (config.url && whiteList.every(item => !config.url.includes(item))) {
config.cancelToken = new CancelToken((cancel) => {
store._axiosPromiseCancel.push(cancel)
})
} else {
store._axiosPromiseCancel.push(config.cancelFun)
}
return config
}, err)
//利用vuex,新建一个store.js,将取消方法cancel放到数组中,
//然后在路由守卫中把数组中存有的cancel方法都执行
const store = { _axiosPromiseCancel: [] }
export default store
//监听路由,取消所有的请求
router.beforeEach((to, from, next) => {
store._axiosPromiseCancel.forEach(e => {
e && e()
})
store._axiosPromiseCancel = []
next()
}
- 不切换路由,切换组件,取消处于pending请求。
与路由切换取消请求逻辑一致,不需要监听路由,根据业务可以把监听放在组件mounted或者destroyed逻辑中。
mounted () {
store._axiosPromiseCancel.forEach(e => {
e && e()
})
store._axiosPromiseCancel = []
}
💡 父组件通过v-if渲染多次相同的子组件,通过加key的方式隔离每个组件