Vue基础:
谈谈对组件的理解
- 组件化开发能大幅提高应用开发效率、测试性、复用性
- 常用的组件化技术:属性、自定义事件、插槽
- 降低更新范围,值重新渲染变化的组件
- 高内聚、低耦合、单向数据流
说说函数组件的优势和原理
- 正常的一个组件是一个类继承了Vue,函数式组件,就是一个普通的函数。
- 函数式组件的特性:无状态、无生命周期、无this。因此性能会高一点。
什么是单页面应用?
单页面应用指的是进入应用程序的时候只会请求一个html文件,用户与应用程序交互时会动态更新页面的内容。虽然浏览器url会发生变化,但是没有新的html文件请求,原理是通过监听浏览器url的变化,然后js会动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前的页面上,整个过程始终只有一个html文件,通过异步请求来实现无刷新响应功能,所以叫单页面应用。
优点:
- 分离前后端关注点,前端负责界面显示,包括页面跳转逻辑;后端则只负责数据管理(存储和计算),各司其职,不会把前后端的逻辑混杂在一起。
- 减轻服务器的压力,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力提高好几倍。
- 同一套后端程序代码/接口,可以多处复用,不仅是同一个应用不用修改就可以用于web界面、手机、平板等多种客户端,同时也可以复用在不同应用上。
缺点:- 首次加载速度慢;
- 对 SEO 不友好,但是可以通过 Prerender 等技术解决一部分;
- 前进、后退失效,需要程序进行管理。
谈谈对Vue的理解
Vue是一套用于构建用户界面的渐进式框架,它的两个核心思想是数据驱动和组件化。Vue的核心库只关注视图层,不仅容易上手,还方便整合与第三方库或既有项目。当与现代化的工具链以及各种支持类库结合使用时,Vue也可以为复杂的单页面应用提供驱动。在Vue核心库(声明式渲染和组件系统)的基础上,我们还可以通过添加路由 、状态管理、构建工具、ui组件等额外工具来构建一个完整的框架。最重要的是,这些功能相互独立,可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。这就是Vue渐进式的好处。
怎么理解自底向上逐层应用?
由基层开始做起,把基础的东西写好,再逐层往上添加效果和功能。
**Vue的两个核心: **
数据驱动
和组件化
如何让 CSS 值在当前的组件中起作用?
在当前组件的style标签中添加scoped属性,会給当前组件的元素动态添加一个data-v-xxxxx唯一属性,然后使用定义样式的时候,会使用属性选择器去选择唯一属性。
Vue常用修饰符有哪些?
1. 表单修饰符:
- .trim修饰符的作用类似于JavaScript中的trim方法,自动去除用户输入内容中首尾两端的空格;
- .number修饰符的作用是将值转成数字,但是先输入数字的话,只取前面数字部分,先输入字母的话,number修饰符无效;
2. 事件修饰符:- .stop修饰符的作用是阻止冒泡;
- .capture事件默认是由里往外冒泡,capture修饰符的作用是反过来,由外往内捕获;
- .once修饰符的作用是,绑定的事件只执行一次;
- .prevent修饰符的作用是阻止默认行为;
- .self修饰符作用是,只有点击事件绑定的本身才会触发事件;
- .native修饰符是加在自定义组件的事件上,保证事件能执行。
3. 鼠标修饰符:- .left、.middle、.right这三个修饰符是鼠标的左中右按键触发的事件。
- 按键修饰符:
.enter监听键盘Enter键。
v-if和v-show的区别?
- v-if是真正的条件渲染,会被转化成三元表达式,如果条件不成立是不会渲染当前指令所在节点的 dom 元素,即dom元素是不存在的,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
- v-show只是基于css的display:none或者block来对当前 dom 进行隐藏或者显示。
- v-if有更高的切换开销,而v-show有更高的初始渲染开销,如果需要非常频繁地切换,则使用v-show较好,如果在运行时条件很少改变或者需要更高的安全性,则使用v-if较好。
- 共同点:都能控制元素的显示和隐藏。
v-if和v-for的优先级:
在 vue 2.x 中,在一个元素上同时使用时, v-for的优先级高于v-if。
在 vue 3.x 中,在一个元素上同时使用时, v-if 的优先级高于 v-for 。
不要将v-if和v-for写在一起,在vue 2.x中v-for的优先级高于v-if,每次渲染都会先循环再进行条件判断,带来性能方面的浪费;在vue 3.x中v-if的优先级高于v-for,如果v-if的条件依赖于v-for的循环,v-if的条件还不存在,会导致异常。
解决方式:
- 如果条件不依赖循环,可以将v-if放在外层盒子或者template上;
- 如果条件在循环体内,可以使用computed先将数据进行过滤,然后再使用v-for循环渲染。(在computed里过滤的成本远比用v-if的成本低得多)或者 将v-for放在template上,将v-if放在元素上。
<div id="app">
<div v-for="item in [1,2,3,4,5]" :key="item" v-if="item === 3">{{ item }}</div>
</div>
vue2编译后:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, _l(([1, 2, 3, 4, 5]), function (item) {
return (item !== 3) ? _c('div', {
key: item
}, [_v(_s(item))]) : _e()
}), 0)
}
}
会先进行v-for循环,然后进行条件判断,如果item === 3时,会执行_e()注释函数,否则正常创建元素。
vue3编译后:
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
(_ctx.item !== 3)
? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, _renderList([1,2,3,4,5], (item) => {
return _createElementVNode("div", { key: item }, _toDisplayString(item), 1 /* TEXT */)
}), 64 /* STABLE_FRAGMENT */))
: _createCommentVNode("v-if", true)
]))
}
先进行条件判断,如果item === 3时,只执行注释方法_createCommentVNode("v-if", true),否则的话才会去执行v-for循环。但是这里item变量来自form循环,还未循环,所以会报错。
Vue 组件通讯有哪几种方式
- props 和$emit:父组件向子组件传递数据是通过 props 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的,父组件注册这个事件进行接收;
- $parent,$children 获取当前组件的父组件实例和当前组件的子组件实例的属性和方法进行通讯;
- $refs 获取组件实例的属性和方法;
- 父组件中通过 provide 来提供变量,然后后代组件中通过 inject 来注入变量;
- eventBus 通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信;
- vuex 任意组件之间的通讯,可以使用状态管理。
vue中eventBus的使用
eventBus 又称为事件总线是,用于组件间通讯的一种方法,它实例化了一个Vue实例,内部仅仅只是它实例方法而已,通过$emit触发事件传递数据,通过$on监听事件的触发,执行回调获取数据,通过$off注销事件,因此它非常的轻便。
1. 定义eventBus,建立eventBus.js文件,挂载到Vue实例上,暴露出去:
import Vue from 'vue';
let bus = new Vue();
Vue.prototype.$eventBus = bus;
2. 触发事件
this.$eventBus.$emit('eventName', param1, param2,...)
3. 监听事件
this.$eventBus.$on('eventName', (param1, param2,...)=>{
// 需要执行的代码
})
4. 移除监听事件
为了避免在监听时,事件被反复触发,通常需要在页面销毁时移除事件监听。或者在开发过程中,由于热更新,事件可能会被多次绑定监听,这时也需要移除事件监听。
this.$eventBus.$off('eventName');
computed和watch的区别?
- computed 是计算属性,依赖其他属性计算值,可以是一个也可以是多个,computed 的值有缓存功能,只有当它依赖的属性值发生变化的时候,在下一次获取 computed 值 的时候才会重新计算。计算属性是对象时,用户可以自行设置 getter 方法和 setter 方法。使计算属性具有可读可写的功能,当对computed进行赋值操作时,会调用 setter 方法,setter接收到修改的值,可以在setter里对依赖属性进行修改。如果 计算属性 是函数的话,那么默认使用 getter 方法,函数的返回值就是 计算属性 的属性值。计算属性不能执行异步操作。
// 计算属性赋值
<div>{{computedSetter}}</div>
<button @click="computedSetterFn">测试计算属性赋值</button>
computed: {
computedSetter: {
get() {
return this.valueMsg + ' computedSetter'
},
set(val) {
this.valueMsg = val
}
},
},
methods: {
computedSetterFn() {
this.computedSetter = '小刘今天很开心呀~'
}
}
- watch 只有在被监听的属性值发生变化时才会执行handler回调函数,有两个参数,一个是回调函数,一个是配置对象。在回调函数中可以进行一些逻辑操作和异步操作,有两个参数,第一个是新值,第二个是旧值;配置对象中通过设置immediate,可以让watch监听器初始化的时候是否立即触发回调函数;如果监听的是嵌套对象的话,可以通过设置deep进行深度监听。
计算属性(computed)一般用在模板渲染中,适合处理一个值依赖了一个或多个响应式数据(多个值影响一个值),利用其缓存的特性,避免每次获取值时,都要重新计算;
侦听属性(watch)适用于观测某个值的变化去完成一段复杂的业务逻 或者 执行异步操作(一个值影响多个值)。
为什么 data 是一个函数?
因为Vue中的组件是可以复用的,但组件中的数据是私有的。 对象是引用类型,当组件中的data数据是对象时,由于地址相同都指向了同一个data对象。当在一个组件中修改data时,其他复用的组件中的data也会被同时修改;组件中的Data是一个函数的话,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。(new Vue 实例里(根实例),data 可以直接是一个对象,是因为,它不会被复用,因此不存在对象的引用问题。)
在Vue2中,动态给vue的data添加一个新的属性时会发生什么?怎样解决?
问题:数据虽然更新了,但是视图没有更新。
原因: vue2是通过object.defineproperty实现对象属性的拦截监听的,在初始化的时候data对象中的所有属性都添加object.defineproperty进行Observer响应观测。当我们访问定义的属性或者修改属性值的时候会触发 getter 或者 setter。但是新添加的属性没有被添加Observer观测,也就没有通过object.defineProperty来为属性添加 getter 和 setter 进行监听,没有设置成响应式。
解决方案:
- Vue.set:通过Vue. set向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新;
- Object.assign:添加到对象的新属性不会触发更新,可以通过创建一个新的对象,使用object.assign,合并原对象和混入对象的属性来实现响应式;
- $forceUpdate:迫使vue实例重新渲染,仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
总结:
- 如果为对象添加少量的新属性,可以直接采用Vue.set;
- 如果需要为新对象添加大量的新属性,则通过object .assign()将新对象合并到响应式对象;
- 实在不知道怎么操作时, 可采取$forceUpdate()进行强制刷新,但是不建议。
自定义指令
vue指令分为
全局自定义指令
和局部自定义指令
。
全局指令使用Vue.directive('指令名',方法) 方法注册,局部的指令在组件内部使用directive选项。
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置;
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中);
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新;
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用;
- unbind:只调用一次,指令与元素解绑时调用。
注册全局自定义指令:
Vue.directive('focus', {
/* 每当指令绑定到元素上的时候,会立即执行这个bind函数,只执行一次 */
// 注意:在每个函数中,第一个参数,永远是el,表示被绑定了指令的那个元素,这个el参数,是一个原生的JS对象
// 在元素 刚绑定的时候,还没有插入到DOM中去,这时候调用focus方法,没有作用
bind: function(el){
// el.focus()
},
/* inserted 表示元素插入到DOM中的时候,会执行inserted函数【触发一次】 */
// 和JS行为有关的操作,最好在inserted中去执行,防止JS行为不生效
inserted: function(el){
el.focus()
},
/* 当VNode更新的时候,会执行updated,可能会触发多次 */
updated: function(){
},
componentUpdated: function (el, binding, vnode) {
},
unbind: function (el, binding, vnode) {
}
});
局部自定义指令注册:
directives: {
/* 每当指令绑定到元素上的时候,会立即执行这个bind函数,只执行一次 */
// 注意:在每个函数中,第一个参数,永远是el,表示被绑定了指令的那个元素,这 个el参数,是一个原生的JS对象
// 在元素 刚绑定的时候,还没有插入到DOM中去,这时候调用focus方法,没有作用
bind: function(el){
// el.focus()
},
/* inserted 表示元素插入到DOM中的时候,会执行inserted函数【触发一次】 */
// 和JS行为有关的操作,最好在inserted中去执行,防止JS行为不生效
inserted: function(el){
el.focus()
},
/* 当VNode更新的时候,会执行updated,可能会触发多次 */
updated: function(){
},
componentUpdated: function (el, binding, vnode) {
},
unbind: function (el, binding, vnode) {
}
}
// Vue3
app.directive('my-directive', {
mounted(el, binding, vnode) {
// 1. 当指令被绑定到元素上时调用。此时元素已经插入到 DOM 中。
// 2. 通常用于执行与 DOM 元素相关的初始化操作,例如添加事件监听器或初始化组件。
},
updated(el, binding, vnode) {
// 1. 当指令所绑定的组件更新时调用。
// 2. 在此钩子中,你可以根据组件的数据变化来更新 DOM 元素。这通常用于响应式更新,例如根据数据变化动态改变元素的样式或内容。
},
unmounted(el, binding, vnode) {
// 1. 当指令被解绑时调用。
// 2. 在此钩子中,你可以执行清理操作,例如移除事件监听器或销毁组件。这有助于避免内存泄漏和其他潜在问题。
}
})
什么时候使用自定义指令?
自定义指令是用来操作DOM的,当有业务有需要操作DOM的时候,可以考虑使用自定义指令。
mixins
什么是mixins: mixins又叫混入,是一种分发Vue组件中可复用功能的方式,混入对象可以包含组件的任意选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
Mixins 的优缺点?
优点:
- 提高代码复用性和可维护性。
- 允许您在多个组件之间共享和重用代码。
缺点:
- Mixins 可能会导致组件之间的依赖关系不清晰。
- Mixins 可能会导致命名冲突。
- 由于 Mixins 可以包含任意数量的属性和方法,因此测试和调试 Mxins 中的代码可能会更加困难。
注意:如果你在 Mixins 中定义了一个 watch 监听器,那么每当一个组件使用这个 Mixins,Vue 就会为该组件创建一个新的监听器。但是每个组件都会拥有自己的监听器实例,而不是共享同一个监听器。
Vue的钩子函数:
什么是vue生命周期?
Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
vue生命周期总共有几个阶段?
它可以总共分为8个阶段:创建前/后, dom挂载前/后,更新前/后,销毁前/销毁后。
第一次页面加载会触发哪几个钩子?
会触发 下面这几个beforeCreate, created, beforeMount, mounted 。
DOM 渲染在哪个周期中就已经完成?
DOM 渲染在 mounted 中就已经完成了。
vue生命周期的作用是什么?
它的生命周期中有8个钩子,让我们在控制整个Vue实例的过程时更容易理解和进行相应的操作。
首次异步请求可以在哪个钩子函数中发起?
- 可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。
- 在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性。
在哪个阶段有$el, 在哪个阶段有$data
- beforeCreate 啥也没有
- created 有data 没有el
- beforeMount 有data 没有el
- mouted 都有
如果加入了keep-alive会多哪两个生命周期函数
activated 和 deactivated
如果加入了keep-alive, 第一次进入组件会执行哪些生命周期函数?
beforeCreate
created
beforeMount
mounted
activated
如果加入了keep-alive, 第二次或者第N次进入组件会执行哪些生命周期函数?
只执行一个生命周期函数: activated
Vue的钩子函数有哪些?(8个常用生命周期钩子函数+keep-alive2个)
vue常用的钩子函数有八个,分别是beforeCreate、created、beforeMount、mounted、befereUpdate、updated、beforeDestroy、destroyed;这八个常用的钩子函数是vue的生命周期,从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程。它可以让我们在控制Vue的整个实例过程时更容易的理解和进行相应的操作。如果使用了keep-alive,还有两个钩子函数activated和deactivated,这两个钩子函数在被缓存的页面里触发,activated是进入页面时触发,deactivated离开时触发。当第二次或第n次进入被keep-alive缓存的页面时,Vue的生命周期钩子函数会失效,只有activated会被触发。
生命周期钩子的作用:
- beforeCreate(创建前):在实例初始化之后,数据观察和事件配置(初始化)之前调用;在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
- created(创建后):在实例创建完之后被立即调用。完成数据观测、属性和方法的运算以及事件回调。在此阶段可以对data里的数据进行赋值操作。但是,挂载阶段还未开始,$el属性目前尚不可使用(不可见),如果想要进行Dom进操作,可以通过vm.nextTick来访问Dom.
- beforeMount(挂载前):在挂载开始之前被调用。相关的render函数首次被调用,编译模板,但是还没有挂载到目标节点上,该钩子函数在服务器端渲染期间不被调用;
- mounted(挂载后):实例被挂载后调用。render函数编译好的模板去替换el属性指向的DOM对象(el被新创建的vm.$el替换),完成真实 Dom 的挂载。在当前阶段,可以访问到 Dom 节点,可以初始化一些第三方的插件。
- befereUpdate(更新前):在数据更新时,虚拟DOM打补丁之前调用。这里适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器,该钩子函数在服务器端渲染期间不被调用。
- updated(更新后):在数据更新后、虚拟DOM重新打补丁和渲染后调用该钩子函数。当这个钩子函数被调用时,组件DOM已经更新,可以执行依赖DOM的操作。然而在大多数情况下,要避免在此期间操作数据,防止陷入死循环。如果要操作数据(响应状态改变),通常最好使用计算属性或watch监听器进行操作。该钩子函数在服务器端渲染期间不可被调用;
- beforeDestroy(销毁前):实例销毁之前调用,在此阶段,实例仍然存在,在此阶段常用于销毁定时器或者解绑事件。该钩子函数在服务器端渲染期间不被调用;
- destroyed(销毁后):实例销毁后调用。该钩子被调用后,Vue实例的所有指令都被解绑,所有的事件监听器都被移除,所有的子实例也都会被销毁,该钩子函数在服务器端渲染期间不可被调用。
Vue3.0生命周期的一些调整:
- vue3生命周期舍弃了beforeCreate和created两个生命周期,用setup取而代之,setup在beforeCreate和created之前调用;
- beforeDestoy和destroyed的名称改为onUnBeforeMount和onUnMounted;
- 其他的生命周期函数在前面加了on,功能没有改变。
什么是keep-alive?
keep-alive 是vue的一个内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。(自身不会渲染成一个dom元素,在包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,主要用于保留组件状态避免重复渲染导致的性能问题。)
keep-alive有以下三个属性:
• include 白名单 只有名称匹配的组件会被缓存
• exclude 黑名单 任何匹配的组件都不会被缓存
• max 最多缓存多少个实例,一旦达到这个数字,新实例被创建之前,会销毁已缓存组件中最久没有被访问的实例。
• 注意:include 和 exclude 首先检查组件的 name 属性,如果 name 不可用,则匹配局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配,其中 exclude 的优先级比 include 高。
keep-alive的特点是什么?
• 它是一个抽象组件,自身不会渲染一个 dom 元素,也不会出现在组件的父组件链中。
• 当组件在 keep-alive 内被切换,组件的 activated 和 deactivated 这两个生命周期钩子函数会被执行。组件一旦被缓存,再次渲染就不会执行 组件的 部分生命周期钩子函数(beforeUpdate和updated不受缓存影响)。
• 要求同时只有一个子组件被渲染。
使用 keep-alive 缓存组件 的一些问题
问题1:如果缓存的组件过多,或者是把不必要的组件也缓存了,会造成内存占用过多。
- 按需缓存:只缓存那些重要、高频或者是不怎么变化的组件。给要缓存的路由做个标记,然后在载入路由时,动态决定是否要缓存,精确控制要缓存的组件。
- 设置缓存上限:如果可能的话,设置一个缓存组件的最大数量,当达到这个数量时,可以自动清除最久未使用的组件。
问题2:需要更新的组件被缓存了。
- activated 钩子:再次进入被缓存的页面时,可以使用activity钩子函数重新请求数据更新数据。
组件缓存的优化写法:
在定义路由时,额外添加路由[元信息],来补充一些信息要素。
{
path: '/',
component: () => import('../views/home/index.vue'),
// 是否缓存
meta: {isKeepAlive: true}
}
在app.vue中根据meta元信息来决定是否缓存组件
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.isKeepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.isKeepAlive"/>
</div>
keep-alive的设计初衷
有些业务场景需要根据不同的判断条件,动态地在多个组件之间切换。频繁的组件切换会导致组件反复渲染,如果组件包含有大量的逻辑和dom节点,极易造成性能问题。其次,切换后组件的状态也会完全丢失。keep-alive的设计初衷就是为了保持组件的状态,避免组件的重复渲染。
keep-alive的使用场景
首页进入到详情页,如果用户在首页每次点击都是相同的,那么详情页就没必要请求N次了,直接缓存起来就可以了,当然如果点击的不是同一个,那么就直接请求,具体结合keep-alive的两个生命周期函数activated和deactivated使用。
slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的一种内容分发机制。插槽slot是子组件模板中的一种特殊标签,插槽中的内容是否显示、怎么显示由父组件决定。slot又分为默认插槽、具名插槽和作用域插槽。
默认插槽:又名匿名插槽,当slot没有指定name属性值的时候就是默认插槽,一个组件只能有一个默认插槽。
具名插槽:带有name属性的插槽也就是具名插槽,一个组件可以有多个具名插槽。
作用域插槽:是默认插槽、具名插槽的变体,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染传给插槽的元素。实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.slot中默认插槽为vm.slot.default,具名插槽为vm.slot.xxx,xxx为插槽名。当组件执行渲染函数时候,它会生成一个VNode,遇到slot标签,会根据slot的name属性去vm.slot中的查找对应的内容进行替换。如果插槽是作用域插槽,则可通过渲染函数的参数来接收传递的数据并进行替换。
组件中配置name选项的好处?
- 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 );
- 可以通过 name 属性实现缓存功能(keep-alive);
- 可以通过 name 来识别组件(跨级组件通信时非常重要);
- 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的。
Vue原理:
Vue源码定义的initState函数内部的初始化顺序:props>methods>data>computed>watch
怎么理解Vue的单向数据流
Vue的单向数据流一般是指数据从父组件传到子组件,子组件没有权力直接修改父组件传来的数据,父级属性值的更新会自行下行流动到子组件中,但是反过来则不行。这样可以防止子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解。每次父级组件发生更新时,所有子组件中的 props 都将会刷新为最新的值。这意味着不应该在一个子组件内部去改变 props的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改父组件的数据时,正确的做法是通过 $emit 派发一个自定义事件,父组件注册接收到后,由父组件修改。
在子组件中直接使用v-model绑定父组件传过来的数据是不合理的,如果希望修改父组件传给子组件的值,可以使用以下两个方法:
- 在子组件data中创建一个变量,用props来进行初始化,再改变这个data中的值;
- 子组件使用$emit发出一个事件,让父组件接收去修改这个值。
请说一下keep-alive的原理。
keep-alive,在组件切换过程中会将包裹的组件实例的状态保留在内存中,防止重复渲染DOM。keep-alive有一个abstract属性为true,确保其不会渲染成DOM元素。在created阶段,初始化一个cache对象、keys数组,cache用来存缓存组件的虚拟dom,keys用来存缓存组件的key值。在mounted阶段实时监听include、exclude这两个的变化,并执行相应的缓存操作。keep-alive是调用render函数来执行缓存操作的,它会获取到keep-alive包裹的第一个组件以及它的组件名称,判断此组件名称是否被白名单、黑名单匹配,如果没有被白名单匹配 或者 被黑名单匹配,则直接返回VNode,不进行缓存。否则会判断组件key是否存在,如果不存在会根据组件ID、tag生成缓存唯一key值,并在缓存集合cache中查找是否已缓存过此组件。如果已缓存过,直接取出缓存组件,并更新缓存key在keys中的位置,将该组件的key重新插入的keys数组的末尾。如果没缓存过,则分别在cache、keys中保存此组件以及他的缓存key,并检查数量是否超过max,如果超过则删除最久没有被访问的组件,也就是缓存中的第一个。
请说明nextTick的原理。
nextTick在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。Vue是异步更新视图的,当数据发生变化时,Vue将开启一个异步任务,视图需要等所有数据更新完成之后再统一进行更新,避免了每次数据更新都会引发不必要的渲染。
nextTick会接收一个回调函数cb作为参数,回调函数cb会被推入到callbacks回调队列数组里,然后会根据全局参数pedding判断全局是否已经开启异步任务,如果没有开启,则调用timerFunc开启一个异步任务,并将pedding设置为true,避免同一时间开启多个异步任务。timerFunc开启的异步任务会根据当前环境判断使用哪个异步任务,根据宏任务和微任务的特性,判断使用顺序为 Promise => MutationObserver => setImmediate => setTimeout。在异步任务里会执行flushCallbacks,它会复制一份callbacks队列,然后清空全局callbacks队列,同时将pending的锁放开,然后遍历执行新队列里方法。
computed计算属性原理:
- 计算属性通过创建Watcher实例和使用Object.defineProperty来实现对计算结果的监听和更新。在初始阶段,vue会遍历计算属性对象中的每一个计算属性,通过创建一个watcher实例,将用户 的getter函数传入watcher去获取值。计算属性创建的 Watcher 使用了lazy配置,因为计算属性的watcher 不会立即执行,因为要考虑到该计算属性是否会被使用,如果没有使用,就不会得到执行,默认值为undefined,结果值会保存在value。订阅者Watcher的dirty属性是计算属性的核心,用于判断是否需要重新求值,在计算属性中默认为true。同时还为每个计算属添加Object.defineProperty进行监听,(如果用户设置了set,这个set会被设置到Object.defineProperty的setter上)并将该计算属性添加到Vue实例上。
- 当计算属性被使用的时候会触发 getter 也就是createComputedGetter方法会被触发,如果当前dirty为true,会执行watcher.evaluate方法求值,并将dirty设置为false。当计算属性依赖的数据发生变化的时候,会通知到计算watcher,执行计算watcher的update方法,将dirty置为true。在下次访问 计算属性 值的时候会触发computedGetter方法,判断dirty的值,如果dirty为true,将调用watcher.evaluate方法重新求值。
watch监听器:
vue在 initState 方法中执行 initWatch 方法注册用户监听器watch。
initWatch 方法对 参数watch 对象进行遍历,当对象的属性值为数组时,对数组进行遍历执行 createWatcher 方法;如果watch对象的属性值不为数组,则直接执行 createWatcher方法。watch监听器有对象、数组、字符串用法,createWatcher方法会处理watch监听器的兼容性写法,并调用 $watch创建用户监听器watch。handler是用户监听器上的回调函数,用户自定义监听器有一个options配置项,immediate 为 true 表示初始化立即执行 cb 函数。 $watch方法中会创建一个订阅者watcher,它的user属性被设置为true,表示它是用户监听器的一个标志。在Watcher订阅者构造函数中,用户watcher初始化会执行 this.get方法对监听属性进行依赖收集,获取监听属性的初始值,并将值缓存起来;watcher的deep属性为true时会执行traverse方法对监听的属性进行深度监听,通过递归的触发数组或对象的get进行依赖收集。
当监听的属性发生变化时,会触发属性的 setter方法,执行属性对应的 dep.notify 方法,会通知到用户监听器 watcher 执行 watcher.update方法进行更新。最终会执行watcher.run方法,该方法会执行 this.get方法获取监听属性的新值,判断新值和缓存的旧值是否相等,不同的话执行 用户设置的 handler 回调函数;这便是 watch 实现 监听的原理。
双向数据绑定和数据响应式的区别?
- 响应式是指通过数据区驱动DOM视图的变化,是单向的过程。
- 双向数据绑定就是无论用户更新View还是Model,另一个都能跟着自动更新。
对于MVVM的理解?
MVVM是Model-View-ViewModel的缩写。
Model:模型层(数据层),主要用于保存一些数据;
View:视图层,负责将数据模型转化成 UI 展现出来,同时也可提供用户操作的入口;
ViewModel:视图模型层,该层是mvvm的核心层,主要作为Model和View两个层的数据连接层,负责两个层之间的数据传递。
- 在MVVM架构下,View和Model之间并没有直接的联系,而是通过ViewModel进行交互。当视图发生变化,会及时通知数据发生变化;当数据发生变化,会及时对视图进行更新。用户只需关注业务逻辑,不需要手动操作DOM,也不需要关注View和Model的同步工作。
为什么说VUE没有完全遵循MVVM?
出于某些特别场景方便的目的,Vue允许通过指令(自定义指令)、$refs等直接操作DOM,而不是完全遵循MVVM模式要求的通过ViewModel自动映射View。只不过建议克制。
Vue双向数据绑定原理(Vue是如何实现MVVM的):
- Vue实现了一个监听器 Observer ,通过递归遍历为每个属性添加Object.defineProperty设置getter和setter进行劫持监听。当属性被访问的时候,会触发getter,在getter里通过dep.depend,将订阅该属性变化消息的订阅者watcher收集起来。当该属性被修改时,会触发setter,setter会调用dep.notify去通知该属性上所有相关的订阅者watcher,订阅者watcher会调用自身的update方法做出相应的回调,更新数据或视图。
- Vue实现了一个compile模板指令解析器,会对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者 Watcher,并将模板中的变量替换成数据或者绑定相应的函数,当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
(1) 在自身实例化时会调用其get方法获取当前被劫持属性对应的值,此时会将当前Watcher实例添加到Dep.target上作为全局唯一Watcher引用。当触发属性的getter方法,往属性订阅器(dep)里面添加自己。
(2) 自身必须有一个update()方法,待属性变动收到dep.notice()通知时,能调用自身的update()方法,触发数据、视图的更新。- Vue整合Observer、Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
v-model的原理:
在 vue 中可以通过v-model 指令来实现双向数据绑定( input、textarea、select 等元素上创建双向数据绑定),v-model只是一个语法糖而已, 通过为指定的属性绑定响应式数据,当数据发生变化的时候通过指定事件更新绑定的属性。 v-model 在内部为不同表单控件做了不同处理。
- input 和 textarea 控件使用 value 属性和 input 事件;
- checkbox 和 radio 控件使用 checked 属性和 change 事件;
- select 控件使用value 属性和 change 事件。
以input控件为例:可以通过v-bind:绑定响应式数据和触发oninput 事件传递数据修改响应式数据
<input v-model="searchText">
<input v-bind:value="searchText" v-on:input="searchText=$event.target.value">
底层原理:通过监听控件上事件值的变化,修改data里对应属性。通过defineProperty来监听每一个属性的get,set方法,从而一旦某个属性重新赋值,就能监听到变化并更新到视图层。
如何实现响应式的:
- Vue是采用数据劫持配合发布者订阅者模式的方式来实现数据响应式的。
- Vue实现了一个Dep,负责收集管理依赖该属性的Watcher订阅者,Dep对象内部有一个subs数组,用于存储订阅该属性消息变化的Watcher订阅者。
- Watcher订阅者将视图与数据关联起来(是Observer和Compile之间通信的桥梁),主要做的事情是:
(1) 在自身实例化时会调用其get方法获取当前被劫持属性对应的值,此时会将当前Watcher实例添加到Dep.target上作为全局唯一Watcher引用,当触发属性的getter方法,往属性订阅器(dep)里面添加自己。
(2) 自身必须有一个update()方法,待属性变动收到dep.notice()通知时,能调用自身的update()方法,触发数据、视图的更新。
- Vue实现了一个Observer观察者,在数据初始化的时候会对data数据进行递归遍历,为每一个属性创建一个Dep实例,负责收集管理依赖该属性的Watcher订阅者。并为所有属性添加Object.defineProperty设置getter和setter进行劫持。
- 当属性被访问的时候,会触发getter,在getter里调用dep.depend方法,将订阅该属性变化消息的订阅者watcher收集起来。
- 当该属性被修改时,会触发setter,setter会调用dep.notify去通知该属性上所有相关的订阅者watcher,订阅者watcher会调用自身的update方法做出相应的回调,更新数据和视图。
Vue2数组响应式:
Vue2实现检测数组变化的方法,是通过重写7个常用的数组方法。Vue2在methodsToPatch数组里定义了push、pop、shift、unshift、splice、sort、reverse这七个需要重写的方法。通过Object.create创建了一个基于数组原型的空对象(目的是为了不污染原生数组),在data 数据 进行Observer观测的时候,将数据是数组的隐式原型(__proto__)指向自定义的数组原型。遍历methodsToPatch数组,通过def方法为每个重写的方法添加object.defineProperty数据劫持,然后执行原生数组对应的方法获取结果,并将其返回值返回出去,同时还会调用dep.notify方法通知所有相关的订阅者watcher做出更新。对push、unshift、splice这三个可以添加新元素的数组方法做特殊处理,定义inserted变量判断是否有新增元素,如果有新增元素则通过响应式数据定义的不可枚举属性__ob__调用Observer的成员方法observeArray来为新增的属性添加响应式。
有两种情况无法检测到数组的变化。
- 当利用索引直接设置一个数组项时,例如 vm.items[indexOfItem] = newValue
- 当修改数组的长度时,例如 vm.items.length = newLength
Object.defineProperty 缺点?
对象新增或者删除的属性无法被 监听到,只有对象本身存在的属性修改才会被劫持。
Object.defineProperty数据劫持方式对数组有什么影响?
使用递归的方式其实无论是对象还是数组都可以进行观测,但是如果 data 包含数组比如 a:[1,2,3,4,5] 那么我们根据下标直接修改数据也能触发 setter,但是如果一个数组里面有上千上万个元素,给每一个元素下标都添加 getter 和 setter 方法 对于性能来说是承担不起的,所以Vue用此方法劫持对象,重写数组原型上的七个常用方法来劫持数组。
vue2重写了哪几个数组原型上的方法?
vue2重写了七个数组原型上的方法,分别是push、pop、shift、unshift、splice、sort、reverse。
实例挂载的过程中发生了什么?
- 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化和建立更新机制。
- 初始化会合并选项配置、创建组件实例、初始化事件,创建各种响应式数据、执行挂载前的一些生命周期钩子函数等。
- 建立更新机制会立即执行一次组件更新函数,首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
整个new Vue阶段做了什么?
执行了vue属性和方法的初始化,将其挂载到Vue原型上。还会执行Vue的构造函数,内部执行了this._init方法。合并了选项API的配置项,初始化事件、初始化data数据等各配置项,进行响应式观测。调用beforeCreate和created钩子函数;接着会判断vm.$option.el是否存在,如果存在使用vm.$mount方法挂载渲染成真实dom。
接着判断有无render方法,优先使用render方法,没有render方法,对DOM或者template进行解析编译,生成render方法。在执行$mount挂载中会执行一个mountComponent方法,这里会实例化一个渲染Watcher,渲染Watcher会调用_update方法执行patch方法对真实DOM进行渲染,同时首次执行渲染函数会创建它内部响应式数据之间和渲染Watcherd 依赖关系。还会执行挂载阶段的beforeMount和mounted生命周期,更新阶段会调用beforeUpdate生命周期。
https://www.jb51.net/javascript/285126z9t.htm
模板编译原理(Vue complier):
- 模板编译的最终目的是将template模板编译成render函数,complier 的主要作用是解析模板,生成渲染模板的 rende,render函数是用来生成虚拟DOM的。
- render函数的优先级是最高的,如果有render函数,优先使用render函数;没有render函数,会将template模板或者DOM元素解析成render函数。
- 步骤:
- 模板编译会先调用parse方法将模板解析成抽象语法树AST,然后使用generate方法将AST生成渲染函数。由于静态节点不需要总是重新渲染,所以在生成AST之后,生成渲染函数之前这个阶段,会调用optimize方法做一个优化操作:遍历一遍AST,给所有静态根节点做一个标记,将静态根节点static标记为true,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就会跳过比较。最后使用generate 方法将ast树转换成render函数。
- 在大体逻辑上,模板编译分三部分内容:
1、parse:接收 template 模板,根据模板的节点和数据生成对应的 AST;
2、optimize:遍历AST的每一个节点,标记静态节点,减少静态节点在新旧节点(diff)中的对比,提高性能;
3、generate :把前两步生成完善的 AST,组成 render 字符串,然后将 render 字符串通过 new Function 的方式转换成渲染函数。
虚拟DOM:
模板转换成视图的过程
- Vue通过将template 解析成AST,然后将AST编译转换成渲染函数(render ) ,执行渲染函数(render ) 就可以得到一个虚拟DOM;
- 在对 Model数据状态 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。
在Vue中,视图的更新是组件级别的。在实例化一个组件的时候,我们可以传入template或自定义render函数来完成组件视图的构建。对于传入template的这种情况,Vue会对其进行编译,生成AST树,根据AST树来生成render函数,将render函数赋值给vm实例属性_render。从源码中可以看到,在实例化组件渲染watcher的时候,会传入一个updateComponent函数,当依赖发生变化的时候,会重新调用updateComponent函数,生成新的vnode,patch到DOM上。
简单的讲,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。
vue抽象语法树AST和虚拟dom的区别是什么?
虚拟dom是将真实Dom以js对象的形式进行抽象表示,而抽象语法树AST则是对语法结构进行抽象表示。抽象语法树AST的终点是渲染函数。渲染函数既是抽象语法树AST的产物,也是虚拟Dom的起源。抽象语法树AST 只会在 编译阶段 出现,它主要用来描述 template 模版。而虚拟Dom 只会在 运行时出现,用js对象来描述整个 DOM 树,每个render周期会产生一次虚拟Dom。
对虚拟DOM的理解(为什么需要虚拟dom?)
从本质上来说,虚拟DOM是对真实DOM的抽象,用js对象来描述DOM节点,使整个DOM结构更加轻巧,以便于实现更高效的DOM更新。
虚拟DOM的基本结构:
虚拟DOM用 tag来描述元素、组件,attrs描述属性,children描述子节点,text描述文本属性,elm描述真实的dom节点,key描述虚拟节点的唯一值。
虚拟DOM的优缺点:
优点:
- 小修改无需频繁更新DOM,更新时会结合对比算法(diff算法)对上次保存的虚拟DOM进行对比,找出最小修改更新到DOM上。从这样可以避频繁修改和重绘回流,提高性能。
- 与渲染工具配合使用,使跨平台渲染成为可能;
*虚拟DOM时通常会进行数据绑定和转义操作,在插入HTML内容可避免直接注入恶意脚本,可以防范XSS的攻击;- 避免了直接操作浏览器原生API时遇到的兼容性问题,并且减少了频繁访问和操纵真实DOM带来的开销。
缺点:- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比直接渲染慢;
- 在特定场景下(如需要高率动画效果),使用虚拟 DOM 可能会致性能下降。这是因为 diff 算法需要花费时间进行比较和计算变化部分,并生成新的 DOM 树。
虚拟DOM比真实DOM快吗?
虚拟DOM比真实DOM快这句话其实是错的,虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM,虚拟DOM和虚拟DOM算法是两种概念,虚拟DOM算法 = 虚拟DOM + Diff算法。
Diff算法的原理:
新旧虚拟DOM对比的时候,Diff算法只会在同层级进行比较, 不会跨层级比较。 所以Diff算法是:深度优先算法,时间复杂度:O(n)。
patch方法
patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改,而不是将整个视图根据新的VNode进行重绘。patch的核心是diff算法,diff算法只对新旧VNode进行同层比较。
这个方法作用就是对比当前同层的虚拟节点是否为同一种类型的节点
没有新节点,直接触发旧节点的destory钩子;
没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接新建,所以只调用 createElm;
旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode 去处理这两个节点;
旧节点和新节点不一样,直接创建新节点,删除旧节点。
sameVnode方法
patch关键的一步就是sameVnode方法,sameVnode方法判断是否为同一类型节点,其中判断新旧节点是否为同一节点的依据就有节点的唯一key值和标签名。
function sameVnode(a, b) {
return (
a.key === b.key && // key值是否一样
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag && // 标签名是否一样
a.isComment === b.isComment && // 是否都为注释节点
isDef(a.data) === isDef(b.data) && // 是否都定义了data,data包含一些具体信息,例如onclick , style
sameInputType(a, b)) || // 当标签为input时,type必须是否相同
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
);
}
patchVnode方法做了以下事情:
- 找到对应的真实DOM,称为el
- 判断newVnode和oldVnode是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将el的文本节点设置为newVnode的文本节点。
- 如果oldVnode有子节点而newVnode没有,则删除el的子节点
- 如果oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
patchVnode里最重要的一个方法updateChildren,新旧虚拟节点的子节点对比
就是首尾指针法,新的子节点集合和旧的子节点集合,各有首尾两个指针
- oldS 和 newS 使用sameVnode方法进行比较,sameVnode(oldS, newS)
- oldS 和 newE 使用sameVnode方法进行比较,sameVnode(oldS, newE)
- oldE 和 newS 使用sameVnode方法进行比较,sameVnode(oldE, newS)
- oldE 和 newE 使用sameVnode方法进行比较,sameVnode(oldE, newE)
- 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnode 的 key 去找出在旧节点中可以复用的位置。
- 循环完毕后会通过新旧节点头尾指针的大小情况判断是否还有多余的节点,如果旧节点的头指针大于尾指针,执行添加操作;如果新节点的头指针大于旧节点的尾指针,执行删除操作。
什么是Diff算法?
Diff算法是一种对比算法,新旧虚拟DOM进行对比,在以新的VNode为标准的情况下,找到更改的虚拟节点,在oldVNode上做小的改动,只更新这个虚拟节点所对应的真实节点,而不用更新其他数据未发生改变的节点,实现精准地更新真实DOM,进而提高效率。
两个特点:
- Vue Diff算法只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
diff算法总结:
- Diff算法是一种对比算法,新旧虚拟DOM进行对比,在以新的VNode为标准的情况下,找到更改的虚拟节点,在oldVNode上做小的改动,实现精准高效的DOM更新。
- vue2中diff算法通过sameVnode判定新旧VNode节点是否为同一节点,如果只有新节点,直接创建DOM,执行初始化操作。如果新旧VNode不是同一节点,则新VNode替换到旧VNode,然后创建DOM。否则如果新旧VNode是同一节点才会调用patchVnode对比子节点,那么patchVnode的过程是这样的,如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可;如果老节点没有子节点而新节点存在子节点,先清空oldVNode的文本内容,然后为oldVNode加入子节点;当vNode没有子节点而oldVNode有子节点的时候,则移除oldVNode的所有子节点;当新老节点都无子节点的时候,只是文本的替换。如果新旧虚拟节点都有子节点,调用updateChildren函数对比并更新子节点。updateChildren函数为新旧节点的子节点都定义了头尾两个指针,给他们进行变量标记,在遍历过程中这几个变量都会向中间靠拢,当新旧虚拟DOM其中一个的开始指针和结束指针重合的时候循环结束。在循环体内,新旧虚拟DOM会进行头头、尾尾、交叉进行快速对比,通过递归调用patchVnode函数进行内容的更新。如果以上4种情况都不满足,则会去遍历oldVNode得到一个key和索引一一对应的集合,然后用vNode(newStartVnode)节点 的 key去集合里查找对应的key,如果没有找到或者找到了元素不一样,就创建一个新的DOM元素,否则使用patchVnode更新虚拟节点的内容,同时将vNode移动到oldVNode前面。如果循环结束后,oldVNode循环先结束,则vNode有多出来的节点,需要给oldVNode添加新的节点;如果vNode先循环完毕,则删除oldVNode多出来的节点。
Vue的diff算法是同级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针方式比较
- 先比较两个根节点是不是相同节点
- 相同节点比较属性,复用老节点
- 再比较儿子节点,考虑老节点和新节点儿子的情况
- 优化比较:头头、尾尾、头尾、尾头
- 比对查找,进行复用
虚拟DOM中key的作用
- key是虚拟DOM的唯一标识,是diff的一种优化策略,根据key,可以更准确的判断两个节点是否为同一节点;
- 在vue2 sameNode函数中,key是判断两个虚拟DOM是否为同一节点的重要标识。如果key为undefined,这时候undefined是恒等于undefined,那么可能会误认为两个虚拟DOM是相同的。
- 如果没有key,当在更新已渲染过的元素列表时,Vue默认采用“就地复用”策略。如果数据项的顺序被改变,由于key值都是undefined会被误认为相同节点,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是直接修改对应索引下已渲染元素的内容;
- 如果有key值,会尽可能地通过对比、哈希查找、移动等方式找到原来的DOM,精准地找到需要更新、删除、添加的位置,避免不必要的修改。
Vue2和Vue3的区别:
1. 生命周期的变化
- vue3生命周期舍弃了beforeCreate和created两个生命周期,用setup取而代之,setup在beforeCreate和created之前调用;
- beforeDestoy和destroyed的名称改为onUnBeforeMount和onUnMounted;
- 其他的生命周期函数在前面加了on,功能没有改变。
2. 多根节点
- Vue2中,编写页面的时候只允许有一个根节点,其他标签必须被包裹在根节点中,否则会报错;
- Vue3支持了多个根节点组件--fragment,可以少写一层。
3. Teleport
vue3提供了Teleport组件可将部分Dom移动到vue app之外的位置,比如Dialog组件。
4. 组合式API
- vue2是选项式API
逻辑组织: 一个逻辑会在不同的位置,比如可能在data、methods、computed等。当代码复杂庞大的时候,导致代码可读性差,一段业务逻辑可能需要上下来回翻看。
逻辑复用:Vue2使用mixin进行逻辑复用,当一个组件混入大量不同的 mixins 的时候,可能会发生命名冲突、数据来源不清晰的问题。- vue3是组合式API
逻辑组织:将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就只需到那个hooks函数中去修改;
逻辑复用:通过编写成多个hooks函数的方式,解决了数据来源不清晰和出现命名冲突的问题。
5. 响应式原理
Vue2响应式原理基础是ES5的一个API Object.defineProperty;采用Object.defineProperty对数据进行劫持,结合 发布订阅模式的方式来实现的。
Vue3响应式原理基础是ES6的ProxyAPI,来对数据进行代理,通过reactive()函数给每一个对象都包一层Proxy,通过 Proxy 监听属性的变化,从而实现对数据的监控。
相比于vue2版本,使用proxy的优势有:
- Object.defineProperty只能监听对象的某个属性,不能对整个对象进行监听,需要采用遍历+递归的方式为对象的每个属性设置 getter、setter进行监听,这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,Vue2额外增加了一个$set()api来对动态新增的对象新属性实现响应式;
Object.defineProperty不能对数组的变化进行监听,因此Vue2采用重写数组七个原型上的方法来实现对数组的监听;
在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象属性都变成响应式的,这无疑会有很大的性能消耗。- proxy可以对整个对象进行监听,不需要再去用遍历递归的方式来监听属性,它还可以监听到动态添加或删除的对象的属性,所以Vue3移除了$set()api;
proxy还可以监听数组,不用再去单独的对数组做特异性操作,通过Proxy可以直接拦截所有对象类型数据的操作,完美支持对数组的监听。
在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次属性的变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是只有真正访问到内部的属性才会变成响应式,而不是无脑递归,可以说是按需实现响应式,减少性能消耗。
6. 虚拟dom
Vue3 相比于 Vue2 虚拟DOM 上增加patchFlag字段。
7. Diff 优化
和vue2相比,增加了静态标记;
Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用;
静态标记,是为了給会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较;
patchFlag字段帮助 diff 区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
8. 打包优化
Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,它是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,进而删除那些没有使用的代码。
在vue2中,一些全局API是在vue实例上,即使未用过,也无法检测到该对象的哪些属性在代码中被使用到。
9. TypeScript支持
Vue3 由TS重写,相对于 Vue2 有更好地TypeScript支持。
setup() 函数特性:
- setup 函数时,它将接受两个参数:(props、context(包含attrs、slots、emit));
- setup函数在 生命周期 beforeCreate 和 Created 两个钩子函数之前执行;
执行 setup 时,组件实例尚未被创建(Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了undefined);- 与模板一起使用:需要返回一个对象 (在setup函数中定义的变量和方法最后都是需要 return 出去的 不然无法在模板中使用);
- setup函数只能是同步的不能是异步的。
注意事项:
- setup函数中不能使用this。Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了undefined);
- setup 函数中的 props 是响应式的,当传入新的 props 时,它将被更新。但是,因为 props 是响应式的,不能使用ES6的解构操作,因为它会消除 prop 的响应性。如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成解构操作:
import {toRefs} from 'vue'
setup(props) {
const { name } = toRefs(props);
console.log(name.value);
onMounted(() => {
console.log('name: ' + props.name);
})
}
Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢?
当访问到深层嵌套对象的话,会触发getter,会判断当前 Reflect.get 的返回值是否是已经代理过的对象,如果是一个普通对象则再使用 reactive 方法做代理, 这样就实现了深度观测。
Vue项目中的性能优化
1. 代码层面的优化:
- 正确使用v-if和v-show;
- v-if和v-for不要一起使用;
- 使用v-for的时候給每个列表项添加唯一key值;
- 展示性数据用object.freeze()方法冻结不可修改;
- 如果需要使用 v-for 给每项元素绑定事件时使用事件代理;
- 使用keep-alive按需缓存一些页面;
- 组件销毁前销毁定时器;
- 路由使用懒加载;
- 第三方模块采用按需导入;
- 采用es模块来编写模块、方法,在使用的时候按需导入模块、方法;
- 长列表采用分页器分页或者虚拟滚动、滚动加载;
2. Webpack 层面的优化:- 为项目目录配置别名;
- 生产移除console;
- 第三方太大的包生产采用cdn的方式引入;
- 压缩代码;
- 使用splitChunks拆分代码,抽离公共文件;
- 优化 SourceMap;