面试笔记二

166 阅读13分钟

1、微信小程序普通分包

  • 声明 subpackages 后,将按 subpackages 配置路径进行打包,subpackages 配置路径外的目录将被打包到主包中
  • 主包也可以有自己的 pages,即最外层的 pages 字段。
  • subpackage 的根目录不能是另外一个 subpackage 内的子目录
  • tabBar 页面必须在主包内
  • 假设小程序目录结构如下:
    ├── app.js
    ├── app.json
    ├── app.wxss
    ├── moduleA
    │   └── pages
    │       ├── rabbit
    │       └── squirrel
    ├── moduleB
    │   └── pages
    │       ├── pear
    │       └── pineapple
    ├── pages
    │   ├── index
    │   └── logs
    └── utils
    
  • 配置
    {
      "pages":[
        "pages/index",
        "pages/logs"
      ],
      "subpackages": [
        {
          "root": "packageA",
          "pages": [
            "pages/cat",
            "pages/dog"
          ]
        }, {
          "root": "packageB",
          "name": "pack2",
          "pages": [
            "pages/apple",
            "pages/banana"
          ]
        }
      ]
    }
    

2、微信小程序独立分包

  • 介绍
    • 独立分包是小程序中一种特殊类型的分包,可以独立于主包其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。

    • 开发者可以按需将某些具有一定功能独立性的页面配置到独立分包中。当小程序从普通的分包页面启动时,需要首先下载主包;而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度

    • 一个小程序中可以有多个独立分包

  • 使用独立分包时要注意
    • 独立分包中不能依赖主包其他分包中的内容,包括 js 文件templatewxss自定义组件插件等(使用 分包异步化 时 js 文件、自定义组件、插件不受此条限制)
    • 主包中的 app.wxss 对独立分包无效,应避免在独立分包页面中使用 app.wxss 中的样式;
    • App 只能在主包内定义,独立分包中不能定义 App,会造成无法预期的行为;
    • 独立分包中暂时不支持使用插件。
  • 配置方法
    • 配置
      {
        "pages": [
          "pages/index",
          "pages/logs"
        ],
        "subpackages": [
          {
            "root": "moduleA",
            "pages": [
              "pages/rabbit",
              "pages/squirrel"
            ]
          }, {
            "root": "moduleB",
            "pages": [
              "pages/pear",
              "pages/pineapple"
            ],
            "independent": true // 启动独立分包
          }
        ]
      } 
      

微信小程序将公共组件放到分包中

  • 通常情况下,公共组件需要放在主包中,因为分包不会在应用启动时立即下载
  • 如果将公共组件放在分包中,可能会因分包尚未下载而导致组件无法使用,出现报错。
  • 若确实需要将公共组件放在分包中,可以使用微信小程序的 preloadRule 预加载规则。通过预加载规则,可以在进入指定页面前提前下载分包
  • 在 app.json 中配置 preloadRule,指定哪些页面需要预加载哪些分包。这样,当进入预加载规则中的页面时,分包会被提前下载分包中公共组件就可以在当前包中正常使用。
{
  "subPackages": [
    {
      "root": "subpackage",
      "pages": [
        "page1",
        "page2"
      ]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      // all: 不限网络,wifi: 仅wifi下预下载
      "network": "wifi",
      // 当进入首页的时候,提前预渲染 subpackage 分包的组件
      "packages": ["subpackage"]
    }
  }
}

微信小程序的登录流程

  • 小小程序端调用 login 接口获取用户的 code
  • 后端接收前端传来的 code,并附上 appid、secret、grant_type 等参数,请求微信的登录校验接口。
  • 微信登录校验接口返回 session_key、unionid、openid 等信息。
  • 后端根据获取到的 openid 和业务系统进行登录处理。

3、异步的理解

  • 异步编程是指在执行过程中,不同任务之间不需要等待上一个任务执行完毕就可以开始执行,而是通过回调函数Promiseasync/await 等机制来处理。异步编程可以在处理网络请求文件读写定时任务等需要等待时间的操作时发挥重要作用。
    • 回调函数:最早的异步编程方式是使用回调函数。通过将需要异步执行的任务放在回调函数中,在任务完成时调用该回调函数来处理结果。
      setTimeout(function() {
        console.log("This is a callback function.");
      }, 1000);
      
    • Promise:Promise 是一种更加灵活和强大的异步编程方式,它提供了更好的处理异步操作和错误处理的机制。
      let promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
          resolve("Promise resolved.");
        }, 1000);
      });
      
      promise.then(function(result) {
        console.log(result);
      });
      
      
    • async/await:async/await 是 ES2017 引入的语法糖,使得异步代码的书写更加简洁和易读。通过 async 声明函数为异步函数,在异步操作前使用 await 关键字来等待异步操作的结果。
      async function fetchData() {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        return data;
      }
      
      

4、JS排序

  • 冒泡排序
function bubbleSort(arr) {
    let len = arr.length;
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

let array = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
  • 快速排序
function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }

    let pivot = arr[0];
    let left = [];
    let right = [];

    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }

    return quickSort(left).concat(pivot, quickSort(right));
}

