原文首地址 掘金 https://juejin.cn/post/6965837907181436936
三连哦 更多好文 https://github.com/lurenacm/againJS
大家好, 我是林一一, 这是一篇关于 vue 的原理面试题, 如果能够完全弄懂相信对大家很有帮助.
面试题篇
1. 老生常谈之, MPA/SPA 的理解, 优缺点是什么?
MPA 多页面应用.
构成: 有多个页面 html 构成,
跳转方式: 页面的跳转是从一个页面到另一个页面
刷新的方式: 全页面刷新
页面数据跳转: 依赖
URL/cookie/localStorage
跳转后的资源 会重新加载
优点: 对 SEO 比较友好, 开发难度低一点.
SPA 单页面应用
页面组成: 由一个外壳页面包裹, 多个页面 (组件) 片段组成
跳转方式: 在外壳页面中跳转, 将片段页面 (组件) 显示或隐藏
刷新方式: 页面片段的局部刷新
页面的数据跳转: 组件间的传值比较容易
跳转后的资源 不会重新加载
缺点: 对 SEO 搜索不太友好需要单独做配置, 开发难度高一点需要专门的开发框架
iframe 实际上是 MPA, 但是可以实现 SPA 的一些效果, 但是本身由不少问题.
2. 老生常谈之, 为什么需要有这些 MVC/MVVM 模式? 谈谈你对 MVC,MVVM 模式的区别,
目的: 借鉴后端的思想, 职责划分和分层
Vue, React 不是真正意义上的 MVVM 更不是 MVC, 两者核心只处理视图层 view.
MVC 模式
单向的数据, 用户的每一步操作都需要重新请求数据库来修改视图层的渲染, 形成一个单向的闭环. 比如
- jQuery+Underscore+Backbone
- .
M:model 数据存放层
V: view: 视图层 页面
C: controller: 控制器 JS 逻辑层.
controller 控制层将数据层 model 层 的数据处理后显示在视图层 view 层, 同样视图层 view 层 接收用户的指令也可以通过控制层 controller, 作用到数据层 model. 所以
MVC 的缺点是视图层不能和数据层直接交互.
MVVM 模式
隐藏了 controller 控制层, 直接操控 View 视图层和 Model 数据层.
M:model 数据模型
V: view 视图模板
VM:view-model 视图数据模板(vue 处理的层, vue 中的 definedProperty 就是处理 VM 层的逻辑)
双向的数据绑定: model 数据模型层通过数据绑定 Data Bindings 直接影响视图层 View, 同时视图层 view 通过监听 Dom Listener 也可以改变数据模型层 model.
数据绑定和 DOM 事件监听就是 viewModel 层 Vue 主要做的事. 也就是说: 只要将
数据模型层 Model
的数据挂载到 ViewModel 层 Vue 就可以实现双向的数据绑定.
加上 vuex/redux 可以作为 vue 和 react 的 model 数据层.
var vm = new Vue()
vm 就是 view-model 数据模型层, data: 就是 vm view-model 层所代理的数据.
综上两者的区别: MVC 的视图层和数据层交互需要通过控制层 controller 属于单向链接. MVVM 隐藏了控制层 controller, 让视图层和数据层可以直接交互 属于双向连接.
3. 说一下对 Vue 中响应式数据的理解
小 tip: 响应式数据指的是数据发生了变化, 视图可以更新就是响应式的数据
vue 中实现了一个 definedReactive 方法, 方法内部借用
Object.definedProperty()
给每一个属性都添加了 get/set 的属性.
definedReactive 只能监控到最外层的对象, 对于内层的对象需要递归劫持数据.
数组则是重写的 7 个
push pop shift unshift reverse sort splice
来给数组做数据拦截, 因为这几个方法会改变原数组
扩展:
- // 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
- .
手动调用 notify, 通知 render watcher, 执行 update
数组中如果有对象类型 (对象和数组) 的话会进行数据拦截.
所以通过修改数组下标和数组长度是不会进行数据拦截的, 也就不会有响应式变化. 例如
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 是什么关系)
tip:Dep 是一个用来负责收集 Watcher 的类, Watcher 是一个封装了渲染视图逻辑的类, 用于派发更新的. 需要注意的是
Watcher 是不能直接更新视图的还需要结合 Vnode 经过 patch()中的 diff 算法才可以生成真正的 DOM
每一个属性都有自己的 dep 属性, 来存放依赖的 Watcher, 属性发生变化后会通知 Watcher 去更新.
在用户获取 (getter) 数据时 Vue 给每一个属性都添加了 dep 属性来(collect as Dependency) 收集 Watcher. 在用户 setting 设置属性值时 dep.notify() 通知 收集的 Watcher 重新渲染. 详情见上面的 defineReactive()
Dep 依赖收集类 其和 Watcher 类 是多对多双向存储的关系
每一个属性都可以有多个 Watcher 类, 因为属性可能在不同的组件中被使用.
同时一个 Watcher 类 也可以对应多个属性.
6. Vue 中的模板编译
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc2f78cda8514103b4d0a29acb1a4c9a~tplv-k3u1fbpfcp-watermark.image" width="80%" height="360px"/>
Vue 中模板编译: 其实就是将 template 转化成 render 函数. 说白了就是将真实的 DOM(模板) 编译成虚拟 dom(Vnode)
第一步是将 template 模板字符串转换成 ast 语法树(parser 解析器), 这里使用了大量的正则来匹配标签的名称, 属性, 文本等.
第二步是对 AST 进行静态节点 static 标记, 主要用来做虚拟 DOM 的渲染优化(optimize 优化器), 这里会遍历出所有的子节点也做静态标记
第三步是 使用 ast 语法树 重新生成 render 函数 代码字符串 code.(codeGen 代码生成器)
为什么要静态标记节点, 如果是静态节点 (没有绑定数据, 前后不需要发生变化的节点) 那么后续就不需要 diff 算法来作比较.
7. 生命周期钩子实现原理
vue 中的生命周期钩子只是一个回调函数, 在创建组件实例化的过程中会调用对应的钩子执行.
使用 Vue.mixin({})混入的钩子或生命周期中定义了多个函数, vue 内部会调用 mergeHook() 对钩子进行合并放入到队列中依次执行
扩展
- // 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. 老生常谈之 vue 生命周期有哪些, 一般在哪里发送请求?
beforeCreate: 刚开始初始化 vue 实例, 在数据观测 observer 之前调用, 还没有创建 data/methods 等属性
created: vue 实例初始化结束, 所有的属性已经创建.
beforeMount: 在 vue 挂载数据到页面上之前, 触发这个钩子, render 函数此时被触发.
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 组件中的 data 为什么必须是一个函数?
这和 JS 本身机制相关, data 函数中返回的对象引用地址不同, 就能保证不同组件之间的数据不相互污染.
Vue.mixin() 中如果混入 data 属性, 那么 data 也必须是一个函数. 因为 Vue.mixin()也可以多处使用.
实例中 data 可以是一个对象也可以是一个函数, 因为我们一个页面一般只初始化一个 Vue 实例(单例)
11. 老生常谈之 vue 中 vm.$nextTick(cb)实现原理和场景
场景:
在 dom 更新循环结束后调用, 用于获取更新后的 dom 数据
实现原理: vm.$nextTick(cb) 是一个异步的方法为了兼容性做了很多降级处理依次有 promise.then,MutationObserver,setImmediate,setTimeout. 在数据修改后不会马上更新视图, 而是经过 set 方法 notify 通知 Watcher 更新, 将需要更新的 Watcher 放入到一个异步队列中, nexTick 的回调函数就放在 Watcher 的后面, 等待主线程中同步代码执行借宿然后依次清空队列中, 所以 vm.nextTick(callback) 是在 dom 更新结束后执行的.
上面将对列中 Watcher 依次清空就是
vue 异步批量更新的原理
. 提一个小思考: 为什么不直接使用 setTimeout 代替? 因为 setTimeout 是一个宏任务, 宏任务多性能也会差. 关于事件循环可以看看 JS 事件循环 https://juejin.cn/post/6950786264941461541
12. 老生常谈之 watch 和 computed 区别
computed 内部就是根据
Object.definedProperty()
实现的
computed 具备缓存功能, 依赖的值不发生变化, 就不会重新计算.
watch 是监控值的变化, 值发生变化时会执行对应的回调函数.
computed 和 watch 都是基于 Watcher 类 来执行的.
computed 缓存功能依靠一个变量 dirty, 表示值是不是脏的默认是 true, 取值后是 false, 再次取值时 dirty 还是 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 的原理和用途
结束
谢谢大家阅读到这里, 如果觉得写的还可以, 欢迎三连呀, 我是林一一, 下次见.
来源: https://segmentfault.com/a/1190000040060771