1.响应式数据的理解
数组和对象类型当值变化时如何劫持到。对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。
这里在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出vue3中是使用proxy来实现响应式数据)
补充:
内部依赖收集是怎样做到的,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新(其实后面会讲到每个对象类型自己本身也拥有一个dep属性,这个在$set中讲解)
这里可以引出性能优化相关的内容:(1)对象层级过深,性能就会差(2)不需要相应数据的内容不要放到data中(3)Object.freeze()可以冻结数据
Src/core/observer/index.js:135
2.vue如何检测数组变化
数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写。
补充:
在vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持。
那如果想更改索引更新数据怎么办?可以通过Vue.$set()来进行处理,核心内部用的是splice方法
Src/core/observer/array.js:8
3.vue中模板编译原理
如何将template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有compiler包的vue体积也会变大。默认.vue文件中的template处理是通过vue-loader来进行处理的并不是通过运行时的编译-后面我们会说到默认vue项目中引入的vue.js是不带有compiler模块的)。
(1)将template模板转换成ast语法树-parserHTML
(2)对静态语法做静态标记-markUp
(3)重新生成代码-codeGen
补充:
模板引擎的实现原理就是new Function + with来进行实现的
Vue-loader中处理template属性主要靠的是vue-template-compiler模块
Src/compiler/index.js:11
4.生命周期钩子是如何实现的
vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
补充:
内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布。
Src/core/util/options.js:146
Core/instance/lifecycle.js:336
5.vue.mixin的使用场景和原理
Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。
补充:
mixin中有很多缺陷“命名冲突问题”、“依赖问题”、“数据来源问题”,这里强调一下mixin的数据时不会被共享的。
Src/global-api/mixin:5
6.nextTick在哪里使用?原理是?
nextTick中的回调是在下次dom更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的dom。原理就是异步方法(promise,mutationObserver.setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)
补充:
Vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的dom。
7.Vue为什么需要虚拟dom?
Virtual dom就是用js对象来描述真实dom,是对真实dom的抽象,由于直接操作dom性能低但是js层的操作效率高,可以将dom操作转化成对象操作,最终通过diff算法比对差异进行更新dom(减少了对真实dom的操作)。虚拟dom不依赖真实平台环境从而也可以实现跨平台。
补充:
虚拟dom的实现就是普通对象包含tag、data、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)。
Src/core/vdom/vnode:3
8.vue中的diff原理
vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针的方式进行比较。
(1)先比较是否是相同节点
(2)相同节点比较属性并复用老节点
(3)比较儿子节点,考虑老节点和新节点儿子的情况
(4)优化比较:头头,尾尾,头尾,尾头
(5)比对查找进行复用
Vue3中采用最长递增子序列实现DIFF算法
Src/core/vdom/patch:501
9.Vue.set方法是如何实现的
为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组
Src/core/observer/index:202
10.vue的生命周期方法有哪些?一般在哪一步发起请求及原因
beforeCreate: 在实例初始化之后,数据观测(data observer)和event/watcher事件配置之前被调用。
created: 实例已经创建完成之后被调用。在这一步,实例已完成以下配置:数据观测(data observer),属性和方法的运算,watch/event事件回调。这里没有$el
beforeMount: 在挂载开始之前被调用:相关的render函数首次被调用。
mounted: el被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate: 数据更新时调用,发生在虚拟dom重新渲染和打补丁之前
updated: 由于数据更改导致的虚拟dom重新渲染和打补丁,在这之后会调用该钩子
beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed: vue实例销毁后调用。调用后,vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
补充:
Created: 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务端渲染支持created方法)
Mounted: 实例已经挂载完成,可以进行一些dom操作
beforeUpdate: 可以在这个钩子中进行异步地更改状态,这不会触发附加的重渲染过程。
Updated: 可以执行依赖于dom的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
Destroyed: 可以执行一些优化操作,清空定时器,解除绑定事件。
11.vue组件间传值的方式及之间的区别?
*props和$emit父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件在做到的
*$parent,$children获取当前组件的父组件和当前组件的子组件。
*$attrs和$listeners A->B->C。Vue 2.4开始提供了$attrs和$listeners来解决这个问题
*父组件中通过provide来提供变量,然后在子组件中通过inject来注入变量。
*$refs获取实例
*envetBus平级组件数据传递这种情况下可以使用中央事件总线的方式
*vuex状态管理
模板 -> ast -> codegen -> render -> 虚拟dom -> 渲染成真实dom
(1)props实现:src/core/vdom/create-component.js:101 src/core/instance/init.js:74 src/core/instance/state:64
(2)事件机制实现:src/core/vdom/create-component.js:101 src/core/instance/init.js:74 src/core/instance.js:12
(3)Parent&children实现:src/core/vdom/creat-component:js:47 src/coer/instance/lifecycle.js:32
(4)Provide&inject实现:src/core/instance/inject.js:7
(5)$attrs&$listener实现:src/core/instance/render.js:49 src/core/instance/lifecycle.js:215
(6)$refs实现:src/core/vdom/modules/reg.js:20
12.$attrs是为了解决什么问题出现的?应用场景有哪些?Provide/inject不能解决它能解决的问题吗?
$attrs主要的作用就是实现批量渲染传递数据。provide/inject 更适合应用在插件中,主要是实现跨级数据传递。
13.vue的组件渲染流程
(1)父子组件渲染的先后顺序
(2)组件是如何渲染到页面上的
*在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签
*在创建虚拟节点是,获取组件的定义使用 vue.extend生成组件的构造函数。
*将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的$mount方法。
*所有组件的创建过程是先父后子。
Src/core/vdom/patch:125
14.vue中组件的data为什么是一个函数?
每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。
Src/core/util/option:121
15.说一下v-if和v-show的区别
V-if在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。V-show会被编译成指令,条件不满足时控制样式将对应节点隐藏(内部其他指令依旧会继续执行)。
频繁控制显示隐藏尽量不使用v-if,v-if和v-for尽量不要连用,应该用computed计算出可用数据再v-for
Src/compiler/codegen/index.js:155
Src/platforms/web/runtime/directives/show.js:155
16.Vue.use是干什么的?原理是什么?
Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。
Src/core/global-api/use.js:5
17.Vue-router有几种钩子函数?具体是什么及执行流程是怎样的?
路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫
完整的导航解析流程:
(1)导航被触发
(2)在失活的组件里调用beforeRouterLeave守卫
(3)调用全局的beforeEach守卫。
(4)在重用的组件里调用beforeRouterUpdate 守卫。
(5)在路由配置里调用beforeEnter。
(6)解析异步路由组件。
(7)在被激活的组件里调用beforeRouterEnter。
(8)调用全局的beforeResolve守卫
(9)导航被确认
(10)调用全局的afterEach钩子
(11)触发dom更新
(12)调用beforeRouterEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
18.Vue-router两种模式的区别?
hash模式 history模式
hash模式:hash + hashChange 兼容性好但不美观
history模式:history + popState 虽然美观,但是刷新会出现404需要后端进行配置
19.函数式组件的优势及原理
函数式组件的特性:无状态,无生命周期,无this
Src/core/vdom/create-component.js:164 src/core/vdom/create-functional-component.js:5
20.v-if与v-for的优先级
v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for再解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。
Src/compiler/codegen/index.js:55
21.组件中写name选项有哪些好处及作用?
(1)可以通过名字找到对应的组件(递归组件)
(2)可用通过name属性实现缓存功能(keep-alive)
(3)可以通过name来识别组件(跨级组件通信时非常重要)
Src/core/vdom/create-element.js:111
22.Vue 事件修饰符有哪些?其实现原理是什么?
事件修饰符有:.capture .once .passive .stop .self .prevent
Src/compiler/helpers.js:69 src/compiler/codegen/event.js:42 src/core/vdom/helpers/update-listeners.js:65
23.Vue.directive源码实现?
把定义的内容进行格式化挂载到 Vue.options 属性上
Src/core/global-api/assets.js
24.如何理解自定义指令?
指令的实现原理,可以从编译原理 => 代码生产 => 指令钩子实现进行概述
(1)在生成ast语法树时,遇到指令会给当前元素添加directives属性
(2)通过genDirectives生成指令代码
(3)在patch前将指令的钩子提取到cbs中,在patch过程中调用对应的钩子
(4)当执行指令对应钩子函数时,调用对应指令定义的方法
Src/compiler/helpers.js:42
Src/compiler/codegen/index.js:309
Src/core/vdom/patch:70
Src/core/vdom/modules/directives:7
25.谈一下你对vuex的个人理解
vuex是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例new Vue)
衍生的问题 action和mutation的区别
核心方法:replaceState subscribe registerModule namespace(modules)
26.vue中slot是如何实现的?什么时候用它?
普通插槽(模板传入到组件中,数据采用父组件数据)和作用域插槽(在父组件中访问子组件数据)
27.Keep-alive平时在哪里使用?原理是?
Keep-alive主要是缓存,采用的是LRU算法。最近最久未使用法。
Src/core/components/keep-alive.js
28.$refs是如何实现的?
将真实dom或者组件实例挂载在当前实例的$refs属性上
29.Vue中使用了哪些设计模式?
*工厂模式:传入参数即可创建实例(createElement)
根据传入的参数不同返回不同的实例
*单例模式:
单例模式就是整个程序有且仅有一个实例
*发布-订阅模式
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
*观察者模式:watcher & dep 的关系
*代理模式(防抖和节流) => 返回替代 (例如:vue3中的proxy)
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
*装饰模式:@装饰器的用法
*中介者模式 => vuex
中介者是一个行为设计模式通过提供一个统一的接口让系统的不同部分进行通信。
*策略模式:策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。
*外观模式 *适配器模式 *迭代模式 *模板方法模式
30.谈谈vue3和vue2的区别
(1)对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)
(2)大量的API挂载在vue对象的原型上,难以实现TreeShaking
(3)架构层面对跨平台dom渲染开发支持不友好
(4)CompositionAPI 受ReactHook启发
(5)对虚拟dom进行了重写,对模板的编译进行了优化操作