let array = [64, 34, 25, 12, 22, 11, 90];
console.log(quickSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
  • 插入排序
function insertionSort(arr) {
    let len = arr.length;
    for (let i = 1; i < len; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
    return arr;
}

let array = [64, 34, 25, 12, 22, 11, 90];
console.log(insertionSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
  • 倒序
function insertionSort(arr) {
    let len = arr.length;
    for (let i = 1; i < len; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
    return arr.reverse(); // 倒序输出
}

let array = [64, 34, 25, 12, 22, 11, 90];
console.log(insertionSort(array)); // 输出: [90, 64, 34, 25, 22, 12, 11]
  • 一串数字,实现3位一个逗号的
function addCommas(num) {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

let number = 1234567890;
console.log(addCommas(number)); // 输出: "1,234,567,890"

// 字符串递归方法 
function formatNumber(num, chart=',', length=3){ 
    let result = '' 
    let nums = num.toString().split('.') 
    let int = nums[0] 
    let decmial = nums[1] ? '.' + nums[1] : '' 
    let index = 0 
    for (let n = int.length - 1; n >= 0; n--) {
        index ++ 
        result = int[n] + result 
        if (index % length === 0 && n !== 0) { 
            result = chart + result 
         } 
    } 
    return result + decmial 
} 
console.log(formatNumber(2223456789.123)) // 2,223,456,789.123

5、事件循环

  • JavaScript事件循环是JavaScript引擎处理异步任务的机制,它确保了JavaScript是单线程执行的同时,能够处理异步任务和事件。事件循环的基本原理如下:
    • 执行同步任务:首先,JavaScript引擎会执行当前的同步任务,这些任务会按照它们在代码中的顺序立即执行。

    • 处理异步任务:如果有异步任务,比如定时器、事件监听器的回调函数、Promise的异步操作等,它们不会立即执行,而是被放入相应的任务队列中。

    • 事件循环:当主线程的同步任务执行完成后,JavaScript引擎会进入事件循环(Event Loop)阶段。在事件循环中,JavaScript引擎会不断地从任务队列中取出任务,并执行它们。这个过程是循环的,直到任务队列为空。

    • 任务队列:任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)。通常,每个事件循环中会优先执行微任务队列中的任务,然后再执行宏任务队列中的任务。

    • 微任务:微任务通常包括Promise的异步操作、MutationObserver的回调等。微任务会在当前事件循环中的所有同步任务执行完成后立即执行,而且会在下一个事件循环之前完成。

    • 宏任务:宏任务包括setTimeout、setInterval、DOM事件等。宏任务会在当前事件循环的所有微任务执行完成后执行,而且它们的执行时间通常会在下一个事件循环开始之前。

6、小程序蓝牙api简单的使用

  • 初始化模块、扫描周边设备
    // 初始化蓝牙模块
    openBluetoothAdapter() {
      wx.openBluetoothAdapter({  
        success: (res) => {
          console.log('openBluetoothAdapter success', res)
          this.startBluetoothDevicesDiscovery()
        },
        fail: (res) => {
          if (res.errCode === 10001) {
            wx.onBluetoothAdapterStateChange(function (res) {
              console.log('onBluetoothAdapterStateChange', res)
              if (res.available) {
                this.startBluetoothDevicesDiscovery()
              }
            })
          }
        }
      })
    }
    // 扫描设备
    startBluetoothDevicesDiscovery() { 
      if (this._discoveryStarted) { // 只进行一次扫描
        return
      }
      this._discoveryStarted = true
      wx.startBluetoothDevicesDiscovery({
        allowDuplicatesKey: true,
        success: (res) => {
          console.log('startBluetoothDevicesDiscovery success', res)
          this.onBluetoothDeviceFound()
        },
      })
    },
    
  • 发现周边的蓝牙设备
    // 周边的蓝牙设备信息列表
    onBluetoothDeviceFound() {
      wx.onBluetoothDeviceFound((res) => { 
        res.devices.forEach(device => {
          if (!device.name && !device.localName) {
            return
          }
          const foundDevices = this.data.devices
          const idx = inArray(foundDevices, 'deviceId', device.deviceId)
          const data = {}
          if (idx === -1) {
            data[`devices[${foundDevices.length}]`] = device
          } else {
            data[`devices[${idx}]`] = device
          }
          console.log(data)
        })
      })
    },
    
  • 连接蓝牙设备
    // 用户点击连接的蓝牙设备
    createBLEConnection(e) {
      const ds = e.currentTarget.dataset
      const deviceId = ds.deviceId // 蓝牙设备的唯一标识符
      const name = ds.name
      wx.createBLEConnection({
        deviceId,
        success: (res) => {
          this.setData({
            connected: true,
            name,
            deviceId,
          })
          this.getBLEDeviceServices(deviceId)
        }
      })
      // 停止搜寻附近的蓝牙外围设备
      this.stopBluetoothDevicesDiscovery() 
    },
    // 获取服务id
    getBLEDeviceServices(deviceId) {
      wx.getBLEDeviceServices({
        deviceId,
        success: (res) => {
          for (let i = 0; i < res.services.length; i++) {
            if (res.services[i].isPrimary) {
              // uuid 用于标识蓝牙设备的服务和特征值,以便进行数据交换和通信
              this.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid)
              return
            }
          }
        }
      })
    },
    getBLEDeviceCharacteristics(deviceId, serviceId) {
      // 获取蓝牙设备的特征值
      wx.getBLEDeviceCharacteristics({
        deviceId,
        serviceId,
        success: (res) => {
          console.log('success', res.characteristics)
          for (let i = 0; i < res.characteristics.length; i++) {
            let item = res.characteristics[i] 
            if (item.properties.read) {
              // 读取蓝牙设备的数据
              wx.readBLECharacteristicValue({
                deviceId,
                serviceId,
                characteristicId: item.uuid,
                success: (res) => { 
                  console.log('读取成功', JSON.stringify(res)); 
                },
                fail: (res) => { 
                  console.log('读取失败', JSON.stringify(res)); 
                }
              })
            }
            if (item.properties.write) { 
              let buffer = new ArrayBuffer(1)
              let dataView = new DataView(buffer)
              dataView.setUint8(0, Math.random() * 255 | 0) 
              // 向蓝牙设备写入数据
              wx.writeBLECharacteristicValue({
                deviceId, 
                serviceId,
                characteristicId: item.uuid,
                value: buffer,
                success:(res)=>{
                  console.log('写入成功', res);
                },
                fail:(res)=>{
                  console.log('写入失败', res);
                },
              })
            }
            if (item.properties.notify || item.properties.indicate) {
              // 监听蓝牙设备特征值变化事件
              wx.notifyBLECharacteristicValueChange({
                deviceId,
                serviceId,
                characteristicId: item.uuid,
                state: true,
              })
            }
          }
        },
        fail(res) {
          console.error('getBLEDeviceCharacteristics', res)
        }
      })
      // 操作之前先监听,保证第一时间获取数据
      wx.onBLECharacteristicValueChange((characteristic) => {
        const idx = inArray(
          this.data.chs, 
          'uuid', 
          characteristic.characteristicId
        )
        const data = {}
        if (idx === -1) {
          data[`chs[${this.data.chs.length}]`] = {
            uuid: characteristic.characteristicId,
            value: ab2hex(characteristic.value)
          }
        } else {
          data[`chs[${idx}]`] = {
            uuid: characteristic.characteristicId,
            value: ab2hex(characteristic.value)
          }
        } 
        console.log(data)
      })
    },
    

7、小程序中,蓝牙与硬件设备传递数据的格式

  • 原始数据:在某些情况下,可以直接传输原始的字节数据(例如 ArrayBuffer)。这种方式适用于自定义的通信协议,需要在设备和小程序之间进行严格的协议约定。

  • JSON 数据:如果设备和小程序之间支持 JSON 格式的数据交换,可以使用 JSON 格式来传输数据。这种方式简单易用,适用于传输结构化的数据,如传感器数据、控制命令等。

  • 自定义协议:有时候会根据具体的需求设计自定义的通信协议,包括数据的格式、命令的定义等。在这种情况下,需要设备端和小程序端都遵循相同的协议规范,以确保数据能够正确地解析和处理。

  • 特定格式的数据包:某些设备可能使用特定格式的数据包进行通信,例如 Modbus、CAN 等工业领域常见的通信协议。在这种情况下,需要根据设备的通信协议来解析和构建数据包。

8、请描述一下前端文件上传的基本流程。

  • 创建文件上传表单: 在 HTML 页面中创建一个表单,用于用户选择要上传的文件。通常使用 <form> 元素,设置 enctype 属性为 "multipart/form-data",以支持文件上传。在表单中添加一个 <input type="file"> 元素,这是用户选择文件的输入框。可能会添加一些其他的表单字段来传递其他数据。

  • 用户选择文件: 用户在页面中点击文件上传输入框,然后选择要上传的文件。选择完成后,文件选择框中会显示所选文件的名称。

  • 获取文件信息: 通过 JavaScript 可以获取用户选择的文件的相关信息,比如文件名、大小、类型等。这些信息可以在上传前进行验证或显示给用户。

  • 提交表单: 当用户点击上传按钮或者表单提交时,浏览器会将表单数据(包括文件数据)发送到服务器。这个过程可以通过表单的 submit 事件或者 JavaScript 中的 AJAX 方法来触发。

  • 上传文件: 一旦表单提交,浏览器会将文件数据上传到服务器。在这个过程中,浏览器会将文件数据打包成一个 HTTP 请求,发送给服务器。服务器接收到请求后,会根据业务逻辑进行文件处理,比如保存文件到服务器本地或者进行其他操作。

  • 处理上传结果: 服务器处理完文件上传请求后,会向浏览器返回一个 HTTP 响应。浏览器接收到响应后,可以根据情况显示上传结果给用户,比如上传成功、上传失败或者其他信息。

9、如何通过 js 获取用户选择的文件的信息,比如文件名、大小、类型等?

  • 获取文件名: 可以通过文件对象的 name 属性获取文件的名称。
  • 获取文件大小: 文件对象的 size 属性返回文件的字节大小。
  • 获取文件类型: 文件对象的 type 属性返回文件的 MIME 类型。
  • 获取最后修改时间: 文件对象的 lastModifiedDate 属性返回文件的最后修改时间。

10、如何在上传文件时实现上传进度的监控?

  • 创建 XMLHttpRequest 对象或使用 Fetch API: 首先,你需要使用 XMLHttpRequest 对象或 Fetch API 发送文件上传请求到服务器。Fetch API 更加现代化和简洁,但是 XMLHttpRequest 在一些旧版本的浏览器中仍然有广泛支持。

  • 监听上传进度事件: 在发送上传请求之前,你需要添加一个事件监听器来监控上传进度。在 XMLHttpRequest 中,你可以监听 progress 事件。而在 Fetch API 中,你可以使用 fetch 函数的返回值中的 Body 对象来监控上传进度。

  • 更新上传进度条或显示上传进度信息: 当上传进度发生变化时,你可以获取上传进度信息,并将其用于更新页面上的上传进度条或者显示上传进度信息。

const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');

uploadButton.addEventListener('click', function() {
    const file = fileInput.files[0];
    if (file) {
        const xhr = new XMLHttpRequest();
        
        // 监听上传进度事件
        xhr.upload.addEventListener('progress', function(event) {
            if (event.lengthComputable) {
                const progress = (event.loaded / event.total) * 100;
                progressBar.value = progress;
                progressText.textContent = `上传进度: ${progress.toFixed(2)}%`;
            }
        });

        // 上传文件
        xhr.open('POST', 'upload.php', true);
        const formData = new FormData();
        formData.append('file', file);
        xhr.send(formData);
    }
});

11、如何使用 Blob 对象处理文件数据?

  • Blob(Binary Large Object)对象是 JavaScript 中用来表示二进制数据的一种数据类型Blob 对象通常用于处理文件数据图像数据音频/视频数据等。你可以使用 Blob 对象进行多种操作,包括读取、处理传输文件数据

  • 创建 Blob 对象: 可以使用 Blob 构造函数创建一个 Blob 对象,它接受一个数据数组和一个参数对象,用于指定数据类型。

    const data = ['Hello', 'world'];
    const blob = new Blob(data, { type: 'text/plain' });
    
  • 读取 Blob 数据: 可以使用 FileReader 对象读取 Blob 对象中的数据。

    const fileReader = new FileReader();
    fileReader.onload = function(event) {
        const data = event.target.result;
        console.log(data); // 打印 Blob 数据
    };
    fileReader.readAsText(blob);
    
  • 将 Blob 数据转换为 URL: 可以使用 URL.createObjectURL() 方法将 Blob 对象转换为一个 URL,用于在页面中显示文件内容,比如图片、音频或视频。

    const blobURL = URL.createObjectURL(blob);
    
  • 获取 Blob 对象的大小、类型等信息: Blob 对象有一些属性可以获取相关信息。

    const size = blob.size; // 获取 Blob 对象的大小,单位为字节
    const type = blob.type; // 获取 Blob 对象的 MIME 类型
    
  • 在 Blob 对象中进行切片:可以使用 slice() 方法从 Blob 对象中提取指定范围的数据块,返回一个新的 Blob 对象。

    const sliceBlob = blob.slice(start, end, contentType);
    

12、什么是 Blob URL?如何释放 Blob URL?

  • Blob URL(Object URL)是一种特殊的 URL:,用于将 Blob 或 File 对象表示为可用于在页面中显示的 URL。它提供了一种在浏览器中直接访问 Blob 或 File 对象数据的方式,通常用于显示图片、音频、视频等内容。

  • Blob URL 的格式为 blob: <origin>/<uuid>,其中 <origin> 表示 URL 的源,通常是当前页面的源,<uuid> 是一个唯一标识符,用于标识特定的 BlobFile 对象。

  • 释放 Blob URL:在使用完 Blob URL 后,为了释放资源并防止内存泄漏,需要手动释放 Blob URL。释放 Blob URL 可以通过调用 URL.revokeObjectURL() 方法来实现,传递要释放的 Blob URL 作为参数。

    const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
    const blobURL = URL.createObjectURL(blob);
    // 使用 blobURL 来显示或处理 Blob 对象的数据
    // 释放 Blob URL
    URL.revokeObjectURL(blobURL);
    

13、如何实现大文件的分片上传?

  • 切片: 将大文件切割成小的数据块(分片),每个数据块的大小通常为固定值或者根据需求动态调整。切片可以在客户端(前端)或者服务器端(后端)完成,常见的做法是在客户端完成切片以减轻服务器负担。

  • 上传: 将每个数据块分别上传到服务器。这可以通过多个并行的 HTTP 请求来实现,每个请求负责上传一个数据块。

  • 合并: 服务器收到所有数据块后,将它们按照顺序合并成完整的文件。合并可以在服务器端完成。

  • 处理并发和错误: 在上传和合并过程中,需要处理并发上传的情况,确保数据块按照正确的顺序合并,并且需要处理上传过程中可能出现的错误,比如网络中断、上传失败等情况。

const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const chunkSize = 10 * 1024 * 1024; // 10MB

uploadButton.addEventListener('click', function() {
    const file = fileInput.files[0];
    if (file) {
        const totalChunks = Math.ceil(file.size / chunkSize);
        let currentChunk = 0;

        // 递归上传分片
        function uploadChunk(start, end) {
            const formData = new FormData();
            formData.append('file', file.slice(start, end));

            fetch('uploadChunk.php', {
                method: 'POST',
                body: formData
            })
            .then(response => {
                if (response.ok) {
                    currentChunk++;
                    if (currentChunk < totalChunks) {
                        const start = currentChunk * chunkSize;
                        const end = Math.min(start + chunkSize, file.size);
                        uploadChunk(start, end);
                    } else {
                        console.log('文件上传完成');
                    }
                } else {
                    console.error('上传失败');
                }
            })
            .catch(error => {
                console.error('上传出错', error);
            });
        }

        // 开始上传第一个分片
        const start = 0;
        const end = Math.min(chunkSize, file.size);
        uploadChunk(start, end);
    }
});

14、在大文件上传中,如何保证数据的完整性和一致性?

  • 使用校验和或哈希值: 在上传文件之前,可以对文件数据计算校验和或哈希值,并将其与文件一起上传到服务器。在服务器端接收到文件后,重新计算文件的校验和或哈希值,然后与客户端提供的值进行比较,以确保文件在传输过程中没有被篡改。

  • 分片上传和校验: 将大文件切分成小的数据块(分片)进行上传,每个分片上传完成后都进行校验和或哈希值的计算,并将结果一同上传到服务器。在服务器端接收到所有分片后,可以对每个分片进行校验,然后再进行合并操作。

  • 使用上传令牌和验证: 在上传文件时,为每个文件生成一个唯一的上传令牌,并将上传令牌一同上传到服务器。在服务器端接收到文件后,验证上传令牌的有效性,以确保文件上传请求是合法的。这可以防止恶意的文件上传请求,并提高数据的安全性和完整性。

  • 使用文件版本号或时间戳: 在文件上传完成后,服务器端可以为上传的文件生成一个版本号或者时间戳,并将其返回给客户端。客户端可以保存这个版本号或时间戳,以后如果需要验证文件的完整性,可以再次发送这个版本号或时间戳到服务器进行验证。

  • 传输层加密: 使用安全的传输协议(如HTTPS)进行文件上传,以确保文件在传输过程中的安全性和完整性。通过传输层加密,可以防止中间人攻击和数据篡改。

15、Three.js 的主要功能和特点是什么?

  • 简化 WebGL 开发: Three.js 封装了底层的 WebGL API,使得开发者可以更轻松地使用 JavaScript 创建和渲染复杂的3D场景,而不需要直接操作 WebGL 的复杂性。

  • 跨平台和跨浏览器兼容性: Three.js 提供了高度的跨平台和跨浏览器兼容性,可以在不同的操作系统和设备上运行,并在主流的现代浏览器中表现良好。

  • 丰富的功能和组件: Three.js 提供了丰富的功能和组件,包括几何体、材质、灯光、相机、渲染器等,以及支持加载和显示3D模型、纹理贴图、动画、物理引擎等高级特性。

  • 高性能: Three.js 在底层优化了 WebGL 渲染引擎,以提供高性能的3D渲染效果,能够有效地处理大量的几何体和复杂的场景。

16、了解 Three.js 中的场景、相机、渲染器、几何体、材质、灯光等基本概念?

  • 场景(Scene):

    • 场景是 Three.js 中包含所有3D对象的容器。它是一个三维空间,可以包含模型、灯光、相机等元素。在场景中,可以设置背景色、雾效等属性。
  • 相机(Camera):

    • 相机定义了观察场景的视角和范围。Three.js 支持不同类型的相机,包括透视相机(PerspectiveCamera)和正交相机(OrthographicCamera)。透视相机类似于人眼的视角,近大远小;正交相机则没有远近之分,对象大小不会随距离变化。
  • 渲染器(Renderer):

    • 渲染器负责将 Three.js 场景渲染到浏览器的画布上。Three.js 提供了几种渲染器,包括 WebGLRenderer、WebGL2Renderer、CanvasRenderer 等,其中 WebGLRenderer 是最常用的,使用 WebGL 技术进行渲染。
  • 几何体(Geometry):

    • 几何体定义了3D对象的形状,比如立方体、球体、圆柱体等。Three.js 提供了许多内置的几何体,也支持通过定义顶点和面来创建自定义的几何体。
  • 材质(Material):

    • 材质定义了3D对象的外观和质地。它决定了对象如何反射光线和显示颜色。Three.js 提供了多种内置的材质,包括基础材质、 Lambert 材质、Phong 材质、Standard 材质等。
  • 灯光(Light):

    • 灯光用于照亮场景中的对象。Three.js 支持多种类型的灯光,包括环境光(AmbientLight)、点光源(PointLight)、聚光灯(SpotLight)、方向光(DirectionalLight)等。

17、了解如何在 Three.js 中加载和显示3D模型?比如OBJ、GLTF等格式?

  • 在 Three.js 中加载和显示3D模型可以通过加载器(Loader)来实现。Three.js 提供了一系列的加载器,用于加载不同格式的3D模型,包括 OBJ、GLTF、FBX 等。下面以 OBJ 格式和 GLTF 格式为例说明如何加载和显示3D模型:
  • 加载和显示 OBJ 格式的3D模型:
    // 创建场景、相机和渲染器
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    // 创建加载器
    const loader = new THREE.OBJLoader();
    
    // 加载OBJ格式的模型
    loader.load(
        'model.obj', // 模型文件的路径
        function (obj) {
            // 添加到场景中
            scene.add(obj);
        },
        function (xhr) {
            // 加载过程中的回调函数
            console.log((xhr.loaded / xhr.total * 100) + '% 已加载');
        },
        function (error) {
            // 加载失败的回调函数
            console.error('加载失败', error);
        }
    );
    
    // 渲染场景
    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }
    animate();
    
  • 加载和显示 GLTF 格式的3D模型:
    // 创建场景、相机和渲染器
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    // 创建加载器
    const loader = new THREE.GLTFLoader();
    
    // 加载GLTF格式的模型
    loader.load(
        'model.gltf', // 模型文件的路径
        function (gltf) {
            // 添加到场景中
            scene.add(gltf.scene);
        },
        function (xhr) {
            // 加载过程中的回调函数
            console.log((xhr.loaded / xhr.total * 100) + '% 已加载');
        },
        function (error) {
            // 加载失败的回调函数
            console.error('加载失败', error);
        }
    );
    
    // 渲染场景
    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }
    animate();
    

18、了解如何在 Three.js 中创建复杂的材质和着色器效果?

  • 自定义着色器(Shader): Three.js 允许你使用自定义的着色器来创建复杂的材质效果。你可以编写顶点着色器和片元着色器,来定义对象的外观和表面特性。

  • 创建着色器材质(ShaderMaterial): 使用 Three.js 的 ShaderMaterial 类,你可以将自定义的着色器与 Three.js 的渲染管线集成起来,从而实现更复杂的材质效果。

// 创建场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 定义自定义的顶点着色器和片元着色器
const vertexShader = `
    void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`;

const fragmentShader = `
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
    }
`;

// 创建着色器材质
const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader
});

