一、从输入URL到页面加载完成的过程
1、浏览器的地址栏输入URL并按下回车
2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期
3、DNS解析URL对应的IP
4、根据IP建立TCP连接(三次握手)
5、HTTP发起请求
6、服务器处理请求,浏览器接收HTTP响应
7、渲染页面,构建DOM树
8、关闭TCP连接(四次挥手)
技术解读:
(1)**解析URL:**首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
(2)**缓存判断:**浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
(3)DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
(4)获取MAC地址(选说) 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
(5)TCP三次握手: 确认客户端与服务器的接收与发送能力,下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
(6)**HTTPS握手(选说):**如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
(7)发送HTTP请求:服务器处理请求,返回HTTP报文(响应)(文件)
(8)页面渲染: 浏览器首先会根据 html 文件(响应) 建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
(9)**TCP四次挥手:**最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
二、请介绍一下Js中的单线程和事件循环
1、Js是单线程,但是浏览器是多线程
2、Js中采用了事件循环(Event Loop)来执行异步任务。
3、所以,事件循环是一种异步编程模型,事件循环会不断地从任务队列(Task Queue)中取出待处理的任务并执行,直到任务队列为空为止。任务可以分为两类:宏任务(Macro Task)和微任务(Micro Task)。
4、微任务会优先于宏任务执行
技术详解:
Js是单线程,但是浏览器是多线程。单线程是为了避免UI操作混乱,所有和UI操作相关的开发语言都应该是单线程。
JavaScript 是一门单线程的编程语言,它采用了事件循环(Event Loop)来执行异步任务。
基本概念
-
事件循环是一种异步编程模型,事件循环会不断地从任务队列(Task Queue)中取出待处理的任务并执行,直到任务队列为空为止。任务可以分为两类:宏任务(Macro Task)和微任务(Micro Task)。
-
宏任务包括了整体代码块、setTimeout、setInterval、setImmediate、I/O 操作等。当宏任务被添加到任务队列时,事件循环会检查当前是否有正在执行的宏任务,如果有,则继续执行当前宏任务;如果没有,则取出任务队列中的第一个宏任务,并执行它。
-
微任务包括了 Promise.then、MutationObserver 等。当微任务被添加到任务队列时,它们会优先于下一个宏任务执行。也就是说,当宏任务执行完毕后,如果存在微任务,则会立即执行所有的微任务,直到微任务队列为空为止。然后再执行下一个宏任务。
事件循环流程
JavaScript 的事件循环流程如下:
- 执行全局主线程代码,遇到宏任务时,将其放入宏任务队列中。
- 遇到微任务时,将其放入微任务队列中。
- 主线程代码执行完毕后,先执行所有微任务,直到微任务队列为空为止。
- 取出宏任务队列中的第一个任务,执行它,并重复步骤 2 和步骤 3,直到宏任务队列为空为止。
示例代码
以下是一个示例代码,展示了事件循环的执行过程:
复制代码console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
});
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
// 输出结果:
// 1
// 6
// 4
// 2
// 3
// 5
- 首先,执行全局主线程代码,输出
1和6。然后,遇到微任务Promise.resolve().then(() => { console.log('4'); ... }),将其放入微任务队列中。 - 接着,执行宏任务
setTimeout(() => { console.log('2'); ... }),将其放入宏任务队列中,同时遇到微任务Promise.resolve().then(() => { console.log('3'); }),将其放入微任务队列中。此时,微任务队列中有一个任务,宏任务队列中有一个任务。因为存在微任务,所以执行所有微任务,即输出4和3。然后继续执行宏任务队列中的第一个任务,输出2,并将另一个宏任务setTimeout(() => { console.log('5'); }, 0)放入宏任务队列中。 - 最后,因为微任务队列为空,事件循环结束。输出结果为
1, 6, 4, 2, 3, 5。
三、Js中实现继承的几种方式
1、原型链继承:将父类的实例作为子类的原型,通过 prototype 进行继承
2、构造继承:将父类的实例属性复制给子类,通过 call 进行继承
3、实例继承:为父类实例添加新特性,作为子类实例返回
4、拷贝继承:将父类实例通过循环拷贝给子类
5、组合继承:就是 原型链继承 和 构造继承,一起使用
6、寄生组合继承:通过寄生方式,砍掉父类的实例属性,避免了 组合继承中,在调用两次父类的构造时,初始化两次实例方法/属性 的缺点
技术详解
在JavaScript中,实现继承的方式有以下几种:
原型链继承:通过将子类的原型指向父类的实例来实现继承。这种方式存在的问题是,所有子类实例共享同一个父类实例,容易造成属性共享和修改父类属性的问题。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
var child = new Child();
child.sayName(); // 'parent'
借用构造函数继承:通过在子类构造函数中调用父类构造函数来实现继承。这种方式可以避免属性共享的问题,但是无法继承父类原型上的方法。
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'child';
}
var child = new Child();
console.log(child.name); // 'parent'
组合继承:将原型链继承和借用构造函数继承结合起来,既可以继承父类原型上的方法,又可以避免属性共享的问题。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'
原型式继承:通过创建一个临时构造函数并将其原型设置为父类实例来实现继承。这种方式类似于原型链继承,但是可以避免创建不必要的父类实例。
function createObject(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var parent = {
name: 'parent',
sayName: function() {
console.log(this.name);
}
};
var child = createObject(parent);
child.name = 'child';
child.sayName(); // 'child'
寄生式继承:在原型式继承的基础上,增强新对象,返回构造函数的方式来实现继承。
function createObject(obj) {
function F() {}
F.prototype = obj;
return new F();
}
function createChild(parent) {
var child = createObject(parent);
child.name = 'child';
child.sayName = function() {
console.log(this.name);
};
return child;
}
var parent = {
name: 'parent',
sayName: function() {
console.log(this.name);
}
};
var child = createChild(parent);
child.sayName(); // 'child'
寄生组合式继承:在组合继承的基础上,使用Object.create()方法来创建父类原型的副本,避免了调用父类构造函数时创建不必要的实例。
function inherit(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);
this.type = 'child';
}
inherit(Child, Parent);
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'
四、介绍一下 Js 中bind(),call()和apply()的区别
JavaScript 中的 bind()、call() 和 apply() 方法都可以用来改变函数内部的 this 指向。
它们有一些重要的区别:
- 三种方法最大的区别在于参数传入方式不同:bind() 方法接受一系列参数列表,而 call() 和 apply() 方法则分别接受一组参数和一个参数列表。具体而言,bind() 将参数作为一个个单独的值传入,而 call() 和 apply() 都允许传递一个数组作为参数。
- 执行时间不同:bind() 绑定后返回一个新函数,并不会立即执行,需要调用该函数才会执行;而 call() 和 apply() 则会立即执行函数。
- 返回值不同:bind() 方法返回一个绑定后的新函数,而 call() 和 apply() 则直接执行原始函数并返回执行结果。
它们的作用分别如下:
- bind() 方法:bind() 可以指定函数内部的 this 指向,并将其绑定到一个新函数上进行返回。该函数并不会立即执行,而是等待调用。bind() 也可以用来实现柯里化(currying)
- call() 方法:call() 可以在指定的 this 值和若干个参数(参数的列表)的前提下调用某个函数或方法。注意,call() 方法需要将参数逐个传递进去,而不能像 apply() 方法一样将所有参数打包成一个数组。
- apply() 方法:apply() 和 call() 的作用非常类似,都是改变函数内部的 this 指向。区别在于,apply() 方法需要将参数打包成一个数组传递进去,而 call() 则是将参数逐个传递。
五、介绍一下 Js 中的闭包及其使用场景
- 官方说法:闭包就是指有权访问另一个函数作用域中的变量的函数。
- MDN说法:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
- 深度回答:浏览器在加载页面会把代码放在栈内存( ECStack )中执行,函数进栈执行会产生一个私有上下文( EC ),此上下文能保护里面的使用变量( AO )不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制。
技术详解
闭包是 JavaScript 中一种常见的编程概念,它允许函数访问其定义时的作用域外部变量。在实际开发中,我们会经常使用到闭包来封装一些变量或函数,以保证其不被外界直接访问和修改。本文将对闭包的概念、特点以及使用场景进行详细介绍。
概念
闭包指的是函数与其创建时所处的词法环境(lexical environment)的组合。简单地说,闭包就是能够读取其他函数内部变量的函数。这个特性是由 JavaScript 的词法作用域实现的,因为函数在创建时会保存其所处的词法环境。
闭包具有以下几个特点:
-
可以访问父级作用域中的变量:在函数内部通过闭包可以访问并修改父级作用域的变量,而且这些变量在函数执行完毕后依然存在。
-
可以将变量私有化:使用闭包可以将某些变量私有化,避免全局变量污染和变量冲突问题,同时也增加了代码的可维护性。
-
可以实现模块化:使用闭包可以将一些相关方法或属性封装在一个作用域内,形成一个独立的模块,从而提高代码的复用性和可读性。
使用场景
闭包在 JavaScript 中有广泛的应用场景,以下是几种常见的使用场景:
私有变量和方法:使用闭包可以将某些变量或方法私有化,在外界无法直接访问或修改,从而保证数据的安全性。
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
在上述代码中,createCounter 函数返回了一个匿名函数,该函数能够访问并修改父级作用域中的 count 变量,而且只能通过 counter 变量调用该匿名函数。
延迟执行:使用闭包可以实现定时器的延迟执行,从而避免了异步回调带来的代码逻辑混乱问题。
function delay(func, time) {
return function() {
setTimeout(func, time);
};
}
const sayHello = function() {
console.log('Hello');
};
const delayedSayHello = delay(sayHello, 1000);
delayedSayHello();
实现模块化:使用闭包可以实现类似于模块化的功能,将一些相关方法或属性封装在一个作用域内,从而提高代码的复用性和可读性。
const myModule = (function() {
let privateVariable = 0;
function privateMethod() {
// ...
}
return {
publicMethod: function() {
privateVariable++;
privateMethod();
console.log(privateVariable);
}
};
})();
myModule.publicMethod(); // 1
myModule.publicMethod(); // 2
六、介绍一下Vue2与Vue3中的双向数据绑定
双向数据绑定就是:数据劫持 + 发布订阅模式(观察者模式)。
Vue2中在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter。并劫持各个属性 getter 和 setter,在数据变化时发布消息给订阅者,触发相应的监听回调,而这之间存在几个问题
- 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
- 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
- Object.defineProperty 无法监听到数组元素的变化,只能通过劫持重写数方法
- 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
- 不支持 Map、Set 等数据结构
Vue3中使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
相对于Object.defineProperty(),有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- 它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。
- Proxy 可以监听数组的变化。
技术详解
在 JavaScript 中,defineProperty 和 Proxy 都是对象的属性访问控制方法,它们有着一些明显的区别。
defineProperty 是一个 ES5 引入的 API,用于在已有对象上定义新属性或者修改现有属性,并且可以精细地控制这个属性的 getter、setter、可枚举性、可配置性和可写性等特性。通过 defineProperty,我们可以对一个对象的属性进行实时监测,从而实现数据绑定和响应式编程等高级功能。
但是,defineProperty 也存在一些限制。它只能监听对象的已有属性,而不能监听整个对象的变化。同时,由于该 API 的使用方式比较繁琐,使得其难以扩展和封装。
相比之下,Proxy 是 ES6 中引入的一个新特性,可以代理 JavaScript 对象的访问,提供了更加灵活的属性访问控制方式。通过在目标对象外层建立一个代理层,Proxy 可以拦截对象的访问、修改、删除、枚举等操作,从而实现更加细粒度的控制。
与 defineProperty 不同的是,Proxy 可以监听整个对象的变化,而不仅仅是某个属性的变化。另外,Proxy 的使用方式相对简单,可以轻松地被扩展和封装,使得其更加适用于面向对象编程和函数式编程等多种编程范式。
总的来说,defineProperty 和 Proxy 都是非常有价值的对象属性访问控制方法。如果需要对对象的某个属性进行深度监测,或者需要更加灵活的属性访问控制方式,那么建议使用 Proxy;如果只需要对对象的某个属性进行简单的监听,那么可以选择使用 defineProperty。
七、介绍一下Vue中template模版的编译原理
同问题:说说从 template 到 render 处理过程(compiler的工作原理)
Vue的模板(template)编译原理可以分为以下几个部分:
-
解析(parse):将模板字符串解析成 AST(抽象语法树)。
-
静态分析(static analysis):对 AST 进行静态分析,标记出其中的静态节点(Static Node)。
-
优化(optimize):遍历 AST,对静态节点进行优化,去掉不必要的操作。
-
代码生成(code generation):将 AST 转换成渲染函数(render function)的可执行代码。
-
最终的渲染:将生成的渲染函数运用到数据上,最终生成视图。
技术详解
具体来说,编译器(compiler)会将模板(template)中的所有内容解析成基本语法块,如元素节点、属性节点、文本节点等,然后将这些语法块逐个解析,并根据解析的结果生成一颗以根节点为根的抽象语法树(AST)。此外,编译器还会在AST中添加静态标记(static flag),用于标记那些不随数据改变而需要缓存的节点,从而优化渲染性能。
接下来,编译器会对AST进行一系列的优化处理,包括**静态节点提升(static node optimization)、标记动态节点(mark dynamic nodes)、移除注释(remove comments)**等。这些优化处理都是为了优化渲染性能,减少不必要的重绘和重排操作。
最后,编译器将优化后的AST转换为可执行的渲染函数(render function),并将其挂载到Vue实例的$options.render属性上。当数据更新时,Vue会重新调用渲染函数(render function)来生成新的虚拟节点(virtual node),然后通过对比新旧虚拟节点来判断是否需要更新视图。
综上所述,Vue的模板编译原理主要是将模板(template)转换为抽象语法树(AST),然后对AST进行优化处理,最终生成可执行的渲染函数(render function)。通过这种方式,大大提高了Vue渲染性能和开发效率。
八、Vue2中的diff流程
Vue2中的虚拟DOM diff算法,其核心是采用双端比较(Two-Ended Algorithm)。
具体来说,Vue2中diff算法的步骤如下:
- 首先比较新旧虚拟DOM树的根节点,如果它们是不同类型的节点,则直接替换整个节点树;
- 如果根节点相同,则比较它们的子节点,这个过程称之为“Diff Children”;
- 在“Diff Children”过程中,使用双端比较算法,即同时从新虚拟DOM树和旧虚拟DOM树的两端开始向中间遍历,找到相应的节点进行比较,找到更新的节点后就停止比较;
- 如果新旧虚拟DOM树的子节点数组长度不同,则根据差异进行添加或删除节点;
- 对于相同位置的节点,判断它们是否相同,如果不同则进行更新;
- 最后返回新的虚拟DOM树。
总的来说,Vue2中的diff算法采用了双端比较的方式,能够有效地减少不必要的比较和操作,提高了页面的渲染效率。
九、Vue3中的diff流程
在Vue3中,Diff流程主要分为两个阶段:标记阶段和应用阶段。其中标记阶段是用来比较新老VNode树的差异,并记录下来需要进行的具体操作;应用阶段则是将这些具体操作应用在真实DOM上,完成页面的更新。
具体来说,Vue3中的diff算法步骤如下:
标记阶段
**
- 遍历新老VNode树:对于新老VNode树进行深度优先遍历,将每个VNode节点进行比较。
- key的比较:如果新老VNode的key不同,则认为这两个节点是不同的,直接跳过。
- 类型的比较:如果新老VNode的类型不同,则认为这两个节点是不同的,直接跳过。
- 跳过相同节点:如果当前新老VNode节点是相同的,则继续递归比较其子节点。
- 标记复用节点:如果找到一个新的VNode节点可以与某个旧的VNode节点复用,则将该旧节点从节点池中删除并记录复用节点。
- 创建新节点:如果新的VNode节点不能复用任何一个旧的VNode节点,则创建一个新的VNode节点并记录它。
- 记录移动节点:如果新的VNode节点不能复用任何一个旧的VNode节点,并且每个旧节点都不等于新节点,则记录需要移动的节点。
**
应用阶段
- 执行patchFn函数:在应用阶段中,Vue3将根据标记阶段中生成的具体操作(如创建、更新、删除节点等)执行patchFn函数,实现对真实DOM的修改和更新。
- 更新子节点:如果当前节点有子节点,则递归调用diff算法进行子节点的比较和更新。
- 删除节点:如果当前VNode节点被标记为需要删除,则将该节点从父节点中删除,并销毁其对应的真实DOM节点。
- 创建节点:如果当前VNode节点是新创建的,则根据其类型、属性和子节点创建对应的真实DOM节点,并插入到父节点中。
- 移动节点:如果当前VNode节点需要移动到其他位置,则将其从原先的位置移动到目标位置。这通常是通过插入或删除元素来实现的。
综上所述,Vue3中的diff算法主要通过对新老VNode树的key、类型以及内容进行比较,实现对节点的创建、更新、移动和删除等一系列具体操作,从而优化页面渲染的性能和效率。
十、Vue中的组件间通信有哪些
Vue 中组件间传值的方法有以下几种:
-
props:父组件通过 props 属性向子组件传递数据。子组件接收该数据后,即可在其模板中直接使用。
-
**emit() 方法触发一个自定义事件,并把需要传递的数据作为参数传入。父组件监听该自定义事件,并在回调中处理数据。通过事件可以实现任意级别的组件间通信。
-
children:父组件可以通过 parent 找到其父组件。这种方式属于直接引用和修改组件对象,需要注意和谨慎使用。
-
provide 和 inject:祖先组件通过 provide 属性向子孙组件传递数据,子孙组件通过 inject 属性来获取这些数据。provide 和 inject 绑定并非响应式的,但是可以将一个观察者实例注入到 provide 中,使得组件在 inject 期间发生变化时获得通知。
-
Vuex 状态管理(Vue3中的pinia):Vuex 是一个专门为 Vue.js 应用程序开发的状态管理库,提供了一种集中式存储管理应用程序中的所有组件的状态。组件通过调用 mutation 方法来改变状态,其他组件通过监听 state 属性来获取最新的状态。
-
兄弟组件间的传值:Vue2中使用eventBus中央事件总线 , Vue3中使用的 mitt库
这些组件间传值的方法各有优缺点,可以根据实际场景来选择使用。需要注意的是,不同的方式可能涉及到数据的响应性、可维护性和复杂度等问题,开发者应该根据实际情况进行权衡和取舍。
十一、Vue-router中如何实现懒加载
- 在路由配置文件中使用动态导入import()语句,并用箭头函数返回实现路由懒加载。
- 配合的是babel中
@babel/plugin-syntax-dynamic-import插件:用于转换动态导入(Dynamic import)语法。此插件已在 Vue-cli中提供的 Babel 插件:@vue/cli-plugin-babel里面内置。 - 路由懒加载
十二、Vue3相对于Vue2进行了哪些优化
- 更灵活的响应式系统:Vue 2.x 中响应式系统的核心是 Object.defineProperty,劫持整个对象,然后进行深度遍历所有属性,给每个属性添加
getter和setter,实现响应式。Vue 3.x 中使用 Proxy对象重写响应式系统。 - 更快的渲染速度:Vue3 的编译器生成的渲染函数比 Vue2 生成的更高效。
- 编译阶段:Vue 2.x 通过标记静态节点,优化 diff 的过程。Vue 3.x中标记和提升所有的静态节点,diff的时候只需要对比动态节点内容。
- 更小的体积:Vue3 将源码拆分为多个独立的模块,这样就可以按需导入所需的模块,从而减小了整个库的体积。
- 更好的 TypeScript 支持:Vue3 对 TypeScript 的支持更加友好,内部使用了更先进的 TypeScript 特性,并为其提供了更好的声明文件。
- 更好的组件系统:比如,Vue3中引入了一个新的
Fragment组件,它可以替代原来的template标签作为根节点 - 新增了setup组合式API
技术详解
Vue3 相对于 Vue2 进行了许多优化和更新,主要包括以下几个方面。
1. 更快的渲染速度
Vue3 的编译器生成的渲染函数比 Vue2 生成的更高效。Vue3 在编译模板时使用了静态分析技术,可以在编译期间确定节点是否是静态的,并将其缓存以便后续操作。这种优化可以减少运行时的内存分配和垃圾回收,从而提高渲染性能。此外,Vue3 还引入了基于 Proxy 的响应式系统,可以避免不必要的观察者反应,并消除了 getter 和 setter 方法的开销。
2. 更小的体积
Vue3.js 将源码拆分为多个独立的模块,这样就可以按需导入所需的模块,从而减小了整个库的体积。此外,Vue3.js 在编译器中引入了静态分析技术,消除了不必要的运行时代码,从而使得打包后的应用程序更小。
3. 更好的 TypeScript 支持
Vue3.js 对 TypeScript 的支持更加友好,内部使用了更先进的 TypeScript 特性,并为其提供了更好的声明文件。这样,开发者可以更加方便地使用 TypeScript 来开发 Vue 应用程序,从而提高了开发效率和代码质量。
4. 更灵活的响应式系统
Vue3.js 中的响应式系统相对于 Vue2.js 更加灵活。Vue3.js 引入了一个新的 reactive 函数,它可以接收一个普通对象并返回一个响应式的对象。此外,Vue3.js 还提供了一些新的 API,如 ref 和 toRefs,使得开发者可以更加方便地管理组件中的数据和状态,并且更加易于调试和测试。
5. 更好的组件系统
Vue3.js 中的组件系统也得到了改进。Vue3.js 引入了一个新的 Fragment 组件,它可以替代原来的 template 标签作为根节点,从而更加灵活。同时,Vue3.js 中的 Teleport 组件可以让组件在 DOM 中任意位置渲染,从而更加灵活和强大。
总之,Vue3.js 相对于 Vue2.js 进行了许多优化更新,提高了应用程序的性能和开发效率。开发者应该尽快学习和掌握 Vue3.js 的新特性,以便更好地开发出高性能、高质量的应用程序。
十三、介绍一下React Fiber
React Fiber 是 React v16 中引入的一种新的协调算法,它被设计用来解决 React 在处理大型应用时可能出现的性能问题。本文将详细介绍 React Fiber 的背景、原理和优势。
背景
在 React v15 以及之前版本中,React 应用是通过递归遍历组件树实现的。当组件状态发生变化时,React 会重新计算并渲染整个组件树,然后将变化的部分更新到 DOM 上。这种算法虽然简单易懂,但是也存在一些缺点:
- 当组件树非常庞大时,递归遍历整个组件树的开销很大,导致应用性能下降。
- 如果某个组件在更新过程中发生了阻塞,那么整个组件树的更新也会被阻塞,用户体验不佳。
为了解决这些问题,React 团队在 v16 中引入了一种新的协调算法——React Fiber。
设计原理
-
React Fiber 的核心思想是将组件树的遍历变成了可中断的异步任务。具体来说,React Fiber 会将整个组件树拆分成多个小的任务单元(Fiber),并按照优先级顺序依次执行这些任务单元。每当执行完一个任务单元时,React Fiber 就会检查当前是否有更高优先级的任务需要执行。如果有,则立即暂停当前任务,并开始执行更高优先级的任务,直到完成后再回来继续执行原来的任务。这样一来,React Fiber 可以让应用更加灵活地响应用户交互和其他事件。
-
在 React Fiber 中,每个任务单元都对应一个 Fiber 对象,该对象包含了当前组件的状态和相关信息。Fiber 对象还存储了组件所在的位置、子组件信息、副作用等信息。当某个任务单元执行时,React Fiber 会利用 Fiber 对象的信息进行 DOM 更新、事件处理等操作。
-
React Fiber 还提供了一系列 API,用于控制任务单元的执行顺序、暂停和恢复任务等操作。这些 API 包括
requestIdleCallback、cancelIdleCallback、setTimeout、setImmediate等。 -
为了支持可中断的异步任务,React Fiber 引入了一种新的数据结构——双缓存技术。具体来说,React Fiber 维护了两个 fiber 树:current fiber 树和 work in progress fiber 树。current fiber 树是当前渲染结果对应的 fiber 树,work in progress fiber 树则是正在进行的更新操作对应的 fiber 树。当更新操作完成后,React Fiber 会将 work in progress fiber 树替换为 current fiber 树,从而实现视图更新。
-
最后,React Fiber 的 diff 算法也有所改进。传统的 diff 算法是基于递归遍历的,而 React Fiber 则采用了迭代遍历算法。具体来说,React Fiber 会将更新操作转换成一系列指令(effect),然后将这些指令保存到 effect list 中。最后,React Fiber 会按照顺序执行 effect list 中的指令,从而完成页面渲染。
优势
相比于传统的递归遍历算法,React Fiber 具有以下优势:
- 更快的渲染速度:由于 React Fiber 可以将任务切分成小的单位执行,因此可以更加高效地渲染组件。
- 更好的用户体验:由于 React Fiber 可以随时中断和恢复任务,因此可以更加灵活地响应用户交互和其他事件。
- 更加可扩展:React Fiber 提供了一系列 API,使得其可以更加方便地集成进其他系统中。
总结来说,React Fiber 是一种全新的协调算法,它通过分离任务单元、采用双缓存技术、支持可中断的异步任务等方式,解决了 React 在处理大型应用时可能出现的性能问题。
十四、React中常用的高阶组件有哪些
React中常用的高阶组件HOC有很多,以下是一些常见的HOC:
-
withRouter:将路由信息注入到组件中,使它们能够访问到路由对象(如location、history和match等)。 -
connect:将React组件与Redux Store连接起来,并将State和Dispatch作为Props传递给组件。这使得组件能够直接从Store中读取和操作数据。 -
memo:对于纯函数组件,使用memo可以缓存组件输出,以提高性能。 -
withStyles:用于添加CSS样式到组件中。 -
redux-thunk:使Action Creator返回一个函数而不是一个Action对象,从而可以执行异步操作并dispatch新的Action。 -
recompose:提供了一组高阶功能,用于增强函数式React组件。例如,compose函数可以将多个HOC组合在一起。 -
react-redux:提供了一组基于Redux Store的React组件,并简化了React与Redux之间的集成。
总之,React中的HOC提供了许多灵活且有用的功能,可以帮助我们更好地组织和重用代码。开发者可以使用这些HOC来封装通用的逻辑和功能,并使组件更加可复用。
十五、React Hook为什么不能放到条件语句中
React Hook 不能放到条件语句中的原因是 React 需要使用 Hook 的规则是必须确保每次渲染时,Hook 调用的顺序都是一致的。也就是说,在一个组件内部,每一次渲染时,Hook 的调用顺序必须是相同的。
如果将 Hook 放到条件语句中,当条件发生变化时,Hook 的调用顺序就可能被打乱,从而导致组件状态不一致、出现错误等问题。因此,React 在运行时会对 Hook 的调用顺序进行验证,来确保 Hook 的使用符合规范。
举个例子,如果我们在组件的函数体内,使用了 useState 这个 Hook 来管理组件的状态,那么这个 Hook 的调用顺序必须始终保持不变。如果我们将这个 Hook 放到条件语句中,那么当条件变化时,这个 Hook 的调用顺序就可能被改变,从而违反了 React Hook 的使用规则。
总之,React Hook 不能放到条件语句中是为了确保 Hook 的使用规范和组件状态的正确性。如果需要在条件语句中使用 Hook,可以使用类组件或自定义 Hook 等其他方式来解决。
十六、React有哪些常用的hooks
React Hooks 是在 React 16.8.0 版本中首次引入的。
React v16.8 中引入了 Hooks ,是 React 一种全新的状态管理方式,它提供了一些可以让函数组件拥有类组件同样功能的 API。
其中最常用的 Hook API 包括:
useState:该 Hook 用于在函数组件中添加一个状态管理器。通过useState,可以创建一个状态变量及其更新函数,并在组件内使用该变量来保存和更新组件的状态。useEffect:该 Hook 用于在组件渲染完成后执行一些副作用操作(例如订阅数据、更新 DOM 等)。通过useEffect,可以在组件加载、更新和卸载时设置和清理副作用操作,并且可以在副作用操作之间共享状态。useContext:该 Hook 用于在组件之间共享一些全局的状态或函数,以避免通过多层嵌套的 Props 传递进行数据传输。通过useContext,可以让组件在全局状态或函数的上下文中运行,并让它们能够方便地读取或更新全局状态或函数。useReducer:该 Hook 用于在组件中使用一种“状态容器”模式,以避免通过多层 Props 传递或 Context 共享进行状态管理。通过useReducer,可以创建一个状态容器及其更新函数,并在组件内使用该容器来保存和更新组件的状态。useMemo:该 Hook 用于在组件渲染完成后缓存一些计算结果,以避免因为重复计算导致的性能问题。通过useMemo,可以创建一个缓存变量,并在组件内使用该变量来保存计算结果并缓存。useCallback:该 Hook 用于在组件渲染完成后,将一些函数进行缓存,以避免因函数重复创建导致的性能问题。通过useCallback,可以创建一个缓存函数,并在组件内使用该函数来代替重复创建的函数。useRef:该 Hook 用于在组件渲染完成后创建一个引用,以便在组件多次渲染时能够保留上一次渲染中的值。通过useRef,可以创建一个引用变量,并在组件内使用该变量来保存一些持久化的数据。useImperativeHandle:该 Hook 用于在组件中实现一些自定义的 Ref 对象,并且要求将一些组件内部的方法或状态暴露给父组件使用。通过useImperativeHandle,可以创建一个自定义的 Ref 对象,并在组件内指定一些公开的方法或属性。useLayoutEffect:该 Hook 与useEffect类似,但它会在浏览器渲染更新之前同步执行副作用操作,以确保 React 组件与浏览器同步更新。通常情况下,应该使用useEffect,但在需要直接操作 DOM 元素或进行测量布局界面时,应当使用useLayoutEffect。useDebugValue:该 Hook 可以帮助开发者在调试工具中显示额外的信息,以便更好地理解 Hook 的使用和行为。通常情况下,这个 Hook 只用于调试过程中,而不是实际的应用程序代码中。
十七、介绍下React中的useEffect
- 在 React 中,
useEffect是一个用于处理副作用的 Hook。 - 副作用是指在组件生命周期中的某些特定时刻需要执行的操作,例如数据获取、订阅事件、手动操作 DOM 等。
useEffect的作用就是在组件渲染完成后执行这些副作用操作。
useEffect 接收两个参数:一个副作用函数和一个依赖数组。
副作用函数是一个函数,它会在组件渲染之后执行。它可以包含任何副作用操作,如订阅、网络请求、DOM 操作等。示例代码如下:
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 执行副作用操作
console.log('Component rendered');
// 清理副作用
return () => {
console.log('Component unmounted');
};
}, []);
return <div>My Component</div>;
}
在上述示例中,我们定义了一个 MyComponent 组件,并在其中使用了 useEffect。在副作用函数中,我们打印了一条消息来表示组件已经渲染完成。此外,我们还提供了一个返回函数,用于清理副作用。该函数将在组件卸载之前执行,以便做一些清理工作,如取消订阅或清除定时器。
第二个参数是一个依赖数组,用于指定副作用函数的依赖项。当依赖项发生变化时,副作用函数将重新执行。如果依赖数组为空,副作用函数只会在组件首次渲染时执行,并在组件卸载时执行清理操作。示例代码如下:
import { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count changed: ${count}`);
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
</div>
);
}
在上述示例中,我们定义了一个 MyComponent 组件,并使用 useState 来保存一个计数器 count。在 useEffect 中,我们传入了 count 作为依赖项,这意味着只有当 count 发生变化时,副作用函数才会被触发。每次点击增加按钮时,count 都会发生变化,useEffect 会记录这个变化并打印相应的消息。
通过使用 useEffect,我们可以在 React 组件中处理各种副作用操作,并且可以在需要时进行清理。这使得我们能够更好地管理组件的生命周期和状态。
十八、介绍一下Promise的状态及其方法
Promise是一种用于异步编程的技术,它将异步操作转换成类似于同步操作的代码形式,提供了一种更加优雅和可读性强的方式来处理异步任务。
在JavaScript中,Promise对象包含三种状态:Pending(进行中)、Fulfilled(已成功)和Rejected(已失败)。
Promise的方法如下:
-
Promise对象可以通过 then() 方法添加成功(Fulfilled)和失败(Rejected)时的回调函数。then() 方法可以链式调用,每次返回一个新的 Promise 对象,因此可以很容易地实现异步任务的连续执行。
-
Promise对象还提供了 catch() 方法用于捕获错误和 finally() 方法用于在 Promise 被解析后运行代码块。
-
Promise.all() 方法接收一个 Promise 数组作为参数,返回一个新的 Promise,只有当所有 Promise 都解析成功时才会被解析,否则该 Promise 会被拒绝。
-
Promise.race() 方法接收一个 Promise 数组作为参数,返回一个新的 Promise,只要有一个 Promise 被解析或拒绝就会被解析或拒绝。
-
Promise.resolve() 和 Promise.reject() 方法分别返回一个已解析和一个已拒绝的 Promise 对象,可以用于快速创建 Promise。
十九、介绍一下async/await 的实现原理
在 JavaScript 引擎中,async/await 函数的实现原理是基于 Promise 对象和生成器函数(Generator Function)的协作。
- 具体来说,async/await 函数内部会将其代码块转换为一个状态机,并使用生成器函数返回的迭代器来进行状态的管理和切换,从而实现异步的调用和处理。
- 当 async 函数被调用时,它会立即返回一个 Promise 对象,并且开始执行其中的代码。当遇到 await 表达式时,async 函数会暂停执行并将控制权转交给生成器函数返回的迭代器对象,该对象会执行一个 next() 方法来将获取到的 Promise 对象进一步传递。
- 在等待的过程中,async 函数会依次执行其代码块中下一个 await 表达式之前的所有同步操作。当等待的 Promise 对象状态变为 resolved 时,async 函数会再次被调用并继续执行,直到代码块执行结束或者抛出异常。
需要注意的是,async/await 函数的实现原理并不是原生的 JavaScript 语法规范所支持的,而是通过编译工具(如 Babel 等)将其代码转换为符合 JavaScript 语法规范的代码实现。
二十、介绍一下let、const、var的区别
1. 块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
2. 变量提升: var存在变量提升,let和const不存在变量提升,只能在声明之后使用,否则会报错。
3. 给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
4. 重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
5. 暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
6. 初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
7. 指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
二十一、箭头函数和普通函数有什么区别
箭头函数的特征:
- 箭头函数没有this, this指向定义箭头函数所处的外部环境
- 箭头函数的this永远不会变,call、apply、bind也无法改变
- 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名
- 箭头函数没有原型prototype
- 箭头函数不能当做一个构造函数,因为 this 的指向问题
- 箭头函数没有 arguments 在箭头函数内部访问这个变量访问的是外部环境的arguments, 可以使用 ...代替
以下是箭头函数和普通函数的主要区别:
- 写法不同:箭头函数使用箭头(=>)来定义,而普通函数使用 function 关键字定义。
- this 的处理方式不同:在箭头函数中,this 的值与外层作用域的 this 绑定。而在普通函数中,this 的值由调用该函数的方式决定。
- 箭头函数没有 arguments 对象:箭头函数中没有自己的 arguments 对象,它的参数只能通过参数列表来传递。
- 箭头函数不能用作构造函数:由于箭头函数中没有自己的 this 值,因此不能用作构造函数来创建对象实例。
二十二、Css中常用的垂直居中解决方案有哪些
vertical-align 属性
用于控制元素内部的行内元素(如文本或图片)的垂直对齐方式。通常与 display: table-cell 结合使用,实现单元格内部的元素垂直居中。
line-height 属性
用于设置行内元素的行高,可以使单行文本元素垂直居中。但是如果元素高度超过一行,则无法实现垂直居中。
flexbox 布局
使用 display: flex 和相关属性可以实现容器内元素的水平和垂直居中。
grid 布局
使用 display: grid 和相关属性可以实现网格布局中元素的水平和垂直居中。
绝对定位 + 负边距
将需要垂直居中的元素绝对定位到容器中心,然后通过负边距调整元素位置。
二十三、什么是BFC
BFC(Block Formatting Context)是 CSS 中一个很重要的概念。它是指一个块级容器,其中的元素按照特定规则布局和渲染,同时也影响着其内部和外部元素的布局。
BFC 特点:
-
BFC 内部的元素会按照垂直方向一个接一个地排列,并且在水平方向上占据整个父容器的宽度。
-
BFC 内部元素的 margin 和 padding 不会与外部元素共享边框,而是互相独立,不会发生重叠。
-
如果两个相邻的块级元素都属于同一个 BFC,那么它们之间的 margin 会产生折叠,即取两个 margin 的最大值作为最终的 margin 值。
-
BFC 可以包含浮动元素,并防止浮动元素溢出到容器外面。
-
BFC 内部的第一个子元素或最后一个子元素,可以通过设置 clear 属性来清除浮动。
如何创建BFC?
-
使用 float 属性:给元素添加 float 属性可以使其成为一个 BFC。
-
使用 position 属性:将元素使用 position 属性设置为 absolute 或 fixed 时,也可以使其成为一个 BFC。
-
使用 display 属性:给元素添加 display 属性设置为 inline-block、table-cell、table-caption 等值,也可以变成 BFC。
-
设置 overflow 属性:将元素的 overflow 属性设置为 auto、scroll 或 hidden,也可以创建一个 BFC。
应用场景:
-
清除浮动:当一个父容器包含多个浮动元素时,可以将其设置为 BFC,防止浮动元素溢出到外面。
-
解决 margin 重叠问题:当两个元素的 margin 发生重叠时,可以将其中之一包裹在一个 BFC 中,使其 margin 与外部元素分离。
-
实现多列布局:使用 column-count 和 column-gap 属性可以让文本内容自动分为多列,但这需要在 BFC 中实现。
总结
BFC 是 CSS 中的一个重要概念,它对于页面布局及解决一些常见问题非常有帮助。了解 BFC 的概念、特点和创建方式,能够更好地掌握其应用场景,提高开发效率和代码质量。
二十四、Css中移动端适配有哪些方案
- 首先,通过
meta标签设置viewport rem单位搭配@media媒体查询:可以通过使用rem单位,它以HTML元素的font-size为比例,也可以搭配postcss-pxtorem搭建项目vw/vh布局:也可以通过使用vw/vh布局,vw/vh 方案与 rem 方案类似,都是将页面分成一份一份的,只不过 vw/vh 是将页面分为 100 份,也可以搭配postcss-px-to-viewport搭建项目- 百分比布局:也可以使用百分比来实现布局,但是需要特定宽度时,这个百分比的计算对开发者来说并不友好,且元素百分比参考的对象为父元素,元素嵌套较深时会有问题。
二十五、什么是Css中的回流(重排)与重绘
回流(重排)(reflow)和重绘(repaint)是浏览器渲染页面时的两个核心概念。
- 回流(重排)指的是当页面中的元素发生布局或几何属性发生变化时,浏览器需要重新计算这些元素的位置和大小,然后重新构建页面的渲染树,这个过程称为回流。由于需要重新计算布局,回流的代价很大,会对页面的性能产生负面影响。
- 重绘指的是当页面中的元素样式发生改变时,浏览器会重新绘制这些元素的外观,但不会改变它们在页面中的位置和大小。重绘的代价相对较小,但仍然会对页面性能产生一定的影响。
重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。
- 重绘是当节点需要更改外观而不会影响布局的,比如改变
color就叫称为重绘 - 回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
以下几个动作可能会导致性能问题:
- 改变
window大小 - 改变字体
- 添加或删除样式
- 文字改变
- 定位或者浮动
- 盒模型
并且很多人不知道的是,重绘和回流其实也和 Eventloop 有关。
- 当 Eventloop 执行完 Microtasks 后,会判断
document是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。 - 然后判断是否有
resize或者scroll事件,有的话会去触发事件,所以resize和scroll事件也是至少 16ms 才会触发一次,并且自带节流功能。 - 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame回调 - 执行
IntersectionObserver回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback回调。
二十六、常用的跨域解决方案有哪些
常用的跨域解决方案包括:
-
CORS:跨域资源共享(Cross-Origin Resource Sharing),是一种允许浏览器向跨域服务器发送 Ajax 请求的机制,支持现代浏览器,服务器端需要设置 Access-Control-Allow-Origin 头信息,指定允许的源或通配符,从而实现跨域请求。
-
代理:在同源页面内部发送 AJAX 请求到同域服务器,由服务器代理转发请求到跨域服务器,最后再将结果返回给同源页面。
-
WebSocket:WebSocket 是一种 HTML5 协议,它使得浏览器和服务器之间可以建立持久化的连接,可以直接使用 Socket 进行通信,避免了浏览器的跨域限制。
跨域资源共享(CORS)
跨域资源共享(CORS)是一种跨域解决方案,它允许浏览器向跨域服务器发出 XMLHttpRequest 请求,从而实现了前端页面和后端 API 的跨域访问。
CORS 通过在 HTTP 头部添加 Access-Control-Allow-Origin 等字段来告知浏览器该请求是否允许跨域,因此需要后端设置相应的响应头信息。
具体而言,可以在响应头中设置 Access-Control-Allow-Origin 字段来指定允许访问的源头,也可以设置其他相关字段:
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers
等,来进一步控制跨域请求的行为。
Vue.js中的proxy代理
Vue.js 提供了一个开箱即用的代理功能,可以通过配置 vue.config.js 文件中的 devServer.proxy 字段来实现代理。
与 CORS 不同,代理是通过将 API 请求转发到同源的服务器上去处理,然后再将其返回给前端页面。
例如,我们可以将所有以 /api 开头的请求都代理到 http://localhost:3000 服务器上去处理,即可实现跨域访问:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
这里通过设置 target 字段来指定代理的目标服务器,changeOrigin 字段允许修改请求头中的 origin 字段,pathRewrite 字段允许对路径进行修改,从而实现对API请求的代理。
二十七、Webpack中有哪些核心概念
Webpack是一个功能强大的打包工具,它能够将多个 JavaScript、CSS、图片等资源打包成一个或多个 bundle 文件。
它有一些核心概念贯穿整个构建流程。下面是Webpack中的几个核心概念:
-
Entry(入口):Webpack在打包时需要从哪个文件开始构建依赖关系图,就是入口。可以设置多个入口文件,以生成多个输出文件。
-
Output(输出):打包后的文件放在哪里,以及如何命名这些文件。可以指定输出目录、文件名、公共路径等。
-
Loader(模块加载器):Webpack只能处理JavaScript文件,而其他类型的文件如CSS、图片等需要通过Loader转换才能被Webpack处理。Loader用于对模块内容进行转换处理。
-
Plugin(插件):Plugin可以用于执行各种任务,例如打包优化、错误处理和环境变量注入等。Webpack本身只提供了一些基本的Plugin,但社区中有很多第三方Plugin可供使用。
-
Mode(模式):Webpack提供了三种模式:development、production和none。不同的模式会启用不同的Webpack内置Plugin和Loader,以便于开发和生产环境的优化。
-
Chunk(代码块):Webpack在打包时会把所有相关联的模块组成一个Chunk。可以通过Code Splitting技术将代码拆分成多个Chunk,以实现按需加载。
-
Module(模块):Webpack把每个文件都看作一个模块,它可以是JavaScript、CSS、图片等。这些模块通过依赖关系进行组合,构成整个应用程序。
以上是Webpack中的七个核心概念,它们共同构成了Webpack的打包机制。熟练掌握这些概念可以帮助我们更好地了解和使用Webpack。
二十八、Vite 和 Webpack 的区别
1、基于ESM的Dev server
在Vite出来之前,传统的打包工具如Webpack是先解析依赖、打包构建再启动开发服务器,Dev Server 必须等待所有模块构建完成,当我们修改了 bundle模块中的一个子模块, 整个 bundle 文件都会重新打包然后输出。项目应用越大,启动时间越长。
而Vite利用浏览器对ESM的支持,当 import 模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。灰色部分是暂时没有用到的路由,所有这部分不会参与构建过程。随着项目里的应用越来越多,增加route,也不会影响其构建速度。
2、基于ESM 的 HMR 热更新
目前所有的打包工具实现热更新的思路都大同小异:主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
Webpack: 重新编译,请求变更后模块的代码,客户端重新加载
Vite: 请求变更的模块,再重新加载
Vite 通过 chokidar 来监听文件系统的变更,只用对发生变更的模块重新加载, 只需要精确的使相关模块与其临近的 HMR边界连接失效即可,这样HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也要好于 Webpack。
3、基于esbuild的依赖预编译优化
4、基于 Rollup的 Plugins
使用Vite插件可以扩展Vite能力,通过暴露一些构建打包过程的一些时机配合工具函数,让用户可以自定义地写一些配置代码,执行在打包过程中。比如解析用户自定义的文件输入,在打包代码前转译代码,或者查找。
最后总结下Vite相关的优缺点:
-
优点:
- 快速的冷启动: 采用
No Bundle和esbuild预构建,速度远快于Webpack - 高效的热更新:基于
ESM实现,同时利用HTTP头来加速整个页面的重新加载,增加缓存策略 - 真正的按需加载: 基于浏览器
ESM的支持,实现真正的按需加载
- 快速的冷启动: 采用
-
缺点
- 生态:目前
Vite的生态不如Webapck,不过我觉得生态也只是时间上的问题。 - 生产环境由于
esbuild对css和代码分割不友好使用Rollup进行打包
- 生态:目前
二十九、Webpack常见的优化方案有哪些
- 升级 webpack 版本,3升4,实测是提升了几十秒的打包速度
- 使用Tree Shaking和Scope Hoisting来减少代码体积和模块构建时间,其中Tree Shaking可以去除未使用的代码,而Scope Hoisting可以将模块内的代码尽量合并到一个函数(单一作用域)中,以减少函数声明和闭包的数量。
- 使用splitChunksPlugin插件来将公共代码抽离成单独的chunk,以减少代码重复和提高缓存命中率。
- 合理配置resolve.alias和resolve.extensions选项来减少Webpack查找文件的时间。
- 针对生产环境,可以开启代码压缩以及多进程并行处理等优化方式,以减少构建时间和服务器负载。
- 使用DLLPlugin和DllReferencePlugin来预先编译一些稳定不变的代码,以减少每次构建的时间。
- 使用HappyPack来启用多线程并发处理,以加速代码构建和增强开发体验。
- 对于图片、字体等资源文件,可以通过url-loader和file-loader等loader设置较小的limit值,将文件转换成base64编码的字符串内嵌在js文件中,以减少http请求次数。
三十、浏览器中强缓存与协商缓存的缓存机制
字段
含义及作用
Expires
指定资源的过期时间。服务器在响应头中设置该字段,告诉浏览器资源的过期时间。如果当前时间小于过期时间,则直接使用缓存的资源,否则重新请求。
Cache-Control
控制缓存行为的指令。通过该字段,服务器可以告知浏览器如何处理缓存。常见的指令包括max-age(缓存有效期)、no-cache(不直接使用缓存,需要协商验证)等。
Last-Modified
资源的最后修改时间。服务器在响应头中设置该字段,标识资源的最后修改时间。浏览器可以将该值存储,并在下次请求时发送给服务器进行协商验证。
ETag
资源的唯一标识符。服务器在响应头中设置该字段,用于唯一标识资源的版本。浏览器可以将该值存储,并在下次请求时发送给服务器进行协商验证。
If-Modified-Since
上次请求资源时的Last-Modified值。浏览器在请求头中设置该字段,用于与服务器进行协商验证,判断资源是否有更新。如果没有更新,服务器返回304 Not Modified状态码。
If-None-Match
上次请求资源时的ETag值。浏览器在请求头中设置该字段,用于与服务器进行协商验证,判断资源是否有更新。如果没有更新,服务器返回304 Not Modified状态码。
200 OK
表示请求成功,返回的是新的资源。若服务器返回该状态码,表明资源未使用缓存,浏览器会将新的资源保存到缓存中,并使用该资源。
304 Not Modified
表示资源未发生变化,可以使用缓存的旧资源。如果服务器确定资源未修改,返回该状态码,浏览器会从缓存中加载资源,并继续使用该资源。
技术解读
强缓存
在发送 http 请求下载资源之前首先检查强缓存。使用的字段在 http/1.0 和 http/1.1 中分别是 Expires 和 Cache-Control。
Expires
http/1.0 中使用的字段是 Expires 即过期时间,存在于服务器返回的响应头中,浏览器在这个过期时间前再次请求同一资源时将直接从缓存里面获取数据,无需再次发送 http 请求。事例如下:
Expires: Wed, 21 Oct 2020 07:28:00 GMT
使用此字段的缺陷是服务器的时间和浏览器的时间可能不一致,这样服务器返回的过期时间就不一定是准确的。因此这种方式在 http/1.1 中被废弃。
Cache-Control
http/1.1 中使用的字段是 Cache-Control, 也存在于服务器返回的响应头中,但采用过期时长而非具体过期时间点,示例如下:
Cache-Control:public, max-age=86400
表示在响应返回后的 86400 秒也就是 24 小时之内可以直接使用缓存。
public 是另一个缓存指令,表示响应可以被任何对象(包括发送请求的客户端、代理服务器等等)缓存,即使是通常不可缓存的内容。此外,还可组合如下的缓存指令:
private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。
no-cache: 跳过当前的强缓存检查,发送 http 请求,即直接进入 协商缓存阶段。
no-store: 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
注意:当 Expires 和 Cache-Control 同时存在的时候,Cache-Control 会被优先考虑。
协商缓存
当强缓存失效,浏览器在 http 请求头中加入某些字段,服务端根据这些字段确定浏览器是否能够使用缓存,这就是协商缓存。这样的字段有两对:Last-Modified/If-Modified-Since和 ETag/If-None-Match。
Last-Modified/If-Modified-Since
- 浏览器第一次给服务端发送请求后,服务器会在响应头中加上 Last-Modified 这个字段和值。
- 浏览器接收到这个字段并在第二次给服务端发送请求时用 If-Modified-Since 字段携带该值。
- 服务端接收到后会和服务端中该资源的最后修改时间作对比。
- 如果请求头中的这个值小于最后修改时间,则返回新的资源,否则返回304,告诉浏览器直接用缓存。
ETag/If-None-Match
- ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,一旦文件内容有改动,这个值就会变化。
- 服务器通过响应头把这个值传给浏览器,浏览器接收到ETag的值,会在下次请求时将这个值放到请求头 If-None-Match 字段中,然后发给服务器。
- 服务端接收到 If-None-Match 后,会跟服务器上该资源的 ETag 进行比对,如果两者不一样,返回新的资源,否则返回304,告诉浏览器直接用缓存。
- 另外,在 ETag 和 If-Match 请求头的帮助下,可以检测到"空中碰撞"的编辑冲突
Last-Modified/If-Modified-Since和 ETag/If-None-Match的比较
-
比较这两种方式,精确度上 ETag 要优于 Last-Modified。因为 Last-Modified 的最小单位是秒,假如文件在一秒内改变了多次,这个时候 Last-Modified 的值并没有变化。
-
性能上 Last-Modified 要优于 ETag,因为 Last-Modified 仅仅只是记录一个时间点,而 Etag 需要根据文件的具体内容生成哈希值。
三十一、Vue中的虚拟DOM
-
定义虚拟DOM:虚拟DOM是一个JavaScript对象,它描述了真实DOM树的结构和属性,但不包含具体的渲染内容。
-
工作原理:当数据发生变化时,Vue会先生成一棵新的虚拟DOM树,然后与旧的虚拟DOM树进行比较,找出两者之间的差异。最后,Vue只更新需要修改的部分,从而避免了整个页面重新渲染的开销。
-
好处:使用虚拟DOM可以大大减少DOM操作的次数,提高性能和用户体验。因为DOM操作是非常耗费资源的,而虚拟DOM可以通过批量更新减少操作次数,从而提升效率。
-
Vue中的虚拟DOM:在Vue中,我们可以使用模板语法来定义组件的结构和属性。Vue将模板编译成渲染函数,这个函数返回一个虚拟DOM节点树。当数据改变时,Vue会调用这个渲染函数生成新的虚拟DOM树,然后与旧的虚拟DOM树进行比较,最终更新真实DOM。
-
总结:虚拟DOM是一种优化性能的技术,它可以减少DOM操作的次数,提高页面的渲染效率和用户体验。在Vue中,我们可以使用虚拟DOM来实现快速响应和流畅的界面交互。
三十二、Vue中是如何将模版语法转换为虚拟DOM的
在Vue中,将模板语法转换为虚拟DOM的过程是通过编译器实现的。
具体来说,这个过程包含以下几个步骤:
-
解析(parse):将模板字符串解析成 AST(抽象语法树)。
-
静态分析(static analysis):对 AST 进行静态分析,标记出其中的静态节点(Static Node)。
-
优化(optimize):遍历 AST,对静态节点进行优化,去掉不必要的操作。
-
代码生成(code generation):将 AST 转换成渲染函数(render function)的可执行代码。
-
最终的渲染:将生成的渲染函数运用到数据上,最终生成视图。
以上过程都是由Vue的Compiler 模板编译器完成的,通常是在构建时完成。最终,我们可以通过调用渲染函数来生成虚拟DOM节点树,并交给Vue的响应式系统处理数据变化和更新视图。
技术详解
在 Vue.js 中,虚拟 DOM(Virtual DOM)是一个重要的概念。虚拟 DOM 是指一个轻量级的 JavaScript 对象结构,它描述了真实 DOM 的层次结构。每个虚拟 DOM 节点都有与之相对应的真实 DOM 节点,并存储着节点的标签名、属性、事件等信息。
Vue.js 通过比较新旧两个虚拟 DOM 树的差异,计算出需要更新的最小操作集合,并将这些操作批量执行,从而更新视图。这种方式虽然增加了一定的计算量,但可以有效地减少真实 DOM 操作带来的性能损失,从而提高应用程序的性能和响应速度。
下面我们通过一些代码示例来更好地理解虚拟 DOM:
<div id="app">
<input v-model="message" />
<p>{{ message }}</p>
</div>
上述代码中,我们定义了一个包含输入框和段落标签的简单模板。我们使用了 v-model 指令将输入框的值绑定到 Vue 实例的 message 属性上,并在段落标签中显示该属性的值。当我们在输入框中输入文本时,Vue.js 会自动更新视图并实时显示输入框的内容。
为了实现此功能,Vue.js 需要跟踪输入框的值,并在值发生变化时更新视图。由于真实 DOM 操作的成本较高,Vue.js 使用虚拟 DOM 来跟踪变化并更新视图。下面是 Vue.js 管理的虚拟 DOM 树:
{
tag: 'div',
attrs: {
id: 'app'
},
children: [
{
tag: 'input',
attrs: {
value: vm.message
},
on: {
input: function(event) {
vm.message = event.target.value
}
}
},
{
tag: 'p',
children: [vm.message]
}
]
}
上述代码中,我们可以看到 Vue.js 维护了一个包含根节点的虚拟 DOM 树。该树包含了所有节点的信息,包括标签名、属性、事件、子节点等。当用户在输入框中输入文本时,Vue.js 会监听 input 事件,并更新 message 属性的值。此时,Vue.js 会通过新的虚拟 DOM 树计算出需要更新的差异,并将差异批量应用到真实 DOM 上,完成视图的更新。
总之,虚拟 DOM 技术使得 Vue.js 更新视图时减少了不必要的 DOM 操作,从而大幅提高了应用程序的性能和响应速度。
三十三、Vue2与Vue3两者Diff流程的区别
Vue2和Vue3在Diff算法的实现上有一些显著的区别,主要表现在以下几个方面:
- 源码架构:Vue2的Diff算法是在虚拟DOM模块中实现的,需与渲染模块和事件模块耦合在一起。而Vue3则将Diff算法单独抽离为一个模块,便于维护和重用。
- 标记方式:Vue2的Diff算法采用的是双端比较的方式,分别从新老节点树的头尾开始比较,寻找相同的节点进行复用。而Vue3则是采用深度优先遍历的方式,对新老VNode树进行标记,并通过最长递增子序列算法进行优化,进一步提高了Diff算法的效率和性能。
- 操作粒度:Vue2的Diff算法是以整个VNode为单位进行修改的,这样可能会涉及到较多不必要的DOM操作,导致性能较低。而Vue3则通过Granular Detection(红色插头)技术,将操作粒度缩小到了最小,只对需要修改的具体属性进行操作,从而实现了更加精细和高效的页面更新。
- 操作时机:Vue2的Diff算法是在数据更新后立即执行的,这可能会与其生命周期钩子函数有所冲突,导致一些不必要的更新。而Vue3则通过scheduler调度器,在下一次事件循环开始前进行异步更新,从而减少了不必要的重复更新。
三十四、Vue中虚拟dom变成实际 dom 是怎么发生的
Vue中虚拟DOM变成实际DOM的步骤:
- 当响应式数据发生变化时,Vue框架会重新渲染组件;
- 在渲染过程中,Vue会调用编译器将template模板转换成render函数,render函数返回的是虚拟DOM对象;
- Vue通过比较新旧虚拟DOM树的差异来确定需要更新的部分,这个过程称之为“Diff算法”;
- Vue将需要更新的部分通过特定的方式更新到真实的DOM上,这个过程称之为“打补丁(Patch)”。
总结来说,Vue中虚拟DOM变成实际DOM的步骤可以概括为:渲染 -> 生成虚拟DOM -> 比对差异 -> 应用变化。
三十五、怎么实现虚拟dom 和实际dom 的分离和衔接
- 通过创建虚拟DOM树来实现虚拟DOM和实际DOM的分离
- 通过Diff算法找到需要更新的节点,然后将这些节点更新到真实的DOM上,实现了虚拟DOM与实际DOM的分离和衔接
技术详解
Vue中通过创建虚拟DOM树来实现虚拟DOM和实际DOM的分离。在Vue更新节点的过程中,首先会对新旧节点进行比对,找出需要更新的节点,然后再将这些节点更新到实际DOM上。
具体流程如下所示:
- 在开始阶段,Vue通过编译器将template模板转换为render函数,并生成一个虚拟DOM树;
- 在更新节点时,Vue使用Diff算法比较新旧虚拟DOM树的差异,找出需要更新的节点;
- 在更新节点之前,Vue会先创建新的虚拟DOM树,并且将这个虚拟DOM树与旧的虚拟DOM树进行比较,找出需要更新的节点,这个过程称之为“Diff”算法;
- 在找出需要更新的节点后,Vue会将这些节点更新到真实的DOM上,这个过程称之为“打补丁(Patch)”。
总的来说,Vue通过Diff算法找到需要更新的节点,然后将这些节点更新到真实的DOM上,实现了虚拟DOM与实际DOM的分离和衔接。由于Vue在更新节点时只更新需要更新的部分,而不是整个页面,因此可以提高页面的性能和效率。
三十六、Virtual Dom的优势在哪里
Virtual DOM 是一个轻量的 JavaScript 对象模型,它以 JS 对象的形式来描述真实的 DOM ,可以在内存中进行操作、比较,然后只对需要更新的部分进行实际的 DOM 操作,从而最小化 DOM 操作的次数,提高渲染效率。
-
提高性能:Virtual DOM 可以记录页面状态的变化,并且可以一次性地批量更新 DOM,从而避免不必要的重绘和重排操作,大大提高了页面的性能。
-
减少内存占用:因为 Virtual DOM 是轻量级 JavaScript 对象模型,它占据的内存空间相对较小,比起将所有页面数据都存储在内存中是更优秀的选择。
-
极佳的跨平台兼容性:由于 Virtual DOM 是在 JavaScript 内存中维护的一份视图层次结构,因此可以在任何支持 JavaScript 的平台上使用,例如服务器端渲染、原生移动应用等。
-
简化开发流程:Vue.js 的 Virtual DOM 可以让开发者专注于数据处理而不必过多关心 DOM 操作。开发者只需要通过 Vue.js 提供的 API 更新组件的状态,然后 Vue.js 会自动实现 DOM 的更新,从而简化了开发流程。
总的来说,Virtual DOM 可以提高性能、减少内存占用、极佳跨平台兼容性以及简化开发流程等优势,是 Vue.js 框架的核心特性之一。
三十七、虚拟DOM真的比真实DOM性能好吗
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
- 它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。
技术详解
虚拟DOM(Virtual DOM)相比真实DOM(Real DOM)在一些特定情况下是具有性能优势的,但并不是绝对的。
首先,虚拟DOM与真实DOM的区别在于:虚拟DOM是一个在内存中维护的JavaScript对象树,而真实DOM则是浏览器渲染页面后生成的一棵对象树。在需要更新页面时,虚拟DOM会先在内存中对比新旧状态之间的差异,计算出需要更新的部分,然后只将这些变化应用到真实的DOM上。
相比之下,真实DOM在更新时需要进行多次重排和重绘操作,而虚拟DOM通过减少DOM操作次数来提高性能。
但是虚拟DOM也有一些缺点,例如需要消耗额外的内存、对于初次渲染的性能影响较大、存在数据同步延迟等问题。
总的来说,虚拟DOM并不是万能的,具体使用场景和优化策略要根据项目需求来实现。在一些复杂交互场景下,使用虚拟DOM可以显著提高渲染效率,但在一些简单的场景下,使用真实DOM也可以达到很好的性能表现。
三十八、举例介绍一下双端比较算法
双端比较算法本质上是将新旧节点进行一次交叉比较,尽可能地重复使用已有的节点来达到最小化DOM操作的目标。
首先,我们定义四个指针:
- 新节点的开始位置 newStartIdx
- 新节点的结束位置 newEndIdx
- 旧节点的开始位置 oldStartIdx
- 旧节点的结束位置 oldEndIdx
初始时,这些指针分别指向 oldVnode 和 newVnode 的第一个和最后一个节点。即:
newStartIdx = 0
newEndIdx = newVnode.children.length - 1
oldStartIdx = 0
oldEndIdx = oldVnode.children.length - 1
接下来,我们需要在循环中移动这些指针,比较新节点和旧节点的元素,找到它们之间的差异。具体操作流程如下:
- 比较新旧节点首尾元素
首先比较 oldVnode[oldStartIdx] 和 newVnode[newStartIdx],如果它们相等,则表示该节点无需更新,直接把指针向后移动即可。然后比较 oldVnode[oldEndIdx] 和 newVnode[newEndIdx],如果它们相等,则同样把指针向前移动。
- 比较其他节点
如果首尾元素对比没有匹配,则需要进行更详细的比较,从而找出需要添加、删除和移动的节点。具体操作如下:
- 首先,在旧节点的范围内查找与 newVnode[newStartIdx] 相同的节点,如果找到就说明该节点可以被复用,把指针向后移动。
- 如果在旧节点中未找到相同的节点,则需要插入 newVnode[newStartIdx] 对应的节点到 oldVnode 中,把指针向后移动。
- 然后,在新节点的范围内查找与 oldVnode[oldStartIdx] 相同的节点,如果找到就说明该节点可以被复用,把指针向后移动。
- 如果在新节点中未找到相同的节点,则说明旧节点中的该节点需要被删除,把指针向后移动。
- 然后,在旧节点的范围内查找与 newVnode[newEndIdx] 相同的节点,如果找到就说明该节点可以被复用,把指针向前移动。
- 如果在旧节点中未找到相同的节点,则需要插入 newVnode[newEndIdx] 对应的节点到 oldVnode 中,把指针向前移动。
- 最后,在新节点和旧节点的范围内查找剩余的节点进行比较,确定需要插入、删除和移动的位置。
通过这样的方式,我们就可以实现双端比较算法了。它可以最大限度地重用已有的节点,避免不必要的DOM操作,从而提高渲染性能。
三十九、举例介绍一下最长递增子序列算法
最长递增子序列问题(Longest Increasing Subsequence,简称LIS)是指在一个序列中找到一个最长的子序列,使得这个子序列中的元素是递增的。比如序列 [10, 9, 2, 5, 3, 7, 101, 18] 的最长递增子序列为 [2, 3, 7, 18],长度为 4。
好的,下面是一种基于动态规划的 JavaScript 实现最长递增子序列算法的方法:
function lengthOfLIS(nums) {
const n = nums.length;
if (n === 0) return 0;
const dp = new Array(n).fill(1); // 初始化每个元素为1,因为每个元素都构成长度为1的LIS
for (let i = 1; i < n; i++) { // 从第二个元素开始遍历
for (let j = 0; j < i; j++) { // 遍历i之前的所有元素
if (nums[j] < nums[i]) { // 如果nums[j] < nums[i],说明nums[i]可以加入到以nums[j]结尾的LIS中
dp[i] = Math.max(dp[i], dp[j] + 1); // 更新以nums[i]结尾的LIS的长度
}
}
}
return Math.max(...dp); // 返回dp数组中的最大值
}
这个算法使用了两层循环。外层循环遍历整个数组,内层循环则去遍历当前元素之前的所有元素。如果某个元素小于当前元素,则说明当前元素可以加入到以该元素结尾的 LIS 中,此时更新以当前元素结尾的 LIS 的长度。最后返回 dp 数组中的最大值即可。
这种算法的时间复杂度为 O(n^2)。如果序列的长度不是特别大,这个算法已经足够快了。如果需要优化时间复杂度,可以使用二分查找等方法,但是相应的实现就会更加复杂。
四十、Vue的diff算法和React的diff算法的异同
React和Vue都使用了虚拟DOM来进行高效的页面更新,但它们在实现细节上有一些差异。下面是React和Vue的diff算法的异同点:
异同点:
-
目标:React和Vue的diff算法的目标都是尽量减少DOM操作的次数,提高页面更新的性能。
-
虚拟DOM:React和Vue都使用虚拟DOM来表示真实的DOM结构并进行比较。虚拟DOM是一个轻量级的JavaScript对象,描述了DOM节点的层级结构、属性和内容。
-
Diff策略:React和Vue的diff算法都采用了基于"同层比较"的策略,即在进行比较时只比较同级别的组件或元素,不会跨层级进行比较。
-
Key属性:React和Vue都鼓励在列表渲染时为每个列表项添加唯一的
key属性,以便更精确地识别每个项的更新和状态变化。 -
Diff过程:React和Vue的diff算法都是基于两个阶段的比较:树的遍历和节点的更新。在树的遍历阶段,算法会逐层比较虚拟DOM树的节点。在节点的更新阶段,算法会根据比较结果决定是否需要更新真实的DOM。
-
更新策略:React的diff算法会尽量复用已有的DOM节点,而不是直接替换整个节点。Vue的diff算法则采用了双向查找的策略,通过在新旧虚拟DOM树之间进行前后比较,找到最小的更新范围。
差异点:
-
Diff起点:React的diff算法从根节点开始比较整棵虚拟DOM树,而Vue的diff算法则从组件树的根节点开始比较。
-
比较策略:React的diff算法采用了全量比较的策略,即每次更新都会遍历整个虚拟DOM树,并进行完整的比较。Vue的diff算法则采用了局部比较的策略,只会比较发生变化的组件及其子组件。
-
数组更新:React在处理数组更新时,使用了一种称为"key-based"的算法,需要为列表项提供唯一的
key属性。Vue的diff算法没有严格要求key属性,但建议为列表项提供稳定的标识符。
需要注意的是,React和Vue的具体实现细节可能随着版本的更新而有所改变。以上是一些通用的差异和共同点,以帮助理解它们的diff算法。
四十一、Vue2与Vue3中的双向数据绑定
双向数据绑定就是:数据劫持 + 发布订阅模式(观察者模式)。
Vue2中在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter。并劫持各个属性 getter 和 setter,在数据变化时发布消息给订阅者,触发相应的监听回调,而这之间存在几个问题
- 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
- 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
- Object.defineProperty 无法监听到数组元素的变化,只能通过劫持重写数组方法
- 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
- 不支持 Map、Set 等数据结构
Vue3中使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
相对于Object.defineProperty(),有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- 它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。
- Proxy 可以监听数组的变化。
技术详解
在 JavaScript 中,defineProperty 和 Proxy 都是对象的属性访问控制方法,它们有着一些明显的区别。
defineProperty 是一个 ES5 引入的 API,用于在已有对象上定义新属性或者修改现有属性,并且可以精细地控制这个属性的 getter、setter、可枚举性、可配置性和可写性等特性。通过 defineProperty,我们可以对一个对象的属性进行实时监测,从而实现数据绑定和响应式编程等高级功能。
但是,defineProperty 也存在一些限制。它只能监听对象的已有属性,而不能监听整个对象的变化。同时,由于该 API 的使用方式比较繁琐,使得其难以扩展和封装。
相比之下,Proxy 是 ES6 中引入的一个新特性,可以代理 JavaScript 对象的访问,提供了更加灵活的属性访问控制方式。通过在目标对象外层建立一个代理层,Proxy 可以拦截对象的访问、修改、删除、枚举等操作,从而实现更加细粒度的控制。
与 defineProperty 不同的是,Proxy 可以监听整个对象的变化,而不仅仅是某个属性的变化。另外,Proxy 的使用方式相对简单,可以轻松地被扩展和封装,使得其更加适用于面向对象编程和函数式编程等多种编程范式。
总的来说,defineProperty 和 Proxy 都是非常有价值的对象属性访问控制方法。如果需要对对象的某个属性进行深度监测,或者需要更加灵活的属性访问控制方式,那么建议使用 Proxy;如果只需要对对象的某个属性进行简单的监听,那么可以选择使用 defineProperty。
四十二、defineProperty和proxy的区别
Object.defineProperty 和 Proxy 都是 JavaScript 中用于监听对象变化的 API,它们的主要目的是在对象的值被修改时能够自动触发一些操作。
下面列举一下它们的区别:
-
语法不同:
Object.defineProperty是一个对象方法,而Proxy是一个构造函数,我们需要使用new关键字来实例化它。 -
监听的对象不同:
Object.defineProperty只能监听一个对象的某个属性,而Proxy能够监听整个对象或数组,我们可以对对象的所有属性或数组的所有元素进行监听。 -
新属性监听:
Object.defineProperty只能监听对象已经存在的属性,不能监听新属性的添加,而Proxy可以实时响应对象属性的添加、删除和修改。 -
性能不同:
Proxy的性能比Object.defineProperty更优,但在某些场景下使用Object.defineProperty会更加适合,例如只需要监听一个对象的一个属性的变化等。
总的来说,使用 Object.defineProperty 更加适用于简单的对象,处理逻辑相对简单,而 Proxy 则更适合复杂的对象或者需要监听多个属性的情况,其处理逻辑更加灵活。
四十三、Vue2中v-model 的原理
v-model 是用来监听用户事件然后更新数据的语法糖。
其本质还是单向数据流,内部是通过绑定元素的 value 值向下传递数据,然后通过绑定 input 事件,向上接收并处理更新数据。
单向数据流:父组件传递给子组件的值子组件不能修改,只能通过emit事件让父组件自个改。
// 比如
<input v-model="sth" />
// 等价于
<input :value="sth" @input="sth = $event.target.value" />
给组件添加 v-model 属性时,默认会把value 作为组件的属性,把 input作为给组件绑定事件时的事件名:
// 父组件
<my-button v-model="number"></my-button>
// 子组件
<script>
export default {
props: {
value: Number, // 属性名必须是 value
},
methods: {
add() {
this.$emit('input', this.value + 1) // 事件名必须是 input
},
}
}
</script>
如果想给绑定的 value 属性和 input 事件换个名称呢?
在 Vue 2.2 及以上版本,你可以在定义组件时通过 model 选项的方式来定制 prop/event:
<script>
export default {
model: {
prop: 'num', // 自定义属性名
event: 'addNum' // 自定义事件名
}
}
四十四、Vue2通过数组下标更改数组视图为什么不会更新
- 因为 Vue 2 使用的是 Object.defineProperty() 对数组进行响应式处理,在对一个对象进行响应式处理时,会遍历对象的所有属性,对其进行劫持,当属性值变化时则会触发视图更新。
- 但是,Object.defineProperty() 只能够劫持到数组索引的读取与更改操作,而不能够劫持到数组长度变化、数组变异方法等其他的数组操作。
- 但是,Vue2 对数组的变异方法进行了重写,实现了视图更新的功能。
- 因此,当通过数组下标更改数组或者使用数组的
length属性缩短数组长度时,Vue2中并不能侦测到以下数组变动。
技术详解
在 Vue 2.x 中,当通过数组下标更改一个数组的数据时,并不会导致组件重新渲染。
这是因为 Vue 2 使用的是 Object.defineProperty() 对数组进行响应式处理。具体来说,Vue 2 在对一个对象进行响应式处理时,会遍历对象的所有属性,对其进行劫持,当属性值变化时则会触发视图更新。
但是,Object.defineProperty() 只能够劫持到数组索引的读取与更改操作,而不能够劫持到数组长度变化、数组变异方法等其他的数组操作。
但是,Vue2 对数组的变异方法进行了重写,实现了视图更新的功能。
因此,Vue 并不能侦测到以下数组变动:
- 当通过数组下标直接更改数组中某一项时,例如
arr[1] = 'new value'; - 当使用数组的
length属性指定数组长度时,例如arr.length = 2。
这些情况下,Vue 并不知道具体哪个元素发生了变化以及该如何更新视图,从而无法实现精确的 DOM 操作,需要手动使用 Vue 提供的变异方法来触发视图更新,例如:
- 通过
Vue.set或vm.$set方法设置指定下标的值; - 通过
splice方法在指定下标位置移除或添加元素。
例如,在 Vue 中使用 vm.$set 方法来更新数组元素:
this.$set(this.items, index, newValue);
或者通过 splice 方法来更新数组元素:
this.items.splice(index, 1, newValue);
这样就可以在更新数组元素时同步更新组件的视图了。
四十五、Vue3通过数组下标更改数组视图为什么会更新
在 Vue 3 中,使用 Proxy 来实现了对数组的响应式监听,相比于 Vue 2 使用的 Object.defineProperty(),Proxy 更加高效和灵活。
因此,在 Vue 3 中,通过数组下标直接更改数组中某一项的值,也能够被 Vue 正确监听到并触发视图更新。
Vue 3 中,数组的修改操作依然分为两类:变异方法和非变异方法。其中,非变异方法包括以下几种:
filter、map、reduce、some、every、slice
这些方法返回一个新数组,且不会修改原数组本身。而变异方法则会修改原数组本身,例如:
push、pop、shift、unshift、splice
对于非变异方法,Vue 3 直接使用了 JavaScript 原生的数组方法,并且将其结果转化为响应式的数组,从而能够正确地进行界面的更新。
对于变异方法,Vue 3 则是使用 Proxy 对数组进行了处理,并且将变异方法进行了重写,从而实现了针对性的数组数据更新,避免了 Vue 2 中需要手动调用特殊方法来更新数组或者借助于 Object.defineProperty() 的方式实现。
因此,在 Vue 3 中,即使是通过数组下标直接修改数组中的某一项,也能够正常触发视图更新,从而实现了更好的响应式体验。
四十六、Vue2中如何监听(检测)对象或者数组某个属性的变化
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
解决方式:
-
this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
this.set(this.obj, "c", "OBKoro1"); // 改变对象
-
或者调用以下几个数组的变异方法:
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
Vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用我们再进行额外的操作,视图自动进行更新。 推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作。
四十七、Vue2中vm.$set 的实现原理
Vue2中的实现原理是通过defineReactive和Observer来实现的。具体步骤如下:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果要更新对象属性,且这个属性不存在于对象中(即它是响应式数据的新增属性),那么Vue2会使用
$set函数来进行设置; vm.$set会先判断对象是否为响应式的,如果不是,则直接赋值即可,如果是,则继续执行以下步骤;vm.$set会先调用Object.getOwnPropertyDescriptor方法获取对象属性描述符,如果该属性已存在,Vue2就不能将其转换成响应式属性了,因此直接返回;- 如果属性不存在,则调用
Object.defineProperty方法,给该属性添加getter和setter方法,并将其转换为响应式属性; - 在setter方法中,更新属性值时,如果新值仍然是一个对象(或者数组),则递归调用
observer函数进行深度观测,以便对新值的变化做出响应。
由于Vue2将对象属性转换为响应式属性的过程只需要在第一次调用$set时进行,所以后续直接使用对象属性的赋值操作就可以起到响应式更新效果。
四十八、Vue2中的自定义指令
在 Vue2 中,我们可以使用 Vue.directive 函数来定义自定义指令。自定义指令用于对 DOM 操作的封装和复用。
在 Vue2 中,自定义指令的生命周期钩子函数的功能分别如下:
bind: 只调用一次,在指令第一次绑定到元素时调用,用于进行一次性的初始化设置。inserted: 被绑定元素插入父节点时调用,用于进行 DOM 操作。update: 被绑定元素所在的模板更新时调用,而无论绑定值是否变化,以及何时发生变化都会调用。componentUpdated: 被绑定元素所在模板完成一个周期的更新时调用,只要组件数据变化,就会调用。unbind: 只调用一次,在指令解绑时调用,用于清除所有的绑定指令。
常用的自定义指令场景:
v-focus:实现页面加载后自动聚焦到某个表单元素上。v-limit:限制表单输入框的输入长度,对于超出长度的字符提供错误提示。v-ellipsis:处理文本溢出的情况,通过省略号的形式显示文本,当用户鼠标悬停在省略号上时显示完整文本内容。v-scroll:在页面需要滚动操作时,当滚动到特定位置或高度时,显示浮动操作按钮或菜单进行快捷操作。v-drag:实现拖拽功能,当用户在指定的区域按下鼠标时,可以移动元素到指定的位置。v-lazy-img:图片懒加载,当图片未出现在用户可视区域内时,不进行请求和加载,当图片出现在用户可视区域内时,进行请求和加载。v-toggle:实现开关切换功能,当用户点击开关时,改变指定元素的显示状态。v-tooltip:鼠标悬停在某个元素上时,显示 tooltip 或 popover 提示框。
技术详解
在 Vue.js 中,自定义指令是一种非常强大的功能,可以让我们扩展 Vue.js 的能力,实现一些特定的需求。本文将介绍自定义指令的基本用法、钩子函数和注意事项。
自定义指令的基本用法
在 Vue.js 中,我们可以通过 Vue.directive 方法来创建一个自定义指令。该方法接收两个参数:指令的名称和一个包含相关配置选项的对象。例如:
Vue.directive('my-directive', {
bind(el, binding, vnode) {
// ...
},
update(el, binding, vnode, oldVnode) {
// ...
},
unbind(el, binding, vnode) {
// ...
}
});
在上面的代码中,我们创建了一个名为 my-directive 的自定义指令,并设置了三个钩子函数:bind、update 和 unbind。这些钩子函数会在指令被绑定、更新和解绑时分别调用。
然后,在模板中使用指令时,我们可以将指令名称以 v- 开头作为属性,指向相应的表达式或值。例如:
<div v-my-directive="'hello'"></div>
在上面的代码中,我们将 my-directive 指令绑定到一个 div 元素上,并将 'hello' 作为指令的值传入。
钩子函数
自定义指令的钩子函数有多个,其中比较常用的有以下几个:
bind: 当指令第一次绑定到元素时调用。可以在此时修改元素的样式、属性等。inserted: 当元素被插入到父组件时调用。可以在此时操作 DOM。update: 指令所在的组件进行 VNode 更新之后调用。可以在此时根据新的值修改元素的样式、属性等。componentUpdated: 指令所在的组件进行 VNode 及其子组件的更新之后调用。unbind: 当指令从元素上解绑时调用。可以在此时删除添加的事件监听器等。
以上钩子函数中,bind 和 update 是最常用的。在这些钩子函数中,我们可以通过 el 参数访问指令所绑定的元素,通过 binding 参数访问指令的绑定信息,通过 vnode 参数访问组件 VNode,通过 oldVnode 参数访问前一个 VNode(仅在 update 和 componentUpdated 钩子中可用)。
使用场景
以下是 Vue 自定义指令常用的使用场景。
1、处理用户输入
自定义指令可以用于处理用户输入,例如限制表单输入、格式化文本等等。比如,我们可以创建一个 v-uppercase 指令来将输入内容转换为大写:
Vue.directive('uppercase', {
update(el, binding) {
el.value = binding.value.toUpperCase();
}
});
然后在模板中使用该指令:
<input v-model="message" v-uppercase>
过这种方式,我们可以确保所有用户输入都被转换为大写。
2、控制元素可见性
自定义指令可以用于控制元素的可见性。例如,我们可以创建一个 v-permission 指令来根据当前用户的权限控制元素的显示或隐藏:
Vue.directive('permission', {
inserted(el, binding) {
if (!checkPermission(binding.value)) {
el.style.display = 'none';
}
}
});
然后在模板中使用该指令:
<button v-permission="'admin'">只有管理员可见</button>
3、处理滚动事件
自定义指令可以用于处理滚动事件。例如,我们可以创建一个 v-scroll 指令来监听元素的滚动事件,并触发相应的操作:
Vue.directive('scroll', {
bind(el, binding) {
el.addEventListener('scroll', () => {
binding.value(el.scrollTop);
});
}
});
然后在模板中使用该指令:
<div v-scroll="handleScroll"></div>
通过这种方式,我们可以监听元素的滚动事件,并根据滚动距离来触发相应的操作。
注意事项
在编写自定义指令时,需要注意以下几点:
- 自定义指令的名称不要与 Vue.js 内置的指令重名。
- 钩子函数中的某些参数可能只在特定情况下可用,需要查看官方文档或根据具体情况决定是否使用。
- 不要在钩子函数中直接修改组件的状态,而是应该使用 Vuex 等状态管理工具来处理状态。
- 虽然自定义指令是非常强大的功能,但是在大型项目中过度使用自定义指令可能会导致代码可读性降低,因此需要权衡利弊。
总结
自定义指令是 Vue.js 中一种非常强大的功能,可以扩展其能力,实现一些特定的需求。我们可以通过 Vue.directive 方法来创建自定义指令,并设置相应的钩子函数来处理相关逻辑。在编写自定义指令时,需要注意名称不要与内置指令重复、钩子函数中的参数使用、状态管理等问题。
四十九、Vue中的$nextTick及使用场景
Vue提供的nextTick的方法,可以在下一次 DOM 更新循环结束之后执行回调函数。可以在回调函数中获取更新后的 DOM。
技术详解
在 Vue.js 中,当我们修改了数据后,可能需要立即对 DOM 进行操作。然而,由于 Vue.js 的异步更新机制,DOM 更新并不会立即发生,这就会导致我们在更新数据后无法立即获取到更新后的 DOM。
为了解决这个问题,Vue.js 提供了一个名为 nextTick 的方法,可以在下一次 DOM 更新循环结束之后执行回调函数。本文将介绍 nextTick 的基本用法、注意事项和一些实际应用场景。
基本用法
如果需要等待多个异步更新完成后再执行回调函数,可以使用 Promise 和 async/await 来简化代码:
Vue.nextTick().then(() => {
// 第一个异步更新已完成
return Vue.nextTick();
}).then(() => {
// 第二个异步更新已完成
});
或者使用 async/await 语法:
async function doSomething() {
await Vue.nextTick();
// 第一个异步更新已完成
await Vue.nextTick();
// 第二个异步更新已完成
}
注意事项
在使用 nextTick 方法时,需要注意以下几点:
nextTick方法是异步执行的,因此不能保证回调函数的执行顺序。- 回调函数中的代码应该尽量简洁,避免过多的计算和 DOM 操作,以提高性能。
- 在某些极端情况下,
nextTick方法可能无法正确工作,例如在使用v-for循环渲染大量数据时。这时候需要使用$nextTick方法,该方法只在当前组件内部起作用,可以避免一些潜在的问题。
nextTick 方法是 Vue.js 中一个非常有用的方法,可以确保回调函数在下一次 DOM 更新循环之后执行。在实际开发中,我们可以使用 nextTick 方法来处理异步更新、获取元素位置、执行动画效果等等。需要注意的是,在使用 nextTick 方法时,需要避免出现性能问题,并注意 $nextTick 方法的使用场景。
五十、Vue中的组件间的通信有哪些
-
props:父组件通过 props 属性向子组件传递数据。子组件接收该数据后,即可在其模板中直接使用。
-
**emit() 方法触发一个自定义事件,并把需要传递的数据作为参数传入。父组件监听该自定义事件,并在回调中处理数据。通过事件可以实现任意级别的组件间通信。
-
children:父组件可以通过 parent 找到其父组件。这种方式属于直接引用和修改组件对象,需要注意和谨慎使用。
-
provide 和 inject:祖先组件通过 provide 属性向子孙组件传递数据,子孙组件通过 inject 属性来获取这些数据。provide 和 inject 绑定并非响应式的,但是可以将一个观察者实例注入到 provide 中,使得组件在 inject 期间发生变化时获得通知。
-
Vuex 状态管理:Vuex 是一个专门为 Vue.js 应用程序开发的状态管理库,提供了一种集中式存储管理应用程序中的所有组件的状态。组件通过调用 mutation 方法来改变状态,其他组件通过监听 state 属性来获取最新的状态。
-
兄弟组件间的传值:Vue2中使用eventBus中央事件总线 , Vue3中使用的 mitt库
五十一、Vue中的过滤器
Vue中的过滤器filter就是一种可以复用的数据转换函数,主要用来对视图中的数据进行处理和格式化。通过管道符“|”来连接数据和过滤器。
技术详解
Vue中的过滤器是一种可重用的数据转换函数,主要用来对视图中的数据进行处理和格式化。使用过滤器可以简化模板中的数据处理逻辑,使代码更加简洁易懂。
Vue中的过滤器可以在模板表达式中使用,通过管道符“|”来连接数据和过滤器。例如:
{{ message | capitalize }}
上面的代码中,message是需要过滤的数据,capitalize是定义的过滤器函数。过滤器函数接收一个参数,即需要过滤的数据,然后对其进行处理并返回处理后的结果。
以下是一个实现将数字转为货币形式的过滤器的示例:
Vue.filter('currency', function (value) {
if (!value) return ''
return '¥' + value.toFixed(2)
})
以上代码使用了全局API Vue.filter 来定义一个名为 currency 的过滤器。该过滤器接收一个数值类型的参数,将其转换为保留两位小数的货币形式,并添加人民币符号前缀。
需要注意的是,尽管过滤器可以解决一些常见的数据处理需求,但是过多的过滤器使用也会影响到性能,因此应该尽可能地减少过滤器的使用。此外,如果需要执行更复杂的数据处理逻辑,应该使用计算属性或方法来代替过滤器。
五十二、Vue中的内置组件有哪些
Vue2中的内置组件:
-
<component>:动态组件,可以根据传递的is属性值渲染不同的组件。 -
<transition>:过渡动画组件,可以在元素插入、更新或移除时添加动画效果。 -
<transition-group>:过渡动画组件,与<transition>类似,但可以处理多个元素的过渡。 -
<keep-alive>:缓存组件,可以将组件缓存起来,避免重复渲染。 -
<slot>:插槽组件,用于插入父组件中的内容。
Vue.js 3.0 中新增了两个内置组件:
-
<teleport>:传送门组件,可以将元素渲染到文档之外的地方。 -
<suspense>:异步加载组件,可以在等待异步组件加载时显示占位内容。
五十三、Vue中的component动态组件
component动态组件(Dynamic Components)可以通过is属性切换不同的组件,从而实现动态渲染的效果。常用于实现 Tab 切换效果、配合使用 keep-alive 来缓存组件。
技术详解
在 Vue 中,动态组件(Dynamic Components)允许我们根据需要切换不同的组件,从而实现动态渲染的效果。
Vue 中的动态组件可以帮助我们轻松地实现以下几个功能:
- 实现按需加载:当页面中存在多个组件时,我们可以通过动态组件来实现按需加载,提高页面性能。
- 实现条件渲染:动态组件允许我们根据条件渲染不同的组件,可以有效地控制组件的显示与隐藏。
- 实现复杂布局:使用动态组件可以更加灵活地控制组件的布局,从而实现复杂的页面布局效果。
动态组件在实际开发中有很多应用场景,以下列举几个常见的场景:
- 根据用户权限动态展示组件:在某些网站或应用程序中,不同用户拥有不同的权限,因此需要根据用户权限来动态展示不同的组件。
- 实现 Tab 切换效果:Tab 切换是页面中常见的交互效果,使用动态组件可以实现 Tab 切换的效果,同时也可以提高页面性能。
- 懒加载:对于一些较为复杂的组件或者页面,可以使用动态组件实现懒加载,只有当用户需要查看这些组件或页面时才进行渲染,从而提高页面性能。
在 Vue 中,动态组件可以通过两种方式来实现:
1. 使用 component 元素
可以使用 component 元素来实现动态组件,如下所示:
<template>
<component :is="currentComponent"></component>
</template>
其中,currentComponent 是一个变量,表示当前要渲染的组件名称。通过在 component 元素上使用 v-bind 指令,并将 is 属性绑定到 currentComponent 变量,就可以实现动态组件了。
2、使用 keep-alive 元素
另外,在使用动态组件时,我们还可以通过 keep-alive 元素来缓存已经渲染的组件。使用 keep-alive 元素可以避免反复销毁和创建组件,提高页面性能。示例代码如下:
<template>
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</template>
五十四、Vue中的 keep-alive 组件
Vue2中的keep-alive组件主要用来缓存组件实例,以便在切换时保留其状态。这样能够提高应用程序的性能,避免了在多个页面之间频繁地创建和销毁组件实例。常用于:多表单切换,对表单内数据进行保存。
使用keep-alive组件时需要注意以下几点:
- keep-alive组件只能用于包含动态组件的
<component>元素上。 - keep-alive组件有两个内置的生命周期钩子函数,activated和deactivated。这些钩子函数分别在缓存的组件被激活和停用时调用,可以通过它们来对缓存的组件进行处理或执行特定逻辑。
- 当一个组件被缓存时,它会被包裹在一个元素中,并且它必须具有一个唯一的key属性,以便在缓存中区分不同的组件。
另外,如果使用keep-alive来缓存组件实例,需要注意以下几点:
- 缓存的组件实例不会随着页面卸载而销毁,因此当组件被缓存时,需要考虑清除可能会导致内存泄漏的一些状态或事件监听器。
- 缓存的组件实例如果本身有自己的生命周期钩子函数,那么这些钩子函数并不会被调用,而是使用activated和deactivated来代替。
技术详解
Vue中的keep-alive组件它可以帮助我们缓存已经渲染的组件,以提高应用程序的性能和用户体验。
作用
Vue中的keep-alive组件可以帮助我们缓存已经渲染的组件,以避免重复渲染造成的性能浪费。具体来说,当我们使用keep-alive组件包裹一个动态组件时,这个动态组件会被缓存起来,而不是每次都重新创建和销毁。这样就可以大大提高应用程序的性能,特别是在处理复杂页面和大量数据时。
用法
使用keep-alive组件比较简单,只需要在需要缓存的动态组件外部套上一个keep-alive标签即可。例如:
<keep-alive>
<router-view></router-view>
</keep-alive>
在这个例子中,我们使用keep-alive组件包裹了Vue Router中的动态组件router-view。这样就可以缓存这个动态组件,在后续的路由切换中不再重新渲染。
除了直接使用keep-alive组件,我们还可以通过设置include和exclude属性来控制哪些组件需要被缓存,哪些不需要。例如:
<keep-alive include="componentA,componentB">
<router-view></router-view>
</keep-alive>
使用场景
keep-alive组件通常适用于以下场景:
- 处理大量数据:当我们需要显示大量数据时,可能需要频繁地创建和销毁组件,这样会影响性能。使用keep-alive组件可以避免这种情况发生,从而提高应用程序的性能。
- 处理复杂页面:当页面比较复杂时,可能需要多次跳转和加载,这也会影响性能。使用keep-alive组件可以减少不必要的重新渲染,提高性能和用户体验。
- 缓存表单数据:在一些需要填写表单的场景中,如果每次切换到其他组件或页面后都需要重新填写表单,这会给用户带来很大的不便。使用keep-alive组件可以缓存已经填写好的表单数据,从而提高用户体验。
总之,Vue中的keep-alive组件是一个非常有用的组件,它可以帮助我们提高应用程序的性能和用户体验。如果你正在开发一个复杂的Vue应用程序,建议你考虑使用keep-alive组件来优化你的应用程序。
五十五、Vue中的slot插槽
在Vue中,slot(插槽)是一种用于扩展组件的功能的技术。通过使用插槽,我们可以将子组件的内容插入到父组件的特定位置上,并实现更灵活的组件复用。
在定义一个包含插槽的组件时,可以在组件中使用<slot> 标签固定一个或多个占位符,用于接收将要放置在该位置的内容。这个占位符就是插槽。
<template>
<div>
<h1>我是组件标题</h1>
<slot></slot>
</div>
</template>
在 Vue 中,插槽被分为以下几类:
- 匿名插槽(Anonymous Slots):没有指定
name的插槽,使用默认的插槽内容。 - 具名插槽(Named Slots):指定了
name的插槽,可以根据指定的名称在父组件中传递不同的内容。 - 作用域插槽(Scoped Slots):具有数据作用域的插槽,让父组件可以将数据传递给子组件并进行处理。
slot插槽使用场景有哪些?
- 组合组件:使用插槽可以将子组件作为父组件的一部分来呈现,实现更加灵活、通用的组件设计和复用。例如,可以将一个复杂的表单拆分成多个小组件,并使用插槽将它们组合起来。
- 具有多种展示方式的组件:通过插槽,我们可以在父组件中根据需要传递不同的内容来改变子组件的展示形式,实现组件的多样化展示。例如,可以创建一个带有两种不同样式的按钮组件,然后在父组件中根据需求传递需要的按钮样式。
- 数据列表:使用作用域插槽可以实现更加灵活的数据列表展示,可以让父组件将数据传递给子组件,并在子组件中进行处理和展示。例如,可以创建一个通用的数据表格组件,然后使用作用域插槽将数据和表格结构传递给子组件,并根据需要进行处理和展示。
五十六、Vue中的data属性为什么是一个函数而不是一个对象
- 在 Vue.js 中,组件的
data属性可以是一个对象或者一个函数 - 但通常建议将其设置为函数。这是因为组件可能会被多次使用,如果
data是一个普通对象,那么该对象会被所有实例共享,导致数据混乱。 - 将
data设置为一个函数可以保证每个组件实例都有自己独立的数据对象,从而避免数据混乱的问题。具体来说,当data选项是一个函数时,Vue.js 在创建新实例时会调用该函数并返回一个全新的数据对象,这样每个实例都拥有独立的数据对象。
技术详解
在 Vue.js 中,我们通常将一个组件的数据定义在 data 属性中。当我们要创建一个新实例时,Vue.js 会将这个 data 对象进行深度拷贝,并返回给新实例作为其数据对象。然而,如果我们每次都使用同一个普通对象来定义 data,那么由于 JavaScript 中对象的引用传递特性,所有实例共享的是同一个对象,这可能会导致一些难以排查的问题。
为了避免这种情况,Vue.js 建议我们将 data 定义为一个函数。这样,在创建新实例时,Vue.js 会调用该函数并返回一个全新的数据对象,从而保证每个实例都有自己独立的数据对象,防止数据混乱的问题。
五十七、Vue渲染列表为什么要加key
- 提升Vue渲染性能:帮助 Vue 跟踪每个节点的身份,进行高效的 DOM 操作;
- 如果不设置
key值,则 Vue 无法准确地知道哪些元素是新增、删除或者移动的,只能重新渲染整个列表,导致性能下降; - 只有通过为每个元素提供唯一的
key值,才能在列表更新时尽量减少不必要的 DOM 操作和页面渲染。
五十八、Vue中的v-show 和 v-if有什么区别
-
v-show 始终会保留对应的 HTML 元素,只是在隐藏时通过 CSS 样式控制元素不可见;而 v-if 会根据表达式的值条件地渲染或销毁对应的 HTML 元素。
-
v-show 是基于 CSS 的切换,切换速度相对较快,但对页面的渲染效率影响较大;而 v-if 是基于 JavaScript 的条件渲染,渲染效率相对较高,但在频繁切换时会创建和销毁 HTML 元素,对页面的性能影响要高于 v-show。
-
v-show 不支持
<template>元素,也不支持 v-else,而 v-if 可以和<template>元素,v-else 指令一起使用。 -
v-show 在初始渲染时会立即渲染出 HTML 元素,而 v-if 只有在表达式的值为 true 时才会进行渲染,因此在一开始时 v-show 的性能要优于 v-if。
-
v-show 适用于频繁切换元素可见性的场景,而 v-if 适用于需要根据条件动态渲染或销毁元素的场景。
综上所述,v-show 和 v-if 在实现机制和使用场景上有所不同,开发者在具体应用时,需要根据实际情况选择使用哪一个。
五十九、为什么避免v-for和v-if在一起使用
Vue 处理指令时,v-for 比 v-if 具有更高的优先级, 虽然用起来也没报错好使, 但是性能不高, 如果你有5个元素被v-for循环, v-if也会分别执行5次。
在Vue.js中,将v-for和v-if一起使用会对性能产生负面影响,因此最好避免这样的用法。
当v-for和v-if一起使用时,Vue.js必须在渲染模板之前计算出每个元素的v-if条件,然后在渲染过程中再应用v-for指令。如果列表中的元素数量很大,这个计算过程就会变得非常耗时,并且会影响页面的渲染性能。
另外,当我们同时使用v-for和v-if时,需要深入理解Vue.js的渲染机制,以确保我们不会出现意想不到的渲染结果。
为了避免v-for和v-if一起使用,我们可以重构代码,将它们拆分成多个嵌套组件或计算属性等解决方案,从而提高性能和可读性。同时,还可以尝试使用computed属性进行筛选,这样可以减少重复计算和内存消耗。
总之,在使用Vue.js时,避免v-for和v-if一起使用是一个良好的习惯,可以提高应用程序的性能和可维护性。
六十、Vue中常用的修饰符有哪些
事件修饰符
-
.stop:阻止事件冒泡 -
.prevent:阻止默认事件 -
.capture:使用事件捕获模式 -
.self:只当事件在该元素本身(比如不是子元素)触发时触发回调 -
.once:只触发一次事件
按键修饰符
-
.enter:回车键(Enter) -
.tab:制表键(Tab) -
.delete(或.del):删除键(Delete 或 Backspace) -
.esc:退出键(Esc) -
.space:空格键 -
.up:上箭头(Up) -
.down:下箭头(Down) -
.left:左箭头(Left) -
.right:右箭头(Right)
系统修饰键
-
.ctrl:Ctrl 键 -
.alt:Alt 键 -
.shift:Shift 键 -
.meta:Meta 键,即 Command 键或 Windows 键
以上是Vue2中常用的事件修饰符、按键修饰符和系统修饰键。
六十一、Vue中 computed 和 watch 的区别
- 核心功能不同:
computed计算属性是一个计算属性,其核心功能是基于已有的数据属性计算得出新的属性值。watch侦听器是监听某个数据的变化并执行回调函数。 - 使用场景不同:
computed适用于那些需要根据已有数据计算得出新值的情况。watch适用于那些需要在数据变化时采取特定操作的情况,例如:异步操作、API 调用等。 - 响应式的原理不同:
computed是一种响应式依赖关系,而watch基于的是“事件监听”。
技术详解
在 Vue 中,computed 和 watch 都是用来处理数据的选项。它们非常相似,但也有一些区别:
- 核心功能不同
computed 是一个计算属性,其核心功能是基于已有的数据属性计算得出新的属性值。当某个依赖的数据发生变化时,computed 会自动重新计算并更新自己的值。因此,可以将 computed 看做是一种“派生状态”。
watch 是一个观察者函数(watcher),可以监听某个数据的变化并执行回调函数。当被监听的数据变化时,watch 将会被触发并执行与之相关的回调函数。watch 的核心功能是“响应式”,它可以对数据变化做出响应。
- 使用场景不同
computed 适用于那些需要根据已有数据计算得出新值的情况,例如:对列表进行过滤、格式化日期等。
computed 的特点是:响应式、缓存和计算结果只有在相关依赖发生变化时才进行更新。
因此,如果一个属性需要频繁计算,又与其他属性有关联,那么使用 computed 可以有效提高应用的性能。
watch 适用于那些需要在数据变化时采取特定操作的情况,例如:异步操作、API 调用等。
watch 的特点是:监听数据变化、执行回调函数并且可以拿到新旧值。
- 响应式的原理不同
computed 依赖于响应式系统的更新机制,当计算属性所依赖的数据发生变化时,会自动触发重新计算。
因此,computed 是一种响应式依赖关系。
watch 使用了 Vue 的观察者机制,底层实现是通过 Object.defineProperty() 来设置 getter 和 setter 函数,并在数据变化时触发回调函数。
因此,watch 基于的是“事件监听”,而不是响应式依赖关系。
综上所述,computed 和 watch 都是处理数据方面的强大工具,但是使用场景以及实现原理有所不同,需要针对具体情况进行选择。
六十二、Vue2中的extends
允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数),和mixins有点类似。
在 Vue.js 2.x 中,extends API 可以用于混合一个对象到组件的选项中。它可以很方便地将多个对象混合成一个对象,并传递给 Vue.js 组件。语法如下:
Vue.component('my-component', {
extends: someComponent,
// 组件选项
})
其中 someComponent 是要被混合的对象,可以是另外一个 Vue.js 组件或一个普通的 JavaScript 对象。通过 extends API 的方式,我们可以将多个组件混合在一起,从而实现一些代码逻辑和功能的复用。
需要注意的是,使用 extends API 时,不应该在被扩展的对象上定义 data 选项,因为该选项应该是一个方法。如果定义了 data 选项,则会导致所有实例共享同一个数据对象,这不是我们所期望的结果。
举个例子,下面示例代码展示了如何通过 extends API 来扩展一个组件:
<!-- 父组件 -->
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Hello World!',
content: 'This is a sample component.'
}
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<h2>{{ subtitle }}</h2>
<p>{{ subcontent }}</p>
</div>
</template>
<script>
export default {
extends: ParentComponent,
data() {
return {
subtitle: 'Sub Title',
subcontent: 'This is a sub content.'
}
}
}
</script>
在上面的代码中,我们将 ParentComponent 组件混合到 ChildComponent 中,并定义了一个新的 data 方法,用于添加子组件的数据选项。这样,我们就可以在 ChildComponent 中使用 ParentComponent 的模板和方法,同时也添加了子组件自己独有的数据。
六十三、Vue2中的Vue.extend
在 Vue.js 2.x 版本中,Vue.extend() 方法被用于创建一个新的 Vue 子类,可以在该子类上扩展一些属性、指令和组件选项等,然后进行实例化。
比如,可以在创建一些类似 loading 式的函数式插件
在 Vue.js 2.x 版本中,Vue.extend() 方法被用于创建一个新的 Vue 子类,可以在该子类上扩展一些属性、指令和组件选项等,然后进行实例化。
它的语法如下:
var SubClass = Vue.extend(options)
其中 options 对象参数用于传递子类的一些选项(如 data, methods, computed 等),继承自父类的选项也可以在 options 对象中设置。通过 Vue.extend() 来创建的子类,可以像父类一样使用 new 进行实例化。
举个例子,在一个 Vue 实例中使用 Vue.extend(),代码如下:
// 父类组件
var ParentComponent = Vue.extend({
props: ['msg'],
template: '<div>{{msg}}</div>'
})
// 子类组件
var ChildComponent = new ParentComponent({
propsData: {
msg: 'Hello, Vue.js!'
}
})
// 手动挂载子类组件到某个元素上
ChildComponent.$mount('#app')
以上代码中,我们通过 Vue.extend() 创建了一个名为 ParentComponent 的组件,该组件具有一个 props 属性,并在模板中使用了该属性。然后,我们可以通过 new ParentComponent() 创建一个名为 ChildComponent 的子类组件,将 propsData 选项用于传递 msg 属性,最后手动将子类组件挂载到 HTML 中。
使用 Vue.extend() 方法可以帮助我们更好地复用组件和逻辑,避免代码重复和冗余。但需要注意的是,在 Vue.js 3.x 中,**Vue.extend()** 方法已经被废弃,使用 **defineComponent()** 方法来进行创建组件的操作。
六十四、简述Vue中mixin、extends 的覆盖逻辑
- Mixin和extends都是Vue中组件复用的方式。
- Mixin在组件中提供了可复用的选项,包括选项对象、方法等。
- extends则主要用于组合类型,可以将多个接口或类组合成一个,从而实现类型的重用。
Mixin和extends在覆盖时的逻辑如下:
-
对于属性合并,Mixin中的属性会被组件中的属性覆盖。当组件与Mixin中都具有同名的属性时,会以组件数据为准,即覆盖Mixin的数据。
-
对于方法合并,Mixin中的方法会被组件中的方法覆盖。当组件与Mixin中都具有同名的方法时,组件的方法会优先于Mixin中的方法执行。
-
生命周期函数的合并顺序为:先执行Mixin中的生命周期函数,再执行组件中的生命周期函数。因此,在使用Mixin时需要注意生命周期函数的命名,避免同名的生命周期函数互相干扰。
-
对于其他选项,Mixin中的选项会被覆盖,与属性、方法的合并逻辑相同。
而对于extends,覆盖逻辑与Mixin基本相同。类似于Mixin,extends中的属性和方法都可以被组件进行覆盖,生命周期函数的执行顺序也是类似的。
总之,Mixin和extends都是用于组件复用的方式,它们的覆盖逻辑会影响到组件的属性、方法、生命周期函数等。理解这些复用方式的覆盖逻辑,可以帮助我们更好地使用Vue构建复杂的应用程序。
六十五、Vue2中怎么封装自定义插件并使用
主要就是暴露一个带install 函数的对象,然后在 main.js 中使用Vue.use。
六十六、vue中的异步组件
异步组件是指在需要时才会加载的组件,在 Vue.js 中通常使用异步组件来优化应用的性能,避免因为加载大量不必要的代码而导致应用首次加载变慢。
在 Vue.js 中,我们可以通过两种方式来定义异步组件:
-
使用工厂函数:将组件定义为一个返回 Promise 的工厂函数。这个 Promise 将在组件需要被渲染时被解析,从而实现组件的异步加载。
-
使用动态 import:使用 ES2015 的 import 函数来定义组件,并且使用 webpack 的代码分割功能来将组件的代码拆分成多个小块。同样可以实现组件的异步加载。
当在需要时渲染该组件时,Vue.js 会自动执行这个工厂函数并加载组件,然后渲染出来。
在 Vue.js 中使用异步组件的好处是能够减少应用的初始加载时间,因为只有在需要使用该组件时才会加载它的代码。这样能够提高应用的响应速度和用户体验。
六十七、Vue中scoped的作用与原理
使用 scoped 的原理是 Vue 会在编译组件时为每个样式规则添加一个唯一的哈希值,然后通过给组件根元素和相关子元素添加属性选择器,将这个哈希值作为前缀,从而实现样式的隔离。具体来说,Vue 将:
<style scoped>
.example {
color: red;
}
</style>
编译成:
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
其中,data-v-f3f3eg9 就是在编译时生成的唯一哈希值。
这样做的好处是,组件样式的作用范围变得更加明确,不会与其他组件样式产生冲突。同时,如果组件被多次引用,每个组件实例都会有自己的唯一哈希值,从而保证了样式的独立性。
需要注意的是,使用 scoped 的样式仅作用于当前组件内部,而不包括子组件。如果希望子组件中的样式也受到作用域的限制,可以通过在子组件的 <style> 标签中也添加 scoped 属性来实现。
总之,使用 scoped 可以让我们更方便地管理组件样式,减少命名冲突和样式污染等问题,是 Vue 中非常实用的特性。
六十八、Vue中beforeDestroy钩子函数的作用
在 Vue 中,beforeDestroy 钩子函数是一个组件实例在销毁之前调用的钩子函数,用于在组件销毁前进行一些必要的操作。
beforeDestroy 钩子函数常用于以下场景:
-
解绑定事件:在组件销毁前解绑组件中绑定的事件,避免内存泄漏。
-
清除定时器:在组件销毁前清除组件中的定时器,避免因为定时器未被清理而导致性能问题。
-
清空数据:在组件销毁前清空组件中的数据,避免数据污染和内存泄漏。
示例代码如下:
export default {
data() {
return {
timerId: null
}
},
created() {
this.timerId = setInterval(() => {
// 定时器处理逻辑
}, 1000)
},
beforeDestroy() {
clearInterval(this.timerId)
}
}
以上示例代码中,当组件实例销毁前,使用 beforeDestroy 钩子函数清除了组件中的定时器,以避免潜在的性能问题。
六十九、Vue2中的mixin混入
在 Vue.js 中,Mixin 是一种对象,它可以包含一组方法和属性,这些方法和属性可以被应用到多个 Vue 组件中。通过混入 Mixin,我们可以让多个组件共享相同的代码逻辑,从而实现代码的复用和可维护性的提高。
优点
- 提高代码的复用性:通过 Mixin 将一组通用的代码逻辑抽离出来,可以在多个组件中共享这些代码,从而避免不必要的代码重复,提高代码的复用性。
- 提高代码的可维护性:使用 Mixin 可以将一些通用的方法、生命周期钩子函数等封装起来,从而更好地维护这些代码,降低出错概率。
- 更好地分离关注点:通过将不同的功能模块封装到不同的 Mixin 中,可以更好地将功能模块与其他代码逻辑分离开来,降低代码的耦合度。
缺点
- 命名冲突:如果多个 Mixin 中定义了相同名称的方法或属性,这样可能会导致命名冲突。在使用 Mixin 时需要注意这一点,避免出现命名冲突的情况。
- 潜在的性能问题:使用 Mixin 可能会引入一些额外的响应式数据对象,这可能会对应用的性能产生一定程度的影响。在使用 Mixin 时需要注意这一点,避免出现性能问题。
七十、Vue中mixin定义与组件相同变量以哪个为主
- 如果在组件中定义了相同名称的变量或方法,则组件中的定义将覆盖 mixin 中的定义。
- 如果在组件中没有定义相同名称的变量或方法,那么 mixin 中的定义将会生效。
Vue 中的变量或方法查找顺序是先查找组件本身是否有定义,如果有则使用组件中的定义,否则才会去查找 mixin 中的定义。这样设计的目的是为了给开发者更大的灵活性,可以根据需要覆盖或继承 mixin 中的内容。
七十一、Vue中先执行父组件的生命周期还是 mixin 的生命周期
在 Vue 中,当一个组件使用了 mixin(混入)时,生命周期钩子函数的执行顺序是先执行 mixin 中的生命周期钩子函数,然后再执行组件本身和父组件的生命周期钩子函数。
具体的执行顺序如下:
- 首先,Vue 会先执行 mixin 中的生命周期钩子函数。
- 然后,Vue 会按照从父到子的顺序依次执行父组件和子组件的生命周期钩子函数,包括
beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy和destroyed。
这样设计的目的是为了确保 mixin 中的逻辑能够在组件本身的生命周期逻辑之前得到执行,以便 mixin 中的功能能够在组件生命周期中发挥作用。因此,如果一个组件同时使用了 mixin,那么 mixin 中定义的生命周期钩子函数会在组件生命周期钩子函数之前被执行。
七十二、Vue-router中使用懒加载的好处
- 懒加载通过实现对打包后的文件进行按需加载和代码分割。将这些内容放在单独的 chunk 中,在需要的时候才进行加载,而不是一开始就全部加载,这可以有效地减少应用的初始化时间、减少资源的浪费和提升用户体验。
- 其实也就是就是因为将这些内容放在了单独的 chunk 中,所以减少了app.js的文件大小,加快了首屏的加载速度。
七十三、Vue-router中如何实现把组件按组分块
webpack
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。
只需要使用命名 chunk,一个特殊的注释语法,也叫:神奇注释或者魔术注释来提供 chunk name (需要 Webpack > 2.4):
const UserDetails = () =>
import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
vite
在Vite中,你可以在rollupOptions下定义分块:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
// https://rollupjs.org/guide/en/#outputmanualchunks
output: {
manualChunks: {
'group-user': [
'./src/UserDetails',
'./src/UserDashboard',
'./src/UserProfileEdit',
],
},
},
},
},
})
七十四、Vue中异步组件与路由懒加载有什么关系
在 Vue.js 中,异步组件和路由懒加载是实现延迟加载的两种常见方式。它们的主要目的都是为了提高应用的性能和加载速度。
- 异步组件是指在需要时才会加载的组件。我们可以使用工厂函数或动态 import 的方式定义异步组件,从而实现组件的异步加载。当在需要时渲染该组件时,Vue.js 会自动执行这个工厂函数并加载组件,然后渲染出来。这样能够提高应用的响应速度和用户体验。
- 路由懒加载也是一种实现延迟加载的方式。通过动态导入 (dynamic import) 的方式,我们可以将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件。这样能够减少应用的初始加载时间,提高应用的性能和用户体验。
可以看到,异步组件和路由懒加载的本质是相同的,都是实现组件的延迟加载。但它们的使用场景略有不同:
- 异步组件适合用于在页面中需要动态加载的组件。
- 路由懒加载适合用于在路由切换时需要动态加载的组件。
在实际开发中,我们可以根据具体的需求选择合适的方式来进行延迟加载。
七十五、Vue-router路由模式有几种,以及两种路由模式的区别
在Vue-Router中,路由模式有两种:哈希模式(Hash mode)和历史模式(History mode)。
- 哈希模式:是Vue-Router的默认路由模式,在url中带有#符号。
- history模式:则是将路由信息作为HTML5历史记录中的一条条目,在url中不带#符号。
技术详解
- 哈希模式是Vue-Router的默认路由模式,其特点是在url中带有#符号。在哈希模式下,所有路由的跳转都会变成url的hash部分的变化,不会触发页面的刷新和服务器的请求,因此可以用来实现单页应用(SPA)的前端路由逻辑。
- 历史模式则是将路由信息作为HTML5历史记录中的一条条目,并且可以通过浏览器前进和后退按钮进行控制。相比于哈希模式,历史模式使得url更加美观,但需要服务器支持更改url。
- 在使用Vue-Router进行开发时,我们可以在创建Vue实例时通过router的mode配置项来指定路由模式
例如:
const router = new VueRouter({
mode: 'history',
routes: [...]
})
需要注意的是,在使用历史模式时,如果我们直接访问某个非根路径的路由,服务器将无法识别这个路由,因此需要在服务器端进行相应的配置,将这些路由重定向到我们程序的入口文件。不同的服务器配置方法可能会不同,需要根据具体情况进行选择和调整。
Vue-Router中history路由模式和hash路由模式有以下几点区别:
-
URL的差异:history模式使用正常的URL路径,例如
http://example.com/user,而hash模式使用URL中的hash值,例如http://example.com/#/user。其中,history模式的URL更加直观和语义化,而hash模式的URL不太友好,但可以实现无刷新更新页面。 -
兼容性的不同:history模式需要浏览器支持HTML5中的history API,而hash模式则兼容所有现代浏览器和较老的IE浏览器。
-
服务器配置的要求:history模式需要在服务器上进行相应的配置,以保证所有非根路径的请求都被重定向到应用入口点,而hash模式则不需要对服务器进行特殊的配置,在任何情况下都可以正常工作。
-
安全性的考虑:history模式可以避免在URL中暴露敏感信息,因为URL中的查询参数、路径参数等信息都可以放在请求体中进行传输,而hash模式则会将所有的信息都暴露在URL的hash部分中,可能会存在一定的安全问题。
七十六、Vue-router中history路由模式的原理
- Vue-Router中的history路由模式是一种使用HTML5中的history API实现前端路由的方式。
- 在这种路由模式下,所有的路由信息都使用浏览器的历史记录进行管理,通过
pushState()和replaceState()方法来更新历史记录中的状态 - Vue-Router会监听到浏览器的popstate事件,并根据当前历史记录中的路由信息重新渲染对应的组件页面。
技术详解
Vue-Router中的history路由模式是一种使用HTML5中的history API实现前端路由的方式。在这种路由模式下,所有的路由信息都使用浏览器的历史记录进行管理,通过pushState()和replaceState()方法来更新历史记录中的状态,从而实现URL的变化和页面的切换。
具体来说,当我们在应用中切换路由时,Vue-Router会调用history API的pushState()方法将新的路由信息添加到浏览器的历史记录中,同时更新浏览器的当前地址栏,但不会进行页面的刷新和访问服务器。此时,如果用户点击浏览器的后退按钮,浏览器就会回退到上一个历史记录中保存的路由状态,同时Vue-Router会监听到浏览器的popstate事件,并根据当前历史记录中的路由信息重新渲染对应的组件页面。
需要注意的是,在使用history路由模式时,我们需要配合服务器进行相应的配置,以使得服务器能够正确地响应对非根路径路由的请求,并将它们重定向到我们的应用程序入口点。一般来说,我们可以在服务器上进行nginx或apache等Web服务器的配置,将所有404错误的请求都转发到我们的应用程序入口点,从而实现路由的正常工作。
总之,Vue-Router的history路由模式通过使用HTML5中的history API来管理路由状态,实现了前端路由的跳转和渲染,从而为前端单页应用提供了更好的用户体验和页面流畅度。
七十七、Vue-router中hash路由模式的原理
- 基于浏览器的hashchange事件,地址变化时,通过window.location.hash 获取地址上的hash值;
- 并通过构造Router类,配置routes对象设置hash值与对应的组件内容。
技术详解
Vue-Router中的hash路由模式是一种使用URL中的hash符号(#)来实现前端路由的方式。在这种路由模式下,所有的路由信息都被编码进URL的hash部分,并由浏览器来管理和维护。
具体来说,当我们在应用中切换路由时,Vue-Router会改变浏览器地址栏中#后面的内容,从而触发浏览器的hashchange事件,并根据当前hash值中保存的路由信息重新渲染对应的组件页面。而且,在hash路由模式下,由于hash部分的变化并不会向服务器发送新的请求,所以我们可以实现前端页面的无刷新更改。
需要注意的是,由于hash值的变化不会向服务器发送新的请求,所以我们可以使用任意的URL来表示同一个路由,例如http://localhost:8080/#/user 和 http://localhost:8080/#/user?id=123 将被视为同一个路由,并被映射到同一个组件页面上。
总之,Vue-Router的hash路由模式通过使用URL的hash部分来管理路由状态,实现了前端路由的跳转和渲染,从而为前端单页应用提供了更好的用户体验和页面流畅度。
七十八、Vue-router中的路由守卫有哪些
- 全局前置守卫(beforeEach):在路由切换之前执行,可以用于进行用户身份验证、权限控制等操作。
- 全局解析守卫(beforeResolve):在路由组件解析之前执行,可以用于执行一些异步操作或动态获取数据。
- 全局后置钩子(afterEach):在路由切换之后执行,通常用于统计页面访问量、页面滚动行为恢复等操作。
- 路由独享守卫(beforeEnter):在单个路由配置中进行设置,在进入该路由之前执行,作用类似于全局前置守卫,但只针对该路由生效。
- 组件内守卫(beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave):在路由实例切换到对应路由组件时,或者组件被复用但路径发生变化时执行。可用于执行一些和组件相关的操作,如数据预取、动画过渡等。
以上是常用的几种路由守卫,使用这些路由守卫可以帮助我们实现更加细致和精准的路由控制和管理,提高应用的用户体验和安全性。
七十九、SPA与前端路由的关系
- SPA(单页面应用)是指只有一张Web页面的应用,与传统的多页面应用不同,当用户与单页应用交互时,它会动态地重写当前页面,而不需要重新加载整个新页面,这可以提高用户的体验,并避免页面之间切换打断用户的操作。
- Vue-router实现了单页应用(SPA)的核心机制之一:前端路由。常用的功能有:定义路由、渲染视图、嵌套路由、路由传参、导航守卫等
SPA(单页面应用)是指只有一张Web页面的应用,它是一种从Web服务器加载的富客户端应用。
与传统的多页面应用不同,当用户与单页应用交互时,它会动态地重写当前页面,而不需要重新加载整个新页面,这可以提高用户的体验,并避免页面之间切换打断用户的操作。
在单页应用中,所有必要的代码,包括HTML、JavaScript和CSS等都只需加载一次,之后只需要局部刷新。SPA常被用于构建Web应用程序和移动应用程序,例如React、Vue.js和AngularJS等框架都是常用的SPA框架。
Vue-router是Vue.js官方提供的一款前端路由库,它实现了单页应用(SPA)的核心机制之一——前端路由。
前端路由的实现原理:是在页面中引入一个根据URL路径改变视图内容的Javascript组件,从而实现在同一个页面下展示不同的内容,而不需要每次请求新的HTML页面。Vue-router通过管理URL和组件之间的映射关系,使得我们可以通过浏览器的前进/后退按钮控制导航,跳转到不同的页面(视图),同时还提供了丰富的路由配置项,例如动态路由、嵌套路由、路由过渡效果等。
Vue-router的主要功能包括:
- 定义路由:通过路由配置项定义路由规则,将URL和对应的组件进行映射。
- 渲染视图:根据当前URL匹配到对应的组件,将其渲染到视图中。
- 嵌套路由:支持子路由、嵌套路由、命名路由等多种路由方式,使得路由配置更加灵活。
- 路由传参:支持通过参数传递数据,包括动态路由、查询字符串、状态对象等多种方式。
- 导航守卫:提供了全局前置守卫、全局后置钩子、路由独享的守卫等多种路由钩子函数,用于控制导航行为和实现路由拦截。
总之,Vue-router提供了一种轻松管理组件之间的映射关系和路由跳转的开发方式,使得前端开发者可以更加专注于业务逻辑的实现,提高了开发效率和用户体验。
八十、Vue-router中router 的区别
$route对象是路由信息对象,包含当前页面的路由信息,如 path、params、query、hash 等。它是一个只读属性。$router对象则是路由实例对象,它是 Vue Router 的核心管理对象,用于创建、定位、切换路由等操作。
技术详解
$route 和 $router 是 Vue Router 中两个常用的对象,它们都与页面路由相关,但具体的作用和使用方式有所不同。
-
$route对象是“路由信息对象”,包含当前页面的路由信息,如 path、params、query、hash 等。它是一个只读属性,可以通过组件中的$route属性来访问,如:export default { mounted() { console.log(this.route.params.id); console.log(this.$route.query.search); } };
上述代码中,我们通过 $route 属性获取了当前页面的路径、参数以及查询字符串等信息,从而实现对页面的精细控制和动态渲染。
-
$router对象则是“路由实例对象”,它是 Vue Router 的核心管理对象,用于创建、定位、切换路由等操作。通过this.$router可以在组件中访问$router对象,如:export default { methods: { goToAbout() { this.$router.push('/about'); } } };
上述代码中,我们通过 $router 对象的 push 方法将页面路由切换至 /about 页面。除此之外,$router 还包括很多常用的方法和属性,如 go、back、forward 等,可以方便地控制和管理页面路由的行为和状态。
总之,router 都是 Vue Router 中的重要对象,在开发 Vue.js 应用时会经常用到。它们之间的差异在于:router 是路由实例对象,提供对路由的导航控制和管理等功能。