前端最新场景题,不来看一看吗?(三)

97 阅读7分钟

1.为什么小程序有两个线程?

这两个线程分别是:

  1. 主线程:也称为逻辑线程或渲染线程。这个线程负责处理小程序的业务逻辑、数据处理和 UI 渲染。开发者编写的 JavaScript 代码在这个线程上执行,包括页面的逻辑处理、数据绑定和更新等。
  2. 工作线程:有时也称为 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 应用程序来说是私有的,并且不会对页面的表现或行为产生直接影响。

  1. 存储私有数据data- 属性可以用来存储页面或应用程序的私有数据,这些数据可以由 JavaScript 访问和操作。

  2. 语义化:自定义数据属性可以用来增强 HTML 元素的语义,提供更多上下文信息。

  3. 兼容性:所有现代浏览器都支持 data- 属性,并且它们不会对页面的布局或样式产生影响。

  4. 命名约定data- 属性的名称应该尽可能描述性,并且遵循连字符(kebab-case)命名约定,例如 data-user-iddata-sort-order

  5. JavaScript 访问:可以通过 JavaScript 的 dataset 属性访问元素的自定义数据属性。 eg:

  6. HTML5 规范:自定义数据属性是 HTML5 规范的一部分,它们被广泛用于现代 Web 开发。

  7. 避免滥用:虽然 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.scrollYwindow.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 属性的作用:

  1. 优化打包体积: 当设置为 false 时,打包工具会认为项目中的所有文件都没有副作用(side effects),因此可以安全地进行代码拆分。这意味着只有实际被引用的模块才会被包含在最终的打包文件中。
  2. 提高应用性能: 通过代码拆分,可以减少初始加载时的 JavaScript 体积,从而加快页面加载速度和改善用户体验。
  3. 控制模块加载: 当设置为数组时,可以指定哪些文件或模块是“安全的”以进行代码拆分。这允许更细粒度的控制,而不是简单地将整个项目标记为无副作用。

eg:

{
  "sideEffects": false
}
这个设置告诉打包工具,整个项目中的所有文件都没有副作用,可以进行代码拆分。

{
  "sideEffects": [
    "./src/*.js",
    "!./src/utils/*.js"
  ]
}
这个设置指定了除了 `./src/utils/` 目录下的 `.js` 文件之外,
其他 `./src/` 目录下的 `.js` 文件都没有副作用,可以进行代码拆分。

可以显著提高前端应用的加载性能,尤其是在大型项目中。