// 创建一个立方体几何体
const geometry = new THREE.BoxGeometry();
// 创建一个立方体网格对象,并应用自定义的着色器材质
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 设置相机位置
camera.position.z = 5;

// 渲染场景
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();

19、了解如何在 Three.js 中实现阴影、物理引擎、后期处理等高级特性?

阴影:

  • Three.js 支持在场景中添加阴影,包括平行光(DirectionalLight)、点光源(PointLight)、聚光灯(SpotLight)等。要启用阴影,需要将 shadow 属性设置为 true,并调整阴影的相关参数,如阴影分辨率、阴影投射和接收等。

物理引擎:

  • Three.js 中并没有内置的物理引擎,但可以通过第三方库来实现物理效果,比如 Ammo.js、Cannon.js 等。这些库可以用来模拟物体之间的碰撞、重力、运动等物理效果,从而增加场景的真实感。

后期处理:

  • Three.js 中可以通过使用 PostProcessing(后期处理)库来实现一些后期效果,比如景深效果、泛光、色调映射等。PostProcessing 库提供了一系列的后期处理效果,可以用来增强场景的视觉效果。

20、如何在 Three.js 中创建不同类型的灯光,比如环境光、点光源、聚光灯等?

  • 在 Three.js 中,可以使用不同类型的灯光来照亮场景中的对象。常见的灯光类型包括环境光(AmbientLight)点光源(PointLight)聚光灯(SpotLight)方向光(DirectionalLight)等。下面是使用不同类型的灯光的示例代码:
  • 环境光(AmbientLight):
    • 环境光是一种均匀分布的光源,不会产生阴影,用于模拟场景中的整体光照效果。
      // 颜色、强度
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); 
      scene.add(ambientLight); 
      
  • 点光源(PointLight):
    • 点光源是一种类似于灯泡的光源,可以向所有方向发射光线,会产生阴影。
      // 颜色、强度、距离
      const pointLight = new THREE.PointLight(0xffffff, 1, 100);
      // 设置光源位置
      pointLight.position.set(0, 10, 0);
      scene.add(pointLight);
      
      
  • 聚光灯(SpotLight):
    • 聚光灯是一种具有方向性的光源,可以通过设置位置和方向来控制光线的照射范围,会产生锥形的光照效果。
      const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4, 1, 1); // 颜色、强度、距离、角度、衰减因子、阴影
      spotLight.position.set(0, 10, 0); // 设置光源位置
      spotLight.target.position.set(0, 0, 0); // 设置光源照射的目标对象
      scene.add(spotLight);
      scene.add(spotLight.target); // 必须将目标对象添加到场景中
      
      
  • 方向光(DirectionalLight):
    • 方向光是一种平行光源,类似于太阳光,光线是平行的,不会因为距离而衰减,可以产生阴影。
      // 颜色、强度
      const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
      // 设置光源位置
      directionalLight.position.set(0, 10, 0);
      scene.add(directionalLight);
      

