前言
大家好,我是前端贰货道士。在整理无论如何,你都必须得知道的js知识时,由于超出掘金字数限制,所以诞生了这篇文章。书接上文岁岁念第24点reduce函数剖析,如果本文对您有帮助,烦请大家一键三连哦, 蟹蟹大家~
碎碎念
此分类用于记载我认为需要整理的js知识
以及我不知道的js知识
。因为是利用零碎时间去整理一些笔记,所以这篇文章后续会持续更新,有兴趣的小伙伴可以先收藏吃灰,哈哈哈。
24. reduce函数剖析(补充)
7. 利用reduce函数将二维数组转换为一维数组
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre, cur) => pre.concat(cur), [])
console.log(newArr) // [0, 1, 2, 3, 4, 5]
8. 利用reduce函数将多维数组转换为一维数组
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
`相较于二维数组的转换,多了一层递归处理`
let newArr = arr => arr.reduce((pre,cur) => pre.concat(Array.isArray(cur) ? newArr(cur) : cur), [])
newArr(arr) // [0, 1, 2, 3, 4, 5, 6, 7]
25. 关于js中的内存机制
js栈内存和堆内存详解—图解基本数据类型和引用数据类型的区别。堆栈
这种数据结构,具有先进后出,后进先出
的特点,先明细几个概念:
-
栈内存: 计算机为浏览器执行
js
代码,在内部开辟的空间,也被称为执行环境栈
。里面存放的是基本数据类型以及对象类型数据的引用地址,地址指向了堆内存里的对象内容。由于栈内存中存放的基础数据类型的大小是固定的,所以栈内存的内存都是操作系统自动分配和释放回收的。 -
堆内存: 堆内存里存放的是除函数的引用类型的值,如数组和对象。由于堆内存所存大小不固定,系统无法自动释放回收,所以需要JS引擎来手动释放这些内存
-
执行上下文: 大致分为三类
a.
全局执行上下文
,有一个全局对象windowb.
函数级上下文
:任何一个函数都有自己特有的执行上下文(函数里面的区域可以访问到函数外部的变量,但是函数外部的区域,是访问不到函数里面的)c.
块级上下文
: 由let
或者const
加一个{}
所组成的区域,就是一个块级上下文(括号区域外是无法访问到括号区域里面声明的let或者const变量的,但是可以访问到括号区域里面的var变量) -
变量对象:存储某个区块代码里声明的值和变量
-
真实JS变量在堆栈中的存储:
function foo() {
var a = 1
var obj = {
name: 'xiaoming'
}
}
foo()
原始类型的值会直接存储在上下文中,而上下文则存储在栈内存中;
引用类型的值实际上会被存储在堆内存中,每一个值都对应着一个地址,然后在栈内存的执行上下文中将变量的值赋值成对应的地址。
- 栈和堆的溢出:
a. 栈:当递归调用方法时,随着栈深度的增加,JVM
维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。
b. 堆:循环创建对象,通俗点就是不断的new
一个对象。
26. 调试小技巧
1. 打印时添加快照
import { cloneDeep } from 'lodash'
// 调试时,由于data是引用类型,在初始化时深层字段可能会发生变化,需要给个快照,记录打印时的真实data值
console.log("this.data", deepClone(this.data))
2. console.time()和console.timeEnd()必须成对出现,且文案需保持一致才能正常匹配及记录时间
console.time('doSomething花费的时间为:')
...doSomething
console.timeEnd('doSomething花费的时间为:')
27. map(Number)、map(String)、map(Boolean)的使用
1. 使用map(Number)将数组中的每项数据转换为Number类型
let arr1 = [1, "2", 3]
arr1.map(Number) // [1, 2, 3]
let arr2 = ['我是cxk', 18]
arr2.map(Number) // [NaN, 18]
`扩展:判断在函数中传入的,可以转换为Number类型的形参个数`
function getLength(...data) {
return data.map(Number).filter((num) => num || num === 0).length
}
getLength() // 0
getLength(1, 2, 'dsadsa', 's', '3') // 3
2. 同样,可以使用map(String)或map(Boolean)将数组中的每项数据转换为String或者Boolean类型
3. 扩展:
ABCDEF在16进制中对应10、11、12、13、14和15
['1', '2', '3'].map(parseInt)结果解析:https://blog.csdn.net/weixin_44135121/article/details/88050214
4. let num = parseInt('1234abcd'); num //1234
let num2 = parseInt('a1234'); num2 //NaN
parseInt(12.6) // 12
28. 细品i = i + x()
和i = x() + i
的区别
`1. i += x()可以转换为i = i + x(), 是先拿i的值, 再执行x()`
let i = 0
function x() {
i++
return 10
}
i += x()
console.log(i) // 输出结果为10
`
解析:
i += x() 可以转换为i = i + x(), 因为x()是放在i的右边, 还未执行, 所以计算的时候, i的值还未变化。
因此结果为i = 0 + 10 = 0
`
`2. i = x() + i: 是先执行x(), 再加i`
let i = 0
function x() {
i++
return 10
}
i = x() + i
console.log(i) // 输出结果为11
`
解析:
i = x() + i, 是先执行x(), 再加i, x()执行后, i的值变为1。
因此结果为i = 10 + 1 = 11
`
29. js偷懒小技巧
`1. 使用array.at()来获取数组元素:索引从0开始,需要传入数字,如果不是数字,会自动转换为数字`
const arr = [1, 3, 5, 6]
`最常用的功能:获取数组的最后一个值`
arr.at(-1) // 6
arr.at(0) // 1
`倒数第二个`
arr.at(-2) // 5
`取不到则为undefined`
arr.at(4) // undefined
arr.at("3123") // undefined
arr.at("大苏打实打实大") // 1
arr.at(NaN) // 1
`传入的如果是正浮点数,则会向下取整`
arr.at(1.4) // 3
arr.at(1.7) // 3
`传入的如果是负浮点数,则会向上取整`
arr.at(-1.4) // 6
arr.at(-1.8) // 6
arr.at(-0.7) // 1
`同样可以使用string.at()来获取字符串元素`
'1356'.at(2) // '5'
'1356'.charAt(2) // '5'
'1356'[2] // '5'
`特别注意: at存在兼容性问题,慎用,但是可以通过其他方法获取数组最后一个值`
`
a. 数组.slice(-1)[0] 最优解
b. 数组[数组.length - 1] 次解
c. 数组.pop() 不推荐,会改变原数组
d. 数组.reverse()[0] 不推荐,同样会改变原数组
`
`2. ??的使用:与或比较类似,区别在于??只有运算符左侧的值为`null`或`undefined`时,才返回右侧的值,否则返回左侧的值`
0 || 1 // 1
0 ?? 1 // 0
30. for in
与for of
的碰撞
for in
能够遍历对象和数组,对于对象得到的是key
值, 对于数组得到的是索引index
值。for of
无法遍历对象,能够遍历数组和带有iterator接口的,例如Set, Map, String
, 得到的是value
值
遍历对象:
遍历数组:
31. 十几行递归算法,助你轻松比对国外系统多语言配置的差异
32. 字体翻译的自定义动态文案
可以考虑定义$
变量,使用replace
方法,将静态文案使用动态变量替换,然后再用v-html
解析需要展示的样式,比如:
`配置字体翻译:`
`1. 在en.js中:`
helpCenter: {
results: '$total results for <span style="color: #384edb">"$keyWord"</span> in All Categories'
}
`2. 在zh-CN.js中:`
helpCenter: {
results: '在所有分类中搜索到关于<span style="color: #384edb">“$keyWord”</span>的$total个结果'
}
<template>
<div class="result" v-html="results"></div>
</template>
<script>
export default {
props: {
totalTitle: Number,
keyWord: String
},
computed: {
results({ totalTitle, keyWord }) {
return this.$t('page.helpCenter.results').replace('$total', totalTitle).replace('$keyWord', keyWord)
}
}
}
</script>
<style lang="scss" scoped>
.result {
color: $color-light-gray;
font-size: 14px;
margin-bottom: 40px;
}
</style>
效果浏览:
33. 谨防解构中的this
陷阱
import msgpack5 from 'msgpack5'
const { encode, decode, register} = msgpack5()
`相当于window.register, register内置方法中,this指向的是window, 会报错`
register(0x42, MyType, this.mytipeEncode(data), this.mytipeDecode(data))
`register内置方法中this指向的是msgpack5(),不会报错`
msgpack5().register(0x42, MyType, this.mytipeEncode(data), this.mytipeDecode(data))
34. 使用定义好的方法替代map进行多层循环拿到数组数据
import { flatMapDeep, isPlainObject, isArray, upperFirst } from 'lodash'
export function flatMapDeepByArray(data, mapArr = [], mapKeyArr = [], needFill = false) {
let flatMapArr = []
if (!mapArr.length) return []
if (isPlainObject(data)) {
const shiftData = data[mapArr.shift()]
flatMapArr = Array.isArray(shiftData) ? shiftData : [shiftData]
} else {
flatMapArr = data
}
//重置mapKeyArr
mapKeyArr = mapKeyArr.slice(0, mapArr.length)
for (let i = 0; i < mapArr.length; i++) {
flatMapArr = flatMapDeep(flatMapArr, (n) => {
const arr = $GET(n, `${[mapArr[i]]}`, [])
const sliceKeyArr = mapKeyArr.slice(0, i + 1)
const sliceMapArr = mapArr.slice(0, i + 1)
sliceKeyArr.map((key, k) => {
arr.map((nItem, index) => {
nItem.$index = index
if (k == sliceMapArr.length - 1) {
return (nItem[`$${key}`] = n)
}
nItem[`$${key}`] = n[`$${key}`]
})
})
return arr
})
}
//需要填充
if (needFill) flatMapArr.map((item) => fillProps(item, mapKeyArr))
return flatMapArr
}
export function fillProps (obj, props) {
if(!isArray(props)) props = [props]
props = props.map(prop => `$${prop}`)
props.map(prop => {
const val = obj[prop]
if(!isPlainObject(val)) return
for(let key in val) {
const valKey = obj[key] ? `${prop}${upperFirst(key)}` : key
obj[valKey] = val[key]
}
})
}
// flatMapDeepByArray的第一个参数为对象/数组, 第二个参数是一个数组,接收N个相关的循环字段,以逗号隔开
// 第三个参数为数组,表示丢失的上层父级信息,比如丢失的上层list字段, ['list']
// 就会记录在返回的对象数组中,并以mapKeyArr作为key值,拼接在对象中
// 栗子:取出多层数组里面的price字段,构成数组
// 第二个实参,数组最后一项对应的不能是基本数据类型
getLowerPrice(detail) {
return '$' + map(min(flatMapDeepByArray(detail,
['productPrototype', 'sizeList', 'sizeLevelCategoryPriceList']), 'price'))
}
35. 闭包中的内存泄漏问题
1. 由于闭包中内层函数需要使用外层函数中定义的变量,这样就会导致函数作用域中的局部变量不会立即销毁。
因此,闭包也可以看做是外层函数中定义变量的生命的延续。当闭包达到一定数量,就会引起内存泄漏问题。
2.`垃圾回收机制的原理:
https://blog.csdn.net/sheng0113/article/details/124366002
https://blog.csdn.net/qq_35246620/article/details/80522720
`
3. `可以利用debugger来调试,查看闭包中的方法和闭包中需要引用的变量`
function getClosure() {
const obj = {
name: 'cxk'
}
return () => {
debugger
console.log(`我是${obj.name}`)
console.log('此处的闭包是getClosure')
}
}
`当然, 一个函数如果多次嵌套函数并引用上个外层函数定义好的变量,也可以有多个闭包`
4. 当删除定义好的变量时,弱引用也会随之删除。因此,可以在页面初始化时,为需要排查可能造成内存泄漏的对象定义weakMap。
当weakMap中对应的key消失时,这也就意味着该对象变量也随之销毁。
<template>
<div class="app-container system-home">
{{ obj}}
<el-button size="small" type="primary" @click="clickHandler">清除</el-button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
name: 'cxk'
}
}
},
mounted() {
this.initWeakMap()
},
methods: {
clickHandler() {
this.obj = null
},
initWeakMap() {
const weakMap = new WeakMap()
// key必须为对象
weakMap.set(this.obj, 'obj')
setInterval(() => {
console.log('weakMap', weakMap)
}, 1000)
}
}
}
</script>
5. 在js中与WeakMap对应地,还存在一个强引用Map,它们之间的区别在于:
(1) 强引用的对象不会被垃圾回收机制回收,但是弱引用对象会被回收。
(2) 强引用可能导致内存无法释放,造成内存泄漏;而弱引用不存在这个问题。
(3) 强引用的key和value,都可以为任意类型;弱引用的key只能是对象,value可以为任意类型。
闭包情况截图如下:
垃圾回收机制是存在一个周期的,它不会马上就回收垃圾,需要有一个过程。但是我们可以通过点击谷歌浏览器工具栏——内存上的回收垃圾图标,将它的垃圾回收过程提前触发。
36. 变量提升
变量与函数的变量提升传送门:https://blog.csdn.net/qq_43692768/article/details/117458927
let与const的暂时性死区:https://juejin.cn/post/7153231797113847845
37. 类
类,相当于vue
中的混入。但是相比于混入,它可以通过new 类名
,产生一个对应构造函数的原型对象。这个对象中定义的方法不会和其他混入中定义的同名方法相互污染
,因为它有自己独特的作用域。
`类相关知识的传送门:https://blog.csdn.net/Han_Zhou_Z/article/details/122380478`
38. 下载后端返回的单个二进制文件
1.我们需要在接口请求中使用`responseType: 'blob'`, 来获取后端返回的二进制文件
2.封装公共方法`downloadBlob`:
downloadBlob(blob, fileName) {
`根据所提供的url地址,创建对象URL, 存储在内存中`
const blobUrl = URL.createObjectURL(blob)
`创建超链接标签link`
const link = document.createElement('a')
`将超链接标签link的跳转指向blobUrl`
link.href = blobUrl
`为超链接标签link下载的文件重命名`
link.download = fileName || '下载文件'
`触发超链接标签的点击事件,开始下载`
link.click()
`因为是用浏览器内存开辟的地址,所以需要及时释放对象URL,否则会一直占用系统内存,导致死机等严重影响性能的事发生`
URL.revokeObjectURL(blobUrl)
}
3. 使用封装好的公共方法:
async exportHandler(row) {
const res = await declareApi.matchExport({
batchIds: [row.id]
})
this.downloadBlob(res, `${row.batchCode}_${row.expressCompanyName}`)
}
- 使用
fetch
请求文件对象(同上述方法
,a
标签下载,blob
流)
async downloadBlob() {
try {
const response = await fetch(
'https://p6.itc.cn/q_70/images03/20220913/7216c364419347a384c9e634220915dc.jpeg'
)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '东方淮竹.jpg'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('下载文件出错:', error)
}
}
- 使用
form
下载文件
将表单添加到dom
中的目的是为了触发表单的提交操作。在javaScript
中,直接调用form.submit()
方法来提交表单,需要确保表单元素已经被添加到dom
中,这样浏览器才能正确地执行提交操作。
表单元素的添加到dom
并不会导致页面的刷新或重定向,它只是为了触发浏览器对表单的提交动作。通过将表单添加到dom
中,浏览器可以检测到表单的存在,并按照指定的action
和method
属性执行相应的提交操作。
如果尝试在未将表单元素添加到dom
的情况下调用form.submit()
,浏览器可能无法正确处理该操作,因为它无法定位表单元素以执行提交。
因此,为了确保表单的正常提交,需要在调用form.submit()
之前将表单元素添加到dom
中。可以将表单添加到<body>
元素或其他合适的容器元素中。一旦提交完成,你可以选择将表单从dom
中移除以保持页面的整洁性。
Tips: 相比a标签,使用表单下载文件存在直接跳转到指定文件的弊端
`1. 直接使用form表单和提交按钮下载文件`
`type="submit",表示当前按钮类型为提交按钮`
<el-form
ref="downloadForm"
action="https://p6.itc.cn/q_70/images03/20220913/7216c364419347a384c9e634220915dc.jpeg"
method="GET"
>
<button class="mt10" type="submit">下载文件</button>
</el-form>
`2. 使用dom创建表单,并触发表单的提交事件来下载文件`
async downloadFile() {
try {
const form = document.createElement('form')
form.method = 'GET'
form.action = 'https://p6.itc.cn/q_70/images03/20220913/7216c364419347a384c9e634220915dc.jpeg'
document.body.appendChild(form)
form.submit()
`移除表单`
form.parentNode.removeChild(form)
`// document.body.removeChild(form)`
} catch (error) {
console.error('下载文件出错:', error)
}
}
39. 对象动态key
值的赋予
const a = 'name'
const obj = {
[a]: 'cxk'
}
obj.a // undefined
obj.name // 'cxk'
40. 使用递归思想对比两个数据(只存在对象、数组和基本类型时
)之间的差异
使用递归,必须得明细以下二个规则:
- 递归必须要有一个出口
- 对于数组和对象这类可递归的数据,需要直接调递归方法本身,并接收递归方法返回的值,而剩下的逻辑交给出口处理就好
import { isPlainObject, isArray } from 'lodash'
data() {
return {
oldArr: [
{
a: 1,
b: 2,
c: {
name: 'cxk'
},
d: [
{
name: 'cs'
},
1,
2
]
},
{
a: 2,
b: 2,
c: 3
},
7
],
newArr: [
{
a: 1,
b: 2,
c: {
name: 'cxk1'
},
d: [
{
name: 'cs1'
},
5,
2
]
},
{
a: 1,
b: 4,
c: 3
},
8
]
}
}
`对传入的基本类型数据的数组进行简单比较:`
compare(oldArr, newArr) {
const res = []
const keyList = Object.keys(oldArr?.[0]) || []
oldArr.map((item, index) => {
keyList.map((key) => {
if (item[key] != newArr[index][key]) {
if (!res[index]) res[index] = {}
res[index][key] = newArr[index][key]
res[index]['index'] = index
}
})
})
return res.filter((bool) => bool)
}
`对数组、对象之类数据进行递归比较`
compare(oldVal, newVal) {
const res = []
if (isArray(oldVal)) {
oldVal.map((item, index) => {
const resArr = this.compare(item, newVal[index])
if (resArr.length) {
res.push(resArr)
}
})
} else if (isPlainObject(oldVal)) {
Object.keys(oldVal).map((key) => {
const val = oldVal[key]
const val1 = newVal[key]
const objRes = this.compare(val, val1)
if (objRes.length) {
res.push(objRes)
}
})
} else {
if (oldVal !== newVal) {
res.push(newVal)
}
}
return res
}
41. 使用Promise.all
处理element
多表单校验问题
async beforeSubmit() {
try {
await this.$refs.orderRef.validate()
`存放多个Promise`
const pArr = this.formList.map(async (item, index) => {
return await this.$refs[`form_${index}`]?.[0].validate()
})
`获取多个Promise的值,如果有一个Promise失败,则直接被catch方法捕获,
如果没有失败的Promise,则直接返回一个数组,数组的长度为Promise的个数,数组中的每一个值都为true,
如[true, true]
`
await Promise.all(pArr)
return true
} catch (error) {
return false
}
}
42. 在模态框弹出时,可以直接使用clearValidate
方法消除element
表单的初始校验
43. window.open
参数详解
- 下载文件:
`在当前页面打开需要下载的zip文件`
window.open(
'zip地址',
'_self'
)
vue
新开页面进行路由跳转
let routeUrl = this.$router.resolve({ name: 'exportRecords' })
window.open(routeUrl.href, '_blank')
44. http
教程解析
45. 下载文件
`获取文件后缀名`
export function getFileSuffix(path = '') {
path = path || ''
const chaLastIndex = path.lastIndexOf('.')
const name = path.slice(chaLastIndex + 1)
return name
}
`转换为base64文件,减少网页请求`
export async function getURLBase64(url, config = {}) {
let res = await getURLData(url, (config = {}))
if (res) return res.target.result
return res
}
export function getURLData(url, config = {}) {
return axios
.get(url, {
`因为是文件,所以使用blob流`
responseType: 'blob',
...config
})
.then((res) => {
const { status, data } = res || {}
if (status >= 200 && status < 300) {
const fileReader = new FileReader()
const p = new Promise((resolve, reject) => {
`订阅必须最快,需要先订阅图片的加载事件,再读取图片文件`
`因为如果极限情况下,文件读取这一过程很快就结束了,此时还没订阅,则无法触发图片的加载事件`
fileReader.onloadend = function (e) {
e.data = data
e.size = data.size
resolve(e)
}
})
`以url的形式读取图片文件`
fileReader.readAsDataURL(data)
return p
}
})
.catch((err) => {
const { message } = err
if (message && message.cancelMessage) {
Message.success('取消下载成功')
}
console.log(err)
return false
})
}
`下载主方法`
export async function downloadImageByAixos(src, name) {
if (name === undefined) {
let tempArr = src.split('/')
name = tempArr[tempArr.length - 1].split('.')[0]
}
const suffix = getFileSuffix(src)
const url = await getURLBase64(src)
if (!url) return Message.warning('下载失败')
`1. 生成一个a元素`
var a = document.createElement('a')
`2. 将a的download属性设置为我们想要下载的图片名称,若name不存在则使用‘下载图片名称’作为默认名称`
a.download = name + '.' + suffix // one是默认的名称
`3. 设置文件转换为base64格式的跳转路径`
a.href = url
`4(第一种方法):直接触发a的点击事件`
a.click()
`(第二种方法):创建一个单击事件`
var event = new MouseEvent('click')
// 触发a的单击事件
a.dispatchEvent(event)
return true
}
46. 加载所有图片文件
// 打印时,需要先加载所有图片
export function loadAllImages(images) {
const promises = Array.from(images).map((image) => {
`此处判断一种图片路径和网页打开路径不同的情况`
if (image.src && image.src !== window.location.href) {
return loadImage(image)
}
})
return Promise.all(promises)
}
export function loadImage(image) {
return new Promise((resolve) => {
try {
if (image && (typeof image.naturalWidth === 'undefined' || image.naturalWidth === 0 || !image.complete)) {
`图片加载成功,就reslove`
image.onload = resolve
`此处有处理图片加载失败的情况,使用默认图片进行占位。所以在后续catch时,直接reslove`
`而不需要在Promise.all后,再使用catch方法对图片加载失败的情况进行处理`
image.onerror = () => {
image.src = defaultImg
}
} else {
resolve()
}
} catch (e) {
resolve()
}
})
}
47. 关于html
代码中input
标签的写法
- 事件的写法:
on小写事件名 = "点击事件方法()"
,方法必须带有括号。参数可以为空,或者为event
,又或者是this(指向input标签本身)
dom
的简写: 在原生html
代码的js
模块,可以直接使用html上的id
名称(哪怕这个名称未定义过) 获取对应的html dom(相当于querySelectorAll,获取所有和这个id相同的dom元素)
- 多选的支持: 直接在
input
标签上使用multiple
即可
48. 开发工具网站(JSON
格式化、base64转换、pdf转图片)
49. 浏览器上的图像转换(经典
)
给定一个url
, 经过aixos
请求(responseType: 'blob'
), 就可以得到一个blob(二进制大文件)
。
50. 获取文件名称、文件后缀和整个文件名
`获取文件名称,最后一个/和最后一个.之间的即为文件名`
export function getFileName(path = '') {
path = path || ''
const pointLastIndex = path.lastIndexOf('.')
const chaLastIndex = path.lastIndexOf('/')
return path.slice(chaLastIndex + 1, pointLastIndex)
}
`获取文件后缀,最后一个.后面的所有内容即为文件后缀`
export function getFileSuffix(path = '') {
path = path || ''
const chaLastIndex = path.lastIndexOf('.')
const name = path.slice(chaLastIndex + 1)
return name
}
`获取整个文件名,即文件名称 + 文件后缀`
export function getFillFileName(path = '') {
return `${getFileName(path)}.${getFileSuffix(path)}`
}
51. MDN——同源概念解析
img
标签访问跨源网络图片,返回403 forbidden
的问题解决(会有安全风险)
** Tips: 使用axios
请求跨源网络的图片,也会有请求失败的可能。**
- 可以直接请求并获取同源网站上的图片;
- 当图片位于非同源网站, 非同源网站的图片允许访问(比如
access-control-allow-origin: *
)且本服务网站允许接收的情况下,可以获取图片; - 当图片位于非同源网站, 非同源网站的图片禁止访问或本服务网站禁止接收的情况下,无法获取图片;
52.vue
与js
文件之间的通信(应用于代码过长,需要抽取js
文件,vue
与js
文件之间无法较好通信的情况)
export function changeThis() {
`打印出来是父组件的vue实例`
console.log('changeThis', this)
return {
callback: () => {
`打印出来是父组件的vue实例`
console.log('this', this)
return this.obj
}
}
}
<template>
<div class="app-container"></div>
</template>
<script>
import { changeThis } from './module/cols'
export default {
mounted() {
`bind得到的是一个函数,如果要执行,需要加()`
changeThis.bind(this)().callback()
`或者使用call方法接收多个参数,并直接执行函数`
changeThis.call(this).callback()
`或者使用apply方法接收一个数组参数,并直接执行函数`
changeThis.apply(this).callback()
},
data() {
return {
obj: {
name: 'cxk',
age: 18
}
}
},
methods: {
changeThis
}
}
</script>
53. 使用hashids
封装短码
我们先用chatGPT
告诉我们什么是短码,以及使用短码的好处:
- 安装
hashids
:npm install hashids --save
短码的封装:
import Hashids from 'hashids'
`编码规则为8位包含大小写字母和数字的字符串`
const hashids = new Hashids('MySalt', 8, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
// 短码封装
export function encode(val) {
return hashids.encode(val)
}
// 短码解析
export function decode(val) {
return hashids.decode(val)[0]
}
`globalConst.js上全局注册和挂载`
import { encode, decode } from '@/utils/hashids'
export default {
install(Vue) {
Vue.prototype.$encode = window.$encode = (data) => encode(data)
Vue.prototype.$decode = window.$decode = (data) => decode(data)
}
}
`mian.js引入并使用globalConst.js`
import Vue from 'vue'
import globalConst from '@/commons/globalConst'
Vue.use(globalConst)
new Vue({
el: '#app',
router,
store,
render: (h) => h(App)
})
hashids
的使用及结果解析:
54. 引入原生element
原生混入,在表单改变时强制触发校验
存在一些特殊情况,在失去焦点时,不会触发表单的自定义校验方法。这个时候,我们可以引入element
原生混入方法,在表单改变时强制触发校验。
import emitter from 'element-ui/src/mixins/emitter'
export default {
mixins: [emitter],
props: {
validateEvent: {
type: Boolean,
default: true
}
},
mounted() {
this.$watch(
function () {
const { provinceCode, cityCode, countyCode } = this.form
return provinceCode + cityCode + countyCode
},
() => {
this.$emit('adressUpdate', this.form)
`触发混入中的dispatch方法,第一个参数为组件名称,第二个参数为时间名称,第三个参数为传参对象`
`传递表单的change事件,以及表单对象的传参,触发element表单校验方法`
this.validateEvent && this.dispatch('ElFormItem', 'el.form.change', [this.form])
}
)
}
}
`element之所以能在失焦时,触发校验,其实就是这个原因,让我们看看源码:`
import emitter from 'element-ui/src/mixins/emitter'
export default {
mixins: [emitter],
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
}
}
}
`element原生组件封装的混入方法:`
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
`其实就是遍历查找到组件所在地方,并向上传递事件和需要的参数`
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
}
55. github
正则大全
55. 分享一个chatGPT VsCode
插件Chat Moss
56. 封装判断当前日期属于周几的方法
function getWeekday() {
const weekdays = ['星期天', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const date = new Date()
const weekdayIndex = date.getDay()
return weekdays[weekdayIndex]
}
57. 使用element
封装的原生方法,正则化后端返回的日期数据
import { formatDate } from 'element-ui/src/utils/date-util'
`可以根据传入的format正则格式化后端返回的日期数据`
export function getFormatData(date = new Date(), format = 'yyyy-MM-DD hh:mm:ss') {
return formatDate(date, format)
}
58. 解构细节
`1. 不能为null或者undefined解构,否则会弹出报错, 因为它们无法转换为对象`
`2. 解构的初值只有在解构的值为undefined时才会生效, 为null也不会生效。`
`也因为解构的第2点细节,我们一般在对象解构时或上一个{}, 防止为null或者undefined时报错`
const App = (props) => {
const { data } = props || {}
const { name, age } = data || {}
}
59. lodash
常用方法举例
`lodash常用方法总结:https://juejin.cn/post/7197296623236087864`
`lodash方法测试: https://jsrun.net`
60. JSON.stringify()
参数讨论
61. 接口访问跨域问题处理
在此只介绍一种代理接口跨域
的方法,利用webpack
,在vue.config.js
中进行配置:
`官方devServer详解: https://webpack.docschina.org/configuration/dev-server/#devserverproxy`
module.exports = {
devServer: process.env.ENV_STAGE === 'serve' ?
{
port: 8080,
proxy: {
'/': {
target: process.env.VUE_APP_BASE_URL,
changeOrigin: true
}
}
}
:
{
port: 8080,
proxy: {
'/api': {
`相当于将所有用/api开头的接口,以https://fanyi-api.baidu.com/api进行替换`
target: 'https://fanyi-api.baidu.com', // 百度翻译API的基础URL
changeOrigin: true
}
}
}
}
62. 利用file-saver
将数据导出为txt
文件并保存在本地
`首先需要安装file-saver插件依赖,npm install file-saver`
import { saveAs } from 'file-saver'
let txt = new Blob(
`第三个参数为保留两字符距离的缩进`
`加入正则的目的是为了消除key上的双引号`
[JSON.stringify(en, null, 2).replace(/"([^"]+)":/g, '$1:')],
{ type: 'text/plain;charset=utf-8' }
)
saveAs(txt, '翻译结果.txt')
63. 利用dom元素
上的scrollIntoView
方法跳转到指定位置
<template>
<div class="app-container">
<el-button class="mb20" size="small" type="primary" @click="linkTo">link to</el-button>
<div
v-for="(item, index) in range"
:key="index" class="item-warpper"
:ref="`item-${index + 1}`"
>
{{ item }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
range: 10
}
},
methods: {
linkTo() {
`使用v-for循环开辟了1到10这10个空间`
`使用dom元素上的scrollIntoView方法滚动到指定位置`
`start:将滚动元素的顶部与滚动容器的顶部对齐。`
`center:将滚动元素的中心与滚动容器的中心对齐。`
`end:将滚动元素的底部与滚动容器的底部对齐。`
`nearest:将滚动元素滚动到滚动容器中最接近视口中心的位置。`
this.$refs['item-6'][0].scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
}
</script>
<style lang="scss" scoped>
.item-warpper {
height: 200px;
border: 1px dotted orange;
background: pink;
margin-bottom: 10px;
}
</style>
64. 固定随机种子生成 + 洗牌算法的应用
`分享一个生成固定随机种子(生成结果在[0, 1)区间)的第三方插件:seedrandom`
`随机从当前日期下的10个用户中抽取3位幸运用户的案例(需要保证每次抽取结果都一致):`
<template>
<div class="app-container">
<el-button size="small" type="primary" @click="clickHandler(10, '2023-06-30', 3)">
生成中奖用户
</el-button>
</div>
</template>
<script>
import seedrandom from 'seedrandom'
export default {
data() {
return {
`对应抽奖的10名用户`
userList: ['用户1', '用户2', '用户3', '用户4', '用户5', '用户6', '用户7', '用户8', '用户9', '用户10']
}
},
methods: {
clickHandler(length, seed, N) {
`生成0到9这个长度为10的数组,对应data中的10位用户`
const target = Array.from({ length }, (item, index) => index)
`设置固定种子,区间在[0, 1)`
const rng = seedrandom(seed)
`对target数组进行随机洗牌`
for (let i = target.length - 1; i > 0; i--) {
`此处必须要使用;进行分隔,不然这两行代码无法被正确识别`
const j = Math.floor(rng() * (i + 1));
`随机交换位置`
[target[i], target[j]] = [target[j], target[i]]
}
return target.slice(0, N).map(item => this.userList[item % this.userList.length])
}
}
}
</script>
Array.from
的用法:
`将字符串转换为数组`
const str = 'hello';
const strArray = Array.from(str);
console.log(strArray); // ['h', 'e', 'l', 'l', 'o']
`将类数组对象转换为数组`
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const array = Array.from(arrayLike);
console.log(array); // ['a', 'b', 'c']
`使用映射函数进行转换(第二个参数和map方法的参数一样,item, index, arr)`
const numbers = [1, 2, 3, 4, 5];
const squaredArray = Array.from(numbers, num => num * num);
console.log(squaredArray); // [1, 4, 9, 16, 25]
65. 关于canvas
注意点的补充及html2canvas
的应用
-
canvas
是有默认宽高的:如果未在
canvas
标签上指定width
和height
的值,未使用style
样式强制添加宽高的大小,也未在css
中为canvas
指定宽度和高度,则canvas
的默认宽度是300px
, 默认高度是150px
。 -
canvas
的宽高优先级:未设置(默认宽高) <
canvas
标签上指定width
和height
(数值,单位默认为px
) < 通过样式为canvas
指定的宽高 <style
上指定的宽高 -
canvas
上绘制的图片被拉伸的问题:在
canvas
标签上的宽高和使用css
(包括style
)设置的宽高不一样的情况下,就会出现这种问题。canvas
可以看作为一张图片,它是基于canvas
标签上的width
和height
进行绘制的。如果它的宽度和高度被css
覆盖掉了,则canvas
上绘制的内容也会跟随着等比例缩放。 -
关于
canvas
的dom
元素上的指定值:canvasDom.width
: 永远指向canvas
标签上的width
属性的大小canvasDom.offsetWidth
:canvas
真实宽度,即通过css
为canvas
设定的宽度 ||canvas
标签上设定的属性大小canvasDom.style.width
: 永远指向在canvas
标签上通过style
设定的画布宽度 -
(js版)
画布上引入图片:
mounted() {
const canvasDom = this.$refs.canvas
const cxt = canvasDom.getContext('2d')
const image = new Image()
image.src = require('../../assets/images/loginBg.png')
image.onload = () => {
cxt.drawImage(image, 100, 200)
}
}
-
canvas
画笔宽度及1px
像素问题处理canvas
画笔是以moveTo
对应的坐标为起点中心,以lineTo
对应的坐标为终点中心进行绘制的。即canvas
画出的线,是以中心点上下延伸lineWidth
大小的一半进行绘制的。 如果给定的moveTo
起点是(0, 50)
,给定的lineTo
终点是(20, 50)
, 画笔的lineWidth
设置为1,此时会以50为Y轴
的中心起点,上下各延伸0.5个像素。但是这个时候悖论就来了,因为canvas
能表示的最小像素为1,所以canvas
实际上会帮我们从50上下各延伸1个像素点,同时将这两个像素点的透明度设置为之前的一半,来标识这两个像素其实是一个像素。因此,以整数点为起点和终点,画笔宽度为1,真正在canvas
上绘制出的大小其实是2px
。 但是如果我们非要画1px
宽度的直线,我们可以为起点和终点指定小数值,比如为Y轴设置大0.5个像素的距离,即起点为(0, 50.5)
, 终点为(20, 50.5)
。 -
html2canvas
的应用
`常规操作:`
<template>
<div class="app-container">
<div ref="captureElement">
<div class="background">
<h1>Hello, HTML2Canvas!</h1>
<p>This is a sample HTML content.</p>
<p>You can capture this element using html2canvas.</p>
</div>
</div>
<loading-btn class="mt10 mb10" type="primary" size="small" @click="captureScreenshot"
>Capture Screenshot</loading-btn
>
<div v-if="screenshot">
<h2>Screenshot:</h2>
<el-image class="image" fit="contain" :src="screenshot" alt="Screenshot" />
</div>
</div>
</template>
<script>
import html2canvas from 'html2canvas'
export default {
data() {
return {
screenshot: null
}
},
methods: {
async captureScreenshot() {
const element = this.$refs.captureElement
`记录canvas的初始宽高`
const width = element.style.width
const height = element.style.height
`强制为canvas的宽高赋值`
element.style.width = '400px'
element.style.height = '200px'
const res = await html2canvas(element, {
`开启跨域设置,需要后台设置cors`
useCORS: true,
`画布根据设备的dpi,进行动态缩放`
scale: window.devicePixelRatio > 1 ? window.devicePixelRatio : 1
})
if (!res) return
this.screenshot = res.toDataURL()
`重置canvas的初始宽高,我们得到的图片大小,会是画布设置的宽高大小,同时又不影响dom上渲染的canvas排版`
element.style.width = width
element.style.height = height
}
}
}
</script>
<style lang="scss" scoped>
.background {
background-image: url('~@/assets/images/loginBg.png');
padding: 15px;
}
.image {
// 对图片进行缩放,缩放起点为左上
scale: 0.5;
transform-origin: top left;
}
</style>
`生成带水印的图片`
<template>
<div class="app-container">
<div ref="captureElement">
<div class="background">
<h1>Hello, HTML2Canvas!</h1>
<p>This is a sample HTML content.</p>
<p>You can capture this element using html2canvas.</p>
</div>
</div>
<loading-btn class="mt10 mb10" type="primary" size="small" @click="captureScreenshot"
>Capture Screenshot</loading-btn
>
<div v-if="screenshot">
<h2>Screenshot:</h2>
<el-image fit="contain" :src="screenshot" alt="Screenshot" />
</div>
</div>
</template>
<script>
import html2canvas from 'html2canvas'
export default {
data() {
return {
screenshot: null
}
},
methods: {
async captureScreenshot() {
const element = this.$refs.captureElement
const watermark = document.createElement('div')
watermark.innerText = 'Watermark'
watermark.style.position = 'absolute'
watermark.style.top = '50%'
watermark.style.left = '50%'
watermark.style.transform = 'translate(-50%, -50%)'
watermark.style.opacity = '0.5'
watermark.style.color = '#ffffff'
watermark.style.fontWeight = 'bold'
watermark.style.zIndex = 999
element.appendChild(watermark)
const res = await html2canvas(element, {
useCORS: true, // 开启跨域设置,需要后台设置cors
scale: window.devicePixelRatio > 1 ? window.devicePixelRatio : 1
})
if (!res) return
this.screenshot = res.toDataURL()
}
}
}
</script>
<style lang="scss" scoped>
.background {
background-image: url('~@/assets/images/loginBg.png');
padding: 15px;
}
</style>
66. 碎碎念串烧
- 在对象中使用
[变量]
做动态key
值 - 父级上点击,进行操作
action1
。父级局部区域的子级点击,不需要进行action1
的操作。假定子级为el-button
,则可以在el-button
组件上,使用@click.native.stop.prevent
,阻止自己点击向父级冒泡 - 遍历对象的属性,可以有以下三种方法:
`采用for in的方式`
for(let key in obj) {
let val = obj[key]
}
`采用Object.keys的方法`
Object.keys(obj).map(key => let val = obj[key])
`采用key of,搭配Object.entries()的形式`
for (let [key, value] of Object.entries(obj)) {
`在循环中使用 key 和 value`
}
- 分片处理及图片压缩,创建
jpg
缩略图
在讲述使用分片处理技术,优化批量上传图片之前,我们得先理解lodash
中的chunk
方法,它将数组按照指定的大小进行分割,并返回一个新的二维数组,且每个子数组的长度不超过指定的大小。
import { chunk } from 'lodash'
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const size = 3
const result = chunk(array, size) `result: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]`
应用场景是:选择完图片文件夹后,为了避免图片文件过大,占用系统内存和CPU
, 需要将其转化为缩略图。就这个案例而言,对于批量处理的图片,使用分片技术的好处在于:未使用分片前,需要同时对N
张图片进行处理,这是极度消耗系统内存和CPU
的;使用分片后,我们只需要在一个迭代里,对M
张图片进行处理。等这M
张图片处理完毕后,才开始进入下一次迭代。我们需要处理Math.ceil(N / M)
个epoch
,但是每一次epoch
占用的资源是极少的, 这样就极大地提升了性能。
async updateFiles() {
const fnList = []
this.tmpFiles.map(async (file, index) => {
if (file.url) return
file.loaded = 0
file.total = 0.000001
file.fileName = getFileName(file.name)
file.fullLoading = true
const { url } = await new Promise(async (resolve) => {
fnList.push({
file,
resolve
})
})
file.url = url
file.fullLoading = false
this.tmpFiles.splice(index, 1, file)
this.revokeFileList.push(file.url)
})
this.files = [...this.tmpFiles]
const chunkData = chunk(fnList, 10)
for (let i = 0; i < chunkData.length; i++) {
const pArr = chunkData[i].map(async ({ file, resolve }) => {
const res = await getThumbnail(file, null, 38)
resolve(res)
return res
})
await Promise.all(pArr)
}
}
注:reslove(res)
是调用执行顺序1中的Promise
, return res
才是返回执行顺序2
中的Promise
结果。在后续的await Promise.all(pArr)
中,如果需要对执行顺序2
中返回的Promise
结果做处理,我们才需要return
。因此,在这串代码中,return res
和await Promise.all(pArr)
其实不需要。
`给定图片文件,获取文件缩略图的方法封装(使用一个默认值为空对象的形参作为方法传参,这也是一贯套路):`
import { isPlainObject } from 'lodash'
import Compressor from 'compressorjs'
`超过3MB即压缩图片`
export const PICBIGSIZE = 3 * 1024 * 1024
`压缩图片最大宽度`
export const COMPRESS_MAX_NUM = 800
/**
* 获取缩略图
* @param file
* @param rawOption
* @returns {Promise<unknown>}
*/
export function getThumbnail(file, rawOption = {}) {
if (!isPlainObject(rawOption)) {
rawOption = {
callback: rawOption
}
}
const { callback, ...option } = rawOption
const isNotImageType = file.type.indexOf('image/') !== 0
const isUseRawFile = file.size <= PICBIGSIZE
const isJpegOrJpg = /^image\/jpe?g$/.test(file.type)
return new Promise(async (resolve) => {
if (isNotImageType) {
//不是图片类型
resolve('')
return
}
if (isUseRawFile) {
resolve(done(file))
return
}
if (isJpegOrJpg) {
const url = await getThumbnailUrlOfJpeg(file, option)
resolve(done(url))
return
}
//非jpeg或者jpg类型图片
const url = await getThumbnailUrl(file, option)
resolve(done(url))
})
`函数中嵌套一个函数,因为这个函数用的比较少,是这个方法专有`
function done(url) {
const isBlob = url instanceof File || url instanceof Blob
if (isBlob) {
url = URL.createObjectURL(url)
}
return {
url: typeof callback === 'function' ? callback(url) : url
}
}
}
/**
* 获取Jpeg/jpg的缩略图
* @param file
* @param options
* @returns {Promise<unknown>}
*/
export function getThumbnailUrlOfJpeg(file, options) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = function (e) {
let array = new Uint8Array(e.target.result),
start,
end
for (let i = 2; i < array.length; i++) {
if (array[i] == 0xff) {
if (!start) {
if (array[i + 1] == 0xd8) {
start = i
}
} else {
if (array[i + 1] == 0xd9) {
end = i
// break
}
}
}
}
if (start && end) {
const arr = array.subarray(start, end)
const fileType = file.type || 'image/jpeg'
resolve(URL.createObjectURL(new File([arr], file.name, { type: fileType })))
} else {
const url = getThumbnailUrl(file, options)
resolve(url)
}
}
reader.readAsArrayBuffer(file.slice(0, 50000))
})
}
export function compressFile(file, options = {}) {
`默认设置`
const defaultOptions = {
maxWidth: COMPRESS_MAX_NUM,
maxHeight: COMPRESS_MAX_NUM,
convertSize: 'Infinity',
checkOrientation: false
}
const assignOptions = Object.assign({}, defaultOptions, options)
return new Promise((resolve) => {
new Compressor(file, {
...assignOptions,
success(result) {
const { width: naturalWidth, height: naturalHeight } = this.image
resolve({
file: result,
name: result.name,
naturalWidth,
naturalHeight
})
},
error(err) {
resolve(null)
}
})
})
}
export async function getThumbnailUrl(
file,
options = {
maxWidth: 100,
maxHeight: 100
}
) {
const res = await compressFile(file, options)
if (!res) return
const { file: result } = res
return URL.createObjectURL(result)
}
`上传图片——分片处理`
async updateFiles() {
// this.fullLoading = true
const fnList = []
this.tmpFiles.map(async (file, index) => {
if (file.url) return
file.loaded = 0
file.total = 0.000001
file.fileName = getFileName(file.name)
file.url = require('@/assets/images/loading.gif')
const { url } = await new Promise(async (resolve) => {
fnList.push({
file,
resolve
})
})
file.url = url
this.tmpFiles.splice(index, 1, file)
this.revokeFileList.push(file.url)
})
await Promise.all(pArr).finally(() => (this.fullLoading = false))
this.files = [...this.tmpFiles]
const chunkData = chunk(fnList, 10)
for (let i = 0; i < chunkData.length; i++) {
const pArr = chunkData[i].map(async ({ file, resolve }) => {
const res = await getThumbnail(file, {
maxWidth: 38,
maxHeight: 38
})
resolve(res)
return res
})
await Promise.all(pArr)
}
}
- 最外层为对象,嵌套多层对象或者数组的数据结构。拿到并记录当前级、父级、祖先级的
key
值,以及当前级对应的value
。
async formatData(obj, ancestors = []) {
for (let key in obj) {
if (['el', 'timezone'].includes(key)) continue // 忽略 key 为 'el' 的节点
const value = obj[key]
const keys = [...ancestors, key]
if (typeof value === 'object') this.formatData(value, keys)
else
this.finalData.push({
keys: keys.join('.'),
value
})
}
}
- 使用短路运算符,
js
识别问题
`使用两个()包裹起来,表明是两个不同的语句`
(this.show) && (this.visible = true)
- 数组.
join
方法
var arr = ["apple", "banana", "orange"]
var str = arr.join("")
console.log(str) // 输出:"applebananaorange"
var arr = ["apple", "banana", "orange"];
var str = arr.join(" and ");
console.log(str); // 输出:"apple and banana and orange"
- 字符串.
split
方法
split()
方法的行为是这样的:将原字符串按照指定的分隔符进行分割,然后将分割后的所有子字符串添加到一个新的数组中返回。
var str = "apple,banana,orange"
var arr = str.split() // 不分割
console.log(arr) // 输出:["apple,banana,orange"]
var str = "apple,banana,orange";
var arr = str.split(",");
console.log(arr); // 输出:["apple", "banana", "orange"]
var str = "apple1banana2orange3";
var arr = str.split(/\d/); // 正则表达式,匹配任意数字
console.log(arr); // 输出:["apple", "banana", "orange", ""]
- 创建相同数据的数组的
N
种方法
`1. 采用for in循环(不推荐)`
const arr = []
for (let i = 0; i < 100; i++) {
arr.push('今晚加班')
}
`2. 采用Array.from创建(推荐)`
`Array.from生成100个undefined值的数组`
const arr = Array.from({ length: 100 }, () => '今晚加班')
`3. 采用new Array创建(最推荐)`
const arr = new Array(100).fill('今晚加班')
`4. 使用new Array和map结合的方式创建(相对推荐)`
`Array构造函数生成的数组,每一项都是空属性,无法被map循环`
`因此需要展开,成为具有100个undefined值的数组,再进行循环`
const arr = [...new Array(100)].map(item => '今晚加班')
`5. 使用Array.apply和map结合的方式创建(相对推荐)`
`Array.apply第二个参数接收一个类数组,返回一个包括100个undefined值的数组`
const arr = Array.apply(null, Array(100)).map(item => '今晚加班')
const arr = Array.apply(null, {length: 100}).map(item => '今晚加班')
flatMap
: 比map
更高级的数组循环方法(ES10
),可以展开多维数组
`展开二维数组`
const arr = [[1, 2], [3, 4], [5, 6]]
const flatArr = arr.flatMap(x => x)
console.log(flatArr) // [1, 2, 3, 4, 5, 6]
`展开多维数组(注意flat方法的兼容性)`
const arr = [[[1, 2]], [[3, 4]], [[5, 6]]]
const arr.flatMap(x => x.flat(Infinity))
console.log(flatArr) // [1, 2, 3, 4, 5, 6]
- 点击父容器中的图片,新开窗口显示
<div class="content" v-html="contentDetail.content" @click="handleClick"></div>
`注意:如果不是在父容器中,而是直接点击图片,则不需要前面的if判断`
handleClick(event) {
if (event.target.tagName === 'IMG') window.open(event.target.src, '_blank')
}
iframe
标签的使用
1. https://www.w3school.com.cn/html/html_iframe.asp
2. https://wangdoc.com/html/iframe
67.addEventListener
方法和on
属性的区别
- 多个监听器的处理方式: 对于相同事件,使用
addEventListener
方法可以添加多个事件监听器,它们会按照添加的顺序依次执行。而使用on
属性,每次赋值都会替换前一个监听器,因此只能保留最后一个赋值的监听器。 - 移除监听器的方式: 使用
addEventListener
方法添加的监听器可以通过removeEventListener
方法进行移除,需要传入具体的事件处理函数。而使用on
属性添加的监听器可以通过将属性值设置为null
来移除监听器。 - 事件处理阶段的控制: 使用
addEventListener
方法可以通过第三个参数(布尔值)来控制事件监听器是在捕获阶段还是冒泡阶段执行(默认为false
,向上冒泡)。而使用on
属性添加的监听器默认在冒泡阶段执行。 - 兼容性:
addEventListener
是标准的dom
方法,支持较新的浏览器,包括现代的移动设备浏览器。而on
属性是早期的事件处理机制,具有更好的向后兼容性,适用于较旧的浏览器。
`1. 在mounted阶段,会依次打印moveHandler1、moveHandler2和moveHandler3`
`2. 在鼠标移动的过程中,也会同时触发moveHandler1、moveHandler2和moveHandler3这三个方法`
<template>
<div class="app-container"></div>
</template>
<script>
export default {
mounted() {
`注意绑定的是方法名称,而不是去执行方法`
// document.onmousemove = this.moveHandler1
document.addEventListener('mousemove', this.moveHandler1)
document.addEventListener('mousemove', this.moveHandler2)
document.addEventListener('mousemove', this.moveHandler3)
},
methods: {
moveHandler1() {
console.log('moveHandler1')
},
moveHandler2() {
console.log('moveHandler2')
},
moveHandler3() {
console.log('moveHandler3')
}
}
}
</script>
`1. 当前代码中使用了两个匿名箭头函数。它们是不同的函数实例,无法正确移除监听器`
<template>
<div class="app-container">
<el-button size="small" type="primary" @click="clickHandler">移除监听</el-button>
</div>
</template>
<script>
export default {
mounted() {
document.addEventListener('mousemove', () => {
console.log('moveHandler')
})
},
methods: {
clickHandler() {
document.removeEventListener('mousemove', () => {
console.log('moveHandler')
})
}
}
}
</script>
`2. 要解决这个问题,需要将匿名箭头函数替换为具名函数,并确保在添加和移除监听器时使用相同的函数引用。`
<template>
<div class="app-container">
<el-button size="small" type="primary" @click="clickHandler">移除监听</el-button>
</div>
</template>
<script>
export default {
mounted() {
`定义在methods里也可以`
this.moveHandler = () => {
console.log('moveHandler')
}
document.addEventListener('mousemove', this.moveHandler)
},
methods: {
clickHandler() {
document.removeEventListener('mousemove', this.moveHandler)
}
}
}
</script>
68. script
标签里defer
(延迟)和async
(异步)加载js
文件的区别:
defer
要等到整个页面在内存中正常渲染结束(DOM
结构完全生成,以及其他脚本执行完成,才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。- 一句话,
defer
是“渲染完再执行”,async
是“下载完就执行”(实测defer
仅在ie8
和ie9
下有效)。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。 - 浏览器对于带有
type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
-
defer属性:
- 带有
defer
属性的脚本会在整个文档解析和渲染完成后执行,但在DOMContentLoaded
事件之前。 - 多个带有
defer
属性的脚本会按照它们在文档中的出现顺序进行加载和执行。 - 脚本的加载不会阻塞页面的渲染,即页面的渲染过程不会因为脚本的加载和执行而中断。
defer
属性仅适用于外部脚本(通过src
属性引入的脚本)。
- 带有
-
async属性:
- 带有
async
属性的脚本在下载完成后立即执行,而不会等待文档的解析和渲染。 - 多个带有
async
属性的脚本的加载和执行顺序是不确定的,谁先下载完成就先执行谁。 - 脚本的加载和执行过程会中断页面的渲染,直到脚本加载和执行完成后才会继续渲染。
async
属性同样只适用于外部脚本。
- 带有
-
type="module"的脚本:
<script type="module">
标记用于加载 ECMAScript 模块。- 这些模块脚本默认具有
defer
属性,即在文档解析和渲染完成后执行,但在DOMContentLoaded
事件之前。 - 模块脚本的加载和执行不会阻塞页面的渲染。
- 不同于传统脚本,模块脚本具有严格的模块作用域,并且通过
import
和export
语句进行模块之间的依赖管理。
defer
和async
属性只适用于外部脚本,即通过src
属性引入的脚本。内联脚本(嵌入在<script>
标签内部的脚本)会自动按照其在文档中的出现顺序执行,并且会阻塞页面的解析和渲染过程。
69. 浏览器渲染机制
1. https://community.eolink.com/d/32427-20
2. https://pcaaron.github.io/pages/fe/chrome/drawing.html#%E6%80%BB%E7%BB%93
3. https://gitee.com/cckevincyh/js-css-block-dom
70. gpt
哥是如何看待js
和css
阻塞页面渲染的?
css
会阻塞页面渲染吗?
js
会阻塞页面渲染吗?
- 为什么需要将
js
代码放在页面底部?
- 为什么不为所有
js
文件都设置为延迟加载呢?
- 浏览器的并行下载机制是什么样子的?
71.对浏览器加载css
和js
文件的理解(自绘思维导图
)
在浏览器中,渲染进程负责处理和呈现网页内容。渲染进程会周期性地执行渲染任务来更新页面的显示。
一般来说,浏览器的渲染进程会以每秒约 60 帧(即每帧大约 16.67 毫秒, 一帧一帧渲染) 的速度执行渲染任务。这个时间间隔被称为“帧时间”或“刷新间隔”。在每个帧时间内,渲染进程需要完成以下主要任务:
- 处理用户输入:包括鼠标点击、键盘输入和触摸事件等用户操作。
- 更新布局和样式:根据 DOM 树和 CSSOM 的变化,计算每个元素的几何属性(如位置、大小)和样式属性(如颜色、字体),并确定元素的显示顺序。
- 执行 JavaScript:执行 JavaScript 代码,包括处理事件、更新页面状态和计算布局等操作。
- 绘制和合成:将元素绘制到屏幕上,并进行图层合成和组合,以生成最终的页面显示。
如果在一个帧时间内存在过多的微任务(例如使用 Promise 和 async/await 等机制创建的异步任务),这些微任务可能会占用大部分的执行时间。这会导致渲染进程无法按时完成绘制和合成操作,从而延迟页面的显示更新。
当页面的显示更新被延迟时,用户可能会感知到页面的“卡顿”或“加载”状态。浏览器可能无法及时响应用户的输入,页面可能无法流畅地滚动或动画效果可能变得不流畅。
为避免过多微任务影响页面渲染性能,尽量减少和合并微任务的数量。用 requestAnimationFrame()
方法来调度一些任务,以确保它们在下一帧时间开始之前执行。
此外可以在独立的线程中执行一些计算密集型的任务,减轻主渲染线程的负担,提高页面的响应性能。
总的来说,过多的微任务会占用渲染进程的执行时间,导致页面渲染延迟。应该注意优化代码,减少微任务的数量,以确保页面的流畅渲染和良好的用户体验。
72. dom
常用坐标属性小节
73. 各类开发环境的问题处理
- 重置电脑上的
git
账号
1. 打开终端或命令提示符窗口。
2. 输入`git config --global credential.helper store`命令,这将告诉Git把用户名和密码存储在本地。
3. 输入`git pull` 或 `git push`命令时,Git会要求您输入新的用户名和密码。
4. 输入您新设置的用户名和密码,然后按回车键即可完成登录。
vsCode
上git
账号重置- npm报错处理 A complete log
- npm install报错处理 code resolve
vscode
无法同时打开多个文件vue
项目创建问题处理github
账号冲突,无法提交本地代码到git- 本地文件发布到
github
上 - 解决
git@github.com: Permission denied (publickey)
74. dom
问题处理
- 使用
dom.textContent.trim()
,获取dom
元素的文本内容
validatorForm(rule, value, cb, form) {
const container = document.createElement('div')
container.innerHTML = form.wirelessDescription.wirelessDescription
`判断是否同时存在文本和图片`
const hasText = container.textContent.trim().length > 0
const hasImage = container.querySelector('img') !== null
const hasBothTextAndImage = hasText && hasImage
if(hasBothTextAndImage) cb('同时存在文本和图片!')
cb()
}
dom.getAttribute
获取dom
元素的属性
const url = img.getAttribute('src')
const width = img.getAttribute('width')
const height = img.getAttribute('height')
75.正则问题处理
value.match(正则)
返回一个数组,显示匹配成功的结果
'${title}321321${Title}${titlle}${title}321321'.match(/\{title\}/g)
// 返回结果: ['{title}', '{title}']
正则.test(value)
返回布尔值,表示value
是否满足这个正则
^: 开头
$:结尾
[]: 表示可以匹配的类型
*: 表示可以匹配任意数量的[]类型
\{: 转义,{
export function validateSku(rule, value, callback) {
const reg = /^[A-z0-9_-]*\{id\}[A-z0-9_-]*$/
if(!reg.test(value)) return callback('SKU格式有误')
return callback()
}
()
是一个捕获组
'For_example_{id}'.match(/^([A-z0-9_-])*(\{id\})([A-z0-9_-]*$)/)
- 正则中的
$n
当正则表达式有多个捕获组时,可以使用相应的$n
来引用每个捕获组的内容,其中n
是捕获组的索引。除此之外,我们还可以使用$n
进行replace
替换。
字符串.replace
的用法:
76. 调试小技巧
- 我们可以在要执行的代码中,将变量挂载到
window
上,比如window.变量 = 变量
。这样,我们就可以在控制台上直接打印变量
了 - 在控制台的代码片段中,输入
debugger
,也能进入调试环境
77. 代码简洁之道
`此处直接使用if return, 没必要使用if else的形式`
export const colorValidator = (rule, value, callback) => {
const matches = value.match(/\{title\}/g)
const oneMoreTitle = matches && matches.length > 1
if (oneMoreTitle || !matches) return callback('必须含有{title}, 且只能含有一个')
if (!/^[0-9a-zA-Z\s{}_-]*$/.test(value)) return callback('只能输入数字、英文和特殊字符')
return callback()
}
78. 代码健壮性处理
const { skuCustomName, id } = item.attributeList?.find(({ skuCustomName }) => skuCustomName) || {}
79. 阿里云图片格式转换———打印png图片呈现灰色背景的问题处理,图片转换为jpg格式
80. fetch
、XHR
与axios
const res = await fetch(
`${process.env.VUE_APP_BASE_API}/xxApi,
{
method: 'POST',
headers: {
`配置各类头部信息...`
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
page: { pageSize: 0, pageIndex: 1 }
}
)
}
)
axios
默认使用 Content-Type: application/json
头部来发送数据。在发送POST
请求时,会自动将传递的参数转换为JSON
格式,并设置正确的 Content-Type
头部。
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText)
`处理响应数据...`
} else {
`处理错误...`
}
}
};
xhr.send()
我们首先创建了一个XMLHttpRequest
对象,然后使用open()
方法指定请求的方法和URL
。使用setRequestHeader()
方法设置请求头,例如,我们在示例中设置了Content-Type
为application/json
。
接下来,我们设置onreadystatechange
事件处理程序,用于监听请求状态的变化。当readyState
变为XMLHttpRequest.DONE
时,表示请求完成。我们根据status
来判断请求的成功或失败,并根据需要处理响应数据或错误。
最后,我们使用send()
方法发送请求。
XHR
的优点包括:
- 在大多数现代浏览器中广泛支持。
- 提供底层的请求控制和事件处理,可以根据需要进行更细粒度的操作。
- 可以通过监听事件来实现进度追踪,如上传和下载进度等。
然而,与Fetch
API 相比,XHR
的语法相对繁琐,需要手动处理状态和错误,并且不支持流式操作。因此,如果在项目中进行网络请求,推荐使用Fetch API
或其他现代的网络请求库,以获得更简洁和易用的接口。
81. 项目打包
`一般来说,package.json的build脚本命令,使用npm run build对项目打包`
`打包完成后,cd dist,进入打包后的dist文件夹`
`npm install -g live-server, 安装live-server插件`
`安装完成后,输入live-server,就可以本地预览打包好的项目了`
`其中,127.0.0.1是本地的ip地址,可以替换为自己对应的局域网ip,分享给其他人`
82. forEach
与map
方法
forEach
与map
方法都无法使用return
和break
提前退出循环,除非使用throw new Error
const numbers = [1, 2, 3, 4]
try {
const filteredNumbers = numbers.forEach((num, index) => {
if (index == 1) {
throw new Error('error')
}
`只会打印0`
console.log('index', index)
})
} catch {}
forEach
方法没有返回值,map
方法返回值为一个数组forEach
和map
方法处理稀疏数组时,会跳过未定义的值,不会调用回调函数
const num = [1, , 3]
num.map(item => item * 2) // [2, empty, 6]
num.map(item => console.log(item)) // 只打印1和3,数组返回为[undefined, empty, undefined]
83. 碎碎念指北
- 不要轻易为任何一个变量
赋值引用数据类型
。必要情况下,请先对引用数据类型深拷贝
后,再进行赋值,以免影响原数据。 - 善用
debugger
。方法开头调用debugger
,可以看到整个方法的执行过程。多处方法加上debugger
, 能够看到一连串方法的执行过程。 - 对长数据进行循环的两种思路:
`不管三七二十一,擒贼先擒王:`
`1. 不用考虑谁长谁短,直接获取最大长度`
`2. 循环最大长度,分别对长短数据取值,不存在的使用{}兼容即可`
finalData({ data }) {
const tempArr = []
data.forEach((item) => {
let { standardList, expressList, ...rest } = item
const maxLeng = Math.max(standardList.length, expressList.length)
for (let i = 0; i < maxLeng; i++) {
const standardItem = standardList[i] || {}
const expressItem = expressList[i] || {}
const {
expressShippingStartWeight,
expressShippingEndWeight,
expressShippingFirstFreight,
expressShippingContinuedFreight
} = expressItem
const {
standardShippingStartWeight,
standardShippingEndWeight,
standardShippingFirstFreight,
standardShippingContinuedFreight
} = standardItem
tempArr.push({
...rest,
id: rest.id + '_' + i,
allData: item,
standardShippingStartWeight,
standardShippingEndWeight,
standardShippingFirstFreight,
standardShippingContinuedFreight,
expressShippingStartWeight,
expressShippingEndWeight,
expressShippingFirstFreight,
expressShippingContinuedFreight
})
}
})
return tempArr
}
}
`假设法: `
`1. 先假定a是最长的数据,循环遍历之,然后与b的数据合并在一起`
`2. 在b数据上,从a.长度上开始截取到b的结尾`
`3. 将两者拼接在一起,就是合并后的数据`
formatPostData(data) {
const expressFreightTemplateConfigToadditCreateROList = []
const tempArr = cloneDeep(data).map(({ expressList, standardList }) => {
const shortData = expressList.map((item, index) => {
return {
...item,
...(standardList[index] || {})
}
})
const restData = standardList.slice(expressList.length)
return [...shortData, ...restData]
})
tempArr.map((item) => {
const expressFreightTemplateToadditCreateROList = []
item.map((sItem) => {
const { countryList, partitionName, minEndWeight, ...rest } = sItem
expressFreightTemplateToadditCreateROList.push(rest)
})
const firstData = item[0] || {}
const { partitionName, countryList } = firstData
expressFreightTemplateConfigToadditCreateROList.push({
partitionName,
expressFreightTemplateToadditCreateROList,
countryIdList: map(countryList, 'id')
})
})
return { expressFreightTemplateConfigToadditCreateROList }
}
showHeader
用于控制el-table
的显隐
`此处没必要用计算属性,给两个option。条件成立时,加上showHeader属性;条件不满足时,去除showHeader属性`
:option="{
...option,
showHeader: index == 0
}"
- 利用展开运算符简化代码
`优化前:`
formatData: (val) => {
this.finalData = []
cloneDeep(val).forEach(item => {
let mapData = item['standardList'] || [{}]
mapData.forEach((sItem) => {
if (!sItem['standardShippingStartWeight']) sItem['standardShippingStartWeight'] = 1
if (!sItem['standardShippingEndWeight']) sItem['standardShippingEndWeight'] = 1
if (!sItem['standardShippingFirstFreight']) sItem['standardShippingFirstFreight'] = 1
if (!sItem['standardShippingContinuedFreight']) sItem['standardShippingContinuedFreight'] = 1
if (!sItem.partitionName) sItem.partitionName = item.partitionName
if (!sItem.countryList) sItem.countryList = item.countryList
sItem.minEndWeight = sItem['standardShippingStartWeight']
})
this.finalData.push(item)
})
}
`优化后:`
formatData: (val) => {
this.finalData = []
cloneDeep(val).forEach(item => {
let mapData = item['standardList'] || [{}]
mapData.map((sItem, index) => {
const { partitionName, countryList } = item
`注意不要直接给sItem赋值,sItem确实变化了,但是原引用地址依旧不会发生变化,所以需要索引`
mapData[index] = {
standardShippingStartWeight: 1,
standardShippingEndWeight: 1,
standardShippingFirstFreight: 1,
standardShippingContinuedFreight: 1,
partitionName,
countryList,
...sItem
}
})
this.finalData.push(item)
})
}
84. lodash
中的groupBy
用法
传送门: https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
`groupBy方法, 第二个参数除了支持字符串,还支持函数的形式。分组会根据函数返回的结果,作为key值进行排序`
const groupData = groupBy(this.data, row => {
return row.sizeId + '_' + row.styleId + '_' + row.materialId
})
85. 使用展开运算符...
操作对象数组前,为了防止互相影响,请先深拷贝
const list = [{ name: '1', id: 1 }, { name: '2', id: 2 }]
const data = [...list, ...list]
list[0].name = 'cxk'
此时data第一项和第三项的name会发生变化,成为cxk
86. 敲好用的html
元素,适配不同大小的屏幕
`指定设备视口宽度的大小为1250px。在移动设备上,用户可以使用手势放大或者缩小网页。`
`PC设备上,在宽度超过1250px的情况下,会自动进行适配处理。`
<meta name="viewport" content="width=1250,user-scalable=yes">
87. 递归展开某个dom
下的子节点
const container = document.createElement('div')
`ctx为html富文本标记`
container.innerHTML = ctx
const list = []
;[...container.childNodes].map((item) => {
list.push(item)
if (item.children) {
const children = [...item.childNodes]
children.map(sItem => list.push(sItem))
}
})
88. 使用vue
指令操作dom
<template>
<CommonCascader
:popperClass="popperClass"
:options="options"
v-select-icon
v-bind="$attrs"
v-on="$listeners"
></CommonCascader>
</template>
<script>
import CommonCascader from '@/components/commonCascader'
export default {
components: { CommonCascader },
props: {
popperClass: {
type: String,
default: 'order-cascader'
},
options: {
type: Array,
default: () => [
{
label: $t('page.productTemplates.option.column.newestOrder'),
name: $t('page.productTemplates.option.column.newestOrder'),
id: [{ asc: false, column: 'create_time' }]
},
{
label: $t('page.productTemplates.option.column.oldestOrder'),
name: $t('page.productTemplates.option.column.oldestOrder'),
id: [{ asc: true, column: 'create_time' }]
}
]
},
iconName: {
type: String,
default: 'icon-ic_sort'
},
text: {
type: String,
default: $t('page.productTemplates.option.column.sort')
},
iconWrapperClass: {
type: String,
default: 'cate-and-search-component-select-icon'
},
textClass: {
type: String,
default: 'sort'
}
},
directives: {
selectIcon: {
inserted(el, binding, vnode) {
`在指令中,使用vnode.context获取当前vue的实例`
const vm = vnode.context
const { iconName, iconWrapperClass, text, textClass } = vm
const icon = document.createElement('i')
const inputDom = el.querySelector('.el-input')
`移除级联选择器的inputDom`
inputDom.parentNode.removeChild(inputDom)
`为icon dom添加类`
`classList.add可不支持链式调用`
icon.classList.add('iconfont', iconName, iconWrapperClass)
if (!text) return el.append(icon)
const span = document.createElement('span')
icon.appendChild(span)
span.innerText = text
span.classList.add(textClass)
el.append(icon)
}
}
}
}
</script>
<style lang="scss">
.cate-and-search-component-select-icon {
border: 1px solid $border-color;
display: inline-block;
height: 40px;
line-height: 40px;
width: 160px;
font-size: 16px;
color: #000;
padding-left: 54px;
cursor: pointer;
&:hover {
color: $color-primary;
border: 1px solid $color-primary;
.sort {
color: $color-primary;
}
}
}
.order-cascader {
.el-cascader-menu__wrap {
height: 90px;
}
}
.sort {
margin-left: 10px;
color: rgba(5, 0, 56, 0.9);
}
</style>
`appendChild`只能添加一个节点作为目标元素的最后一个子节点。
`append`可以添加多个节点或文本作为目标元素的子节点,并且可以接受字符串参数。
`appendChild`是`DOM`的方法,而`append`是`JavaScript`的方法(引入了`ES6`)。
89. 如何判断两个数组的内容是否相等(不限位置,要考虑重复元素
)
`Arrary.prototype.indexOf()使用严格相等算法 => NaN值永远不相等`
`Array.prototype.includes()使用零值相等算法 => NaN值视作相等`
[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1
NaN == NaN // false
`因此,[NaN].includes(NaN)也可以用于判断当前值是否为NaN`
isArrSame(arr1, arr2) {
if (arr1.length !== arr2.length) return false
const set = [...new Set([...arr1, ...arr2])]
function getCounts(arr) {
return set.map((item) => arr.filter((ele) => [ele].includes(item)).length).join('')
}
return getCounts(arr1) === getCounts(arr2)
}
判断当前值是否为NaN
的其他几种方法:
89. 使用第三方库async-validator
校验
`子组件validatorInput:`
`组件玩具,主要是运用第三方库的思想。`
`如果需要放在表单中等其他相对复杂的场景,需要重新封装`
`也可以判断飘红的样式是否存在,来判断校验是否通过`
<template>
<div :class="customClass">
<el-input v-bind="finalAttrs" v-on="$listeners" v-model="text" @blur="blur"></el-input>
<div v-if="isError" class="edit_text__error">
{{ validateMessage }}
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator'
const noop = function () {}
export default {
props: {
label: String,
value: String,
field: String,
customClass: String,
required: {
type: Boolean,
default: undefined
},
rules: {
type: Array,
default: () => []
}
},
created() {
this.text = this.value
},
data() {
this.defaultOption = Object.freeze({
size: 'small',
placeholder: `请输入${this.label}`,
clearable: true
})
return {
text: '',
validateState: false,
validateMessage: ''
}
},
computed: {
finalAttrs({ defaultOption, $attrs }) {
return { ...defaultOption, ...$attrs }
},
isError({ validateState }) {
return validateState === 'error'
}
},
watch: {
value(n) {
this.text = n
}
},
methods: {
blur() {
this.initRules()
this.validate()
if(this.isError) return
this.$emit('blur')
},
initRules() {
if (!this.required) return this.rules
return [
...this.rules,
{
required: true,
message: `${this.label}必填`
}
]
},
validate(callback = noop) {
const rules = this.initRules()
if ((!rules || rules.length === 0) && this.required === undefined) {
callback()
return true
}
this.validateState = 'validating'
const descriptor = {}
descriptor[this.field] = rules
`descriptor: { 需要判断的字段名field: 校验规则rules }`
const validator = new AsyncValidator(descriptor)
const fieldValue = this.text
`将字段名field和值fieldValue放入校验`
validator.validate(
{
[this.field]: fieldValue
},
`firstFields的值与第三个方法的errors参数有关, 默认为false`
`如果firstFields为true, 则errors参数只返回第一个校验失败字段的第一个校验错误信息`
`如果firstFields为false, 则errors参数返回所有校验失败字段的所有校验错误信息`
`{ firstFields: true }, `
(errors, invalidFields) => {
`errors为字段校验错误的信息`
`invalidFields为校验错误的字段`
this.validateState = !errors ? 'success' : 'error'
this.validateMessage = errors ? errors[0].message : ''
callback(this.validateMessage, invalidFields)
}
)
}
}
}
</script>
<style scoped lang="scss">
.edit_text__error {
color: $--color-danger;
font-size: 12px;
padding-top: 4px;
margin: 5px 0;
}
</style>
`父组件:`
<template>
<div class="app-container">
<validatorInput
customClass="name"
label="姓名"
v-model="name"
size="medium"
field="name"
:rules="rules"
:required="true"
@blur="blurHandler"
/>
</div>
</template>
<script>
import { charLenLimitConstructor } from '@/utils/validate'
import validatorInput from './module/validatorInput'
export default {
components: { validatorInput },
data() {
return {
name: 'cxk',
rules: [{ validator: charLenLimitConstructor(1, 5) }]
}
},
methods: {
blurHandler() {
...do something
}
}
}
</script>
<style>
.name {
width: 240px;
}
</style>
90. echart
封装
`组件:`
<template>
<div class="chart-wrapper">
<div class="title mb20" v-if="title">
{{ title }}
</div>
<slot name="before"></slot>
<div class="chart" ref="chart"></div>
<slot name="after"></slot>
</div>
</template>
<script>
import { createOption } from './const'
import { isObject } from 'lodash'
const echarts = require('echarts')
export default {
props: {
title: String,
data: {
type: [Array, Object],
default: () => []
},
handleChartData: {
type: Function,
default: (data) => {
if (!isObject(data)) data = {}
return createOption(data)
}
},
afterInit: Function
},
data() {
return {
myChart: null,
formatData: null
}
},
watch: {
data: {
handler() {
this.refresh()
}
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.offEvent()
},
methods: {
async init() {
await this.initChart()
this.onResize()
},
async initChart() {
const myChart = this.myChart = echarts.init(this.$refs.chart)
this.formatData = await this.handleChartData(this.data)
myChart.setOption(this.formatData)
this.bindEvent()
if (this.afterInit) {
this.afterInit(this.formatData, myChart, this)
}
},
bindEvent() {
this.offEvent()
Object.keys(this.$listeners).map(event => {
const fn = (...args) => {
this.$listeners[event](...args)
}
this.myChart.on(event, fn)
this.$once('offEvent', () => {
this.myChart.off(event, fn)
})
})
},
offEvent() {
this.$emit('offEvent')
},
onResize() {
window.addEventListener('resize', this.refresh)
this.$once('beforeDestroy', () => {
window.removeEventListener('resize', this.refresh)
})
},
refresh() {
this.myChart?.dispose()
this.$nextTick(() => {
this.initChart()
})
}
}
}
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 100%;
.title {
font-weight: 600;
}
.chart {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
}
}
</style>
`公共方法: 初始化echart配置,额外配置项传入会自动合并`
import { isArray, map, merge, isPlainObject, isUndefined, max, isFunction, isString } from 'lodash'
import { setPx } from '@/components/avue/utils/util'
const DEFAULT_ITEM_COLOR = '#6797ee'
const DEFAULT_SIZE = 12
export function createOption(data) {
if (data?.chartType == 'radar' || data?.radar) {
return createRadarOption(data)
}
if (data?.chartType == 'pie') {
return createPieOption(data)
}
return createDefaultOption(data)
}
export function createRadarOption(data) {
const { max } = Math
let { series, radar = {}, ...restOption } = data
if (!isArray(series)) series = [series]
const indicator = $GET(radar, 'indicator', [])
radar.indicator = createIndicator()
const option = merge(
{},
{
tooltip: {
trigger: 'item'
}
},
restOption,
{
radar,
series: createSeries()
}
)
return option
function createIndicator() {
const maxValue = series.reduce((cur, prev) => {
const seriesData = prev.data
seriesData.map(item => {
const value = isArray(item.value) ? item.value : [item.value]
const itemMaxValue = max(...value)
if (cur < itemMaxValue) {
cur = itemMaxValue
}
})
return cur
}, 0)
return indicator.map(item => {
if (!isPlainObject(item)) item = { name: item }
if (isUndefined(item.max)) item.max = maxValue
return item
})
}
function createSeries() {
const defaultSeries = {
type: 'radar',
label: {
show: true,
position: 'top'
},
emphasis: {
focus: 'series'
},
itemStyle: {
color: DEFAULT_ITEM_COLOR
}
}
return series.map(item => {
return merge({}, defaultSeries, item)
})
}
}
export function createPieOption(data) {
let { series, ...restOption } = data
if (!isArray(series)) series = [series]
const option = merge(
{},
{
tooltip: {
trigger: 'item'
},
label: {
formatter: '{b}\n{c}'
},
legend: createLegend()
},
restOption,
{
series: createSeries()
}
)
return option
function createLegend() {
return {
show: false,
legend: map(series, 'name')
}
}
function createSeries() {
const defaultSeries = {
type: 'pie',
radius: ['40%', '70%']
}
return series.map(item => {
return merge({}, defaultSeries, item)
})
}
}
export function createDefaultOption(data) {
let { series, ...restOption } = data
if (!isArray(series)) series = [series]
const option = merge(
{},
{
grid: {
right: 50
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: {
type: 'value'
},
xAxis: {
type: 'category'
},
dataZoom: [
{
type: 'inside',
endValue: 10,
zoomOnMouseWheel: false,
moveOnMouseWheel: true
}
],
legend: createLegend()
},
restOption,
{
series: createSeries()
}
)
formatGrid()
return option
function formatGrid() {
const grid = option.grid
if (!isUndefined(grid.left)) return
const fontSize = $GET(option, 'yAxis.axisLabel.fontSize', DEFAULT_SIZE)
const fontFamily = $GET(option, 'yAxis.axisLabel.fontFamily', 'sans-serif')
const formatter = $GET(option, 'yAxis.axisLabel.formatter', null)
const rich = $GET(option, 'yAxis.axisLabel.rich', null)
if (rich) return
const span = document.createElement('span')
Object.assign(span.style, {
fontSize: setPx(fontSize),
position: 'absolute',
opacity: 0,
fontFamily,
clip: 'rect(0px, 0px, 0px,0px)'
})
document.body.appendChild(span) //通过span获取宽度
const allData = series.reduce((cur, prev) => {
const prevData = prev.data || []
const lenData = prevData.map((item, index) => {
//经验值,添加宽度
if (isString(formatter)) {
item = formatter.replace(/\{value}/i, item)
} else if (isFunction(formatter)) {
item = formatter(item, index)
}
item = `__.${item}`
span.innerHTML = item.toString()
return span.offsetWidth
})
cur.push(...lenData)
return cur
}, [])
span.parentNode.removeChild(span)
const maxValue = max(allData)
const maxLeft = max([maxValue, 50])
grid.left = maxLeft
return option
}
function createLegend() {
return {
legend: map(series, 'name')
}
}
function createSeries() {
const defaultSeries = {
barGap: '0%',
label: {
show: true,
position: 'top',
color: DEFAULT_ITEM_COLOR
},
emphasis: {
focus: 'series'
},
itemStyle: {
color: DEFAULT_ITEM_COLOR
}
}
return series.map((item, index) => {
if (index > 0) {
delete defaultSeries.barGap
}
return merge({}, defaultSeries, item)
})
}
}
91. 在模态框内,点击文字,显示全屏的图片轮播
`核心思想:`
`1. 借鉴el-image的思想,调用其全屏显示图片轮播的组件`
`2. 模态框内的操作,显示全屏图片轮播,需要将图片轮播组件,插入到body`
<template>
<div>
<dialogTable
btnType="text"
width="800"
:btnText="$t('page.combination.view')"
:title="$t('menu.combination')"
:hasFooter="false"
:data="tableData"
:option="option"
@open="onopen"
>
<template #header>
<card customClass="mb10" :data="product" />
</template>
<template #pic="{ row }">
<defaultImg
class="global-image_multiply"
:disabled="true"
:previewSrcList="allImages(row)"
:src="src(row)"
imgSize="70"
>
</defaultImg>
</template>
<template #name="{ row }">
{{ $getValueOfLanguage(row, 'name') }}
</template>
<template #menu="{ row }">
<color-text-btn v-if="permissionList.print" @click="printHandler(row)">
{{ $t('menu.printFiles') }}
</color-text-btn>
</template>
</dialogTable>
<ImageViewer
v-if="show"
v-appendToBody
:zIndex="zIndex"
:onClose="() => (show = false)"
:urlList="urlList"
/>
</div>
</template>
<script>
import card from './card'
import defaultImg from '@/views/components/defaultImg'
import ImageViewer from 'element-ui/packages/image/src/image-viewer'
import { flatMapDeepByArray, checkPermission } from '@/utils'
import productCombineApi from '@/api/product/productCombineApi'
export default {
components: { card, defaultImg, ImageViewer },
props: {
data: Object
},
data() {
return {
product:{},
tableData:[],
show: false,
zIndex: 9999,
checkedData: {},
permissionList: {
print: checkPermission(['toadditbusiness:productTemplates:combination:print'])
}
}
},
computed: {
option() {
return {
menu: true,
page: false,
column: [
{
prop: 'pic',
label: $t('menu.printFiles')
},
{
prop: 'name',
label: $t('page.combination.name')
}
]
}
},
src() {
return (data) => $GET(data, 'customProductList[0].customShowImageList[0].clearPath', this.$DEFAULT_PIC)
},
allImages() {
return (data) => flatMapDeepByArray(data.customProductList, ['customShowImageList', 'clearPath'])
},
urlList({ checkedData }) {
return flatMapDeepByArray(checkedData.customProductList, ['customShowImageList', 'clearPath'])
}
},
methods: {
printHandler(data) {
this.checkedData = data
this.show = true
},
async onopen() {
const res = await awaitLoading(productCombineApi.detail({
id:this.data.id
}))
if (!res) return
this.tableData = $GET(res,'detail.productPrototypeCollectionList',[])
this.product = res.detail
}
}
}
</script>
<style scoped lang="scss">
.global-image_multiply {
display: inline-block !important;
}
</style>
`append-to-body指令:`
export default {
inserted(el) {
document.body.appendChild(el)
},
unbind(el) {
el.parentNode.removeChild(el)
}
}
92. (重点
) 如果需要操作dom
,首先就应该想到使用指令处理(文字超出指定宽度,使用tooltip
显示,否则直接显示文字)
`第一种方法:content的返回可能时异步情况,而使用指令可以解决这种异步问题`
<template>
<el-tooltip
:popperClass="`unique-tooltip-wrapper ${$attrs.popperClass}`"
:content="content"
:disabled="disabled"
v-bind="finalAttrs"
>
<span class="text-cut" :style="style" v-show-tooltip>
{{ content }}
</span>
</el-tooltip>
</template>
<script>
import { setPx } from '@/components/avue/utils/util'
const defaultOption = {
placement: 'top-end',
effect: 'light'
}
export default {
props: {
content: {
type: String,
required: true
},
widthThreshold: {
type: Number,
default: 200
}
},
data() {
return {
disabled: true,
contentWidth: null
}
},
directives: {
showTooltip: {
inserted(el, bing, vNode) {
const context = vNode.context
context.disabled = true
if (el.offsetWidth < context.widthThreshold) return
context.disabled = false
}
}
},
computed: {
finalAttrs({ $attrs }) {
return { ...defaultOption, ...$attrs }
},
style({ widthThreshold }) {
return { maxWidth: setPx(widthThreshold) }
}
}
}
</script>
<style lang="scss">
.unique-tooltip-wrapper {
max-width: 200px;
line-height: 20px;
}
</style>
<baseTooltip class="itemSon" :content="item.consigneeName"></baseTooltip>
<baseTooltip class="itemSon" :content="item.contactMobilePhone"></baseTooltip>
<baseTooltip class="itemSon" :widthThreshold="300" :content="item.detailAddress"></baseTooltip>
`获取给定字符串的宽度:`
`JavaScript 中,原型是一个对象,它包含了当前类型的所有实例共享的属性和方法。`
`通过在String.prototype.pxWidth上挂载canvas,我们可以确保在整个应用程序中只创建一个canvas元素,`
`而不是每次调用函数时都创建一个新的。`
`这种做法可以节省内存和性能开销,特别是在需要频繁调用该函数时。`
`因为canvas元素的创建和销毁是比较昂贵的操作,所以将其缓存起来可以提高函数的执行效率。`
`防止每次都新建一个canvas`
`每次检查canvas有没有创建,如果有创建,就调用原型链上缓存的canvas,没有的话就新建一个,从而提高性能`
String.prototype.pxWidth = function (font) {
var canvas =
String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement('canvas')),
context = canvas.getContext('2d')
font && (context.font = font)
var metrics = context.measureText(this)
return metrics.width
}
`第二种方法,完全使用指令的方式解决`
import Vue from 'vue'
`引入组件`
import { Tooltip } from 'element-ui'
import { merge } from 'lodash'
import { setPx } from '@/components/avue/utils/util'
export default {
inserted(el, binding) {
`使用函数的形式,是为了在数据更新时,也能重新触发`
el.fn = (binding) => {
`配置默认属性`
const defaultOption = {
placement: 'top-end',
effect: 'light',
disabled: true,
content: el.textContent,
widthThreshold: 200,
popperClass: 'unique-tooltip-wrapper'
}
const { value = {} } = binding
`合并成为最终属性`
const option = merge({}, defaultOption, value)
const { widthThreshold } = option
const disabled = el.offsetWidth < widthThreshold
`el是显示文本的那一层,添加超出...和块级类以及最大宽度`
if(!disabled) el.classList.add('text-cut', 'block')
el.style.maxWidth = setPx(widthThreshold)
`使用new Vue, render生成新的vue实例`
const instance = (el.tooltipInstance = new Vue({
el: document.createElement('div'),
render(h) {
return h(
Tooltip,
{
props: { ...option, disabled },
},
`此处为span,是因为指令元素上有需要是否tooltip显示的文字`
[h('span', null, '')]
)
}
}))
const { $el } = instance
`使鼠标悬浮的tooltip的组件dom, 和el成为兄弟关系,此处主要是为了挂载tooltip的dom`
el.parentNode.appendChild($el)
`使鼠标悬浮的tooltip的组件dom, 成为el的父级`
$el.appendChild(el)
}
el.fn(binding)
},
componentUpdated(el, binding) {
el.fn(binding)
},
unbind(el) {
`销毁`
el.fn = null
if (el.tooltipInstance) {
el.tooltipInstance.$destroy()
el.tooltipInstance = null
}
}
}
<span class="itemSon" v-tip>{{ item.consigneeName }}</span>
<span class="itemSon" v-tip>{{ item.contactMobilePhone }}</span>
<span class="itemSon" v-tip="{ widthThreshold: 300 }">{{ item.detailAddress }}</span>
93. pdf
合并第三方库的使用
`pdfMergeUtils.js`
`使用error-first回调原则,封装pdf合并组件`
import { Message } from 'element-ui'
import PDFMerger from 'pdf-merger-js/browser'
import { validatenull } from '@/components/avue/utils/validate'
export async function pdfMerge(rawPdfList) {
if (validatenull(rawPdfList)) {
Message.error('数据不能为空')
return [true]
}
const merger = new PDFMerger()
for (const file of rawPdfList) {
await merger.add(file)
}
const mergedPdf = await merger.saveAsBlob()
return [null, URL.createObjectURL(mergedPdf)]
}
async printMark(row) {
const { shippingMarkPath, urgentShippingMarkPath } = row
const pdfList = [shippingMarkPath, urgentShippingMarkPath].filter(Boolean)
if (!pdfList.length) return
let src = pdfList[0]
if (pdfList.length > 1) {
let err = null
;[err, src] = await pdfMerge(pdfList)
if (err) return
}
await this.$lodopPrintPdf({
`src为pdf合并后的blob路径`
printable: src
})
}
94. 使用lodash
的uniqBy
方法,为对象数组去重
_.uniqBy([2.1, 1.2, 2.3], Math.floor) `// => [2.1, 1.2]`
_.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x') `// => [{ 'x': 1 }, { 'x': 2 }]`
95. 每次拖拽表格内的图标后,需要实时请求后端接口, 更新列表的数据显示
`指令封装:`
import Sortable from 'sortablejs'
import { validatenull } from '@/components/avue/utils/validate'
import { isPlainObject, merge, isString } from 'lodash'
export default {
inserted(el, binding, vNode) {
let oldDomList = []
let newDomList = []
let sortable = null
let defaultOption = {
container: '.el-table__body-wrapper tbody',
dataProp: 'tableData',
sortProp: 'sortNumber',
// handle是拖拽图标对应的类名
handle: '.draggable-btn',
onStart() {
const { container, handle } = finalOption
oldDomList = querySelectorAllInContainer(container, handle)
},
onEnd() {
const result = {
oldData: [],
newData: []
}
const { container, handle, onAfterDrag, dataProp, sortProp } = finalOption
newDomList = querySelectorAllInContainer(container, handle)
// 拖动过程中,不变的有sortNumber, tableData
// 所以,在找新旧dom的映射关系时,都是以原始tableData数据作为参考
// 新数据:tableData[在tableData中的对应索引]
// 旧数据: tableData[index]
newDomList.forEach((item, index) => {
const fIndex = oldDomList.findIndex((oldItem) => item == oldItem)
const oldData = vm[dataProp][index]
const newData = vm[dataProp][fIndex]
result.oldData[index] = {
...oldData,
[sortProp]: oldData[sortProp]
}
result.newData[index] = {
...newData,
[sortProp]: oldData[sortProp]
}
})
// 只有调用后端接口后,tableData才会发生变化,视图才会更新,但是每一行的sortNumber仍然是不变的
onAfterDrag(result)
},
// 如果不穿onAfterDrag, 默认使用以下方式更新排序
onAfterDrag(result) {
const { sortProp, dataProp } = finalOption
const newData = result.newData || []
// 不改变tableData的引用地址,因为新旧数据要以tableData作为参照
vm[dataProp].length = 0
newData.map((item) => {
if (!isPlainObject(item)) {
vm[dataProp].push(item)
return
}
// 不请求接口,只改变数据,在需要请求时再调用接口,更新排序
vm[dataProp].push({
...item,
[sortProp]: item[sortProp]
})
})
}
}
const vm = vNode.context
let { value: option } = binding
if (!isPlainObject(option)) {
option = {
onAfterDrag: option
}
}
const finalOption = merge({}, defaultOption, option)
let { container = el, handle, dataProp, onAfterDrag } = finalOption
if (!handle) {
throw new Error('缺少必要参数 handle')
}
// 监听表格数据,只要表格数据发生变化,就会销毁sortable实例,并重新创建这个实例
el.unWatch = vm?.$watch(
() => {
return vm[dataProp]
},
(newVal) => {
if (validatenull(newVal)) return
vm.$nextTick(() => {
sortable && sortable.destroy()
finalOption.container = isString(container) ? el.querySelector(container) : container
finalOption.onAfterDrag = isString(onAfterDrag) ? vm[onAfterDrag] : onAfterDrag
sortable = Sortable.create(finalOption.container, finalOption)
})
},
{
immediate: true
}
)
},
unbind(el) {
el.unWatch && el.unWatch()
}
}
function querySelectorAllInContainer(container, selector) {
const selectors = container.querySelectorAll(selector)
if (!selectors) return []
return [...selectors]
}
`指令运用:`
<template>
<baseTable
ref="table"
:sup_this="sup_this"
:list="option.list"
:resetMergeData="resetMergeData"
v-sortTable="{
onAfterDrag: onAfterDrag
}"
>
...
</baseTable>
</template>
<script>
export default {
methods: {
async onAfterDrag(result) {
const newData = $GET(result, 'newData', [])
let list = newData.map(({ id, sortNumber }) => ({
id,
sortNumber
}))
const res = await awaitResolveDetailLoading(userApi.changeOrder({ list }))
if (!res) return
this.$message.success('操作成功')
this.init()
}
}
}
</script>
96. git
写错代码分支
97. 本地git
分支出错(比如出现提示需要合并,但是找不到需要合并的文件的诡异现象,需要删除此次的错误提交
)
`创建并切分支,或者直接切换到已存在的本地分支`
git checkout -b dev-v4.0.8
`删除需要删除的本地分支`
git branch -D track-v1.0.0
`切换到和之前相同名称的远程分支, 这个时候本地的错误提交就被清空了,和远程最新分支保持一致`
git checkout track-v1.0.0
98. 从vue
源码角度考虑vue
组件methods
中的this
指向问题
将vue
中method
的this
指向为当前的vue
实例,并返回一个新的函数
这里深刻体现了vue
代码编程的严谨性,兼容了一些比较老的浏览器。如果函数的原型链上有bind
方法,就使用原生的bind
方法;如果原型链上不存在bind
方法,则使用vue
自己重新实现的bind
方法。
ctx
为上下文信息,将fn
的this
指向ctx
,对应上文就是定义该方法的vue
组件对应的实例
99. this
指向问题
window.name = 'cxk'
const data = {
name: '你干嘛哎哟',
`this取决于调用它的对象,通常是你希望的结果`
`也因此在对象方法中,如果希望正确使用this,一般使用常规函数的形式`
fn: function () {
return this.name
},
`箭头函数没有自己的this, 这个this指向的是定义时的上下文this`
fn1: () => this.name
}
`对象调用fn:this指向对象, 返回对象的name`
data.fn() `结果:'你干嘛哎哟'`
const { fn } = data
`fn()等价于window.fn():函数的调用者为window, this指向window,返回window.name`
fn() `结果:'cxk'`
data.fn1() `'cxk'`
const { fn1 } = data
fn1() `结果:'cxk'`
100. 使用正则匹配,兼容多种特殊情况
`//aaa, ///aaa, aaa, ///lmis///aaa, ////lmis////aaa或////lmisaaa,最终都会识别为/lmis/aaa`
const reg = new RegExp(`^((/)*${REQUEST_PREFIX})?/*`)
obj.requestUrlRegexp = requestUrlRegexp.replace(reg, `/${REQUEST_PREFIX}/`)
101. 闭包在工作中的应用场景
el-form
的自定义表单校验规则
`字符长度限制 validator构造函数`
export function charLenLimitConstructor(min, max, err) {
const reg = new RegExp(`^.{${min},${max}}$`)
err = err || `请输入${min}-${max}个字符`
return function (rule, value, callback) {
if (isEmpty(value)) {
return callback()
}
reg.lastIndex = 0
const rsCheck = reg.test(value)
if (!rsCheck) {
callback(err)
} else {
callback()
}
}
}
rules: [
{
required: true,
message: requireFun('档位名称')
},
{
validator: charLenLimitConstructor(1, 64)
}
]
- 为含有内置参数的
spanMethod
方法传入自定义参数
`解决方法一(使用闭包1):`
<div class="table-wrapper mb20" v-for="(item, index) in data" :key="index">
<p>{{ title(item) }}</p>
<baseTable :data="item" :option="option" :spanMethod="spanMethod(item)"> </baseTable>
</div>
spanMethod(data) {
return (params) => {
return createSpanMethod(params, data, [
[
{
...doSomeRules
}
]
])
}
}
`解决方法二 (使用闭包2):`
<baseTable :data="item" :option="option" :spanMethod="(parms) => spanMethod(parms, item)">
</baseTable>
`解决方法三 (使用bind方法传递参数,不会执行函数):`
`那么,spanMethod第一个形参就变成了item, 第二个形参才是内置参数`
<baseTable :data="item" :option="option" :span-method="spanMethod.bind(null, item)">
</baseTable>
102. return
一个map
方法会得到数组。如果在return
的map
方法里,再return
一个map
方法,则会得到一个二维数组
`mock数据:`
const mockData = [
{
startTime: '2024-05-20 08:30:00',
endTime: '2024-06-24 08:30:00',
updateTime: '2024-06-24 08:30:00',
recordList: [
{
sizeName: '35',
gearsList: [
{
name: '成本价',
price: 60
},
{
name: '项目总监价',
price: 65
},
{
name: '分销团队价',
price: 68
},
{
name: '自营/合伙人价',
price: 70
}
]
},
{
sizeName: '36',
gearsList: [
{
name: '成本价',
price: 70
},
{
name: '项目总监价',
price: 75
},
{
name: '分销团队价',
price: 80
},
{
name: '自营/合伙人价',
price: 82
}
]
}
]
},
{
startTime: '2024-06-24 08:30:00',
endTime: '',
updateTime: '2024-06-27 09:00:00',
recordList: [
{
sizeName: '35',
gearsList: [
{
name: '成本价',
price: 65
},
{
name: '唱',
price: 423
},
{
name: '跳',
price: 321
},
{
name: 'rap',
price: 70
},
{
name: '你干嘛哎哟',
price: 90
}
]
},
{
sizeName: '36',
gearsList: [
{
name: '成本价',
price: 66
},
{
name: '唱',
price: 121
},
{
name: '跳',
price: 60
},
{
name: 'rap',
price: 70
},
{
name: '你干嘛哎哟',
price: 80
}
]
}
]
}
]
formatData(data) {
return data.map((item) => {
return item.recordList.map((record) => {
const { sizeName, gearsList } = record
const { startTime, endTime, updateTime } = item
const result = {
startTime,
endTime,
updateTime,
sizeName
}
gearsList.forEach((gear, index) => {
const { price, name } = gear
result[`price${index + 1}`] = price
result[`name${index + 1}`] = name
})
return result
})
})
}
103. 重点:考虑计算属性(嵌套
、对象数组)中的响应式
- 谨防相互依赖的
嵌套
计算属性存在的陷阱`
`在计算属性a返回值不变,但是其依赖的解构项发生变化时。与计算属性a相互依赖的计算属性B, 此时也会重新触发更新`
<template>
<div class="app-container">
<el-button type="primary" size="small" @click="clickHandler">change</el-button>
<h3>{{ idol }}</h3>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
name: 'cxk'
}
}
},
computed: {
name({ obj }) {
return obj.name
},
`此处使用name,相当于把计算属性name放到这里进行调用`
`所以虽然name的值没有发生变化,但是obj的引用地址变了,最后还是触发了name的getter`
`因此也就触发了idol计算属性,在页面控制台上不断打印"我触发了"这句话`
idol({ name }) {
console.log('我触发了')
return `我的偶像是${name}`
}
},
methods: {
clickHandler() {
this.obj = { name: 'cxk' }
}
}
}
</script>
- 不同于定义在
data
中会被收集依赖的变量。如果在计算属性中的对象数组,某个对象的某个值发生变化,但只要不是引用地址变化,都不会触发响应式。
<template>
<div class="app-container">
<p>Array: {{ JSON.stringify(products) }}</p>
<p>Sum of Prices: {{ B }}</p>
<el-button type="primary" size="small" @click="changePrice">Change Price</el-button>
</div>
</template>
<script>
export default {
computed: {
products() {
return [
{ id: 1, name: 'Product A', price: 10 },
{ id: 2, name: 'Product B', price: 20 },
{ id: 3, name: 'Product C', price: 30 }
]
},
B({ products }) {
`B不会触发更新`
`这是因为products的值虽然发生了变化,但引用地址并没有变化,所以依赖项products也就没有变化`
`但如果将products放在data中,就会触发响应式。因为定义data中的变量,深层次的属性会收集依赖`
return products.reduce((total, product) => total + product.price, 0)
}
},
methods: {
`不断点击按钮,products的值发生变化,但并未触发响应式更新,页面还是保持之前的结果`
`这是因为定义在计算属性中的对象数组,深层次的属性并未收集依赖,只是对最外层进行监听`
`这也就意味着,只有计算属性的引用地址发生变化时,才会触发响应式`
changePrice() {
this.products[0].price += 5
}
}
}
</script>
结语
大概就这样吧~