前端知识汇总-持续更新
持续更新工作中的一些经验教训
# 1. JS 中的 this 是由函数调用者调用的时候决定的。
this 是在函数被调用的时候决定的,如果没有显示的调用者,this 指向的是全局对象,在浏览器中指向的是 window,在 node 指向的是 global。但是在严格模式下指向的是 undefined。
# 2.如果你使用了 ES6 的 class 语法,所有在 class 中声明的方法都会自动地使用严格模式
# 3.箭头函数中的 this 永远绑定了定义箭头函数所在的那个对象
# 4.React 为什么需要手动绑定 this?
以 1-3 条知识为基础,react 的 render 函数中事件绑定原写法为
<button onClick={this.onDismiss}>删除</button>
相当于:
class App {
onDismiss() {
console.log(this)
}
render() {
const onDismiss = this.onDismiss
onDismiss()
}
}
const app = new App()
app.render() // 输出结果为:undefined
2
3
4
5
6
7
8
9
10
11
12
13
实例 app 的 render 方法里面,将 App 对象的方法 onDismiss 方法赋值给 onDismiss,然后调用 onDismiss(),onDismiss 在调用时并没有显示的调用者,因此调用者会为全局对象,根据上面第 2 条,class 中使用的为严格模式,所有 this 指向的为 undefined.
以上,react 中事件调用需要手动绑定 this,在构造函数中手动 bind 或者使用箭头函数都可以
# 5.requestAnimationFrame 回调函数中的时间?
requestAnimationFrame 回调函数的时间是 DOMHighResTimeStamp,它是自当前文档生命周期开始以来经过的毫秒数,而不是调用 requestAnimationFrame 开始的时间戳,如果中途调用 cancelAnimationFrame 取消了循环,在下次再调用 requestAnimationFrame 的时候时间并不会重置,如果需要重置,需要自己维护时钟。
# 6 数组循环中使用 await async
在循环中使用 await 有 2 中方法,一种是使用 for in 语法,如下:
async function start() {
const array = [1, 2, 3, 4, 5]
for (const a in array) {
const result = await Promise.resolve(array[a])
console.log(result)
}
console.log('end')
}
start()
2
3
4
5
6
7
8
9
10
另外一种是使用 Promise.all 结合 map 方法,如下:
async function start() {
const array = [1, 2, 3, 4, 5]
await Promise.all(
array.map(async a => {
const result = await Promise.resolve(a)
console.log(result)
})
)
console.log('end')
}
start()
2
3
4
5
6
7
8
9
10
11
12
2 种方式都有着相同的输出:
1
2
3
4
5
end
2
3
4
5
6
# 7 Vue 页面刷新不触发 beforeDestroy
Vue
生命周期是Vue
对象的生命周期,生命周期函数是Vue
对象在创建、挂载和销毁中调用的一些钩子函数,浏览器刷新重载是浏览器触发,与 Vue 对象无关,因此,在页面刷新的时候不会触发 Vue 对象的beforeDestroy
与destroyed
生命周期函数。如果我们需要在页面刷新的时候处理某些逻辑的话,需要监听 window 对象的beforeunload
事件。
我们可以在 Vue 组件的created
函数中添加beforeunload
监听:
created () {
window.addEventListener('beforeunload', handler)
},
2
3
在 Vue 组件的beforeDestroy
中移除监听:
beforeDestroy () {
window.removeEventListener('beforeunload', handler)
}
2
3
# 8 时间格式化在IOS上面的问题。
在IOS端,时间字符串分隔符为/
,需要将其他分隔符如-
转为/
再进行初始化,否则会提示NAN。
// 例如 time = '2020-08-25'
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
time = time.replace(new RegExp(/-/gm), '/')
2
3
# 9.Axios 在 GET 请求中传递数组问题
今天在重构之前写的代码的时候,发现一个接口请求数据有问题,传递给后端的是一个数组,但是在控制台里面却变了样,参数名称后面诡异的加了一个'[]',回忆了一下自身并没有对这个参数进行了格式化操作,Google 一番发现这是 axios 的锅
经过实验,axios 的 get 方法中使用 params 时对于 js 数组类型的参数的默认操作比较诡异,会使得参数名后带上'[]'字符串,原因是 axios 对 params 格式化采用的是qs (opens new window)这个库,因此可以使用 qs 自带的 arrayFormat 参数配置解决这个问题,大致配置如下:
const qs = require('qs')
axios.get(url, {
params: {
arr: [1, 2, 3]
},
paramsSerializer: function(params) {
return Qs.stringify(params, { arrayFormat: 'repeat' })
}
})
2
3
4
5
6
7
8
9
10
后端接收为 url-query 数组,即'id=1&id=2&id=3'
# 10. vue 组件没有 prototype
如果是在组件中需要挂载全局变量的话,没法直接使用 this.prototype,组件实例没有这个属性,只有proto,但是这个属性是被废弃的,不建议使用。在组件中使用 this.$options._base 即为 Vue 构造函数,在构造函数原型上挂载属性即可挂载到 Vue 全局中,在所有组件中使用。 参考:Vue 技术揭秘:构造子类构造函数 (opens new window)
# 11.在项目中组件的封装
项目中的组件常分为基础组件和业务组件,基础组件为对一些页面基本构成部分的封装,如 element-ui 等属于这一类,业务组件则为适应业务要求而对基础组件进行的再封装,一般基础组件的适应性更强。
在组件封装中不要过度封装,组件应该着重于展示部分,接收数据,根据数据展现出应有的形态,应本着高内聚,低耦合原则。prop 尽量只传递一些基本数据类型,并做数据验证。不要将非展示的逻辑强行封装到组件中,导致组件适应性差,不应把父组件应处理的逻辑放到组件中处理,组件只接受数据,返回渲染结果
业务组件可以适当的将业务逻辑封装进组件内部,有时候业务性质高度统一,但是又是不同的页面只是数据接口改变而已,这样可以将整个页面进行封装,实现复用。
如上图所示,在产品中已对 QueryForm 和 table 进行了一次封装,但是现在有 2 个类似的页面,都有对表格数据的增删改查需求,表格操作按钮的弹窗和逻辑都高度相似,UI 部分和业务逻辑部分都基本不变,只是接口名改变了。简单的方式就是新建一个文件,代码复制一遍。如果封装的话,需要把各自的增删改查接口通过 prop 传进组件里面才能实现最大的复用。因此我选择了后者,如果后面再出一个类似的页面就可以直接使用了。但是问题是需要增删改查接口传递的参数一致才行,不然需要针对每个接口做数据格式化,写一堆 if else 语句来进行判断,因此业务组件的耦合性相当高,扩展性不强。如果把页面所需要处理的事件都冒泡到父组件来处理的话,那也就失去了组件化的意义。如何对组件进行封装以及封装到哪种地步都需要根据情况来考虑。
我封装的组件使用方式如下所示:
<template>
<div>
<div v-if="showIndex" class="flex-1-box">
<service-list
service-type="mysql"
:get-table-list-api="getMysqlList"
:init-api="init"
:update-api="update"
:rest-api="resetPsw"
/>
</div>
<router-view v-else></router-view>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 12.alert,notification 组件
这类组件的封装,其实内部维护一个数组,将新加的 message 添加进数组,渲染一个列表,然后根据设置 duration 设置一个延迟函数,移除添加进去的 message,message 一条一条的渲染出来并在一定时间后消失。