21、什么是 Promise?它解决了什么问题?

  • Promise 是一种用于处理异步操作的对象。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise 提供了更直观和更强大的方式来处理回调地狱问题和链式异步操作。

22、Promise 的三种状态是什么?

  • Pending(待定):初始状态,既没有被兑现,也没有被拒绝。
  • Fulfilled(已兑现):操作成功完成。
  • Rejected(已拒绝):操作失败。

23、Promise 的 .then() 和 .catch() 方法有什么作用?

  • .then() 用于处理成功的操作,它接收两个回调函数,第一个是成功时调用,第二个是失败时调用(可选)。
  • .catch() 用于处理失败的操作,它是 .then(null, errorHandler) 的语法糖。

24、如何使用 Promise.all() 和 Promise.race()?

  • Promise.all() 用于等待所有 Promise 都完成,如果有一个失败,则整个返回的 Promise 失败。
  • Promise.race() 用于等待第一个完成的 Promise,无论成功还是失败。

25、Promise 中的 finally 方法有什么作用?

  • finally 方法用于在 Promise 结束时,无论结果是成功还是失败,执行一些清理操作。

26、Promise 和 async/await 的区别是什么?

  • Promise 是用于处理异步操作的对象,而 async/await 是基于 Promise 的语法糖
  • 使用 async/await 可以使异步代码看起来像同步代码,增加可读性

