一、 打包工具 webpack / vite
1.1 webpack
webpack 是一款主流的模块化打包工具 提供对前端所有资源的模块化打包方案
场景 1: ES6 语法(开发阶段) ==> ES5 语法 (生产阶段)
场景 2: 将零散的模块文件打包到统一的文件中 避免由于模块文件过多造成的频繁的网络请求
场景 3: 实现所有前端资源的模块化(.js / .scss / .png / .ts) 打包
1.1.1 快速上手 (基本使用)
- 在项目目录中, 执行
yarn add webpack webpack-cli --dev安装 webpack 依赖 - 执行
yarn webpack命令, 对项目目录中的文件进行打包, 打包后的文件输出到 dist 目录
1.1.2 配置文件及基础配置
- webpack4 以上的版本支持按照约定零配置直接打包, 以
src/index.js为入口文件 -> 以dist/main.js为输出文件 - 使用
webpack.config.js配置文件可以对 webpack 进行自定义打包配置- entry: 指定入口文件
- output: 指定输出文件
- filename: 输出文件名
- path: 输出文件路径 (必须为绝对路径, 可以使用
path.join(__dirname, 'output')获取绝对路径) - publicPath: 资源目录, 默认值为空字符串
- mode: 指定工作模块
- module:
- rules: 配置 loader 加载规则 一个对象数组
- test: 正则匹配正在使用的 loader 文件
- use: 指定使用的 loader 可以接收 loader 名字符串(或指定 loader 文件路径) 配置对象 或由多个 loader 名字符串 配置对象组成的数组, 数组中配置的多个 loader 会按照从后往前的顺序依次执行
- loader: loader 名
- options: 配置选项 用于指定 loader 的配置参数
- plugins: 配置插件 接收一个数组
// webpack.config.js
const path = require("path");
module.exports = {
mode: "none",
entry: "./src/style.css",
output: {
filename: "bundle.js",
path: path.join(__dirname, "output"),
},
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [],
};
1.1.3 工作模式
webpack 可以通过命令行参数
--mode=指定工作模式, 默认不指定时执行production模式, 也可以在配置文件中 通过 mode 配置项进行指定
- production 生产模式: 会启用内置的插件对打包的代码压缩优化等操作
- development 开发模式 更注重打包效率 不对代码进行压缩
- none 纯打包模式 以原始的状态 打包代码
1.1.4 打包结果运行原理
以 none 模块打包代码, 查看打包后的输出文件 并进行分析
- 入口函数
- 定义了一个对象来缓存加载的模块
installedModules - 定义了一个函数
__webpack_require__来加载模块,返回 exports - 在定义的
__webpack_require__函数上过载了一些数据和工具函数 - 在最后调用
__webpack_require__函数来加载入口模块 并返回 exports
- 定义了一个对象来缓存加载的模块
- 入函数参数传入的是一个由模块函数组成的数组,每个模块被包裹在一个相同结构的函数当中
function(module, __webpack_exports__, __webpack_require__){},用来形成模块私有作用域 - 每个模块函数中都会执行webpack_require.r(webpack_exports),用来为 exports 对象定义一个__esModule 标记
- 模块中使用webpack_require加载其他依赖的模块,并执行模块中相应的代码
1.1.5 资源模块加载
webpack 使用 loader 实现对模块的加载, 默认 loader 只对 js 文件进行加载与解析,对于 css 等其他资源模块的加载, 则可以通过配置额外的 loader 来实现 loader 是 webpack 实现前端模块的核心,通过不同的 loader 可以实现加载任何类型的资源
- 在 webpack.config.js 中, 通过 module 属性, 配置 loader 加载规则
- module
- rules: 配置 loader 加载规则 一个对象数组
- test: 正则匹配使用 loader 的文件
- use: 指定使用的 loader, 可以接收 loader 名字符串, 或者由多个 loader 名字符串组成的数组, 数组中配置的多个 loader 会按照从后往前的顺序依次执行
- rules: 配置 loader 加载规则 一个对象数组
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
];
}
1.1.6导入资源模块
通常情况下, 应当使用js文件作为模块打包的入口, 其他资源文件import的方式引入 webpack建议应当在对应的代码模块(按需加载) , 而不是在全局入口引入
1.1.7 常用资源加载器-loader
- file-loader: 文件资源加载器
- url-loader: 类似于file-loader,区别是当文件小于指定大小(可通过options下的limit选项配置)时,可以直接返回DataURL,将图片等转换成base64编码直接存放在js文件中,将小文件通过这种方式处理,可以减少页面加载时,浏览器请求资源文件的次数,提高加载效率
- url-loader依赖于file-loader,无法单独使用
- css-loader: 编译css文件到js中, 以style形式挂载到html
- eslint-loader: 语法格式校验
- babel-loader: ES语法
- html-loader: 加载处理html文件
1.1.8 资源加载方式
- 遵循ES Modules标准的import声明
- 遵循CommonJS标准的require声明
- 遵循AMD标准的define函数和require函数
- css文件中的@import与css属性中使用的url(例如background-image)
- html中的src、a标签的href等
1.1.9 核心工作原理
- 以指定js文件为入口, 查找资源文件以来, 并生成依赖树
- 遍历依赖树, 将依赖的资源模块交给相应的loader进行处理
- 将最终处理的结果汇总到输出的bundle.js文件中
1.2 vite
1.2.1 vite是什么
Vite是一个快速、轻量级的开发构建工具,它利用现代浏览器的原生ES模块加载功能,实现了开发环境中的快速冷重载和构建速度。Vite的开发体验非常好,因为它能够在开发时实时更新页面,而不需要对整个项目进行重新构建。
相比于Webpack的构建过程,Vite的开发速度更快,也更适合小型、简单的项目。但是缺点是: Vite目前还不支持像Webpack那样的插件生态系统,因此其可扩展性还有待提高,灵活性不如webpack.
1.2.2 Vite相比于Webpack打包更快?
编译和打包速度方面:
- 在
Webpack中,每次修改代码后都需要对整个项目进行重新编译,然后重新生成大量的代码和资源文件 - 在
Vite中,它使用了浏览器原生的ES模块加载器,当开发者修改代码后,Vite会即时在浏览器中编译和打包代码,然后将更改的部分直接传递给浏览器,并重新加载这部分代码
vite按需打包:
Vite还使用了缓存机制和按需加载的方式,这也是它快速打包的原因之一。当开发者第一次访问项目时,Vite会对项目进行编译和打包,并缓存生成的文件。这样,当开发者下一次打开项目时,Vite只需要编译和打包发生更改的部分,而不需要重新编译和打包整个项目
二、JavaScript篇
script标签的属性
1、src:可选,链接外部文件
2、 type :用script元素嵌入js代码记得要加type="text/javascript"
3、 charset:字符编码属性,可选。默认是utf-8编码,主要表示通过src属性指定的代码的字符集,大多浏览器会忽略它的值,所以不必使用。
4、language:脚本类型属性,不是标准组成的一部分,已废弃。大多数浏览器会忽略这个属性,已没必要使用。
5、defer:如果script标签设置了该属性,则浏览器会异步的下载该文件并且不会影响到后续DOM的渲染; 如果有多个设置了defer的script标签存在,则会按照顺序执行所有的script; defer脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行。
6、async
- async的设置,会使得script脚本异步的加载并在允许的情况下执行
- async的执行,并不会按着script在页面中的顺序来执行,而是谁先加载完谁执行。
- ps:defer和async的区别
- ①defer和async都属于异步加载, defer会比async稳定。
- ②defer是延迟执行(推迟解释,当前html页面解析完成后执行)js,async是当前js文件加载完成后执行js
2.1 js的数据类型
- JavaScript共有八种数据类型
- 基本数据类型: Undefined 、Null 、Boolean 、Number 、String 、Symbol 、Bight
- 引用数据类型: Object 、Function 、Array 其中Symbol和Bight是es6中新增的数据类型
Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。 BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
2.2 堆和栈的区别
- 在内存中 分为堆区和栈区
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
- 在数据结构中:
- 栈区数据的存取方式是先进后出
- 堆区是一个优先级队列 按照优先级进行排序 优先级可以按照大小来规定
- 数据的储存方式
- 基本数据类型直接存储在栈区,空间小、大小固定,属于被频繁使用的数据
- 引入数据类型存储在了堆区,占据空间大、大小不固定,如果存储在栈中,将会影响运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址. 当解释器寻找引用值,会首先检索其在栈中的地址,取得地址后从堆中获取实体
2.3 数据类型检测方式
typeof instanceof constructor toString四种常用方法
- typeof 优点: 使用简单 缺点: 只能检测出除null外的基本数据和引用数据类型的function
- instanceof 优点: 能检测出引用数据类型 缺点: 无法检测出基本类型
- constructor 优点: 能检测出所有的类型(除null和undefined) 缺点: constructor易被修改
- Object.prototype.toString.call 优点:能检测出所有的类型 缺点IE6以下不支持
2.4 字符串/数字类型转换
- 字符串 转 数字型:
- 前置
+号 - 使用
Number()方法 只能将纯数字型字符串转为数字型,若出现非数字型,则返回NaN - 使用
parseInt()方法 将字符串型转为整数数字型 若第一个是数字,判断到第一个非数字型;若第一个字符是非数组 返回NaM - 使用
parseFloat()方法 将字符串型转为浮点数数字型 和parseInt()类似,会返回浮点数
- 前置
- 数字型 转 字符串
- 模板字符串拼接空字符串
- 使用
toString()方法 - 使用
String()方法
2.5 JS中的隐式转换
在if语句、逻辑语句、||、 && 、 == 等情况下都可能出现隐式转换
2.6 数组遍历方法
- forEach
- map
- filter
- every
- some
- find
- reduce
2.7 浅拷贝&深拷贝
- 浅拷贝: 指的是创建新的数据,新数据有原始数据属性值的一份拷贝
- 如果原始数据是基本数据,拷贝的就是基本类型的值;如果原始数据是引用数据类型,拷贝的是内存地址
- 即浅拷贝是拷贝了一层
- 常见的浅拷贝
- Object.assign
- Object.create
- slice
- concat
- 展开运算符...
- 深拷贝:开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
- 常见的深拷贝
- _cloneDeep()
- JSON.stringfy()
- 手写循环递归
- 注意: JSON.stringify深拷贝的缺点
- 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
- 如果对象中有函数或者undefined,则会直接被丢掉
2.8 this
this的理解: this是一个在运行时才进行绑定的引用, 在不同的情况下它可能会被绑定不同的对象
判断this的指向:
- 函数调用模式, 当一个函数不是一个对象的属性时 直接作为函数来调用 this指向全局对象
- 方法调用模式, 如果一个函数作为一个对象的方法来调用时,this指向这个对象
- 构造器调用模式, 如果一个函数用new调用时,函数执行前会新创一个对象 this指向这个对象
- apply、bind、call调用模式, 这三个方法都可以指定this指向.
2.9 浏览器的垃圾回收机制
- 内存的生命周期
JS 环境中分配的内存, 一般有如下生命周期:
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉
- 垃圾回收的概念
垃圾回收: javascript代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收.
回收机制:
- javascript 具有自动垃圾回收机制, 会定期对那些不再使用的变量、对象所占用的内存进行释放, 原理就是找到不再使用的变量, 然后释放掉其占用的内存
- javascript 中存在两种变量: 局部变量和全局变量. 全局变量的生命周期会持续到页面卸载; 而局部变量声明在函数中,它的生命周期从函数执行开始,知道函数执行结束, 在这个过程中,局部变量会在堆和栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放.
- 不过, 当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收.
- 垃圾回收的方式
- 引用计数法 IE采用的是引用计数算法,就是跟踪记录每个值被引用的次数。当一个变量被引用类型赋值时,则这个值次数就是1。相反,如果这个值的引用又取得了另一个值,则这个值引用次数-1。当这个引用次数为0时,这个变量已经没有意义,占有的内存空间会被释放出来。缺点:引起【循环引用】的问题:如
obj1和obj2通过属性进行相互引用,这两个的引用次数都是2.- 标记清除法 现在主流的浏览器不使用引用计数算法。大多是标记清除法,当变量进入执行环境时,就标记这个变量,被标记的变量是不能被回收的,当变量离开执行环境时,会再次标记。有离开环境标记的变量会被释放内存。
- 如何减少垃圾回收
虽然浏览器可以进行垃圾回收,但是代码复杂时,垃圾回收的性能比较差,应该尽力减少垃圾回收.
- 数组优化: 在清空一个数组时,可以赋值给一个
[], 但是也会创建一个新的空对象,可以将数组的长度设置为0Object优化: 对象尽量复用,对于不再使用的对象,将其设为null- 对
Function进行优化: 在循环的函数表达式,如果能复用, 尽量放在函数外
- 内存泄漏 是指由于疏忽或错误造成程序未能释放已经不再使用的内存
原因:
- 全局变量: 使用未声明的变量, 会创建一个全局变量,且这个变量会一直在内存中无法被回收
- 计时器 / 回调函数: 设置了
setInterval定时器, 忘记销毁定时器。或者函数里有对外部变量的引用,那么这个变量会一直留在内存中,无法回收- DOM引用:获取一个DOM元素的引用,若这个元素被删除,但是这个DOM的引用一直保留,则也无法被回收
- 闭包:不合理使用闭包,导致有变量一直保留在内存中
2.10 闭包和作用域
闭包
概念: 闭包就是指有权访问另一个函数作用域中的变量的函数。闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。闭包是一种保存和保护内部私有变量的机制。
作用:
- 创建私有变量。可以在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量。
- 变量对象继续留在内存。因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收
闭包使用场景: 在实际的项目中,会基于闭包把自己编写的模块内容包裹起来,这样编写就可以保护自己的代码是私有的,防止和全局变量或者是其他的代码冲突,这一点是利用保护机制。
- 例如:
return回一个函数- 函数作为参数
- IIFE(自执行函数)
- 循环赋值
- 使用回调函数就是在使用闭包
- 防抖节流
- 函数柯里化
[但是]不建议过多的使用闭包,因为使用不被释放的上下文,是占用栈内存空间的,过多的使用会导致导致内存泄漏。
闭包的执行过程:
- 形成私有上下文
- 进栈执行
- 初始化作用域链(初始化两头作用域:当前作用域、上级作用域)
- 初始化
this - 初始化
arguments - 赋值形参
- 变量提升
- 代码执行
- (遇到变量先查看是否是自己私有,如果不是私有按照作用域链上查找,若不是上级就继续向上查找,直到查找到null,变量查找其实就是作用域链的拼接过程)
- 正常情况下,代码执行完成之后,私有上下文出栈被回收.但是若当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就形成了不被销毁的上下文,闭包
执行上下文
- 执行上下文的类型
- 全局执行上下文。 任何不在函数内部的都是全局执行上下文,会先创建一个全局的window对象,并且设置this的值等于全局对象,一个程序中只有一个全局执行上下文。
- 函数执行上下文。当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数执行上下文可以有无数次。
- 执行上下文的三个阶段
- 创建阶段 -> 执行阶段 -> 回收阶段
作用域
作用域就是变量作用的有效范围。在一定空间里可以对变量数据进行读写等操作,这个空间就是变量的作用域
- 全局作用域
- 就是直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量
- 全局变量在全局的任何位置都可以使用,全局作用域无法访问到局部作用域的变量
- 全局作用域在页面打开时创建,在页面关闭时销毁。
- 函数作用域(局部作用域)
- 调用函数时会创建函数作用域,函数执行完毕后,该作用域销毁。每调用一次函数就会创建一个新的函数作用域,之间是相互独立的
- 在函数作用域中可以访问全局变量,在函数的外面无法访问函数内的变量。
- 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有就向上一作用域中寻找,直到找到全局作用域,如果全局作用域中仍然没有找到,则会报错。
- 块级作用域
- ES6新引入的概念
- 使用
let和const声明的变量,外部是访问不到的,这种作用域的规则就叫块级作用域。 - 通过var声明的变量或者非严格模式下创建的函数声明没有块级作用域
作用域链
当js中使用一个变量的时候,首先会在当前作用域下寻找该变量,若没有找到,则会去他的上级作用域寻找,以此类推直到找到全局作用域,这样的变量作用域访问的链式结构,被叫做作用域链.
- 作用:
保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数
2.11 谈谈JS中的预解析
JS在运行一份代码时, 会执行下面的操作
- 把变量的声明提升到当前作用域的最前面, 注意 只会提升声明, 不会提升赋值
- 把函数的声明提升到当前作用域的最前面, 注意 只会提升声明, 不会提升调用
- 先提升function 再提升var 函数提升的优先级大于变量提升,函数提升在变量提升之上
2.12 变量提升与函数提升的区别
- 变量提升:
JS执行代码时,会进行预解析,预解析期间会把
变量声明与函数声明提升至对应作用域最顶端,函数内声明的变量只会提升至该函数作用域最顶层,函数内声明的变量只会提升至该函数作用域最顶端.当函数内部定义的一个变量与外部相同时,那么函数内的变量就会提升至最顶部
- 函数提升
函数提升只会提升函数声明式写法,表达式的写法不存在函数提升 函数提升的优先级大于变量提升,函数提升在变量提升之上
2.13 函数式编程
主要的编程范式有三种: 命令式编程、声明式编程和函数式编程 相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程
函数式编程优缺点:
- 优点:
- 更好的管理状态:宗旨是无状态,或者说更少的状态,能最大的减少出错的情况
- 更简单的复用:固定输入 -> 固定输出,没有其他外部变量影响,无副作用,代码复用时,考虑的情况更少
- 更优雅的组合:往大了说,网页是由各个组件组成的,往小了说,一个函数也是由多个小函数组成的,带来了更强的复用性、组合性
- 减少代码量,提高维护性
- 缺点:
- 性能:函数式编程的短板之一就是性能,往往会对一个方法过度包装
- 资源占用:在JS中为了实现对象状态的不可变,往往会创建新的对象,所以对垃圾回收有了更多需求
- 递归陷阱:在函数时编程中,为了实现迭代,通常会采用递归操作
函数声明与函数表达式的区别
- 函数声明:funtion开头,有函数提升
- 函数表达式: 不是funtion开头,没有函数提升
2.14 高阶函数和函数柯里化
- 高阶函数:高阶函数是指使用其他函数作为参数、或者返回值是一个函数的函数
- 柯里化函数: 是把接受多个参数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回结果的一个函数
- 函数柯里化好处:
- 参数复用: 需要输入多个参数,最后只需要输入一个,其他的参数通过arguments获取
- 提前确认: 避免重复判断某一条件是否符合,不符合则return
- 延迟运行: 避免重复的执行程序,等需要结果的时候再执行
2.15 箭头函数
使用箭头=>来定义函数。相当于是匿名函数,省去了function,简化了函数定义
箭头函数的特征:
- 没有this,this永远指向定义箭头函数时所处的外部环境
- 箭头函数只能声明匿名函数,但是可以通过表达式的形式让箭头函数具名
- 没有原型
prototype - 不能当作一个构造函数,因为this的指向问题
- 箭头函数没有
arguments,在箭头函数内部访问arguments其实访问的是外部环境的arguments,可以用...代替
2.16 call、apply、bind
共同点:
- 都可以改变this指向
- 三者的第一个参数都是
this要指向的对象,如果没有这个参数或者参数为undefined或null,则默认指向全局对象window
不同点:
call和apply会调用函数,并且改变函数内部的this指向call和apply传递的参数不同,call传递的参数用逗号隔开,apply用数组传递,apply和call是一次性传入参数,bind可以多次传入bind是返回绑定this之后的函数
2.17 创建对象的方式
- 字面量形式,直接创建对象
- 函数方法
- 工厂模式, 主要工作原理是用函数来封装创建对象的细节,通过调用函数来达到复用的目的
- 构造函数模式
- 原型模式
- 构造函数模型+原型模型
- 动态原型模式
- 寄生构造函数模式
- class创建
工厂模式
工厂模式是用来创建对象的一种最常见的设计模式,不暴露创建对象的具体逻辑,而是把逻辑封装在函数里,这个函数就被视为一个工厂.只需要传入正确的参数,就能创建需要的对象
2.18 js内置的常用对象
- Number 数值对象,数值常用方法
- Number.toFixed() 参数:数点后数字的个数 返回 给定数字的字符串
- Number.toString() 将一个数字转换为字符串
- Number.valueOf() 返回原始数值
- String 字符串对象, 字符串常用方法
- Length 获取字符串的长度
- Split() 将一个字符串切割成数组
- concat() 连接字符串
- indexOf() 返回一个子字符串在原始字符串中的索引值.如果没有找到,则返回固定值-1
- lastIndexOf() 从后向前检索一个字符串
- slice() 抽取一个字符串
- Array 数组对象
- join() 将一个数组转成字符串,返回字符串
- reverse() 翻转数组
- delete 该运算符只能删除数组元素的值,但所占的内存空间还在,总长度没变
- shift() 删除数组中的第一个元素,返回该元素的值.改变原数组
- pop() 删除数组中的最后一个元素,返回删除的值.改变原数组
- unshift() 在数组前面添加一个或多个数组元素.改变原数组
- push() 在数组结尾添加一个或多个数组元素.改变原数组
- concat() 合并两个或多个数组.返回一个新数组
- slice() 分割数组,返回一个新数组
- splice() 移除或者替换已有的元素.改变原数组
- toLocaleString() 把数组转换为局部字符串
- toString() 把数组转换为字符串
- forEach() 遍历所有元素.没有返回值
- map() 遍历所有元素,返回一个新数组
- filter() 过滤符合条件的数组.不改变原数组
- find() 查找满足条件的第一个值,若没有返回undefined
- some() 判断是否有至少一个符合条件的,返回值是布尔值
- every() 判断是否所有元素都满足条件,返回布尔值
- reduce() 可用于但不局限于累加 文档
- fill() 填充数组
- flat() 数组扁平化
- Object 基础对象
- Object.constructor 对象的构造函数
- Object.hasOwnProperty() 检查属性是否被继承
- Object.isPrototypeOf() 一个对象是否是另一个对象的原型
- Object.propertyIsEnumerable() 是否可以通过for/in 循环看到属性
- Object.toLocaleString() 返回对象的本地字符串表示
- Object.toString() 定义一个对象的字符串表示
- Object.valueOf() 指定对象的原始值
- Date 日期时间对象
- Date.getTime() 返回Date对象的毫秒表示
- Date.getFullYear() 返回Date对象的年份字段
- Date.getMonth() 返回Date对象的月份字段
- Date.getDate() 返回一个月中的某一天
- Date.getDay() 返回一周中的某一天
- Date.getHours() 返回Date对象的小时字段
- Date.getMinutes() 返回Date对象的分钟字段
- Date.getSeconds() 返回 Date对象的秒字段
- Math 数学对象
- Math 对象是一个静态对象
- Math.PI 圆周率
- Math.abs() 绝对值
- Math.ceil() 向上取整(整数+1,小数去掉)
- Math.floor() 向下取整(直接去掉小数)
- Math.round() 四舍五入
- Math.pow(x,y) 求x的y次方
- Math.sqrt() 求平方根
- RegExp 正则表达式对象
- RegExp.exec() 检索字符串中指定的值.返回找到的值,并确定其位置
- RegExp.test() 检索字符串中指定的值. 返回true或false
- RegExp.toString() 把正则表达式转换成字符串
2.19 hasOwnProperty instanceof方法
hasOwnproperty()方法会返回一个布尔值, 指示对象自身属性中是否具有指定的属性instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
2.20 原型对象和原型链
**构造函数的内部的prototype属性指向的对象, 就是构造函数的原型对象. **
原型对象包含了可以由该构造函数的所有实例共享的属性, 这个原型对象又会有自己的原型,这种链式查找过程称之为原型链
原型链的终点是null
Object.prototype.proto
2.21 JS实现继承的方法
- 原型链继承
- 关键: 子类构造函数的原型为父类构造函数的实例对象
- 借用构造函数继承
- 关键: 用.call()和.apply() 方法,在子类构造函数中,调用父类构造函数
- 组合继承
- 关键: 原型链继承+借用构造函数继承
- 原型式继承
- 关键: 创建一个函数, 将要继承的对象通过参数传递给这个函数,最终返回一个对象
2.22 异步与事件循环
- 首先讲一下JavaScript事件执行的特点,js是一门单线程语言,同一时间只能执行一个事件,js内部主要通过事件循环来实现单线程异步执行。
- 区别同步和异步任务。
- 同步任务:从上往下按顺序依次执行,把这个任务完全执行完毕,才执行后面的代码
- 异步任务:等待同步代码执行完,才执行回调函数,异步回调即使触发,也是先放到任务队列执行,只有同步任务或前面的异步任务执行完毕才执行
- 事件循环机制
- js将所有任务分为同步/异步任务,所有任务都放在了主线程上执行,形成一个执行链。
- 执行链外有存储异步任务的任务队列,分别是微任务队列、宏任务队列。
- js执行执行顺序:
-
- 进入到
script标签,就进入第一次事件循环
- 进入到
-
- 遇到同步代码,立即执行;遇到宏任务,放到宏任务队列;遇到微任务,放到微任务队列
-
- 执行完所有的同步代码
-
- 执行微任务队列
-
- 微任务队列清空,寻找下一个宏任务。循环执行步骤2
- 常见的宏任务和微任务
- 宏任务:script标签(整体代码)、事件、网络请求(ajax)、setTimeout
- 微任务:Promise.then(promise本身是同步,then/catch回调是异步)、async/await
2.23 promise
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
所谓的回调地狱就是:回调函数层层嵌套,造成代码性能差、可维护性差、复用性差
promise本身是一个同步任务,真正异步的是它的两个回调函数resolve()和reject()
promise实例的状态
- promise的状态 pending(进行中)、resolved(已完成)、rejected(已失败);当一个事件交给promise时,状态就是
pending,事件完成时状态变为resolved,事件没有完成失败了就变成rejected - 如何改变状态:
resolve(value)-> 将pending状态变为resolved;reject(error)->将pending变为rejected; 抛出异常,也是rejected状态 注意: promise状态只能改变一次,不可逆
创建promise实例
- new Promise()
new Promise((resolve,reject) => {...})
// 一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject
- resolve
Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:
Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});
- reject
Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:
Promise.reject(new Error(“出错了!!”));
promise实例方法
- then
- 可以接受两个回调函数作为参数. 第一个回调函数是pormise对象状态变为
resolved时调用,第二个回调函数时promise对象状态变为rejected时调用,第二个参数可以省略. then方法返回的是一个新的promise对象. 也可以采用链式写法.then().then()
- 可以接受两个回调函数作为参数. 第一个回调函数是pormise对象状态变为
- catch
- 相当于
then方法的第二个参数,指向rejected状态时调用. 还有一种情况时执行resolve函数,如果出现异常,则进入catch方法
- 相当于
- finally
- 用于指定不管 Promise 对象最后状态如何,都会执行的操作
promise静态方法
- all
- 常用于并发任务.接收一个数组, 数组的每一项都是一个
promise对象,返回一个promise实例.如果数组中所有的promise状态都变成resolved时,all方法的状态才会变成resovled, 如果有一个状态变成rejected,那么all方法状态就会变成rejected
- 常用于并发任务.接收一个数组, 数组的每一项都是一个
- race
- 和all方法类似,接收的参数也是数组,和all方法不同的是,
race方法的状态取决于第一个promise对象的状态,如果第一个执行的promise状态是resolved那么race方法状态就是resolved,反之同理
- 和all方法类似,接收的参数也是数组,和all方法不同的是,
- any
- 接收一个数组, 数组的每一项都是primise对象.如果数组内有一个promise对象的状态变为了
resolved,那么any方法返回的promise对象状态就是resolved,如果数组内promise的状态都是rejected,那么返回的promise对象状态就是rejected
- 接收一个数组, 数组的每一项都是primise对象.如果数组内有一个promise对象的状态变为了
- resolve、reject
- 生成对应状态的promise实例
.all .race .any三个方法的区别
all: 成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。 race: 哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 any: 返回最快的成功结果,如果全部失败就返回失败结果。
2.25 async/await
一句话概括 async/await是一种语法糖,是处理异步操作的高级写法。也是回调地狱的最终解决方法。
-
async函数的返回值
async函数返回一个 Promise 对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。 -
await在等什么?
await等待的是一个表达式, 这个表达式的计算结果是Promise对象或者其他值. await不仅用于等promise对象, 可以等任意表达式的结果.
- async/await捕获异常
async函数内部的异常通过.catch或者try/catch来捕获
try/catch 能捕获所有异常,try语句抛出错误后会执行catch语句,try语句内后面的内容不会执行
catch()只能捕获异步方法中reject错误,并且catch语句之后的语句会继续执行
- async/await对比promise的优势
代码更利于阅读, 虽然promise摆脱了回调地狱,但是
.then()的链式调用也增加了很多阅读负担 promise传递中间值比较麻烦,但是async/await几乎是同步的写法async/await捕获错误的方式良好,使用try/catch来捕获错误
2.26 ES6新增语法
let和const
- let
- 声明变量
- 没有变量提升
- 不可重复声明变量
- 具有块级作用域
- 可以在声明变量后赋值
- const
- 只读变量,声明后不可修改
- 没有变量提升
- 不可重复声明
- 具有块级作用域
- 声明变量后必须立刻赋值
重点 面试经常问到
let、const、var三者的区别
- 块级作用域: 由
{}包括, let和const具有块级作用域, var不存在块级作用域.块级作用域解决了es5的两个问题- 内层变量会覆盖外层变量
- 用来计数的循环变量泄露为全局变量
- 变量提升: var存在变量提升, let和const不存在变量提升, 只能在声明之后,否则会报错
- 给全局添加属性:浏览器的全局对象是window, Node的全局对象是global.var声明的变量为全局变量, 并且会将该变量添加为全局对象的属性, 但是let和const不会
- 重复声明: var声明变量时, 可以重复声明变量, 若再次声明则会覆盖之前的声明. const和let不允许重复声明
- 初始值设置: 在变量声明时, var和let可以不用设置初始值. const声明变量必须设置初始值
- 指针指向: var和let可以改变指针指向(可以重新赋值), const声明的变量不允许改变指针的指向
解构赋值
- 对象解构赋值
- 数组解构赋值
- 函数参数解构
2.27 DOM
DOM事件流
- 事件捕获阶段
- 处理目标阶段
- 事件冒泡阶段
事件冒泡: 事件开始由最具体的元素(⽂档中嵌套层次最深的那个节点)接收到后,开始逐级向上传播到较为不具体的节点
<html>
<head>
<title>Document</title>
</head>
<body>
<button>按钮</button>
</body>
</html>
如果点击了上面页面代码中的 <button> 按钮,那么该 click 点击事件会沿着 DOM 树向上逐级传播,在途经的每个节点上都会发生,具体顺序如下:
button 元素 -> body 元素 -> html 元素 -> document 对象
事件捕获:
事件委托:
三、 VUE
3.1 vue基本原理
当一个vue实例创建时,vue会遍历data中的属性,用Object.defineProperty(vue3.0使用proxy),将它们转为getter/setter,并在内部追踪依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的watcher实例,它会在组件渲染的过程中把属性记录为依赖,当依赖的setter被调用时,通知watcher重新计算,最后使组件更新。
vue优点:
- 轻量:只关注视图层,是一个构建数据的视图集合,只有几十kb
- 简单易学:华人开发,中文文档,方便理解学习
- 双向数据绑定:在数据操作方面简单,保留了
angular的特点 - 组件化:保留了
react的优点,实现了html的封装和复用,便于构建单页面应用 - 视图:数据、结构分离,使更新数据更加方便,不需要改变逻辑代码,只需要操作数据
- 虚拟DOM:
dom操作十分消耗性能,不再使用原生dom操作节点,极大提高了性能。主要原因虚拟DOM,保证了性能下限。 - 运行速度:同样是操作虚拟
dom,就性能来说,vue优于react
3.2 vue响应式原理
-
数据劫持 数据劫持比较好理解,通常我们利用
Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,从而进行进一步操作。 -
发布/订阅模式 在软件架构中,发布订阅是一种消息范式,消息的发送者(发布者)不会将消息直接发布给订阅者。而是把发布的消息分为不同类别,无需了解订阅者。同理,订阅者可以接收一个或多个类别的消息,也无需了解发布者。
结论:发布者和订阅者是互不影响,发布者只需要把消息发布出去,订阅者只需要接收自己需要的消息。
- 响应式原理
Vue响应式原理采用数据劫持+发布-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter、getter,在数据发生变化时发布消息给订阅者,触发相应的监听回调。 主要分为以下几个步骤:
- 遍历
Observe的数据对象,包括子属性对象的属性,加上setter和getter属性。如果给对象中某个属性赋值,那么就会触发setter,从而监听到数据变化 Compile解析模板指令,将模板中的变量替换为数据,然后初始化渲染页面视图,将每个执行对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据发生更新,立即更新视图。Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(Dep)里面添加自己。②自身必须有一个update()方法。 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调。- MVVM作为数据绑定的入口,整合
Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
Observe(被劫持的数据对象) Compile(vue的编译器) Wather(订阅者) Dep(用于收集Watcher订阅者们)
补充知识点
Object.defineProperty的使用方式,有什么缺点
Object.defineProperty( obj, prop, descriptor )
三个参数:
obj 要定义的对象
prop 要定义或修改的属性名称或 Symbol
descriptor 要定义或修改的属性描述符(配置对象)
缺点: 在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
3.3 MVVM模式
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表UI视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
优点:
- 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性:
- 提⾼可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码
- 自动更新dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动dom中解放
缺点:
- Bug调试困难。如果界面显示异常,无法第一时间判断是View还有Model代码有了问题。数据绑定使一个位置的Bug迅速传递到了其他位置,且无法打断点debug
- 如果模块特别大,model占用内存空间也会很大,如果没有及时释放,则会花费更多内存
3.4 虚拟DOM
虚拟(Virtual) DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,相当于在js和真实dom中间加来一个缓存,利用dom diff算法避免没有必要的dom操作,从而提高性能。当然算法有时并不是最优解,因为它需要兼容很多实际中可能发生的情况,比如后续会讲到两个节点的dom树移动。
在vue中一般都是通过修改元素的state,订阅者根据state的变化进行编译渲染,底层的实现可以简单理解为三个步骤:
- 1、用JavaScript对象结构表述dom树的结构,然后用这个树构建一个真正的dom树,插到浏览器的页面中。
- 2、当状态改变了,也就是我们的state做出修改,vue便会重新构造一棵树的对象树,然后用这个新构建出来的树和旧树进行对比(只进行同层对比),记录两棵树之间的差异。
- 3、把记录的差异再重新应用到所构建的真正的dom树,视图就更新了。
它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tag、props、children
-
tag:必选。就是标签。也可以是组件,或者函数
-
props:非必选。就是这个标签上的属性和方法
-
children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素
虚拟DOM的解析过程:
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
为什么使用虚拟DOM 保证性能下限,在不进行手动优化的情况下,提供较好的性能
修改DOM时真实DOM操作和Virtual DOM的过程, 对比他们重排重绘的性能消耗
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新
结论:
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
- 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的
3.5 Diff算法
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
3.6 Vue常用指令及其易混点
- v-on 给标签绑定函数, 可以缩写为
@例如绑定点击事件@click=fn, 函数必须写在methods里.stop阻止默认事件.prevent阻止默认行为.native监听组件根元素的原生事件
- v-bind 动态绑定 作用: 及时对页面的数据进行更改, 可以简写为
:.sync语法糖, 扩展成一个更新父组件绑定值得v-on侦听器
- v-model 数据双向绑定
.lazy取代input监听change事件.number输入字符转为有效得数字型.trim输入首尾空格过滤
- v-slot 缩写为
#组件插槽 - v-for 遍历数据数组, 循环生成标签
- v-show 显示/隐藏内容
- v-if 显示与隐藏
- v-else 必须和v-if连用
- v-text 解析文本
- v-html 解析html标签
v-if、v-show、v-html原理
v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
v-show 和 v-if区别
相同点:表达式为true时显示元素, 表达式为false时隐藏元素
不同点:
v-if根据条件渲染,如果表达式为false就不会生成dom元素,切换时,实际上是dom元素得创建和销毁;v-show则不管表达式是什么,都会生成dom元素,改变的是display:block / display:none
结论 v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
v-for和v-if一起使用
v-for 比 v-if 具有更高的优先级, 虽然用起来也没报错好使, 但是性能不高, 如果你有5个元素被v-for循环, v-if也会分别执行5次
v-for中得key
提升vue渲染性能
- 1.vue在渲染的时候,会 先把 新DOM 与 旧DOM 进行对比, 如果dom结构一致,则vue会复用旧的dom。 (此时可能造成数据渲染异常)
- 2.使用key可以给dom添加一个标识符,让vue强制更新dom
比如有一个列表 li1 到 li4,我们需要在中间插入一个li3,li1 和 li2 不会重新渲染,而 li3、li4、li5 都会重新渲染
因为在不使用 key 或者列表的 index 作为 key 的时候,每个元素对应的位置关系都是 index,直接导致我们插入的元素到后面的全部元素,对应的位置关系都发生了变更,所以全部都会执行更新操作, 这是不可取的
而在使用唯一 key 的情况下,每个元素对应的位置关系就是 key,来看一下使用唯一 key 值的情况下
这样如图中的 li3 和 li4 就不会重新渲染,因为元素内容没发生改变,对应的位置关系也没有发生改变。
这也是为什么 v-for 必须要写 key,而且不建议开发中使用数组的 index 作为 key 的原因
总结一下:
- key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效
- Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能
- 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果
- 从源码里可以知道,Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的
为什么不建议用index索引作为key?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
v-model实现原理
- 作用在表单元素
- 动态绑定了
input的value值 并在触发input事件时,动态赋值给value
- 动态绑定了
<input v-model="sth" />
<-- 等同于 -->
<input
:value="message"
@input="message = $event.target.value"
/>
3.7 动态绑定class和style
- 动态绑定
class
:class="{ '类名': bool, '类名': bool ......}"
// 如果值为true 该类样式就会被应用在元素身上, false则不会 注意点:如果类名有 - ,则需要使用引号包起来
- 动态绑定style
<div v-bind:style="styleObject"></div>
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
3.8 data、computed、watch、methods
Vue的配置选项以data、computed、watch、methods常见
- data
- Vue实例的数据对象 注意: data必须是一个函数,不能是一个对象
- 原因:如果data是对象,其他组件实例化时,也会使用data这个对象,会产生冲突
- computed
- 计算属性,结果会被缓存,当依赖项改变时,会立刻重新计算,但不支持异步。
- 计算属性的结果依赖于
data里声明过的,或者事父组件传递过来props中的数据进行计算 - 如果
computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值。 computed中属性有一个get方法和一个set方法,当数据变化时,会调用set方法
- watch
- 不支持缓存,数据变化时,就会触发相应的操作。可以监听到
data和computed属性值的变化 - 支持异步监听,当一个属性发生变化时,就会执行相应的操作
- 函数有两个参数,分别是
immediate:组件加载立即触发回调函数;deep:深度监听,可以监听到数据内部的变化。比如数组中的对象发生变化。
- 不支持缓存,数据变化时,就会触发相应的操作。可以监听到
- methods
- 放置所有的方法
3.9 组件
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件
使用组件的好处:
- 降低整个系统的耦合度,在保持接口不变的情况下,可以替换不同的组件快速完成需求
- 方便调试,由于系统是通过组件组合起来的,可以快速定位到报错的组件
- 提高复用性和可维护性,更利于开发和系统的整体升级
vue2和vue3如何注册全局组件
- Vue2使用
Vue.component('组件名',组件对象) - Vue3使用
const app = createApp(App)
app.component('组件名',组件对象)
组件通信
每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成完整系统
- 父子间组件通信
- 子组件通过
props属性来接收父组件数据,然后父组件在子组件上注册监听事件,子组件通过emit触发事件来向父组件传递数据。 - 通过
ref属性给子组件设置一个名字。父组件通过$refs组件名来获取子组件,子组件通过$parent获取父组件,也可以实现父子间组件通信 - 使用
provide/inject,在父组件中通过provide提供变量,在子组件中通过inject来将变量注入到组件中。只要调用inject那么就可以使用provide中的数据
- 子组件通过
- 兄弟组件通信
- 使用
eventBus的方法。本质是创建一个空的Vue实例,作为消息传递的对象,通信的组件引入这个实例,通信的组件通过这个实例上监听和触发事件,来实现消息的传递 - 通过
$parents/$refs来获取到兄弟组件,可以实现通信
- 使用
- 任意组件
- 使用
eventBus,核心就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件
- 使用
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。
这个时候可以使用vuex ,vuex的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的
注意: 子组件可以直接改变父组件的数据吗?
子组件不可以直接修改父组件的数据,因为Vue的特点就是单向数据流,每次父组件发生更新时,子组件中的所有prop,都会变成最新的值,即父组件的值会流向子组件,但是反之不行,会导致数据流混乱。
组件缓存 keep-alive
组件的缓存可以在进行动态组件切换的时候对组件内部数据进行缓存,而不是走销毁流程
使用场景: 多表单切换,对表单内数据进行保存
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。 当组件在
<keep-alive>内被切换,它的activated和deactivated这两个生命周期钩子函数将会被对应执行 。
-
参数
- include(包含): 名称匹配的组件会被缓存-->include的值为组件的name。
- exclude(排除): 任何名称匹配的组件都不会被缓存。
- max - 数量 决定最多可以缓存多少组件。
-
使用
- 搭配
<component></component>使用 - 搭配路由使用 ( 需配置路由meta信息的
keepAlive属性 )
- 搭配
组件插槽slot
指令 v-slot:组件插槽, 缩写为#
组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。
作用:通过插槽可以让用户扩展组件, 更好的复用和灵活使用。比如:布局组件、表格项、下拉选择框、弹框等
- 分类
- 默认插槽
- 子组件用
<slot>标签来确定渲染的位置,标签里面可以放置DOM结构,当父组件使用的时候没有往插槽里
- 子组件用
- 具名插槽
- 子组件用
name属性来表示插槽的名称,如果不传是默认插槽。 - 父组件在使用默认插槽基本上加上
slot属性,值为子组件插槽name属性值
- 子组件用
- 作用域插槽
- 子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件
v-slot接受的对象上 - 父组件中在使用时通过
v-slot:(简写:#)获取子组件的信息,在内容中使用
- 子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件
- 默认插槽
总结:
v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default,可以省略default直接写v-slot - 缩写为
#时不能不写参数,写成#default - 可以通过解构获取
v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"
异步组件 好处:
- 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
- 核心就是把组件定义变成一个函数,依赖
import()语法,可以实现文件的分割加载。
3.10 Vuex
首先vuex的出现为了解决组件化开发过程中,组件通信的复杂和混乱的问题。
- 将在多个组件中需要共享的数据放到
state中 - 要获取或格式化数据需要使用
getters - 改变
state中的数据,可以使用mutation,但是只能有同步操作,在组件中调用的方式是this.$store.commit('xxxx') Action也是改变state中的数据,不过是提交的mutation,可以包含异步操作,在组件里调用方式是this.$store.dispatch('xxx');在actions里面使用的commit('调用mutation')Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vue3.0 使用了更为简单的pinia
vuex的核心属性
state唯一数据源,和vue实例的data遵守一样的规则mutation是更改store中状态的唯一方式,类似于事件,通过store.commit方法触发action类似于 mutation,不同在于action 提交的是 mutation,而不是直接变更状态,action 可以包含任意异步操作module由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)getters可以认为是store的计算属性,getter的返回值会根据他的依赖项缓存起来,如果依赖值改变那么就会重新计算。
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
- 一个应用vuex的实例(其中的一个模块)
const state ={
detailInfo: {}
}
const mutations = {
SET_DETAILINFO_STATE(state,value){
state.detailInfo = value
}
}
const actions = {
async getDetailInfo({commit}, skuId){
const res = await reqDetailInfo(skuId)
commit('SET_DETAILINFO_STATE',res)
}
}
const getters = {
categoryView(state) {
return state.detailInfo.categoryView || {};
},
spuSaleAttrList(state) {
return state.detailInfo.spuSaleAttrList || [];
},
skuInfo(state) {
return state.detailInfo.skuInfo || {};
},
skuImageList(state) {
return state.detailInfo.skuInfo.skuImageList || [];
},
}
vuex和localStorage
- 存储方式
vuex存储在内存中;localStorage则以文件的方式存储在本地,且只能存储字符串类型,复杂数据类型需要用JSON.stringify/parse方法处理
- 应用场景
vuex是一个专门为vue.js开发的状态管理模式,用于组件之间的传值,可以做到数据的响应式localStorage是本地存储,将数据存储到浏览器,一般用于跨页面传数据使用,数据没有响应式
- 永久性
- 刷新页面时,
vuex存储的值会丢失,localStorage不会
- 刷新页面时,
vuex在组件中使用
使用辅助函数更加方便
- mapState
- mapGetters
- mapMutations
- mapActions
- 组件中使用的实例
...
<floor v-for="floor in floorList" :key="floor.id" :floor='floor' />
...
methods: {
...mapActions('home', ['getFloorList']) // 由此获得方法getFloorList
},
computed:{
...mapState('home', ['floorList']) // 由此获得变量floorList
},
mounted(){
this.getFloorList()
}
3.11 生命周期
vue实例从创建到销毁的过程就是生命周期
也就是从开始创建、初始化数据、编译模板、挂载dom -> 渲染、更新视图 -> 渲染、准备销毁、销毁等一系列过程
vue的生命周期函数分为四个阶段 ,一共有八个钩子函数
创建阶段
beforeCreate(创建前):此时data、computed、watch、methods都没被设置,都无法访问created(创建后):vue实例创建完成,data、computed、watch、methods都已经配置完成,但此时dom元素还未挂载
渲染阶段
beforeMount(渲染前):在渲染(挂载)前调用,相关的render函数被首次调用,实力已经完成编译模板的配置,把data里面数据和模板生成html,注意 此时html还未挂载到页面上mounted(渲染)后:dom元素完成挂载到页面上,html渲染到html页面。
更新阶段
beforeUpdate(更新前):响应式数据更新前调用,此时响应式数据更新了,但是真实DOM还未渲染updated(更新后):此时DOM已经根据新的响应式数据更新了,可以执行依赖于DOM的操作
销毁阶段
beforeDestory(销毁前):vue实例销毁之前调用,此时vue实例还是可用的,this还是可以到vue实例destoryed(销毁后):vue实例彻底销毁
特殊的生命周期函数
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。
用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
created和mounted的区别
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
一般在哪个生命周期请求异步数据
我们可以在钩子函数created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性
$nextTick使用
一句话就可以把$nextTick讲清楚:
放在
$nextTick当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样拿到的数据就是最新的
Vue的响应式并不只是数据发生变化之后,DOM就立刻发生变化,而是按照一定的策略进行DOM更新。
DOM更新有两个选择:一个是本次事件循环的最后进行一次DOM更新,另一个是吧DOM更新放在下一轮的事件循环中。vue优先采用第一种,当环境不支持时才触发第二种。
注意 $nextTick采用的是微任务
3.12 路由router
SPA(单页面应用)极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。
前端路由提供了解决思路:VueRouter
- Vue Router 是官方的路由管理器。它和 Vue.js 的核心深度集成,路径和组件的映射关系, 让构建单页面应用变得易如反掌。
- router-link 实质上最终会渲染成a链接
- router-view 子级路由显示
- keep-alive 包裹组件缓存
$route和$router的区别
- $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
- $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
VueRouter的使用方式
- 使用
Vue.use()将VueRouter插入 - 创建路由规则
- 创建路由对象
- 将路由对象挂到
Vue实例上 - 设置路由挂载点
vue-router路由模式
vue-router就是将组件映射到路由, 然后渲染出来的。并实现了三种模式
Hash模式、History模式以及Abstract模式。默认Hash模式
-
hash模式
-
是指url 尾巴后的 # 号以及后面的字符。hash 虽然出现在url中,但不会被包括在http请求中,对后端完全没有影响,因此改变hash不会被重新加载页面。
-
原理:
- 基于浏览器的hashchange事件, 地址变化时,通过
window.loaction.hash获取地址上的hash值, 并通过构造Router类, 配置routes对象设置hash值与对应的组件内容
- 基于浏览器的hashchange事件, 地址变化时,通过
-
优点:
- hash值会出现在URL中, 但不会被包含HTTP请求中, 所以hash值改变不会重新加载页面;
- hash值改变会触发
hashchange事件,可以控制浏览器的前进后退; - 兼容性最佳
-
缺点:
- 浏览器的地址栏中带
#, 美观性差; - hash有体积限制,只能添加短字符串;
- 只能修改
#后面的部分, 有局限性
- 浏览器的地址栏中带
-
-
history模式
- 原理
- 基于HTML5新增的
pushState()和replaceState()两个api,以及浏览器的popstate事件,地址变化时,通过window.location.pathname找到对应的组件。并通过构造Router类,配置routes对象设置pathname值与对应的组件内容。
- 基于HTML5新增的
- 优点:
- 没有
#, 美观性好 - pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
- 浏览器的进后退能触发浏览器的popstate事件,获取
window.location.pathname来控制页面的变化
- 没有
- 缺点:
- URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器
- 原理
路由跳转方式
1、this.$router.push()跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面
2、this.$router.replace()跳转到指定的url,但是history中不会添加记录,点击回退到上上个页面
3、this.$router.go(n)向前或者后跳转n个页面,n可以是正数也可以是负数
编程式导航使用的方法
- 路由跳转:
this.$router.push() - 路由替换:
this.$router.replace() - 后退:
this.$router.back() - 前进:
this.$router.forward()
路由的传参方式
声明式导航传参
在router-link上的to属性传值
- 方式1
- 传值: /path?参数名=值
- 接收值:
$route.query.参数名
- 方式2
- /path/值/值 (需要路由对象提前配置 path:"/path/参数名")
- 接收值:
$route.params.参数名
编程式导航传参
this.$router.push() 可以不参数,根据传的值自动匹配是path还是name
因为使用path会自动忽略params ,所以会出现两种组合
- name + params 方式传参 A页面传参
this.$router.push({
name: 'xxx', // 跳转的路由
params: {id: id // 发送的参数 }
})
B页面接收参数
this.$route.params.id
- path + query 方式传参
A页面传参:
this.$router.push({
path: '/xxx', // 跳转的路由
query: { id: id // 发送的参数 }
})
B页面接收参数
this.$route.query.id
params和query的区别
- 用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是
this.$route.query.name和this.$route.params.name - url地址显示: query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
- 数据:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。
路由配置项
路由配置参数:
- path: 跳转路径
- component: 路径相对的组件
- name: 命名路由
- children: 子路由的配置参数(嵌套路由的配置参数)
- props: 路由解耦
- redirect: 重定向路由
路由重定向和404
- 路由重定向
- 匹配path后, 强制切换到另一个目标path上
- redirect是设置要重定向到哪个路由路径
- 网页默认打开, 匹配路由
/, 强制切换到/find上 redirect配置项,值是要强制切换的路由路径- 强制重定向后, 还会重新来数组里匹配一次路由规则
- 404 页面
- 如果路由hash值没有和数组里规则匹配
path:'*'匹配任意路径- 默认给一个404页面
Vue-router 导航守卫
- 全局守卫:
beforeEach全局前置守卫 进入路由之前 一般用于判断是否登录, 如果没有登录跳转到登录页beforeResolve全局解析守卫 注意:在 beforeRouteEnter 调用之后调用afterEach全局后置钩子 进入路由之后
- 路由独享守卫:
beforeEnter有三个参数to、from、next
- 组件内守卫:(这三个钩子函数都有三个参数to、from、next)
beforeRouteEnter进入组件前触发此时访问不到thisbeforeRouteUpdate当前地址改变并且改组件被复用时触发beforeRouteLeave离开组件被调用
3.13 插件
插件通常用来为 Vue 添加全局功能
大概功能有以下几种
- 添加全局方法或者属性。如:
vue-custom-element - 添加全局资源:指令/过滤器/过渡等。如
vue-touch - 添加全局公共组件 Vue.component()
- 添加全局公共指令 Vue.directive()
- 通过全局混入来添加一些组件选项。如
vue-router - 添加
Vue实例方法,通过把它们添加到Vue.prototype上实现。 - 一个库,提供自己的
API,同时提供上面提到的一个或多个功能。如vue-router
vue2/vue3怎么封装自定义插件并使用/Vue.use()
- vue2
- 在compoents.index.js里,定义一个函数或对象,在里面可以使用Vue.compoent全局注册组件,并暴露出去
- 在main.js里使用Vue.use( ),参数类型必须是 object 或 Function
- vue3
- 在compoents.index.ts里,定义一个函数或对象,在里面可以使用app.compoent全局注册组件,并暴露出去
- 在main.ts里使用app.use( ),参数类型必须是 object 或 Function
如果是 Function 那么这个函数就被当做 install 方法
如果是 object 则需要定义一个 install 方法
vue的性能优化
编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
打包优化
- 压缩代码
- 使用cdn加载第三方模块
- 多线程打包
- splitChunks抽离公共文件
- sourceMap优化
用户体验
- 骨架屏
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
遇到过的面试题
scoped作用与原理
- 作用
- 组件的
css作用域, 避免被父级组件的类名污染
- 组件的
- 原理
- 给元素添加一个自定义属性
v-data-xxx
- 给元素添加一个自定义属性
如何构建一个vue项目
- vue3的项目用
vite来构建也可以用create-vue - 引入需要的插件:
- 路由
vue-router - 状态管理
vuex/pinia - UI组件库
element/antd axios- 时间戳格式化工具
moment/day.js
- 路由
- 代码规范
ESlint - 代码格式化
Prettier
三、THREE的理解
对webgl的介绍
想要了解 three.js,我们先了解一下 WebGL 是什么。WebGL 是一个只能画点、线和三角形的非常底层的系统。简单来说就是使用 WebGL 提供的api可以在画布的三维坐标中,绘制点、线、三角形。我们所看见的立体图形都是通过三角形组合而来的。
想要用WebGL来做一些实用的东西通常需要大量的代码,这就出现了对 WebGL Api 进行封装后的库 three.js。它封装了诸如场景、灯光、阴影、材质、贴图、空间运算等一系列功能,让我们不需要再去关心WebGL的复杂原理。
我认为three.js分为的模块有
场景(Scene)
网格模型(Mesh)
纹理(Texture)
几何体(Geometry)
材质(Material)
灯光(Light)
摄影机(Camera)
渲染器(Renderer)
3D 对象(Object3D)
3.1 搭建3D场景
WebGL 是一种能令开发者在 <canvas> 标签内绘制 3D 图形的 JavaScript API 即可
并且这主要都是通过 GPU 完成的。
所谓的「3D 场景」实际上只是通过「透视」与「光影」,利用了人的视觉错觉所营造的一种假象
3.1.1 引入THREE.JS
需要使用的 build/three.min.js 文件只有大约 599 KB。将其以脚本引入的方式嵌入 HTML 文档
3.1.2 创建3d场景
场景(Scene)是一个用于装载 3D 物体,摄影机和灯光的「容器」。在 Three.js 中,我们通过实例化 Scene 构造函数的方式创建场景:
const scene = new THREE.Scene()
3.1.3 添加物体
定义一个物体时,需要依次指定一个物体的「形状」和「材质」,通过一种特殊的类「Mesh」,我将其称为「网格模型」,在 Three.js 中,Mesh 是表示三维物体的基础类,它将接收两个参数:
- geometry:定义物体的形状;
- material:定义物体的材质;
three.js提供的众多基础物体的api:
- 长方体: BoxGeometry
- 圆柱体: CylinderGeometry
- 球体: SphereGeometry
- 圆锥: ConeGeometry
- 矩形平面: PlaneGeometry
- 圆平面: CircleGeometry
材质api:
材质Material:
- 网格基础材质: MeshBasicMaterial (不够光照影响)
- 网格漫反射材质: MeshLambertMaterial
- 网格高光材质: MeshPhongMaterial
- 物理材质: MeshPhysicalMaterial / MeshStandardMaterial
- 点材质: PointsMaterial
- 线基础材质: LineBasicMaterial
- 精灵材质: SpriteMaterial (创建精灵模型对象,不需要几何体geometry参数 因为默认是一个矩形形状,默认长宽都是1)
//创建一个长方体几何对象Geometry
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,//0xff0000设置材质颜色为红色
});
const mesh = new THREE.Mesh(geometry, material) // 注意模型和材质一定要对上
scene.add(mesh) // 添加到3d场景
3.1.4 设置光源
光源api: 环境光: AmbientLight 点光源: PointLight 聚光灯光源: SpotLight 平行光: DirectionalLight
// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(80, 100, 50);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
directionalLight.target = mesh;
scene.add(directionalLight);
3.1.5 设置相机
Threejs如果想把三维场景Scene渲染到web网页上,还需要定义一个虚拟相机Camera,就像你生活中想获得一张照片,需要一台用来拍照的相机。
Threejs提供了正投影相机OrthographicCamera和透视投影相机PerspectiveCamera
// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera();
// 相机在Three.js三维坐标系中的位置
// 根据需要设置相机位置具体值
camera.position.set(200, 200, 200);
//相机观察目标指向Threejs 3D空间中某个位置
// camera.lookAt(0, 0, 0); //坐标原点
camera.lookAt(mesh.position);//指向mesh对应的位置
- 设置相机的四个参数 在创建相机时 指定四个参数
参数说明: fov — 摄像机视锥体垂直视野角度 aspect — 摄像机视锥体长宽比 near — 摄像机视锥体近端面 far — 摄像机视锥体远端面
通过这四个参数可以定义一个四锥体3D空间 视锥体 只有视锥体以内的物体,才会渲染出来
const camera = new THREE.PerspectiveCamera(30,width/height,1,3000) // 指定四个参数
3.1.6 渲染
const width = window.innerWidth
const height = window.innerHeight
//webgl渲染器
const render = new THREE.WebGLRenderer({
antialias:true // 开启优化锯齿
})
render.setPixelRatio(window.devicePixelRatio) // 防止输出模糊
render.setSize(width,height)
document.body.appendChild(render.domElement)
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, render.domElement);
// 渲染循环
function renderFn() {
render.render(scene, camera);
requestAnimationFrame(renderFn);
}
renderFn();
// 画布跟随窗口变化
window.onresize = function () {
render.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
其他设置
- 移动位置:position
- 改变尺寸:scale
- 旋转:rotation
mesh.position.set(1, 0 ,1)
mesh.rotation.set(Math.PI * 0.5, Math.PI * 0.3, Math.PI * 0.6)
3.2 详谈几何体 geometry
在 Three.js 的世界观中,几何体(Geometry)由顶点(vertices),线,面组成,被用来定义物体的「形状」和「大小」。
如果您想要在 3D 世界中「创造」某个物体,您需要首先确定这个物体「长什么样」?然后您就可以通过以下三种方式,创造出该物体:
- 使用 Three.js 提供的几何体对象;
- 使用 Three.js 提供的 API 创建自定义几何体(例如创建粒子动画);
- 通过 3D 软件导入模型;
几何体(Geometry),材质(Material)和网格对象(Mesh)三者的关系像是一个金字塔。
总结:「几何体」描述物体的形状和大小,「材质」描述物体的外观和质地,「网格对象」则将两者合并在一起,并提供使物体移动,旋转的能力。
3.2.1 BufferGeometry 对象
BufferGeometry 对象和「缓冲」相关,具体而言,该对象能够将几何体的相关数据(如顶点,UV,法线等)存入 GPU 的缓冲区(即显存),从而极大的提高 GPU 渲染性能与内存使用效率。
为了绘制几何体,我们需要设定几何体的「顶点(vertices)」,每个顶点都至少由 3 个数字组成,表示其在空间直角坐标系内的位置。
在 JavaScript 世界中,我们通常使用 Float32Array 这一类型数组。
关于 Float32Array
Float32Array 是 JavaScript 提供的一种类型数组,用来存储 32 位浮点数(即表示 3.4028235 * 10 ^ 38 ~ 1.17549435 * 10 ^ -38 之间的任意数)
3.2.2 自定义几何体
// 创建空的几何体对象
const geometry = new THREE.BufferGeometry()
// 通过类型化数组 Float32Array创建一组xyz坐标数据用来表示几何体的顶点坐标
const vertices = new Float32Array([
0,0,0 ,// 顶点1坐标
50,0,0, // 顶点2坐标
0,100,0, // 顶点3坐标
0,0,10, // 顶点4坐标
0,0,100, // 顶点5坐标
50,0,10 , // 顶点6坐标
])
// 通过threejs的属性缓冲区对象 BufferAttribute 表示threejs几何体顶点数据
// 创建属性缓冲区对象 3个为一组 表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices,3)
// 设置几何体顶点 .attributes.position
// 通过geometry.attributes.position 设置几何体顶点位置属性的值BufferAttribute
geometry.attributes.position = attribue
/*
点模型 Points 和网格模型Mesh一样 都是threejs的一种模型对象
网格模型Mesh有自己对应的网格材质 点模型Points有对应的点材质 PointsMaterial
*/
// 点渲染模型
const material = new THREE.PointsMaterial({
color:0xffff00,
size:10.0 // 点对象像素尺寸
})
// const points = new THREE.Points(geometry,material) // 渲染成点
const points = new THREE.Mesh(geometry,material) // 渲染成面
// 点模型添加到3d场景
scene.add(points)
3.2.3 THREE.js提供的几何体
- BoxGeometry:创建立方体;
- CapsuleGeometry:创建一个「胶囊」体;
- CircleGeometry:创建一个圆形或扇形;
- ConeGeometry:创建一个椎体,或部分椎体;
- CylinderGeometry:创建一个柱体,或部分柱体;
- DodecahedronGeometry:创建一个十二面体;
- ExtrudeGeometry:根据路径创建一个受挤压的多边体;
- LatheGeometry:创建一个类似花瓶的形状;
- OctahedronGeometry:创建一个八面体;
- PlaneGeometry:创建一个平面;
- SphereGeometry:创建一个球体;
- TetrahedronGeometry:创建一个四面体;
3.3 详谈材质 material
「材质(material)」是用来定义物体外观的属性。它包含如何渲染物体的信息,如颜色,光照,反射等等 「纹理(Texture)」是材质的一种属性,它可以被用来在物体表面添加图像,图案,颜色等。纹理可以用来模拟各种不同的表面特性,例如木头、金属、石头等等。
「材质」和「纹理」的概念,可以想象一件木质家具。该家具的「材质」是木头,但它的外观可能会因为在木表面上应用不同的纹理而产生不同的效果。例如,一个家具制造商可能会在木表面上应用一层光滑的漆,使其看起来更加光滑;另一个家具制造商则可能会在木表面上应用一个粗糙的纹理,使其看起来更加天然。
在 Three.js 中,材质被用于为几何体的每个可见像素着色,这一过程的算法实现被称为「着色器(shader)」(一种在 GPU 上运行的程序,它用于计算几何体的每个像素的颜色和外观,可以对材质进行定制化的着色和光照计算,以及其他各种效果)
- 属性: map 用于添加「颜色纹理(Color Texture)」(也称为反照率纹理(Albedo Texture)),给物体表面添加图案,细节或者其他颜色纹理
- 属性: color
注意: 需要通过实例化 THREE.Color 对象才能成功配置 或者配置对象里直接设置颜色
material.color = new THREE.Color("red") - 属性: opacity opacity 属性用来控制物体的透明度,它的取值范围在 0 到 1 之间。若要想 opacity 值生效,需要同时设置 transparent 属性为true
- 属性: side
material.side = THREE.DoubleSide设置成功后,两面都可渲染
3.3.1 基础材质(MeshBasicMaterial)
「基础材质(MeshBasicMaterial)」是 Three.js 中最基础,最简单的一种材质类型,没有高级的渲染效果,只能实现基本的平面渲染。它不会产生阴影、反射或其他高级效果,只能显示一个单一的颜色或纹理。
3.3.2 漫反射材质(MeshLambertMaterial)
响应光线的材质(🚨 这意味着当您使用该材质时,如果场景中没有光源,您将无法看到物体!),一种用于非光亮表面的材料,因为它不存在镜面高光。(💡 需要光照!)
3.3.3 高光材质(MeshPhongMaterial)
「高光材质(MeshPhongMaterial)」和上面的漫反射材质十分相似,但却支持所缺失的镜面反光效果。基于光照模型进行渲染的高光材质结合了漫反射、镜面反射和环境光三种光照效果,可以实现更真实的光照表现。但这一方面也意味着更大的性能开销。
有两条多的属性
- shininess 用于控制物体表面材质的反光效果,该值越高,表面越亮;
- specular 用于控制镜面反射的颜色;
material.shininess = 100
material.specular = new THREE.Color("green")