大家好,我是林一尼。这是一道关于Vue原理的面试题。如果你能完全理解它,我想这对每个人来说都是非常有用的。
面试问题
1。了解 MPA/SPA 的优点和缺点是什么?
MPA
多页应用程序。
- 作文:有多页
html
作文, - 跳转方式:页面跳转是从一页到另一页
- 刷新方式:整页刷新
- 页面数据跳转:依赖
URL/cookie/localStorage
- 跳转后的资源
会重新加载
- 优点:SEO友好且开发难度较低。
SPA
单页应用程序
- 页面构成:这个被一个shell页面包围,由多个页面片段(组件)组成
- 跳转方式:跳转到shell页面,显示或隐藏fragment页面(组件)
- 刷新方式:页面片段部分刷新
- 页面数据跳转:组件之间传值更方便
- 跳转后的资源
不会重新加载
- 缺点:对SEO搜索不太友好,需要单独配置。开发比较困难,需要专门的开发框架
iframe 基本上就是MPA
,但是可以实现SPA
的一些效果,但是用起来有很多问题。
2。陈词滥调,为什么我们需要这些 MVC/MVVM 模式?谈谈你的 MVC 和 MVVM 模式之间的区别,
目标:借鉴后端思路,职责分工、分层
- Vue 和 React 都不是真正意义上的 MVVM,更不用说 MVC 了。它们的核心只涉及显示层
view
。
MVC模式
对于单向数据,用户的每一步操作都需要重新请求数据库来改变显示层的外观,形成一个封闭的单向循环。例如jQuery+underscore+backbone
。
- M:
model
数据存储层 - 问:
view
:查看图层页面 - C:
controller
:controller js 逻辑层。
controller
控制层对数据层model层
传来的数据进行处理并显示在显示层view层
上,显示层view层
还可以通过控制层model层
接收用户指令,显示在显示层view层
上。数据层model
。所以MVC的缺点是视图层不能和数据层直接交互。
MVVM模式
隐藏controller
控制层并直接操作View
显示层和Model
数据层。
- M:模型数据模型
- Q:查看视图模板
- VM:视图模型-视图数据模板(vue处理的层,vue中定义的属性是处理VM层的逻辑)
双向数据绑定:model
数据模型层通过数据绑定直接影响视图层Data Bindings
,视图层view
还可以通过监听model
修改数据模型层。
- 数据绑定和DOM事件监控是
viewModel
层Vue
主要做的事情。即只要将来自数据模型层Model
的数据挂载到ViewModel
层,就可以实现双向数据绑定。 - 加上
vuex/redux
可用作 的model
数据层。
var vm = new Vue()
vm是view-model
的数据模型层,data:是vmview-model
所在层表示的数据。
- 总结一下两者的区别:MVC的显示层和数据层的交互必须经过控制层。
controller
是单向链接。 MVVM隐藏了控制层controller
,让显示层和数据层直接协同工作,是双向的连接。
3。说说你对 Vue 中响应式数据的理解
小提示:响应式数据是指数据发生变化,视图可以更新,即响应式数据
vue
中实现。该方法内部借用Object.definedProperty()
将属性get/set
添加到每个属性。definedReactive
只能监控外部对象并递归劫持内部对象的数据。
数组- 被重写为7
push pop shift unshift reverse sort splice
来截取数组的数据,因为这些方法会改变原始数组 - 展开:
definedReactive
方法在// src\core\observer\index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 准备给属性添加一个 dep 来依赖收集 Watcher 用于更新视图。
const dep = new Dep()
// some code
// observe() 用来观察值的类型,如果是属性也是对象就递归,为每个属性都加上`get/set`
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 这里取数据时依赖收集
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// childOb 是对对像进行收集依赖
if (childOb) {
childOb.dep.depend()
//这里对数组和内部的数组进行递归收集依赖,这里数组的 key 和 value 都有dep。
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 属性发生改变,这里会通知 watcher 更新视图
}
})
}
上面的Dep(类)是用来做什么的?答:Watcher
和Watcher
是用来收集视图的?答案:watcher
是一个类,用于更新视图的
4。 Vue 如何检测数组的变化?
- vue 并没有对数组中的每一项使用
definedProperty()
来截取数据,而是重写了数组方法push pop shift unshift reverse sort splice
。 - 手动调用通知,通知renderwatcher并执行更新 如果
- 数组中存在对象类型(
对象和数组
),则进行数据拦截。 - 所以通过调整数组下标和数组长度,就不会出现数据拦截,也不会发生响应变化。例如,
arr[0] = 1, arr.length = 2
不响应 - 展开:
// src\core\observer\array.js
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 新增的类型再次观察
if (inserted) ob.observeArray(inserted)
// 手动调用 notify 派发更新
ob.dep.notify()
return result
})
})
5。 Vue 如何依赖直接借记? (dep和Watcher是什么关系)
提示:Dep
是负责收集Watcher
的类,Watcher
是包含发送更新的显示逻辑的类。请注意Watcher 是不能直接更新视图的还需要结合Vnode经过patch()中的diff算法才可以生成真正的DOM
- 每个属性都有自己的
dep
属性来存储依赖的Watcher
。属性更改后,Watcher
会通知您需要更新。 - 当用户(
getter
)获取数据时,Vue 将属性dep
添加到每个要收集的属性(收集为依赖项)Watcher
。当用户setting
设置属性值时,dep.notify()
会通知收集的Watcher
有新视图。详情请参阅上文defineReactive()
Dep依赖收集类
和Watcher类
是多对多的双向存储关系- 每个属性可以有多个
Watcher 类
,因为该属性可以在不同的组件中使用。 - 同时,一个
Watcher 类
还可以匹配多个属性。
6。 Vue 中的模板编译
Vue 中的模板编译:基本上就是将 template
转换为 render
函数。说白了,就是把真实的DOM(模板)
编译成虚拟的dom(Vnode)
- 第一步是将字符串 转换为
ast 语法树
(解析器-解析器)。这里使用了大量的正则化来匹配标签名称、属性、文本等。 - 第二步,将AST标记为静态节点
static
,主要用于虚拟DOM(optimize优化器)渲染优化。这里所有的子节点都被遍历并且也被静态标记为 - 第三步是使用
ast语法树
重新生成代码序列代码。 (codeGen 代码生成器)
为什么需要静态标记节点?如果是静态节点(没有绑定数据,前后不需要变化的节点),那么就不需要diff算法进行比较。
7。生命周期钩子实现原理
- vue中的生命周期钩子只是一个回调函数,在创建组件实例化的过程中调用相应的钩子来执行。
- 使用混合挂钩或在生命周期中定义多个函数,Vue将在内部调用来合并挂钩并将其排队等待执行
- 扩展
// src\core\util\options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal) // 合并
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
8。平凡的生命周期是什么?请求通常发送到哪里?
beforeCreate
:刚刚开始初始化vue实例,在数据观察之前调用observer
,data/methods
之类的属性还没有创建created
:Vue实例已完成初始化,所有属性已创建。beforeMount
:这个钩子在vue链接页面上的数据之前被触发,此时渲染函数被触发。mounted
:el被创建的vm.$el
替换,vue初始化的数据挂载到页面上,在这里可以访问到真实的DOM。通常在这里请求数据。beforeUpdate
:在数据更新时调用,即在虚拟dom再次渲染之前。updated
:数据变化导致虚拟dom重新渲染后发生。beforeDestroy
:该钩子在实例被销毁之前调用,并且实例仍然存在。vm.$destroy
激活两种方法。destroyed
:Vue实例销毁后调用。将联系所有事件侦听器。
请求详情根据具体业务需求来决定发送到哪里ajax
9。 Vue.mixin({})的使用场景和原理
- 使用场景:用于提取公共业务逻辑以供复用。
- 实现原理:调用
mergeOptions()
方法,利用策略模式合并多个属性。如果混合数据与组件的数据冲突,则使用组件自己的数据。 Vue.mixin({})
缺陷: 1. 可能会导致混合属性名与组件属性名发生名称冲突; 2. 数据源依赖问题- 展开
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// some code
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
// 递归遍历合并组件和混入的属性
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
10。老生常谈:为什么vue组件中的数据需要是一个函数?
- 这个和js本身的机制有关。
data
函数中返回的对象引用地址不同,这样可以保证不同组件之间的数据不会互相污染。
如果 Vue.mixin()
与属性data
混合,则data
也必须是一个函数。因为Vue.mixin()
还可以用在多个地方。
在实例- 中,
data
可以是一个对象或函数,因为我们通常在页面上只初始化一个Vue实例(单例)
11。老生常谈:vm.$nextTick(cb)在vue中的实现原理和场景
- 场景:
在 dom 更新循环结束后调用,用于获取更新后的 dom 数据
- 实现原理:
vm.$nextTick(cb)
是异步方式,为了兼容性做了很多降级处理,其次是promise.then,MutationObserver,setImmediate,setTimeout
。数据改变后,视图并不会立即更新,而是通过set
方法通知Watcher
更新,并将需要更新的Watcher
放入异步队列中,回调函数nexTick
放置在 中。 之后,等待主线程中的同步代码执行保持过夜,然后逐一清空队列,以便vm.nextTick(callback)
在dom
更新完成后执行。
将上面一栏的Watcher
一一清除即可得到vue 异步批量更新的原理
。让我想想:为什么不直接使用setTimeout
呢?由于setTimeout
是一个宏任务,多个宏任务的性能会很差。至于事件循环,可以看看JS Event Loop
12。看和计算之间的区别是陈词滥调
computed
是在Object.definedProperty()
的基础上内部实现的
computed
如果依赖值没有改变,缓存功能不会重新计算它。watch
正在监控值的变化。当值改变时,执行相应的回调函数。computed
和watch
都是在Watcher类
的基础上执行的。
computed
缓存功能取决于一个变量dirty
,该变量指示该值是否为脏值。默认值为 true
。取值后是false
。再次取值时,仍取回false
值。
// src\core\instance\state.js computed 取值函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 判断值是不是脏 dirty
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// src\core\instance\state.js watch 实现
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 实例化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
参考
Vue模板编译原理
Vue.nextTick原理及使用
结束
感谢大家到目前为止的阅读。如果你觉得文笔还可以,欢迎来到三联。我是林一伊。直到下一次。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。