27、手写一个promise,如何实现?该注意什么?简略说一下

  • 状态管理:一个 Promise 有三种状态:pending、fulfilled 和 rejected。
  • then 方法:实现 then 方法,用于注册成功和失败的回调。
  • 异步执行:Promise 的回调应该异步执行,确保符合规范。
  • 状态变更:状态只能从 pending 变为 fulfilled 或 rejected,且一旦改变就不能再变。
  • 处理链式调用:then 方法返回一个新的 Promise,从而实现链式调用。
class MyPromise {
  constructor(executor) {
    // 初始状态为pending
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // resolve 函数,用于将 Promise 状态转变为 fulfilled
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(callback => callback(this.value));
      }
    };

    // reject 函数,用于将 Promise 状态转变为 rejected
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(callback => callback(this.reason));
      }
    };

    // 立即执行 executor 函数,并捕获可能抛出的异常
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  // then 方法,用于注册 fulfilled 和 rejected 状态的回调函数
  then(onFulfilled, onRejected) {
    // 确保 onFulfilled 和 onRejected 是函数,否则赋予默认函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

    // 返回一个新的 Promise 对象,用于支持链式调用
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });

    return promise2;
  }
}

// resolvePromise 函数,用于处理 then 方法返回的结果 x
function resolvePromise(promise2, x, resolve, reject) {
  // 如果 promise 和 x 是同一个对象,抛出类型错误,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // 判断是否已经调用过 resolve/reject,避免重复调用
  let called = false;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        // 如果 x 是一个 Promise,继续递归解析
        then.call(x, y => {
          if (called) return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if (called) return;
          called = true;
          reject(err);
        });
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    resolve(x);
  }
}

