无论如何,你都必须得知道的js知识(续)

10,983 阅读34分钟

前言

  大家好,我是前端贰货道士。在整理无论如何,你都必须得知道的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. 全局执行上下文,有一个全局对象window

    b. 函数级上下文:任何一个函数都有自己特有的执行上下文(函数里面的区域可以访问到函数外部的变量,但是函数外部的区域,是访问不到函数里面的)

    c. 块级上下文: 由let或者const加一个{}所组成的区域,就是一个块级上下文(括号区域外是无法访问到括号区域里面声明的let或者const变量的,但是可以访问到括号区域里面的var变量)

  • 变量对象:存储某个区块代码里声明的值和变量

  • 真实JS变量在堆栈中的存储:

function foo() {
    var a = 1
    var obj = {
        name: 'xiaoming'
    }
}

foo()

  原始类型的值会直接存储在上下文中,而上下文则存储在栈内存中;

  引用类型的值实际上会被存储在堆内存中,每一个值都对应着一个地址,然后在栈内存的执行上下文中将变量的值赋值成对应的地址。 image.png

  • 栈和堆的溢出:

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. 扩展: 

ABCDEF16进制中对应101112131415
['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 infor of的碰撞

  • for in能够遍历对象和数组,对于对象得到的是key值, 对于数组得到的是索引index值。
  • for of无法遍历对象,能够遍历数组和带有iterator接口的,例如Set, Map, String, 得到的是value

遍历对象:

image.png

遍历数组:

image.png

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>

效果浏览:

image.png

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可以为任意类型。

闭包情况截图如下:

image.png

  垃圾回收机制是存在一个周期的,它不会马上就回收垃圾,需要有一个过程。但是我们可以通过点击谷歌浏览器工具栏——内存上的回收垃圾图标,将它的垃圾回收过程提前触发。

image.png

36. 变量提升

变量与函数的变量提升传送门:https://blog.csdn.net/qq_43692768/article/details/117458927
letconst的暂时性死区: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中,浏览器可以检测到表单的存在,并按照指定的actionmethod属性执行相应的提交操作。

  如果尝试在未将表单元素添加到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
}

image.png

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. 浏览器上的图像转换(经典)

  MDN-blob的定义

  给定一个url, 经过aixos请求(responseType: 'blob'), 就可以得到一个blob(二进制大文件)

4.png

2.png

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.vuejs文件之间的通信(应用于代码过长,需要抽取js文件,vuejs文件之间无法较好通信的情况)

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告诉我们什么是短码,以及使用短码的好处:

image.png

  • 安装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的使用及结果解析:

1I6``FNE115X47(TZMMI2`0.png

image.png

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. 固定随机种子生成 + 洗牌算法的应用

image.png

`分享一个生成固定随机种子(生成结果在[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标签上指定widthheight的值,未使用style样式强制添加宽高的大小,也未在css中为canvas指定宽度和高度,则canvas的默认宽度是300px, 默认高度是150px

  • canvas的宽高优先级:

    未设置(默认宽高) < canvas标签上指定widthheight(数值,单位默认为px) < 通过样式为canvas指定的宽高 < style上指定的宽高

  • canvas上绘制的图片被拉伸的问题:

    canvas标签上的宽高和使用css(包括style)设置的宽高不一样的情况下,就会出现这种问题。canvas可以看作为一张图片,它是基于canvas标签上的widthheight进行绘制的。如果它的宽度和高度被css覆盖掉了,则canvas上绘制的内容也会跟随着等比例缩放。

  • 关于canvasdom元素上的指定值:

    canvasDom.width: 永远指向canvas标签上的width属性的大小

    canvasDom.offsetWidth: canvas真实宽度,即通过csscanvas设定的宽度 || 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>
image.png

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`
}

在讲述使用分片处理技术,优化批量上传图片之前,我们得先理解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)
  }
}

image.png

注:reslove(res)是调用执行顺序1中的Promise, return res才是返回执行顺序2中的Promise结果。在后续的await Promise.all(pArr)中,如果需要对执行顺序2中返回的Promise结果做处理,我们才需要return。因此,在这串代码中,return resawait 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 仅在 ie8ie9 下有效)。另外,如果有多个defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的。
  • 浏览器对于带有 type="module"<script> ,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
  1. defer属性:

    • 带有defer属性的脚本会在整个文档解析和渲染完成后执行,但在DOMContentLoaded事件之前。
    • 多个带有defer属性的脚本会按照它们在文档中的出现顺序进行加载和执行。
    • 脚本的加载不会阻塞页面的渲染,即页面的渲染过程不会因为脚本的加载和执行而中断。
    • defer属性仅适用于外部脚本(通过src属性引入的脚本)。
  2. async属性:

    • 带有async属性的脚本在下载完成后立即执行,而不会等待文档的解析和渲染。
    • 多个带有async属性的脚本的加载和执行顺序是不确定的,谁先下载完成就先执行谁。
    • 脚本的加载和执行过程会中断页面的渲染,直到脚本加载和执行完成后才会继续渲染。
    • async属性同样只适用于外部脚本。
  3. type="module"的脚本:

    • <script type="module">标记用于加载 ECMAScript 模块。
    • 这些模块脚本默认具有defer属性,即在文档解析和渲染完成后执行,但在DOMContentLoaded事件之前。
    • 模块脚本的加载和执行不会阻塞页面的渲染。
    • 不同于传统脚本,模块脚本具有严格的模块作用域,并且通过importexport语句进行模块之间的依赖管理。

    deferasync属性只适用于外部脚本,即通过src属性引入的脚本。内联脚本(嵌入在<script>标签内部的脚本)会自动按照其在文档中的出现顺序执行,并且会阻塞页面的解析和渲染过程。

image.png

69. 浏览器渲染机制

link标签预加载资源

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哥是如何看待jscss阻塞页面渲染的?

  • css会阻塞页面渲染吗?

image.png

image.png

  • js会阻塞页面渲染吗?

image.png

  • 为什么需要将js代码放在页面底部?

image.png

  • 为什么不为所有js文件都设置为延迟加载呢?

image.png

  • 浏览器的并行下载机制是什么样子的?

image.png

71.对浏览器加载cssjs文件的理解(自绘思维导图)

image.png

image.png

  在浏览器中,渲染进程负责处理和呈现网页内容。渲染进程会周期性地执行渲染任务来更新页面的显示。

  一般来说,浏览器的渲染进程会以每秒约 60 帧(即每帧大约 16.67 毫秒, 一帧一帧渲染) 的速度执行渲染任务。这个时间间隔被称为“帧时间”或“刷新间隔”。在每个帧时间内,渲染进程需要完成以下主要任务:

  1. 处理用户输入:包括鼠标点击、键盘输入和触摸事件等用户操作。
  2. 更新布局和样式:根据 DOM 树和 CSSOM 的变化,计算每个元素的几何属性(如位置、大小)和样式属性(如颜色、字体),并确定元素的显示顺序。
  3. 执行 JavaScript:执行 JavaScript 代码,包括处理事件、更新页面状态和计算布局等操作。
  4. 绘制和合成:将元素绘制到屏幕上,并进行图层合成和组合,以生成最终的页面显示。

  如果在一个帧时间内存在过多的微任务(例如使用 Promise 和 async/await 等机制创建的异步任务),这些微任务可能会占用大部分的执行时间。这会导致渲染进程无法按时完成绘制和合成操作,从而延迟页面的显示更新。

  当页面的显示更新被延迟时,用户可能会感知到页面的“卡顿”或“加载”状态。浏览器可能无法及时响应用户的输入,页面可能无法流畅地滚动或动画效果可能变得不流畅。

  为避免过多微任务影响页面渲染性能,尽量减少和合并微任务的数量。用 requestAnimationFrame() 方法来调度一些任务,以确保它们在下一帧时间开始之前执行。

  此外可以在独立的线程中执行一些计算密集型的任务,减轻主渲染线程的负担,提高页面的响应性能。

  总的来说,过多的微任务会占用渲染进程的执行时间,导致页面渲染延迟。应该注意优化代码,减少微任务的数量,以确保页面的流畅渲染和良好的用户体验。

72. dom常用坐标属性小节

3595e7b145b44780a8b7fb6cd46d726b_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp

73. 各类开发环境的问题处理

  • 重置电脑上的git账号
1. 打开终端或命令提示符窗口。
2. 输入`git config --global credential.helper store`命令,这将告诉Git把用户名和密码存储在本地。
3. 输入`git pull` 或 `git push`命令时,Git会要求您输入新的用户名和密码。
4. 输入您新设置的用户名和密码,然后按回车键即可完成登录。

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()
}
  • ()是一个捕获组

image.png

'For_example_{id}'.match(/^([A-z0-9_-])*(\{id\})([A-z0-9_-]*$)/)
  • 正则中的$n

  当正则表达式有多个捕获组时,可以使用相应的$n来引用每个捕获组的内容,其中n是捕获组的索引。除此之外,我们还可以使用$n进行replace替换。

  \w的含义传送门: 匹配数字、字母和下划线

image.png

  字符串.replace的用法:

image.png

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. fetchXHRaxios

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-Typeapplication/json

  接下来,我们设置onreadystatechange事件处理程序,用于监听请求状态的变化。当readyState变为XMLHttpRequest.DONE时,表示请求完成。我们根据status来判断请求的成功或失败,并根据需要处理响应数据或错误。

  最后,我们使用send()方法发送请求。

  XHR的优点包括:

  • 在大多数现代浏览器中广泛支持。
  • 提供底层的请求控制和事件处理,可以根据需要进行更细粒度的操作。
  • 可以通过监听事件来实现进度追踪,如上传和下载进度等。

  然而,与FetchAPI 相比,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. forEachmap方法

  • forEachmap方法都无法使用returnbreak提前退出循环,除非使用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方法返回值为一个数组
  • forEachmap方法处理稀疏数组时,会跳过未定义的值,不会调用回调函数
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>

image.png

image.png

`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的其他几种方法:

image.png

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>

image.png

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. 使用lodashuniqBy方法,为对象数组去重

_.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>
image.png

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指向问题

image.png

  将vuemethodthis指向为当前的vue实例,并返回一个新的函数 image.png

  这里深刻体现了vue代码编程的严谨性,兼容了一些比较老的浏览器。如果函数的原型链上有bind方法,就使用原生的bind方法;如果原型链上不存在bind方法,则使用vue自己重新实现的bind方法。 image.png

  ctx为上下文信息,将fnthis指向ctx,对应上文就是定义该方法的vue组件对应的实例 image.png

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'`

image.png

image.png

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>
image.png image.png image.png image.png

102. return一个map方法会得到数组。如果在returnmap方法里,再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>

结语

  大概就这样吧~