- vue 响应式原理
// 调用 walk 方法,遍历 data 中的每一个属性,监听数据的变化。
function walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
// 执行 defineProperty 监听数据读取和设置。
function defineReactive(obj, key, val) {
// 为每个属性创建 Dep(依赖搜集的容器,后文会讲)
const dep = new Dep();
// 绑定 get、set
Object.defineProperty(obj, key, {
get() {
const value = val;
// 如果有 target 标识,则进行依赖收集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 修改数据时,通知页面重新渲染
dep.notify();
}
});
}
// 依赖
class Dep {
// 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型。
static target: ?Watcher;
// subs 存放搜集到的 Watcher 对象集合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 搜集所有使用到这个 data 的 Watcher 对象。
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 搜集依赖,最终会调用上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();
}
}
}
// 监听
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 将 vm._render 方法赋值给 getter。
// 这里的 expOrFn 其实就是 vm._render,后文会讲到。
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 给 Dep.target 赋值为当前 Watcher 对象
Dep.target = this;
// this.getter 其实就是 vm._render
// vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 将当前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一样,会调用 get 方法,更新视图
const value = this.get();
}
}
// 渲染界面时候会实例化Watcher,从而订阅渲染用到的data的属性。
// 渲染的代码如下
const updateComponent = () => {
vm._update(vm._render());
};
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);
// new Watcher 会执行 Watcher.get 方法
// Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// updateComponent 会执行 vm._update(vm._render())
// 调用 vm._render 生成虚拟 dom
// 调用 vm._update(vnode) 渲染虚拟 dom
渲染视图时候实例化 Watcher 并传递参数 getter 为 updateComponent。 实例化时候,调用 Watcher 的 get 方法,这个方法首先执行 Dep.target = this(注意,这是精简后的代码,还有其他与当前无关的逻辑后面会提及),将自身绑定到调用 getter,即 updateComponent。 在执行 updateComponent 的过程中,会用到 data 的某些属性,这样就会触发属性的 get 方法,在上面设置 data 响应式代码中我们看到 get 方法判断如果存在 Dep.target,就将这个依赖收集到 dep 的依赖池(subs)中。 当 data 属性改变,会触发 set 方法,从而调用 dep.notify(),在 dep.notify 方法中调用每个 watcher 的 update 方法,然后将 watcher 加入到异步队列中。 在下个 tic 清空异步队列时候(flushSchedulerQueue)会调用 watcher.run,watcher.run 调用 getter 方法,即 updateComponent,从而更新视图。 简单总结为,在组件初始化时候遍历 data 的属性,为每个属性设置 get 方法和 set 方法,在 get 方法中收集依赖,在 set 方法里通知订阅者,更新视图时候创建订阅者,更新视图时候如果依赖了 data 的某个属性,就会触发这个属性的 get 方法时候,该订阅者(更新视图的方法)就会被 data 的属性收集,在更新属性时候触发 set 方法,从而触发界面更新。
- computed 原理
- 1).在初始化时候实例化 watcher,实例化 watcher 时候对依赖的 data 属性取一次值,从而触发 data 属性收集依赖。当改变 data 属性时,会通知订阅者 watcher,由于 watcher 设置了 lazy 选项,因此会将 watcher 置为 dirty(即数据更新),但不会重新计算。
- 2).设置 computed 的 get 方法,在访问 computed 的时候,判断如果是 dirty,就重新计算,否则直接返回当前的值。
- v-model的作用(双向绑定)
- 1).创建订阅者,当组件的 data 属性改变时候,修改表单元素的 value。
- 2).给表单元素创建事件(change 事件或者 input 事件),事件的回调中,修改组件的数据。
- vue 框架原理
new Vue()之后,Vue 会从根组件开始,遍历组件树,对每个组件做处理.对于一个 vue 组件,vue 首先会进行模板编译,将模板编译为 render 函数,render 函数返回虚拟 dom,同理对子组件进行同样操作.如此形成一个虚拟 dom 树,vue 会把虚拟 dom 映射到真实 dom 并渲染到指定节点上,这就就是视图渲染.vue 在组件初始化的时候会将 data 设置为响应式,并将与之依赖的渲染方法,computed,watch 收集起来.当数据变更后,vue 会依据初始化时收集的依赖,更新视图
- vue3 和 vue2 的区别
- 1). 新增组合式 API,更好地聚合功能代码。
- 2). 全局 Vue API 更改为使用应用程序实例,createApp 创建一个应用实例。例如 Vue.use()改为 createApp().use()。可以定义多个根组件
- 3). 同一元素上的 v-if 和 v-for 的优先级发生改变,v-if 无法访问 v-for 的变量。(vue2 vfor > vif, vue3 反过来)
- 4). teleport,(例,modal 弹窗)
- 5). css 中使用 v-bind,将脚本的变量放入样式中,支持 js 表达式
- 6). 生命周期不同
- vue 父子组件挂载顺序
-
加载渲染过程
-
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created ->子 beforeMount -> 子 mounted -> 父 mounted
-
更新过程
-
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
-
销毁过程
-
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
- vue computed 和 watch 的区别
- 1). 入和出, c: 多入单出, w: 单入多出
- 2). 性能: c: 依赖变化后会标记 dirty,等到访问时才更新视图,若依赖不变,则从缓存中读取值
- 3). 写法: c: 有 return, w: 不一定
- 4). 时机: c: 初次生成赋值时,就开始计算, w: 首次不会运行,除非设置 immediate 为 true
- vue 组件 data 为什么是函数?
如果 data 不是一个函数,或者函数中不是返回一个对象字面量,那么实例化多个组件的话,不同组件间会共享同一个 data,data 的改变会影响到每个实例,这时不符合预期的。
- vue 组件 data 用箭头函数行不行?
- 1). 可以使用箭头函数,但是需要注意 this 指向。
- 2). 如果使用箭头函数,data 函数中的 this 不会指向 vue 实例,如果需要访问 vue 实例,可以通过 data 函数的参数来实现。
- vue 中的 v-if 指令和 v-show 指令区别是什么?
行为不同:v-if 指令在满足条件时候才会渲染 DOM,v-show 一开始就渲染 DOM,满足条件时候才设置 CSS 的 display 属性让元素显示出来。 应用场景不同:一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
- v-for 和 v-if 放在一起用好吗
-
v-if 和 v-for 不要同时使用,因为会在每次渲染时候都要遍历列表并判断是否需要渲染,这个遍历操作其实是有一部分冗余或者完全不必要的。
-
1). 如果是为了过滤一个列表中的项目(v-for 循环,v-if 过滤条件),可以将列表作为计算属性,在 computed 中过滤出需要渲染的列表,再进行渲染。这样避免了每次渲染都计算(只在 computed 依赖的属性变化时候才计算),同时渲染列表是过滤了的,那么循环的次数也可能减少。
-
2). 如果是为了控制整个列表的展示和隐藏(v-for 循环,v-if 判断整个列表是否需要展示),可以将判断条件放到父元素(ul、ol)上。这样展示和隐藏的判断只需要执行一次(在列表最开始)。
-
Vue3 修改了 v-if 和 v-for 的优先级,v-if 没有权限访问 v-for 的变量,这个需要注意。
- vue 组件样式污染
在 style 标签中添加“scoped”属性,可以避免当前组件的 CSS 污染全局。 添加了这个属性后,vue-loader 会给组件的每个元素添加一个 data 属性,并且将 CSS 代码编译,添加这个 data 属性的选择器。
- Vue 如何给一个对象添加新的属性
- 使用 Vue.set(object, propertyName, value)或者 vm.$set((object, propertyName, value)。这两个方法相同。
export function set(target: Array<any> | Object, key: any, val: any): any {
// 判断是否时基础类型/null/undefined
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn();
}
// 判断是否是数组,且索引有效,就直接使用splice替换目标元素
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 如果是对象属性,且不是新的属性,则直接修改
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
- vue的compile过程
-
Vue在实例化组件时候会生成虚拟DOM,Vue先判断是否有render函数,如果有的话调用render生成虚拟DOM;如果没有render函数,则获取template选项,template选项可以是选择器、模版字符串、dom元素,Vue根据template选项进行模板编译;如果没有template,则获取el以及其子内容作为模版。
-
模板编译有三个步骤
-
1). 将模板解析为AST。(Abstract Syntax Tree,抽象语法树)。
-
2). 遍历AST标记静态节点。
-
3). 使用AST生成渲染函数
- vue的computed和watch的实现原理
-
computed: computed是data属性的一个订阅者,它在初始化时候被data属性收集依赖,当computed依赖的data属性改变后,标记该computed为dirty,即数据更改过,当渲染使用到computed时候,再计算出computed的值从而得到最新的正确的值。
-
watch: 在组件初始化时候,遍历所有的watch,对每个watch创建订阅者,绑定依赖的data属性,当data属性改变后发布给订阅者,然后会执行相应地回调。
- vue的模板渲染
Vue会根据将模板编译成render函数,调用render函数生成虚拟dom,然后将虚拟dom映射成真实dom。 当数据变化时候,Vue会触发更新视图,调用render函数返回新的虚拟dom,对比新旧虚拟dom,修改真实dom,从而更新界面
- vue数据双向绑定原理
Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图
-
缺陷:
-
1). 一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出
-
2). 不能监听对象的新增属性和删除属性
-
3). 当监听的下标对应的数据发生改变时,无法正确的监听数组的方法
-
4). vue中通过重写了7个能够改变原数组的方法来进行数据监听(pop/push/shift/unshift/reverse/sort/splice)
-
vue3 proxy优势
-
1). Proxy 可以直接监听对象而非属性;
-
2). Proxy 可以直接监听数组的变化;
-
3). Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具备的;
-
4). Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
-
5). Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
- vue如何检测到数组的变化
- vue2通过重写数组的方法实现(push,pop,shift,unshift,sort,reverse,splice)
- vue3通过proxy数据代理实现
- diff算法
-
从组件根节点开始同层比较
-
vue2:
-
1). 是否值得比较(判断key,key相同值的比较,不同直接创建新节点)
-
2). 引用一致可认为无变化,直接复用;如果新旧节点只是文本发生变化,就替换文本;新旧节点的子节点一个有一个没,那就直接干掉或者创建子节点;如果都有子节点,且子节点引用不一致,updateChildren
-
3). 新旧子节点比较时,头头,尾尾,头尾,尾头,相互比较,存在一致就复用旧节点,如果不一致则在旧节点中查找是否存在可复用的新节点,可以复用就拿来,没有的话,直接创建新节点,然后向中间移动索引,如果索引比较oldStartIndex大于oldEndIndex或者newStartIndex大于newEndIndex则停止.旧节点如果多出来直接干掉,新节点多出来就直接创建
-
vue3:
-
静态标记,非全量diff
-
vue3的diff算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最⻓递增子序列
-
1).Vue 3在创建虚拟DOM树的时候,会根据DOM中的内容会不会发生变化,添加一个静态标记。之后在与上次虚拟节点进行对比的时候,就只会对比这些带有静态标记的节点.
-
2).使用最⻓递增子序列优化对比流程,可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作
-
具体过程: 对新旧vdom的前置元素和后置元素比较,抽出不一致vdom再做最长递增子序列,序列位置不动,其余位置可复用的做位置移动,没有的直接创建,多出来的直接干掉
- vue nextTick vue的nextTick方法的原理及使用场景?
Vue使用MutationObserver/Promise/setTimeout实现nextTick。Vue判断浏览器兼容性,按照MutationObserver -> Promise -> setTimeout的优先级实现nextTick。场景是在修改数据之后,想要访问修改后的DOM时候,可以用Vue.nextTick,在下个循环中访问,这样才能访问到修改后的结果。
- vue组件watch中deep和immediate的作用
- watch有两个选项:deep和immediate,deep决定是否深度监听,即是否监听多层对象数据.
- immediate决定是否立即执行,如果为true,会在绑定时候(初始时候)立即执行,如果为false,只在监听的值变更时候执行。默认为false。
- 讲讲slot?有什么作用?
插槽是Vue 实现的一套内容分发的 API,它支持在渲染一个自定义组件时候,向它“插入”一些内容。这个自定义组件会将起始标签和结束标签之间的内容渲染到自己模板中“slot”组件所在的位置上。Vue的插槽和React的render props的作用相同,React也会将自定义组件开始和闭合标签之间的内容作为children属性挂载到props上,从而在自定义组件中渲染处理。插槽(和render props)提供了更灵活的组件组合和复用方式,在布局等应用场景让代码更直观和易于维护。
- Vue组件为什么采用异步渲染?异步渲染原理是什么?nextTick的原理?
-
vue可以依据数据的变化而更新视图,但是如果每次数据变更都引起视图变更则会引起不必要的渲染,造成性能问题
-
原理:
-
1). data的属性更新
-
2). 通知依赖的render
-
3). render的watcher加入队列(队列去重)
-
4). nextTick清空watcher队列,执行各watcher中的回调
- 自定义指令
- 使用Vue.diractive全局注册或者在组件的directive属性中,通过钩子来实现自定义指令。
- vue2
Vue.directive('my-directive', {
bind: function () {},
inserted: function () {},
update: function () {},
componentUpdated: function () {},
unbind: function () {}
})
- vue3
<script setup>
//在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
//--
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
//
// 没有beforeCreate
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
- Vue.use方法的使用
-
通过调用Vue.use()方法来安装插件,插件是一个构造函数,它必须实现一个install方法,Vue会调用install方法,并传入Vue变量,让插件可以使用Vue来向Vue添加全局功能。
-
vue2
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
- vue3
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}
- vue的eventbus的实现
- 利用vue实例的off,$emit来订阅,解绑,和发送事件,需要的组件会订阅相关内容以获取最新的状态,实现组件通信,状态共享. 所以
Vue.prototype.$eventBus = new Vue()
- vue父子通信方式,兄弟通信方式
-
父子通信:
-
1).props
-
2).事件
-
3).ref
-
兄弟通信
-
1).eventBus
-
2).vuex
- vdom的好处,为什么要使用vdom
-
1). 创建真实DOM的代价高 : 直接操作 dom 是有限制的,比如:diff、clone 等操作,⼀个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff ⼀些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了
-
2). 触发多次浏览器重绘及回流 : 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
-
3). 方便实现跨平台
- 为什么Vue是渐进式框架?
渐进式的概念:分层设计,每层可选,不同层可以灵活接入其他方案。
那么Vue的设计分这样几层
1、declarative rendering(声明式渲染)
2、component system(组件系统)
3、client-side routing(前端路由)
4、state management(状态管理)
5、build system(构建系统)
使用Vue时候可以只用它核心的视图层,也可以使用其路由、状态管理、脚手架等,如果用户有个人偏好也可以接入其他解决方案,因此Vue是渐进式的框架。
- 什么是MVVM?与MVC有什么区别?
MVC将应用抽象为数据层(Model)、视图层(View)、逻辑层(controller),降低了项目耦合。但MVC并未限制数据流,Model和View之间可以通信。
MVP则限制了Model和View的交互都要通过Presenter,这样对Model和View解耦,提升项目维护性和模块复用性。
而MVVM是对MVP的P的改造,用VM替换P,将很多手动的数据=>视图的同步操作自动化,降低了代码复杂度,提升可维护性。
那么什么是MVVM?MVVM是一种软件架构设计模式,它抽离了视图、数据和逻辑,并限定了Model和View只能通过VM进行通信,VM订阅Model并在数据更新时候自动同步到视图。
- 讲一下vue的keep-alive组件,什么时候会用到?
keep-alive是一个内置组件,它所包裹的组件会在不同的渲染中缓存状态。用在需要让自定义组件在不同的渲染中保持状态不变的场景。例如一些表单组件,如果已经填写好一些内容,然后切到其他组件,再切换回表单时候,应该保持已经填写好的内容,这时候可以选择使用keep-alive。