28、常见的 CSS 选择器

  • 通配符选择器 (*): 选择所有元素。
    * {
      color: blue;
    } 
    
  • 类型选择器(元素选择器): 选择所有指定类型的元素。
    p {
      color: red;
    } 
    
  • 类选择器: 选择所有包含指定类的元素。
    .class-name {
      color: green;
    }
    
  • ID 选择器: 选择具有指定 ID 的元素。
    #id-name {
      color: purple;
    }
    
  • 属性选择器: 选择具有指定属性的元素。
    [type="text"] {
      color: orange;
    }
    
  • 伪类选择器: 选择处于特定状态的元素。
    a:hover {
      color: yellow;
    }
    
  • 伪元素选择器: 选择元素的特定部分。
    p::first-line {
      color: pink;
    }
    
  • 后代选择器: 选择指定元素的所有后代元素。
    div p {
      color: brown;
    }
    
  • 子选择器: 选择指定元素的所有直接子元素。
    div > p {
      color: blue;
    } 
    
  • 相邻兄弟选择器: 选择指定元素之后的相邻兄弟元素。
    h1 + p {
      color: green;
    } 
    
  • 通用兄弟选择器: 选择指定元素之后的所有兄弟元素。
    h1 ~ p {
      color: orange;
    } 
    

29、CSS 权重问题

  • CSS 权重由四个部分组成,可以用四位数表示 (a, b, c, d)。每个部分代表不同类型的选择器:
  • 权重排序规则
    • 内联样式:最高权重 (1, 0, 0, 0)
    • ID 选择器:其次 (0, 1, 0, 0)
    • 类选择器、属性选择器、伪类选择器:再次 (0, 0, 1, 0)
    • 类型选择器、伪元素选择器:最低 (0, 0, 0, 1)
  • 如果遇上!important
    • !important 的优先级高于任何不包含 !important 的样式声明,即使这些声明的选择器权重较高
    • 当多个规则都使用了 !important,就会比较它们的选择器权重,权重高的规则优先级更高

30、uniapp 和 taro 这类框架,为什么能编译成小程序、H5这些呢?

1. 编译原理

  • 抽象层(Abstraction Layer)UniAppTaro 提供了一个抽象层,开发者在这个抽象层进行开发。这个抽象层包括通用的 API组件框架,使得开发者可以编写与平台无关的代码。

  • 适配层(Adapter Layer)适配层负责将抽象层上的代码转换成各个平台的代码。适配层中包含了不同平台的适配器,用于处理平台之间的差异性

  • 编译流程:

    • 代码编写:开发者编写基于抽象层的代码,这些代码使用了统一API组件

    • 代码解析:编译器解析开发者编写的代码,生成中间表示(Intermediate Representation, IR)。

      • 中间表示 (IR) 是编译器技术中的一个概念。它是一种介于源代码目标代码之间的抽象形式,方便编译器进行各种优化代码生成
    • 平台转换:根据目标平台,编译器中间表示转换成对应平台的代码。例如,将 Vue 代码转换成微信小程序的 WXMLWXSS,或转换成 H5HTMLCSS

    • 生成产物:编译器生成目标平台代码产物,这些产物可以直接运行在目标平台上。

