1.为什么小程序有两个线程?
这两个线程分别是:
- 主线程:也称为逻辑线程或渲染线程。这个线程负责处理小程序的业务逻辑、数据处理和 UI 渲染。开发者编写的 JavaScript 代码在这个线程上执行,包括页面的逻辑处理、数据绑定和更新等。
- 工作线程:有时也称为 Web Worker 线程。这个线程用于执行一些耗时的计算任务,以避免阻塞主线程,从而提高小程序的响应性能。在小程序中,工作线程可以用来进行如大量数据处理、复杂计算等操作。
功能:
- 避免阻塞:将计算密集型任务放在工作线程上执行,可以避免长时间的计算任务阻塞主线程,从而影响用户界面的响应性。
- 提高性能:通过在不同线程上分配任务,可以更有效地利用 CPU 资源,提高小程序的运行效率。
- 分离关注点:主线程专注于 UI 渲染和交互逻辑,工作线程处理后台任务,这种分离有助于代码的组织和维护。
2.web应用中,如何对静态资源加载失败的场景做降级处理?
css:
设置一个默认的背景图
<img src="image.jpg" alt="" onerror="this.onerror=null;this.src='default-image.jpg';">
设置多个背景图
background-image: url('image1.jpg'), url('default-image.jpg');
js:
const images = document.querySelectorAll('img');
images.forEach(img => { img.onerror = function()
{ this.src = 'default-image.jpg';
} });
function loadScript(url)
{ var script = document.createElement('script');
script.src = url; document.body.appendChild(script); }
fetch('image.jpg') .then(response => response.blob()) .then(blob => {
const url = URL.createObjectURL(blob); document.getElementById('image').src = url;
}).catch(() => {
document.getElementById('image').src = 'default-image.jpg';
});
3.html中,前缀为data-开头的元素属性是什么?
以 data- 开头的属性被称为自定义数据属性(Custom Data Attributes)。
这些属性允许开发者存储额外的信息,这些信息对于 Web 应用程序来说是私有的,并且不会对页面的表现或行为产生直接影响。
-
存储私有数据:
data-属性可以用来存储页面或应用程序的私有数据,这些数据可以由 JavaScript 访问和操作。 -
语义化:自定义数据属性可以用来增强 HTML 元素的语义,提供更多上下文信息。
-
兼容性:所有现代浏览器都支持
data-属性,并且它们不会对页面的布局或样式产生影响。 -
命名约定:
data-属性的名称应该尽可能描述性,并且遵循连字符(kebab-case)命名约定,例如data-user-id或data-sort-order。 -
JavaScript 访问:可以通过 JavaScript 的
dataset属性访问元素的自定义数据属性。 eg: -
HTML5 规范:自定义数据属性是 HTML5 规范的一部分,它们被广泛用于现代 Web 开发。
-
避免滥用:虽然
data-属性非常有用,但应该避免过度使用或存储敏感信息,因为这些信息可以被任何访问页面的用户看到。
<div id="user" data-user-id="123" data-sort-order="asc"></div>
const userElement = document.getElementById('user');
console.log(userElement.dataset.userId); // 输出 "123"
console.log(userElement.dataset.sortOrder); // 输出 "asc"
4.移动端如何实现上拉刷新,下拉加载?
-
touchstart: 手指触屏触发的事件,主要工作是在触发时获取鼠标点击的Y坐标,event.touches[0].pageY。
-
touchmove: 手指滑动触发的事件, 主要工作是在触发时获取移动的Y坐标减去开始时的Y坐标,得出移动的距离,然后利用transform改变容器的位置。
-
touchend: 手指松开触发的事件,主要工作是释放鼠标让div恢复原来位置。
<template>
<div>
<div id="scroll-container"
@touchstart.stop="handlerTouchStart"
@touchmove.stop="handlerTouchMove"
@touchend.stop="handlerTouchEnd"
ref="scrollContainer"
:class="{'transition': isTransition}"
>
<!-- 根据isDisplay.refresh 动态隐藏显示 -->
<div :class="['refresh', {'display': isDisplay.refresh}]">
<!-- 添加isShrnked 加载 和 箭头相互转换 -->
<!-- 添加rotate类 反转箭头 下箭头和上箭头互相转换 -->
<img
:src="isShrinked? loadImg : refreshImg"
:class="{'rotate': isRotate}"
>
</div>
<slot></slot>
<!-- 根据isDisplay.load 动态隐藏显示 -->
<div :class="['load', {display: isDisplay.load}]">
<img :src="loadImg">
</div>
</div>
</div>
</template>
<script>
// 拖拽状态 true:下拉 false:上拉
let SCROLLSTATUS
export default {
props: {
// 能够拖拽的最大距离
maxDistance: {
style: Number,
default: 300
},
// 定义触发加载刷新事件的拉伸长度
triggerDistance: {
style: Number,
default: 100
}
},
data () {
return {
startLocation: '', // 记录鼠标点击的位置
moveDistance: 0, // 记录移动的位置
distance: '', // 记录移动的距离
isTransition: false, // 是否启动transition
isDisplay: {
refresh: true,
load: true
},
// 添加emit缓存数组,并以undefined填充 emitEvents: new Array(2).fill(undefined),
// 把图片地址抽离出来 方便动态切换
loadImg: 'https://img.lanrentuku.com/img/allimg/1212/5-121204193R5-50.gif',
refreshImg: 'https://www.easyicon.net/api/resizeApi.php?id=1190769&size=48',
isRotate: false, // 是否选择箭头
isShrinked: false // 是否收缩完成
}
},
methods: {
// 获取手指触屏时的屏幕Y轴位置
handlerTouchStart (e) {
this.isTransition = false
this.startLocation = e.touches[0].pageY
// 重置箭头反转
this.isRotate = false
// 重置箭头
this.isShrinked = false
},
// 获取手指移动的距离
handlerTouchMove (e) {
if (this.moveDistance > this.maxDistance + 1) {
this.isRotate = true return
}
this.moveDistance = Math.floor(e.touches[0].pageY - this.startLocation) this.$refs.scrollContainer.style.transform = `translateY(${this.moveDistance}px)`
// 显示加载 刷新图片 if (this.moveDistance > this.triggerDistance && this.isDisplay.refresh) {this.isDisplay.refresh = false }
else if (this.moveDistance < -this.triggerDistance && this.isDisplay.load)
{ this.isDisplay.load = false } // 缓存刷新的emit if (this.moveDistance > this.triggerDistance && !this.emitEvents[0])
{ this.emitEvents[0] = function () {
this.$emit('refresh', this.displayDiv)
} }
// 缓存加载的emit
if (this.moveDistance < -this.triggerDistance && !this.emitEvents[1])
{ this.emitEvents[1] = function () {
this.$emit('load', this.displayDiv) }
} },
// 获取手指松开的Y轴位置
handlerTouchEnd (e) {
// 记录拖拽状态是为上拉还是下拉
SCROLLSTATUS = this.moveDistance > 0 this.isTransition = true
// 开启transition
this.$refs.scrollContainer.style.transform = 'translateY(0px)'
if (Math.abs(this.moveDistance) < this.triggerDistance)
return (this.moveDistance = 0)
this.moveDistance = 0 // 清除已移动的距离
// 拖拽距离是否大于指定的触发长度
// 容器位置恢复后触发
setTimeout(() => { this.shrinked() }, 700)
// 遍历emit并执行
this.emitEvents.forEach((fn, index) => {
if (!fn) return this.emitEvents[index] = undefined fn.apply(this) })
},
// 容器恢复后的操作
shrinked ()
{ if (SCROLLSTATUS)
{
// 下拉恢复完,箭头转为加载
this.isShrinked = true
} else
{
// 上拉恢复完
} },
// 该方法通过$emit()传给外部组件调用 然后隐藏刷新、加载的gif图片
displayDiv () { this.isDisplay.refresh = true this.isDisplay.load = true }
}
}
</script>
5 如何判断dom元素是否在可视区域?
Element.getBoundingClientRect() 方法: 这个方法返回元素的大小以及它相对于视口的位置。
window.scrollY 和 window.scrollX: 这些属性分别表示垂直和水平方向上的滚动距离。可以通过它们来判断元素是否在可视区域内。
使用 IntersectionObserver API:
function observeElement(element, callback) {
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback(true);
} else {
callback(false);
}
});
}, {
root: null, // 使用浏览器视窗作为交叉区域的容器
rootMargin: '0px',
threshold: 0.1 // 10% 可见时触发回调
});
observer.observe(element);
}
6.前端如何用canvas实现电影院选票功能?
<template>
<div class="cinema-selector">
<canvas ref="seatCanvas" @click="handleCanvasClick"></canvas>
</div>
</template>
<script>
export default {
name: 'CinemaSelector',
data() {
return {
seats: [],
numRows: 10, // 行数
numCols: 15, // 列数
seatWidth: 50, // 座位宽度
seatHeight: 40, // 座位高度
seatPadding: 5, // 座位间隔
};
},
mounted() {
this.initSeats();
this.drawSeats();
},
methods: {
initSeats() {
this.seats = this.createSeatsArray(this.numRows, this.numCols, 'available');
// 假设这些座位已经被购买
this.markSeatsAsPurchased([[0, 3], [1, 7], [5, 2]]);
},
createSeatsArray(rows, cols, status) {
return Array.from({ length: rows }, () => Array(cols).fill({ status }));
},
markSeatsAsPurchased(purchasedIndices) {
purchasedIndices.forEach(([row, col]) => {
if (this.seats[row] && this.seats[row][col]) {
this.seats[row][col].status = 'purchased';
}
});
},
drawSeats() {
const canvas = this.$refs.seatCanvas;
const ctx = canvas.getContext('2d');
const totalWidth = this.numCols * (this.seatWidth + this.seatPadding) - this.seatPadding;
const totalHeight = this.numRows * (this.seatHeight + this.seatPadding) - this.seatPadding;
canvas.width = totalWidth;
canvas.height = totalHeight;
for (let i = 0; i < this.numRows; i++) {
for (let j = 0; j < this.numCols; j++) {
const x = j * (this.seatWidth + this.seatPadding);
const y = i * (this.seatHeight + this.seatPadding);
const seat = this.seats[i][j];
ctx.fillStyle = this.getColorByStatus(seat.status);
ctx.fillRect(x, y, this.seatWidth, this.seatHeight);
}
}
},
getColorByStatus(status) {
switch (status) {
case 'available':
return '#4CAF50'; // 绿色,表示可用
case 'selected':
return '#FFEB3B'; // 黄色,表示选中
case 'purchased':
return '#F44336'; // 红色,表示已购买
default:
return '#CCCCCC'; // 默认颜色
}
},
handleCanvasClick(event) {
const canvas = this.$refs.seatCanvas;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect(); // 获取canvas元素的位置和尺寸
// 计算相对于canvas的点击位置
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// 计算点击的座位列和行索引
const colIndex = Math.floor(x / (this.seatWidth + this.seatPadding));
const rowIndex = Math.floor(y / (this.seatHeight + this.seatPadding));
// 检查索引是否在有效范围内
if (colIndex >= 0 && colIndex < this.numCols && rowIndex >= 0 && rowIndex < this.numRows) {
const seat = this.seats[rowIndex][colIndex];
// 检查座位是否是可售或已选中状态
if (seat.status === 'available' || seat.status === 'selected') {
// 切换座位状态
this.toggleSeatSelection(rowIndex, colIndex);
// 重新绘制座位图
this.drawSeat(rowIndex, colIndex);
}
}
},
// 绘制单个座位的方法
drawSeat(row, col) {
const ctx = this.$refs.seatCanvas.getContext('2d');
const x = col * (this.seatWidth + this.seatPadding);
const y = row * (this.seatHeight + this.seatPadding);
const seat = this.seats[row][col];
ctx.fillStyle = this.getColorByStatus(seat.status);
ctx.fillRect(x, y, this.seatWidth, this.seatHeight);
},
// 切换座位状态的方法
toggleSeatSelection(row, col) {
const seat = this.seats[row][col];
if (seat.status === 'available') {
seat.status = 'selected';
} else if (seat.status === 'selected') {
seat.status = 'available';
}
},
},
};
</script>
<style>
.cinema-selector canvas {
border: 1px solid black;
cursor: pointer;
}
</style>
7.如何设置失效时间来清除本地存储?
- 在存储数据时,同时存储一个时间戳或者有效期限,之后在读取数据时检查当前时间与存储的时间戳或期限。
// 存储数据和过期时间
function storeDataWithExpiration(key, data, expirationInMilliseconds) {
const storageData = {
data,
expiration: Date.now() + expirationInMilliseconds
};
localStorage.setItem(key, JSON.stringify(storageData));
}
// 读取数据并检查是否过期
function retrieveDataWithExpiration(key) {
const storedData = JSON.parse(localStorage.getItem(key));
if (storedData && storedData.expiration > Date.now()) {
return storedData.data;
} else {
// 数据已过期或不存在,可能需要删除该项
localStorage.removeItem(key);
return null;
}
}
或者: 如果数据只在页面会话期间有效,可以使用SessionStorage代替LocalStorage。SessionStorage的数据在页面会话结束时自动清除
或者:store.js
//设置过期时间
var expiration = new Date().getTime() + 10000;
store.set('bar', 'foo', expiration);
8 用node.js 实现一个命令行工具,统计输入目录下,指定代码的行数?
可以使用内置的 fs(文件系统)模块来读取文件,
以及 path 模块来处理文件路径,
使用 readline 模块来逐行读取文件。
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// 异步递归函数,用于遍历目录
async function countLinesInDirectory(directory, extension = '.js') {
let totalLines = 0;
try {
const files = await fs.promises.readdir(directory);
for (const filename of files) {
const filePath = path.join(directory, filename);
const stats = await fs.promises.stat(filePath);
if (stats.isDirectory()) {
// 如果是目录,则递归调用
totalLines += await countLinesInDirectory(filePath, extension);
} else if (path.extname(filename) === extension) {
// 如果是指定扩展名的文件,则统计行数
totalLines += await countFileLines(filePath);
}
}
} catch (error) {
console.error(`读取目录 ${directory} 时发生错误: ${error.message}`);
}
return totalLines;
}
// 异步函数,用于统计单个文件的行数
function countFileLines(filePath) {
return new Promise((resolve, reject) => {
const lineCount = {};
const stream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});
rl.on('line', () => {
lineCount.total = (lineCount.total || 0) + 1;
});
rl.on('close', () => {
resolve(lineCount.total);
});
rl.on('error', (error) => {
reject(`读取文件 ${filePath} 时发生错误: ${error.message}`);
});
});
}
// 解析命令行参数
const [, , directoryPath] = process.argv;
if (!directoryPath) {
console.error('请提供目录路径');
process.exit(1);
}
// 执行统计并输出结果
(async () => {
try {
const totalLines = await countLinesInDirectory(directoryPath);
console.log(`总计统计到 ${totalLines} 行代码`);
} catch (error) {
console.error(`发生错误: ${error.message}`);
}
})();
node count-lines.js /path/to/directory .js,
其中 `/path/to/directory` 是你想要统计的目录路径,`.js` 是你想要统计的文件扩展名。
eg:
node test.js src/store .js,
总计统计到 172 行代码
node test.js src/views .vue,
总计统计到 2758 行代码
9.package.json里面的sideEffects属性作用?
在 package.json 文件中,sideEffects 属性用于告诉构建工具和打包器哪些文件是“安全的”以进行代码拆分(也称为按需加载或懒加载)。这个属性在现代前端工程中尤为重要,特别是在使用像 webpack 或 Parcel 这样的模块打包工具时。
sideEffects 属性的作用:
- 优化打包体积: 当设置为
false时,打包工具会认为项目中的所有文件都没有副作用(side effects),因此可以安全地进行代码拆分。这意味着只有实际被引用的模块才会被包含在最终的打包文件中。 - 提高应用性能: 通过代码拆分,可以减少初始加载时的 JavaScript 体积,从而加快页面加载速度和改善用户体验。
- 控制模块加载: 当设置为数组时,可以指定哪些文件或模块是“安全的”以进行代码拆分。这允许更细粒度的控制,而不是简单地将整个项目标记为无副作用。
eg:
{
"sideEffects": false
}
这个设置告诉打包工具,整个项目中的所有文件都没有副作用,可以进行代码拆分。
{
"sideEffects": [
"./src/*.js",
"!./src/utils/*.js"
]
}
这个设置指定了除了 `./src/utils/` 目录下的 `.js` 文件之外,
其他 `./src/` 目录下的 `.js` 文件都没有副作用,可以进行代码拆分。
可以显著提高前端应用的加载性能,尤其是在大型项目中。