前端零碎
前言
大年初一, 新年快乐!
本文为长篇, 用于记录一些前端把玩时遇到的小坑。
前端零碎
基础练习失误记录
<01> | Ts中使用构造函数时出现的小失误
往往遇到bug时, 最难发现的就是一些最简单的失误, 比如这一次的脑残失误,如下图:
可以先观察下下面这段脑残代码的问题(点击下图放大观看)
接下来运行代码后, 看下这段代码中所显示的三处控制台'log函数'对this的打印结果
那么揭晓答案: 问题不在于key,更不在于这三个函数的定义; 而是在于stats这个字段的定义 -- 由于其初始化没有经过构造函数才引起了这个现象, 而我们调用log打印的三个函数又偏偏都是经stats主机调用的, 因此都会出现这个现象。(而其他正常调用的情况下, key不会出现undefine的现象 -- 比如从外部类实例来调用这些函数)
ok, 是不是很好笑, 至少笔者是这么认为的, 找这个低级失误的bug竟能浪费笔者十多分钟哈哈!!! 那么, 下面就把 与前面两张图片对照用的 正确写法图以及其运行结果图放上, 让大家笑个够吧:
<02> | Ts中使用$event时,遇到< EventTarget类型中不存在属性”value” >
问题说明及解决方法
如下图所示:
在html标签 或 是在脚本代码的事件函数中, 均会由于此类型问题报错
参考–https://qa.1r1g.com/sf/ask/3102492851/
扩展
通过event.target来获取 value的值, 但是有时候会获取失败得到undefined:
当你使用
event.target
来获取事件目标元素的value属性时,有时候可能会获取失败并返回undefined
。这通常是由于以下几种原因造成的:
事件目标元素没有value属性。并非所有元素都有value属性,只有表单元素(如<input>、<select>和<textarea>)才有value属性。如果你尝试从一个没有value属性的元素中获取该属性,将会返回undefined。
事件目标元素不是你期望的元素。当你在一个容器元素上监听事件时,如果容器内的子元素触发了事件,那么事件会冒泡到容器元素上。此时,如果你使用event.target来获取事件目标元素,那么获取到的将是触发事件的子元素,而不是容器元素本身。
为了解决这个问题,你可以检查事件目标元素是否具有value属性,并确保它是你期望的元素。如果你想获取容器元素本身的属性,而不是子元素的属性,你可以使用event.currentTarget
来获取当前正在处理事件的元素。
<03> | Js/Ts中,import、export、export default的用法
前言
很多时候都会将一个模块,一条数据或者是工具方法抽离出来,在我们需要的时候再导入到我们需要的页面中,但总是记不清什么时候用export 什么时候用export default,什么时候需要{},什么时候又不需要,今天就来梳理一下
export 与 export default的区别
先来说一说export 与 export default的区别,以便我们更好的知道在什么时候什么场景去使用。
1、export与export default均可用于导出常量、函数、文件、类
等;
2、你可以在其它文件或模块中通过import 导入名 form '文件路径'
的方式将其导入使用;
3、在一个文件或模块中,export、import可以有多个
,export default
仅有一个;
4、通过export方式导出,在导入时要加{}
,export default则不需要;
5、使用export 导出后,import {}可以按需导入,减小项目大小,而 export default 是全部导入,开发中更推荐 export,这也是为什么我们引入的大多数第三方包的时候都是通过import {}的方式导入的;
export 与 import
通过 export 方式导出,在导入时要加 大括号 { } ,可以通过该方式实现按需加载,一般会用于项目的工具模块,需要哪个方法就引入哪个方法;
导出
1 | // say.js |
或者
1 | // say.js |
导入 导入名称要与导出名称一样
1 | // main.js |
“as” 别名
Import “as”
我们也可以使用 as 让导入具有不同的名字,例如:
1 | // main.js |
Export “as”
导出也具有类似的语法。
我们将函数导出为 hi 和 bye:
1 | // say.js |
1 | // main.js |
Import *
通常,我们把要导入的东西列在花括号 import {…} 中,但是如果有很多要导入的内容,我们可以使用 import * as 将所有内容导入为一个对象,例如:
1 | // main.js |
但是我们通常不会这样去写,原因如下:
1、现代的构建工具(如webpack 和 Vite等工具)将模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码,这样全都引入的话就没法达到这种效果。
2、明确列出要导入的内容会使得名称较短:sayHi() 而不是 say.sayHi()。
3、导入的显式列表可以更好地概述代码结构:使用的内容和位置。它使得代码支持重构,并且重构起来更容易。
Export default 与 import
该方式导入导出都不需要 { }
在实际中,主要有两种模块。
- 包含库或函数包的模块,像上面的 say.js。
- 声明单个实体的模块,例如模块 user.js 仅导出 class User。
大部分情况下,开发者倾向于使用第二种方式,以便每个“东西”都存在于它自己的模块中。
当然,这需要大量文件,因为每个东西都需要自己的模块,但这根本不是问题。实际上,如果文件具有良好的命名,并且文件夹结构得当,那么代码导航(navigation)会变得更容易。
模块提供了一个特殊的默认导出 export default 语法,以使“一个模块只做一件事”的方式看起来更好。
参考–https://blog.csdn.net/yml15180824993/article/details/126135715
参考–https://zh.javascript.info/import-export
<04> | Ts中, 遇到错误”此表达式不可调用,”…” 类型的部分要素不可调用。ts(2349)“ 如何解决
参考–https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
问题说明及解决方法
以下问题是在今日通过作基础练习熟悉vue时碰到的, 解决过程也在图片中做了标注:
<05> | Js中, JavaScript 中 call()、apply()、bind() 的用法
1、 call()、apply()、bind() 都是用来重定义 this 这个对象的
如:
1 | obj.myFun.call(db); // 德玛年龄 99 |
以上出了 bind 方法后面多了个 () 外 ,结果返回都一致!
由此得出结论,bind 返回的是一个新的函数,你必须调用它才会被执行。
2、 对比call 、bind 、 apply 传参情况下
如
1 | obj.myFun.call(db,'成都','上海'); // 德玛 年龄 99 来自 成都去往上海 |
微妙的差距!
从上面四个结果不难看出:
call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,’成都’, … ,’string’ )。
apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,[‘成都’, …, ‘string’ ])。
bind 除了返回是函数以外,它 的参数和 call 一样(如图中所示, 传入数组时出现了不匹配的 “整体嵌入 + undefined现象”)
当然,三者的参数不限定是 string 类型,允许是各种类型,包括函数 、 object 等等!
原文链接->https://www.cnblogs.com/Shd-Study/p/6560808.html
<06> window.onresize的使用
window.onresize = function(){}
作用: 当窗口大写发生像素变化,就会触发等号右边的函数function(){}
可以利用这个事件完成响应式布局, 它还有另一种常见写法:
window.addEventListener('resize',function() {});
在事件函数function(){}
中, 可以通过 document.body.clientWidth 或 window.innerWidth等调用,来获取当前的页面宽度, 进而实现布局的响应式。
注意:
- 1、 在vue中, window.onresize只能在一个组件中使用,如果多个组件调用则会出现覆盖情况。 因此,在App.vue中调用一次即可, 通过pinia在全局定义个 有关实时页面宽度的 响应式变量, 然后在
function(){}
中对此全局变量作初始化, 此后, 当其他组件中有用到宽度变量的地方直接通过pinia调用即可。 (当然, 涉及到html的调用时, 也可以通过computed来简化下html侧的代码。) - 2、 据说此事件谷歌浏览器中会偶尔出现执行两次的bug(没验证过, 笔者折腾这些前端知识是用来玩wails的):
解决方法是新建一个标志位 延时复位控制它不让它自己执行第二次,代码如下:(解决方案及此处代码块, 均来自网络, 未验证)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var firstOnResizeFire = true;//谷歌浏览器onresize事件会执行2次,这里加个标志位控制
window.onresize = function()
{
if (firstOnResizeFire) {
NfLayout.tabScrollerMenuAdjust(homePageWidth);
firstOnResizeFire = false;
//0.5秒之后将标志位重置(Chrome的window.onresize默认执行两次)
setTimeout(function() {
firstOnResizeFire = true;
}, 500);
}
homePageWidth = document.body.clientWidth; //重新保存一下新宽度
}
<07> Ts中extends和implements
1.ts 中 extends 和 implements
- ts 中 extends 可以理解为 es6 class 对应的 extends
可以实现类的继承 class Son extends Father {}
可以实现和接口的继承
可以在定义变量时, 实现类型的限制, 限制出一个类型子集。
1
2
3
4
5
6interface ISon extends IFather {
sonValue: number; // ISon上除了从IFather继承的属性,还增加了sonValue
}
function aaa<T extends ISon>(x : T):T {
// ...
}
2.implements 理解为实现,A implements B,A 上要有 B 对应的属性和方法,不能用于两个 interface 之间
- 类和类之间
class Son implements Father {}
// 用于类之间,此时没有继承的效果,而是要求Son上要有定义Father类的属性方法 - 类和接口之间:
class Son implements IFather {}
// 用接口去规范class, 要求Son的属性和方法等要按照IFather接口中定义的来
<08> CSS 如何画一个三角形?原理是什么?
https://blog.csdn.net/bai101724/article/details/127836184
<09> cb(),关于js中的回调的解释
https://www.impressivewebs.com/callback-functions-javascript/
<10> HTML5滑动(swipe)事件,移动端触摸(touch)事件
https://blog.csdn.net/weixin_36381252/article/details/117888216
WebApi的移动端常用事件
click点击事件
单击事件,类似于PC端的click,但在移动端中,连续click的触发有200ms ~ 300ms的延迟。
touch触摸事件
触摸事件,有touchstart touchmove touchend touchcancel 四种之分,常用的有:
touchstart:当有新手指触控到绑定的元素,会触发一次事件。
touchmove:当有手指放绑定的元素上会一直触发,从触发条件准确的说只有手指移动时才触发。但是经过测试,这一项检测十分灵敏,人为手指保持不动,系统也会侦测到细小的移动。所以会一直触发。
touchend:当有手指从绑定元素上抬起,会触发一次。
touchcancel:可由系统进行的触发(不常用事件),比如手指触摸屏幕的时候,突然alert了一下,或者系统中其他打断了touch的行为,则可以触发该事件。
事件列表
在移动端中上面的三个触摸事件每个事件都有以下列表
changedTouches:保存了所有引发事件的手指信息
targetTouches:保存了当前对象上所有触摸点的列表;
touches:保存了当前所有触碰屏幕的手指信息事件属性(只读属性)
移动端触摸事件属性里的数组元素的属性:每个事件有列表,每个事件列表还有以下属性
pageX //相对于页面的 X 坐标,与 clientX 不同的是,他包括左边滚动的距离,如果有的话。pageY //相对于页面的 Y 坐标,与 clientY 不同的是,他包括上边滚动的距离,如果有的话。
clientX //相对于视区的 X 坐标,不会包括左边的滚动距离。
clientY //相对于视区的 Y 坐标,不会包括上边的滚动距离。
screenX //相对于屏幕的 X 坐标
screenY //相对于屏幕的 Y 坐标
identifier // 表示每 1 个 Touch 对象 的独一无二的 identifier。有了这个 identifier 可以确保你总能追踪到这个 Touch对象。
target //手指所触摸的 DOM 元素
Touch.radiusX //能够包围用户和触摸平面的接触面的最小椭圆的水平轴(X轴)半径。这个值的单位和 screenX 相同。只读属性。
Touch.radiusY //能够包围用户和触摸平面的接触面的最小椭圆的垂直轴(Y轴)半径。这个值的单位和 screenY 相同。只读属性。
Touch.rotationAngle //它是这样一个角度值:由radiusX 和 radiusY描述的正方向的椭圆,需要通过顺时针旋转这个角度值,才能最精确地覆盖住用户和触摸平面的接触面。只读属性。
Touch.force //手指挤压触摸平面的压力大小,从0.0(没有压力)到1.0(最大压力)的浮点数。只读属性。
var pos = {x:e.touches[0].clientX,y:e.touches[0].clientY} /获取移动端拖动滑动坐标/
const touchY = e.touches[0].clientY - 79; //手指拖动竖坐标
比如:想获取手指拖动滑动的坐标位置,直接使用event.clientX是不起作用的,要使用event.changedTouches[0].clientX才好,
如果是jquery的event对象,使用event.originalEvent.changedTouches[0].clientX。
调用事件方法:
jquery方法:$(document).bind(“touchend”, function(e){});
javascript方法:document.addEventListener(“touchend”,function(e){});
tap类触碰事件
触碰事件,我目前还不知道它和touch的区别,一般用于代替click事件,有tap longTap singleTap doubleTap四种之分,有时会用tap代替click事件
tap: 手指碰一下屏幕会触发
longTap: 手指长按屏幕会触发
singleTap: 手指碰一下屏幕会触发
doubleTap: 手指双击屏幕会触发
swipe滑动事件
滑动事件,有swipe swipeLeft swipeRight swipeUp swipeDown 五种之分
swipe:手指在屏幕上滑动时会触发
swipeLeft:手指在屏幕上向左滑动时会触发
swipeRight:手指在屏幕上向右滑动时会触发
swipeUp:手指在屏幕上向上滑动时会触发
swipeDown:手指在屏幕上向下滑动时会触发
gesture手势事件
当两个手指触摸屏幕时就会产生手势,手势通常会改变显示项的大小,或者旋转显示项。有三个手势事件,分别如下。
gesturestart:当一个手指已经按在屏幕上面另一个手指有触摸屏幕时触发。
gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。
gestureend:当任何一个手指从屏幕上面移开时触发。
当有新手指触控到绑定的元素,会触发一次事件。
<11> let、const、var的区别
总结, 不要用var, 垃圾js存在的历史包袱问题。
而let和const和其它语言的区别不大, 推荐使用。
https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/
<12> 立即执行函数表达式(IIFE)
如下示例
1 | const res_data: { message: string }; |
<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/async_function</
<13> vue3中的响应式触发
若是将一个普通对象, 赋值给一个响应式对象, 则不会改变js原有的引用链条。
包括数组的索引更改, 数组的push等操作, 也包括对象中普通字段的值变更等等…
在赋值发生后, 若是同步/异步地变更普通对象中的字段, 则响应式对象中的对应字段值, 也会随着变化, 但是不会触发响应式更新。
在赋值发生后, 若是同步/异步地变更响应式对象中的对应字段, 则此普通对象中的这一字段, 也会随着变化, 而且通过响应式对象来操作时会触发响应式更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36type A = {s:number,ss:Array<string>,c:Array<A>}
const c = ref<A>({
s:1,
ss:["11"],
c:[]
})
const b:A = {s:2,ss:["22"],c:[]}
c.value.c.push(b)
console.log("c=",c.value)
console.log("b=",b)
function abc(){
// // 从普通对象做变更, 虽然引用特性不变, 但无法触发响应式更新
// b.s = 3
// b.ss.push("33")
// // 从响应式对象做同样操作, 可以触发响应式更新
c.value.c[0].s = 3
c.value.c[0].ss.push("33")
// // 数组索引值变更, 也会触发响应式更新
// c.value.c[0].ss[0] = "33"
console.log("111123123123123c",c.value)
console.log("1212312321b",b)
}
<template>
<div>{{c}}</div>
<div>{{b}}</div>
<button style="width: 50px;height:25px;" @click="abc"></button>
</template>
若是在函数中, 创建了一个普通对象, 并将此对象作为函数的返回值。 那么引用性依然和参数传递一致。
也就是说, 若是将此返回值,赋值给一个响应式对象, 则不会改变js原有的引用链条。包括数组的索引更改, 数组的push等操作, 也包括对象中普通字段的值变更等等…
在赋值发生后,即使是在函数返回之前通过异步方式延时变更普通对象中的字段, 也依然符合直觉。响应式对象中的对应字段值, 也会随着变化, 但是不会触发响应式更新。
在赋值发生后, 若是同步/异步地变更响应式对象中的对应字段, 则此普通对象中的这一字段(无论是返回后的值, 还是函数中此对象真正返回之前), 也会随着变化, 而且通过响应式对象来操作时会触发响应式更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41type A = {s:number,ss:Array<string>,c:Array<A>}
const e = ref<A>({
s:1,
ss:["11"],
c:[]
})
const newd = ()=>{
const d:A = {s:2,ss:["22"],c:[]}
// 模仿异步调用
async function simulateAsyncOperation() {
return new Promise((resolve) => {
setTimeout(() => {
// d.s = 3
// d.ss.push("33")
//console.log("nanshou")
resolve('异步操作完成');
}, 8000); // 延迟2秒
});
}
simulateAsyncOperation().then((message) => {
console.log("nanshou111111111111111")
d.s = 3
d.ss.push("33")
});
return d
}
const d:A = newd()
e.value.c.push(d)
// 定时轮询地查看变量值。
setInterval(function(){
console.log("111123123123123e",e.value)
console.log("1212312321d",d)
},
2000)
最近, 在项目中, 遇到了一个bug, 打破了我的上述总结, 不过还没完全定位, 具体现象是, 对于普通字段类型, 在普通对象中对其做变更, 引用性存在被破坏的可能(但若本身就具有引用性质的字段,如数组, 就不存在此问题)。(只是初步的bug定位, 还未最终确定)已经排除了对此bug的初步定位的错误, 我之前的结论可以保持不变了。 因为检查其调用链后, 发现了在使用此对象时存在解构的操作, 也就是说, 其对象中的引用字段虽为受到影响, 但是普通字段却因为对象的解构而有了新的地址。
解决方案
在TypeScript中,由于其静态类型特性,你不能直接向一个已定义并指定了类型的对象添加新的字段。但是,你可以通过一些方法来实现这个需求:使用索引签名:你可以在你的类型或接口中添加一个索引签名,来允许额外的属性。例如:
1
2
3
4
5
6
7
8interface MyObject {
a: number;
b: number;
[key: string]: number;
}
let obj: MyObject = { a: 1, b: 2 };
obj.c = 3; // 这是允许的,因为我们在MyObject中定义了一个索引签名- 使用类型断言:你可以使用类型断言来告诉TypeScript编译器,你知道你正在做什么,它应该允许你添加新的属性。例如:
1
2
3
4
5
6
7interface MyObject {
a: number;
b: number;
}
let obj = { a: 1, b: 2 } as MyObject;
(obj as any).c = 3; // 使用类型断言来添加新的属性
请注意,虽然这些方法可以让你向对象添加新的属性,但它们可能会降低TypeScript的类型安全性。在使用这些方法时,请确保你理解了它们的含义和潜在的风险。希望这个答案对你有所帮助!
- 这里,可以采用第2种方式来解决此问题
虽然,但是, 还是请注意代码的解构, 以保证调用的位置。 比如, 这里的是应该在最终加入实时树之前来处理那个最终对象才对, 而不是在过程中去操作。 (当然,包括m的异步逻辑, 不应和同步逻辑出现在一个函数封装中, 你知道我在说什么)
仅更改响应式变量时
补充一个新结论: Vue不能检测到数组的项修改(根据index)和length修改, 虽然删除数组中的元素会引起后面元素的索引被动改变,但这种索引的改变并不会触发Vue的响应式更新。只有当你修改了数组中的元素的值时,Vue才会检测到这个变化,并更新DOM。
也就是说, 你不必担心你的操作会引发一连串的响应式变更。 因为尽管是你删除了一个数组元素, 也只会响应式变更这个被删除元素的dom, 而不会影响其它dom的重新渲染。
补充一些响应式触发的典型案例
先说结论, 本小节对于普通变量的操作不会引起响应式触发的定义结论是没问题的。
至于某些某些 “疑似” 触发的现象, 稍作分析即可得出结论:
第一个, 对于某些对普通数组, 执行删除操作的情况, 意外引发 “疑似” 响应式变更的现象分析。
先说结论: 实际并没有触发响应式变更。
接下来看具体示例代码, 说下为何会有”疑似”现象的发生:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<!-- <q-page class="row items-center justify-evenly"> -->
<div class="row items-center justify-evenly">
<list v-for="item in fruits_ref" v-bind:key="item">
<li>{{ item }}</li>
</list>
</div>
<!-- </q-page> -->
</template>
<script setup lang="ts">
import { ref } from 'vue';
let fruits = ['Apple', 'Banana', 'Mango', 'Orange', 'Peach'];
console.log(fruits);
setTimeout(() => {
let index = Math.floor(Math.random() * fruits.length);
fruits.splice(index, 1);
console.log(fruits);
}, 3000);
const fruits_ref = ref(fruits);
</script>以上案例(可以自行赋值到项目中尝试)
代码内容是, 我们 有一个
fruits
的普通数组变量, 并通过ref对其做了响应式包装得到 响应式变量fruits_ref
, 我们的列表是根据 响应式变量fruits_ref
来渲染的, 3s后, 我们会随机删除 普通数组变量fruits
中的一个元素。 此时观察 ui 是否变化:- (最大最小化操作后) 不变 (这是正常现象, 一般的css布局可能并不会引起dom元素的重新渲染)
- (最大最小化操作后) 变化 (此时, 你的列表可能在某组件库的布局组件之中, 而布局组件引起了页面的重新渲染–如代码中注释掉的 quasar 组件库中的
<q-page></q-page>
布局组件)此时, 尽管完全不使用 响应式变量
fruits_ref
, 在v-for的列表项中将其替换为普通变量fruits
, 仍然会发生最大最小化操作时的重新渲染。
以上是以布局组件造成了重新渲染为例说明的情况, 具体其它的组件中, 如选择组件的抽屉等, 也都有可能触发重新渲染(这个自行验证), 总之这些重新渲染, 与我们普通数组的更改无关–即普通数组变更不会引起响应式变更。
<14> vue3-i18n的$t()使用时, 如何避免异步的空字符串引起的错误
1 | import { computed } from 'vue'; |
在这个示例中,message是一个computed属性,它会在asyncKey变化时重新计算。如果asyncKey是一个空字符串,那么message也会是一个空字符串;否则,message会是对应的本地化消息。
<15> js/ts 中的真假值默认判定
在JavaScript中,空字符串''
在if判断中被视为false
。这是因为在JavaScript中,以下值被视为假值(falsy):
false
0
和-0
''
和""
(空字符串)null
undefined
NaN
所有其他值,包括所有对象(包括空对象{})都被视为真值(truthy)。所以,如果你在if语句中使用空字符串,它将被视为false
。
<16> scrollWidth,clientWidth,offsetWidth的区别
先来看 new bing 的回答
在JavaScript中,你可以使用以下方法来获取HTML元素的宽度:
offsetWidth
: 这个属性返回元素的像素宽度,包括元素的宽度、垂直滚动条(如果存在)、左边框和右边框。例如:
1
2 var element = document.getElementById('your-element-id');
var width = element.offsetWidth;
clientWidth
: 这个属性返回元素的内部宽度(以像素为单位),包括填充,但不包括边框、滚动条和外边距。例如:
1
2 var element = document.getElementById('your-element-id');
var width = element.clientWidth;
getBoundingClientRect()
: 这个方法返回一个对象,该对象包含了元素的大小和其相对于视口的位置。你可以从中获取到元素的宽度。例如:
1
2
3 var element = document.getElementById('your-element-id');
var rect = element.getBoundingClientRect();
var width = rect.width;请注意,这些方法返回的都是像素值,你可能需要根据实际情况对其进行转换。
然后, 我们看下网络上的回答:
参考自: https://blog.51cto.com/u_15311558/3174457
scrollWidth:对象的实际内容的宽度,不包边线宽度,会随对象中内容超过可视区后而变大。
clientWidth:对象内容的可视区的宽度,不包滚动条等边线,会随对象显示大小的变化而改变。
offsetWidth:对象整体的实际宽度,包滚动条等边线,会随对象显示大小的变化而改变。
也就是说:
元素内无内容或者内容不超过可视区,滚动不出现或不可用的情况下。
scrollWidth=clientWidth,两者皆为内容可视区的宽度。
offsetWidth为元素的实际宽度。元素的内容超过可视区,滚动条出现和可用的情况下。
元素的内容超过可视区,滚动条出现和可用的情况下。
scrollWidth>clientWidth。
scrollWidth为实际内容的宽度。
clientWidth是内容可视区的宽度。
offsetWidth是元素的实际宽度。
<17> display的 inline、block、inline-block
- block : 会独占一行, 可以通过设置其width的值来改变对所在行的占用情况。 可以设置 width、height、margin、padding属性。 如div等默认都是此类型。
- inline: 行内元素, 一般更具内容文本的大小来动态改变尺寸。 没有width、height等属性, 就算是获取也只能得到0。因此无法通过ResizeObserver之类的方式来被动监听其尺寸的变化。 不可以设置 width、height属性, 而且 也只能设置 水平方向上的 margin、padding属性。
因为默认监听的尺寸指的就是正常的width和height, 但inline情况下都为0, 并不能展现实际尺寸。
获取其尺寸一般通过offsetWidth这种方式才可以成功获取。 - inline-block: 故名思意, 将对象设置为inline对象, 但对象的属性作为block对象呈现, 之后的内联对象会被排列在同一行内。 (即, 尺寸仍会根据文本内容的变化而变化, 但其width、height等属性不再是 0 )
虽然但是, 在布局时建议尽量不要使用 行块盒。 因为其同时包含了 块级格式化上下文(部分)、行级格式化上下文(复杂,全部)。 块级别上下文一般指的就是html块级div元素, 行级别上下文一般指的就是文本。 (也就是说, 打包构建时的 代码混淆所引起的代码换行消失, 是可能影响最终的布局情况的。 即造成了开发环境和运行环境不一致的问题。)
不过, 我们可以通过将 span之类的元素, 设置为此格式, 从而使得 ResizeObserver之类的方式可以成功监听其尺寸的变化。
参考 https://www.colgin.me/get-width-of-element/
<18> ResizeObserver的使用
参考: https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver
<19> vue3使用, v-for 中的 Ref 数组 | vue3中访问底层dom元素, 获取dom对象
参考: https://www.javascriptc.com/vue3js/guide/migration/array-refs.html
参考: https://vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for
在 Vue 3 中,要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):
1 | <div v-for="item in list" :ref="setItemRef"></div> |
结合选项式 API:
1 | export default { |
结合组合式 API:
1 | import { ref, onBeforeUpdate, onUpdated } from 'vue' |
注意:
itemRefs
不必是数组:它也可以是一个对象,其 ref 会通过迭代的 key 被设置。如果需要,
itemRef
也可以是响应式的且可以被监听。
<20> 实现’可选中’即’获取焦点’的功能
默认可以获得焦点的HTML元素,比如输入框(<input>
),选择框(<select>
),文本区域(<textarea>
)等等。
而为了提高完整的可方位性, 我们通常希望可以使用户更容易看到哪个元素当前处于焦点状态。因此在tailwindCSS中, 我们可以使用 focus:ring-2
。
当这些元素获得焦点时,focus:ring-2
会添加一个2像素宽的”tailwindCSS美化版轮廓”。
focus:ring-2
这个类在Tailwind CSS中并不仅限于按钮, 而是可以应用于任何元素。
如何获取div等默认无焦点元素的焦点
一般来说,<div>
元素默认是不能获取焦点的,因此focus:ring-2
这个类在<div>
元素上可能不会有任何效果。
但是, 我们可以通过添加tabindex
属性来使<div>
元素能够获取焦点。(还有一种方式, 不过具有破坏性)
1 | <div tabindex="0" class="focus:ring-2">我是一个可以获取焦点的div元素</div> |
在这个例子中,tabindex="0"
使得这个<div>
元素能够通过键盘的Tab键获取焦点(当然,也可以通过’鼠标点击’或’触摸屏点击’来获得)。当它获取焦点时,focus:ring-2
类会添加一个2像素宽的轮廓。
还有一种具有破坏性的方式(即超出了我们的需求范围):
contenteditable
属性可以使<div>
元素变成可编辑的,从而使它能够获取焦点。例如:
1 <div contenteditable="true">我是一个可以获取焦点的div元素</div>这个元素设置为true后, 我们的界面就变成真的可编辑的了。(非常可怕, 用户可以删除或修改界面上的一起文本内容。)
对于焦点的操作:
我们可以通过dom对象来操作焦点, 使其 使能(
focus()
) 或 失能(blur()
)。比如, 在滚动时间发生时获取焦点, 并在滚动时间结束后失去焦点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 获取你想要在滚动事件发生时获取焦点的元素
let element = document.getElementById('your-element-id');
// 设置一个变量来保存延时器的ID
let timerId = null;
// 监听滚动事件
window.addEventListener('scroll', function() {
// 在滚动事件发生时使元素获取焦点
element.focus();
// 清除上一个延时器
clearTimeout(timerId);
// 设置一个新的延时器,在500毫秒内没有新的滚动事件发生时,认为滚动事件结束
timerId = setTimeout(function() {
// 在滚动事件结束时取消元素的焦点
element.blur();
}, 500);
});当然, 也可以使用Lodash库的debounce函数这种现成已经封装好的防抖函数。
再比如, 使用户获取的焦点,在一定时间内无操作后, 自动消失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 获取你想要操作的元素
let element = document.getElementById('your-element-id');
// 设置一个变量来保存定时器的ID
let timerId = null;
// 监听元素的focus事件
element.addEventListener('focus', function() {
// 在元素获取焦点后开始计时,如果在5秒内没有任何操作,则取消元素的焦点
timerId = setTimeout(function() {
element.blur();
}, 5000);
});
// 监听元素的keydown和mousedown事件,如果有任何操作,则清除定时器
element.addEventListener('keydown', clearTimer);
element.addEventListener('mousedown', clearTimer);
function clearTimer() {
clearTimeout(timerId);
}
在HTML中,一次只能有一个元素获取焦点。这是由于浏览器和操作系统的设计决定的,以便用户可以使用键盘(例如Tab键)在可聚焦的元素之间导航。
然而,你可以通过JavaScript来跟踪用户在哪些元素上进行了交互。(比如实现个简陋的多选功能)
例如,你可以监听每个元素的focus和blur事件,然后在事件处理函数中更新一个数组,这个数组用来存储用户交互过的元素的引用或ID。
以下是一个简单的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 创建一个数组来存储用户交互过的元素的ID
let focusedElementIds = [];
// 获取你想要跟踪的元素
let elements = document.querySelectorAll('.your-elements-class');
// 为每个元素添加focus和blur事件监听器
elements.forEach(function(element) {
element.addEventListener('focus', function() {
// 在元素获取焦点时,将它的ID添加到数组中
focusedElementIds.push(element.id);
});
element.addEventListener('blur', function() {
// 在元素失去焦点时,从数组中移除它的ID
let index = focusedElementIds.indexOf(element.id);
if (index !== -1) {
focusedElementIds.splice(index, 1);
}
});
});
<21> 关于vue3如何渲染连引用地址都替换的真正全新数组

<22> 滚动性能优化
其实这一节, 完全有必要单独开一期。 不过目前仅做一些总结性的工作, 等有时间了再自己写。
https://zhuanlan.zhihu.com/p/30078937
https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
MDN官方推荐的滚动事件限流优化方案
https://juejin.cn/post/7134648288925450248
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
<23> keep-alive 组件的使用
正常来说, 以下是其的常用方式, 是没问题的。但是如果下面组件的呈现方式是仅依赖与router-view的, 那么就会造成keep-alive钩子函数无法正确触发--即 onActivated 和 onDeactivated 两个函数
1 | <template> |
假如以上组件是只能通过 router-view 来显示的, 那么这个组件中的 keep-alive就不起作用了, 毕竟没有在外部的router-view来配置, 就算起了作用, 也是无法控制其生命周期的。
你的理解是正确的。在你的例子中,如果
如果你想要在路由切换时保持
1 | <template> |
在这个例子中,所有通过
然而,如果你只想缓存特定的组件,你可以使用
1 | <template> |
在这个例子中,只有MoviesList组件会被缓存,其他的组件不会被缓存。
vue3与vue-router4
显然, 最新版本的使用方式出现变化, 我们需要通过vue-router提供的router-view组件的默认插槽的返回值结合<component :is="Component" />
来使用。
比如没有插槽的<router-view/>
可以等于下面的写法:
1 | <router-view v-slot="{ Component }"> |
因此, 可keep-alive的结合应该这样写:
1 | <router-view v-slot="{ Component }"> |
注意,我们可以通过keep-alive的 include
来决定哪些需要keep。 不过目前由于其仅支持字符串, 因此在使用setup语法糖的组件, 无法被正确识别, 具体原因见这几个链接:
https://github.com/vuejs/rfcs/discussions/273?sort=top
https://stackoverflow.com/questions/65619181/how-to-make-certain-component-keep-alive-with-router-view-in-vue-3
虽然在官网看到了这个, 但目前依旧无法正确使用
![]()
与pinia等状态管理插件的比较
当然, 如果你pinia一把梭的话, 也可通过pinia来保持状态, 这是完全没有问题的。 不过有一点不好的是, 如果你需要保持状态的组件非常大(即加载时间过长), 实际是没有必要在跳转的操作中卸载dom的, 也就是说, 虽然pinia可以帮助正常保持状态, 但实际上的dom仍旧是经历了卸载操作后又重新载入的新dom, 然后在新dom上使用了pinia的数据以保持了状态。
所以使用pinia一把梭的保持状态, 在遇到大dom时, 会造成加载过慢甚至卡顿这种体验不好的现象。而keep-alive是直接保留了原始dom, 从而通过内存不被释放而保存状态–即最终回来的还是同一个dom。
因此, 这种场景推荐使用keep-alive, 因为它可以通过空间换时间的方式, 免除重新加载的这个载入过程。
<24> vue,在事件处理函数中同时使用默认的事件对象和自定义参数
在实际开发过程中, 我们使用的一些事件提供了默认的传出参数值, 因此我们无法直接在原函数的参数上做文章–即原默认传出参数的函数格式是不能改变的。因此我们需要使用一些特殊的方式来实现:
比如,我们可能需要在实际调用的函数中,用到一些作用域范围内的变量, 比如遍历中的item。 那么就必须通过传参的方式, 在函数中使用。 但由于原函数格式不能改变, 因此我们需要使用一些特殊的方式来实现。
- 使用
.bind() 或 .call () 或.apply ()
等方式来将’当前使用此函数的作用域范围’的一些变量值 通过自定义参数 传递 到函数作用域中使用。 - 使用语法糖
()=>funcName(item)
来传递自定义的参数变量 item (推荐, 可读性更高, 更易维护)如果我们需要在事件调用的函数内, 仅需要使用自定义参数, 那么我们可以在调用时这样写
()=>funcName_1(item)
或这样写(defaultObject)=>funcName_1(item)
即忽略其默认传出参数即可。 实际使用中, 也可对默认的传出参数选择性使用, 自由选择即可, 我们只需要保证 '=>' 右边的函数与我们实际定义的调用函数格式一致即可, 以及 '=>'左边的格式要么空着,要么就严格按默认传出参数的格式填写。
实际定义中
1
2
3
4
5
6function funcName_1(item:string){
console.log(`item=`, item)
}
// 结果
// item=...如果我们需要在事件调用的函数内, 同时使用到默认传出的参数对象和自定义参数, 那么我们可以在调用时这样写
(defaultObject)=>funcName_2(defaultObject, item)
即defaultObject代表默认的传出参数的格式, 可以是一个对象(通常是这样, 所有传出字段集中在一个对象里), 也可以是多个分开的对像, 总之 '=>' 左边的式子一定要按格式来, 然后在右边实际的调用函数中, 仅使用我们需要的作为入参即可
实际定义中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function funcName_1(item:string){
console.log(`item=`, item)
}
// 这里只是举了个极端的例子, 即如果funcName_1在其它地方也被使用, 同时我们想要在funcName_2中复用这部分逻辑的情况。
function funcName_2(defaultObject:any, item:string){
console.log(`defaultObject=`, defaultObject)
console.log(`item=`, item)
funcName_1(item)
}
// 结果
// defaultObject=...
// item=...
// item=...
<25> 前端开发中, 常见的外部链接跳转功能
可以将 <a>
标签的 href
属性和 target
属性与 JavaScript 中的 window.open()
函数的两个参数作一个比较
在一个 <a>
标签中:
href
属性指定了要跳转的URL。target
属性决定了如何打开这个URL。例如,target="_blank"
会在新的浏览器标签页或窗口中打开链接。
相应地,在 window.open()
函数中:
- 第一个参数是要打开的URL,这与
<a>
标签的href
相对应。 - 第二个参数是指定新窗口的名称或是一个特定的选项(比如
_blank
、_parent
、_self
、_top
),这与<a>
标签的target
属性相对应。
例如,以下两种方式实际上是等效的:
使用HTML和 <a>
标签:
1 | <a href="https://example.com" target="_blank">Visit Example.com</a> |
使用JavaScript和 window.open()
函数:
1 | document.getElementById('myButton').addEventListener('click', function() { |
两者都会在新的浏览器标签或窗口中打开链接。在处理跳转链接时,更推荐使用 <a>
标签的原因是它更语义化(表明这是一个链接),并且更好地兼容无障碍访问需求,因为屏幕阅读器和其他辅助技术可以识别这个元素作为一个链接。
然而,在一些情况下,比如当跳转需要在用户执行某个动作后触发,但又不适合直接使用链接时,window.open()
就显得十分有用。例如,在提交表单后打开一个新窗口,或者在动态获取URL后触发跳转。在这些情况下,就需要用到 window.open()
函数。
HTML中的<a>
标签的target属性和JavaScript的window.open()函数的第二个参数,都可以指定在哪里打开所请求的链接
下面是这些特定选项的含义:
- _self - 这是默认选项。它会在当前正在查看的框架或窗口中打开文档。 (常用)
- _blank - 它会在新的未命名的窗口中打开文档。最典型的例子就是在新的浏览器标签页中打开链接。 (常用)
- _parent - 这个选项用于在父框架中打开文档。如果当前页面没有父框架,则此选项的行为类似于_self。
- _top - 如果文档是嵌套框架集(frame)的一部分,_top会使整个浏览器窗口(跳过所有框架)显示请求的文档。如果没有框架,这个选项的行为和_self相同。
在使用 target 属性或 window.open() 方法时,通常我们讨论的特定选项有_blank,_self,_parent,和_top。然而,_system 并非一个标准的选项,至少在HTML规范或JavaScript中的 window.open() 方法中不是。
可能会出现一些框架或环境提供额外的特定选项来处理特殊情况,比如在混合移动应用开发框架(如Cordova或Ionic)中,您可能会遇到 _system 这样的非标准选项。例如,在Cordova中,您可以使用:
_system: 在系统的默认浏览器中打开URL,而不是应用内浏览器(webview)。
window.location.href 和 window.open 和 标签页对比
window.location.href
和 <a>标签(不带target属性或设置为_self)
/ window.open() (第二个参数设置为空字符串或_self)
是一样的作用。都是用于在当前页面打开链接。(再详细一些的点, 可以向gpt4提问)
由于是基于当前页面打开url, 因此通常不会用其来访问实际的url链接( 即跳出我们当前应用的网址链接一般使用 _blank
)。 _self
常用于打开一些本地的应用程序, 由于它们是一些特殊的url, 因此并不会在当前应用程序页面的实际url地址。如:
- 邮箱:
mailto:someone@example.com
- 安卓Play:
https://play.google.com/store/apps/details?id=com.example
- iosPlay:
https://apps.apple.com/app/idxxxxxxxx
- 微软商店:
ms-windows-store://pdp/?ProductId=xxxxxxxx
- MacAppStore:
macappstore://itunes.apple.com/app/idxxxxxxxx
- steam商店:
steam://store/APP_ID
例如:steam://store/123456
<26> 选择phaser的原因
参考:https://aping-dev.com/index.php/archives/239/
cocos creator
Cocos2d-x is an open-source and cross platform open source free 2D game engine for mobile game development known for its speed, stability, and easy of use. (Cocos2d-x是一个开源和跨平台的开源免费2D游戏引擎,用于移动游戏开发,以其速度,稳定性和易于使用而闻名。)
- 开发环境只支持 Windows 和 Mac,不支持 Linux
- 拥有 UI 编辑器,可方便地在界面对组件进行拖动、旋转等操作
- 支持 JavaScript 和 TypeScript
- 不方便与 react、vue 等 web 框架结合
- 示例相对较少
- 跨平台,游戏可以快速发布到 Web、iOS、Android、HarmonyOS、Web、Windows、Mac,以及各个小游戏平台
phaser
Desktop and Mobile HTML5 game framework. A fast, free and fun open source framework for Canvas and WebGL powered browser games. (桌面和移动HTML5游戏框架。一个快速,免费和有趣的开源框架,用于Canvas和WebGL驱动的浏览器游戏。)
- 开发环境支持 Linux、Windows 和 Mac
- 开局一个canvas, 剩下全靠自己画
- 支持 JavaScript 和 TypeScript
- 方便与 react、vue 等 web 框架结合:
- 拥有丰富的示例:https://helpcenter.phasereditor2d.com/
- Games can be compiled to iOS, Android and native desktop apps via 3rd party tools.(游戏可以通过第三方工具编译到iOS,Android和本机桌面应用程序。)
<27> 空格 和 enter键, 会触发 vue组件上的鼠标点击事件@click, 如何阻止这个行为
在网页开发中,按下空格键或回车键触发 @click
事件是一个常见的行为,特别是在可交互的元素上。这个行为通常是由浏览器的默认行为或组件库的设计决定的。
原因分析
可访问性(Accessibility):
- 在网页开发中,为了支持键盘操作,很多可交互的元素(如按钮、链接等)都默认支持通过键盘触发点击事件。这意味着当你在这些元素上按下空格键或回车键时,浏览器会模拟一次点击事件,以便用户无需使用鼠标也能与页面交互。
- 比如,HTML 原生的
<button>
或<a>
标签默认就会在按下空格键或回车键时触发点击事件。
浏览器默认行为:
- 在可点击元素(如
<button>
、<a>
、或具有role="button"
的元素)上,按下空格键或回车键会触发浏览器的默认行为,进而触发@click
事件。
- 在可点击元素(如
Vue 的组件设计:
- 如果你在 Vue 组件中使用了带有
@click
事件的元素,并且该元素是可交互的(例如button
、a
,或者具有tabindex
属性的div
),那么空格键和回车键按下时可能会触发这个事件。 - 这也是 Vue 提供的一种方式,让你可以利用 HTML 的可访问性特性而无需额外处理键盘事件。
- 如果你在 Vue 组件中使用了带有
解决方法
如果你不希望空格键或回车键触发 @click
事件,可以采取以下几种方法:
在
@click
中检查事件来源:
你可以在@click
事件处理函数中检查事件的触发来源,判断是否是由键盘事件触发的,从而决定是否处理该事件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<div @click="handleClick">Clickable Div</div>
</template>
<script>
export default {
methods: {
handleClick(event) {
if (event.detail === 0) {
// 由键盘触发,不处理
return;
}
// 处理点击事件
console.log('Mouse click');
}
}
};
</script>在元素上阻止默认行为:
可以在按下空格键或回车键时,阻止浏览器的默认行为。1
2
3
4
5<template>
<div @click="handleClick" @keydown.space.prevent @keydown.enter.prevent>
Clickable Div
</div>
</template>使用专门的键盘事件:
如果你只想响应鼠标点击事件,可以单独监听键盘事件而不是@click
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<div @click="handleClick" @keydown="handleKeydown">
Clickable Div
</div>
</template>
<script>
export default {
methods: {
handleClick(event) {
console.log('Mouse click');
},
handleKeydown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
console.log('Keyboard interaction');
}
}
}
};
</script>
<28> 对于vue模板语法中, 有时我们需要使用lambda表达式, 来即时回调一些函数
比如, 在@click=”()=>{ }” 函数中, 如果需要在某些字符串赋值时, 字符串内需要使用 "
双引号, 但由于lambda两边是双引号, 因此会引起 语法分析器 的失败错误, 因此无法执行。
此时,如果这个字符串是用于html中的, 我们是可以使用 "
, 来在html中展示双引号的。
不过, 更推荐的是定义函数来调用, 而不是使用lambda表达式, 毕竟在模板语法中使用的lambda, 是会影响执行效率造成性能损失的。
<29> 对于match和matchAll等前端正则捕获字符串的功能简介。
1 | // 示例 1:多个捕获组 |
如果要匹配多个出现的情况,需要使用 matchAll()
或者加上 g
标志:
1 | // 使用全局标志 g 来匹配多个实例 |
总结:
match()
不带g
标志时:[0]
是完整的匹配文本[1]
,[2]
,[3]
等是各个捕获组
match()
带g
标志时:- 返回所有匹配的文本数组
- 不返回捕获组信息
matchAll()
带g
标志时:- 返回一个迭代器,包含所有匹配信息
- 每个匹配都包含完整文本和捕获组
总结
按下空格键或回车键会触发 @click
事件,是为了增强可访问性和用户体验。如果你不希望这种行为发生,可以通过检查事件来源或阻止默认行为来避免。
存储相关
localStorage与sessionStorage
localStorage的特点:
- localStorage将数据存储在本地,可以直接获取,减少了客户端和服务器端的交互。
- 不参与和服务器的通讯,localStorage的值的类型限定为字符串类型。
- 数据对象没有过期时间。
sessionStorage与localStorage的区别:
localStorage属于永久性存储;sessionStorage属于临时性存储,存储的信息将随会话的结束而被清空。
同一浏览器中,相同的域名和端口下的不同页面中,localStorage的存储信息是可以共享的,sessionStorage的存储信息无法共享,仅适用于当前页面。
示例:
1 | //localStorage为长期缓存 |