2. 多端适配

  • UniAppTaro 提供了一套通用API组件,开发者可以通过这些 API组件进行开发,而不需要关心具体平台的实现细节。通用 API组件编译时映射对应平台的 API 和组件。例如:
    • 调用 uni.request 在不同平台上对应微信小程序的 wx.request、H5 的 fetch 等。
    • 使用 uni.navigateTo 实现页面跳转,在不同平台上会映射相应的导航 API。
  • 平台差异处理
    • 对于一些特定平台差异,框架提供了平台特定代码块条件编译来处理。开发者可以通过条件编译语法在代码中写入不同平台的特定实现
    // #ifdef MP-WEIXIN
    // 微信小程序特有的代码
    wx.showToast({
      title: 'Hello WeChat'
    });
    // #endif
    
    // #ifdef H5
    // H5 平台特有的代码
    alert('Hello H5');
    // #endif
    

3. 编译器和运行时

  • 编译器

    • 编译器是 UniAppTaro 的核心部分,负责将通用代码转换成目标平台的代码。编译器包括以下几个部分:
      • 解析器解析源代码,生成抽象语法树(AST)。
      • 转换器:将抽象语法树转换成目标平台的中间表示
      • 生成器:将中间表示生成目标平台的代码。
  • 运行时

    • 运行时部分负责目标平台上执行生成的代码,并提供一些通用的功能,例如状态管理事件处理等。运行时会根据平台的特性进行优化调整,以保证应用的性能兼容性

31、async、await的设计和实现。

1、Promise

  • async/await 是构建在 promise 之上的。promise 是一种表示异步操作最终完成或失败的对象。它有三种状态:pending、fulfilled、rejectedasync 函数总是返回一个 promise 对象。
    function asyncTest() {
        return new Promise((resolve, reject) => {
            // 异步操作
            // 如果成功,调用 resolve(result)
            // 如果失败,调用 reject(error)
        })
    }
    

2、async函数

  • async 函数是用于定义异步操作的函数。在函数体内部,可以使用 await 关键字等待一个 promise 解决,而不会阻塞其他代码的执行。
    async function asyncFunc() {
        try {
            let result = await asyncTest()
            console.log(result)
        } catch (error) {
            console.error(error)
        }
    }
    

3、设计和实现

  • async / await 的设计目标是简化异步代码的书写和理解。它使得开发者能够以类似同步的方式编写异步代码,避免了回调地狱和提高了代码可读性。
  • 实际上, async/awaitpromise 的语法糖,它背后仍然依赖于 promise 的机制。 async 函数会返回一个 promise 对象,而 await 表达式会暂停执行 async 函数,等待 promise 的解决。当 promise 解决后, await 表达式将返回解决的值,并继续执行 async 函数。
  • 在现代的浏览器和 Node.js 环境中,async/await 已得到广泛支持,使得异步代码的编写更加方便。这种设计和实现方式旨在提高异步代码的可维护性和可读性,同时减少错误的可能性。

32、动态导入

  • 动态导入(Dynamic Import)是 JS 的一种功能,允许在运行时按需加载模块,而不是在脚本开始执行静态地导入所有模块。动态导入通过调用 import() 函数来实现,该函数返回一个 Promise,因此可以与 async/await 语法结合使用,使代码更加灵活高效

  • 按需加载:

    • 动态导入可以按需加载模块,减少初始加载时间,提高应用性能。例如,在单页面应用(SPA)中,可以根据用户操作按需加载页面组件。
    if (condition) {
      import('./moduleA.js').then(module => {
        module.doSomething();
      });
    } else {
      import('./moduleB.js').then(module => {
        module.doSomethingElse();
      });
    }
    
  • 延迟加载:

    • 可以延迟加载某些模块,直到它们真正需要时才加载,优化资源利用。
    async function init() {
      const { initModule } = await import('./initModule.js');
      initModule();
      
      // 延迟加载其他模块
      setTimeout(async () => {
        const { otherModule } = await import('./otherModule.js');
        otherModule.doSomething();
      }, 1000);
    }
    init(); 
    
  • 条件加载:

    • 根据运行时条件加载不同的模块。
    async function loadComponent(componentName) {
      const component = await import(`./components/${componentName}.js`);
      return component.default;
    }
    
    loadComponent('Header').then(Header => {
      // 使用 Header 组件
    }); 
    
  • 分割代码:

    • 在大型应用中,可以使用动态导入来分割代码,减少单个文件的大小,提升加载速度和响应速度。
    async function loadPage(pageName) {
      const page = await import(`./pages/${pageName}.js`);
      page.render();
    } 
    // 根据路由加载不同的页面
    loadPage('home'); 
    
  • 动态导入的优势

    • 提高性能:动态导入允许按需加载模块,减少初始加载时间和内存占用。

    • 增强灵活性:可以根据运行时的条件和用户行为加载不同的模块,提升用户体验。

    • 支持异步操作:动态导入返回 Promise,可以与 async/await 结合使用,简化异步代码的编写。

33、import 怎么实现的

  • 模块解析:当 JavaScript 引擎遇到 import 语句时,首先会解析模块路径,确定要导入的模块。

  • 模块加载:引擎通过模块加载器加载指定的模块。模块加载器会检查模块是否已经加载过,如果是,则直接返回已加载的模块。

  • 模块执行:加载器解析模块内容,并执行模块代码。对于尚未执行的模块,会按照模块依赖关系进行递归加载和执行。

  • 导出内容:模块执行完成后,它会导出指定的内容。导出的内容将会被其他模块引用和使用。

34、CI/CD流程的具体步骤

  • 代码提交: 开发人员将代码推送到版本控制系统(如GitHub)。

  • 触发构建: CI工具检测到新的提交后,触发构建流程。

  • 依赖安装: 安装项目依赖(如通过npm或yarn)。

  • 静态代码分析: 运行ESLint等工具进行代码质量检查。

  • 单元测试: 运行单元测试,确保代码功能正确。

  • 构建: 将代码打包为生产环境可用的文件(如webpack打包)。

  • 部署: 将构建后的文件部署到测试环境或生产环境。 通知:通过电子邮件、Slack等方式通知相关人员构建和部署结果。

35、什么是FOUC(无样式内容闪烁)?你如何来避免FOUC?

  • FOUC 是指在网页加载过程中,用户在短暂的时间内看到未应用样式的内容。这种现象通常发生在 CSS 样式表未完全加载之前,浏览器先渲染了未应用样式的 HTML 内容,然后再应用样式,导致内容闪烁。

  • 避免方法:

    • 确保所有的 CSS 样式表都放在 <head> 标签内,这样浏览器在解析 HTML 时可以优先加载 CSS,防止未应用样式的内容渲染。

    • 尽量避免在 CSS 文件中使用 @import 语句,因为这会延迟样式的加载。可以通过 标签一次性加载所有 CSS 文件。

    • 将关键的 CSS 直接内联到 HTML 文档的 <head> 部分。这种方法可以确保关键样式在页面加载时立即应用,从而减少 FOUC。

    • 使用 asyncdefer 属性来延迟 JavaScript 的加载和执行,以便让浏览器优先处理 CSS 和 HTML。这有助于减少 FOUC。

    • 对于 React、Vue 等前端框架,可以使用服务器端渲染(SSR)技术。在服务器上预先渲染 HTML 和 CSS,从而在客户端加载时减少 FOUC。

36、什么是requestAnimationFrame?

  • 它是一个浏览器提供的高效 API,用于在浏览器的下一次重绘之前执行动画更新。它提供了一种更流畅和高效的方式来进行动画,使开发者可以精确地同步代码执行与屏幕的刷新率,从而创建平滑的动画效果。

  • 高效动画:

    • requestAnimationFrame 会在浏览器准备下一次重绘时调用指定的回调函数,从而使动画与浏览器的刷新频率同步。
    • 浏览器的刷新频率通常是每秒 60 次(60Hz),因此回调函数大约每 16.7 毫秒执行一次。
  • 节能和优化:

    • 在隐藏或不可见的标签页中,requestAnimationFrame 会暂停调用,这样可以节省 CPU 和电池资源。这相比于 setInterval 或 setTimeout 更为节能。
  • 平滑的动画:

    • 通过同步到屏幕刷新,requestAnimationFrame 能够提供更平滑的动画效果,减少由于动画帧和屏幕刷新不匹配而产生的卡顿或跳帧。

37、sass 和 less 的区别。

1. 语法

  • Sass 有两种语法:缩进语法(.sass 文件)和 SCSS 语法(.scss 文件)。
  • Sass
    • 缩进语法(Sass):

      • 无需大括号 {} 和分号 ;。
      • 使用缩进表示层级关系。
      $primary-color: #333 
      
      body
        color: $primary-color
      
    • SCSS 语法:

      • 与 CSS 语法几乎相同。
      • 使用大括号 {} 和分号 ;。
      $primary-color: #333;
      
      body {
        color: $primary-color;
      } 
      
  • Less
    • Less 的语法更接近 CSS,只需扩展一些特性。
    @primary-color: #333;
    
    body {
      color: @primary-color;
    }
    

2. 变量

  • Sass 使用 $ 符号定义变量。
$primary-color: #333;
  • Less 使用 @ 符号定义变量。
@primary-color: #333;

3. 混合(Mixins)

  • Sass

    • 支持参数和默认值。
    • 可以使用 @include 调用。
    @mixin border-radius($radius) {
      -webkit-border-radius: $radius;
        -moz-border-radius: $radius;
          -ms-border-radius: $radius;
              border-radius: $radius;
    }
    
    .box { @include border-radius(10px); }
    
  • Less

    • 支持参数和默认值。
    • 直接调用混合。
    .border-radius(@radius) {
      -webkit-border-radius: @radius;
        -moz-border-radius: @radius;
          -ms-border-radius: @radius;
              border-radius: @radius;
    }
    
    .box { .border-radius(10px); }
    

4. 函数

  • Sass
    • Sass 提供更强大的内置函数库,如颜色操作、数学运算、字符串操作等。
    $base-color: #036;
    $lighter-color: lighten($base-color, 20%);
    
  • Less
    • Less 也提供了一些内置函数,但相对较少。
    @base-color: #036;
    @lighter-color: lighten(@base-color, 20%);
    

5. 插值

  • Sass
    • Sass 支持字符串插值,可以将变量值嵌入到选择器或属性值中。
    $name: "main";
    .#{$name} {
      color: blue;
    }
    
  • Less
    • Less 也支持插值,但语法略有不同。
    @name: "main";
    .@{name} {
      color: blue;
    } 
    

6. 控制指令

  • Sass
    • Sass 提供了更丰富的控制指令,如 @if, @for, @each, @while 等。
    @for $i from 1 through 3 {
      .item-#{$i} { width: 2em * $i; }
    } 
    
  • Less
    • Less 也支持条件语句和循环,但语法和功能上稍逊于 Sass。
    .loop (@i) when (@i > 0) {
      .item-@{i} { width: (@i * 2em); }
      .loop(@i - 1);
    }
    .loop(3); 
    

7. 扩展性

  • Sass:由于其语法和功能的丰富性,Sass 通常被认为在大型项目中更具可扩展性。
  • Less:Less 更接近 CSS,对于那些希望逐步增强现有样式的项目来说,可能更容易上手。

8. 编译工具

  • Sass:官方提供了多种编译工具,如 Dart Sass 和 LibSass,也有广泛的第三方工具支持。
  • Less:Less 也有官方的编译器和多种第三方工具支持,但相对来说选择较少。