1. Vue3 和 Vue2 有哪些区别?
- 数据响应式原理变化,Vue2:
Object.defineProperty()
;Vue3:Proxy
- Vue3 新增内置组件:
Fragment
(文档碎片)、Suspense
(异步组件)、Teleport
(瞬移组件) - Vue3 提供
Composition API
(可以自定义一些响应式数据, 而不是直接放在data
选项中; 可以进行组合(把相同的功能放到方法里hooks
));Vue2 是Options API
;自定义函数(hooks) - Vue3 中生命周期前面都加了
on
,移除了beforeCreate
和created
钩子函数, 用setup
代替 - Vue3 源码采用
TS
开发,Vue2 采用flow
;-> Vue3 对 TS 支持更加友好 - Vue3 源码采用
monorepo
方式进行管理,将模块拆分到packages
目录中 - Vue3 支持
tree-shaking
,不使用就不会打包,提升性能 - Vue3 中对模块编译进行了优化,编译时生成
block tree
,可以对子节点进行收集。可以减少比较,并且采用了patchFlag
标记动态节点。 - Vue3 中对全局
API
的改变 - 新增 v-memo 指令
- 新增了
开发环境
的两个钩子函数,在组件更新时onRenderTracked 会跟踪组件里所有变量和方法的变化、每次触发渲染时
onRenderTriggered` 会返回发生变化的新旧值,可以让我们进行有针对性调试 - 支持在
<style></style>
里使用v-bind
,给CSS
绑定JS 变量(color: v-bind(str))
缺点:Vue3
不兼容 IE11
扩展:Vue3
对于 Vue2
在性能上的优化(从 compile
和 runtime
两方面)?
- Vue 设计思想
- 拆分模块:
Vue3.0
更注重模块上的拆分,在 2.0 中无法单独使用部分模块。需要引入完整的Vuejs
(例如,只想使用响应式部分,但需要引入完整的Vuejs
),Vue3
中的模块之间耦合度低,模块可以独立使用。 - 重写 API:
Vue2
中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具Tree-Shaking
机制实现按需引入,减少打包后体积。 - 扩展更方便:
Vue3
允许自定义渲染器,扩展能力强。不会发生以前的事情,改写Vue
源码改造渲染方式。
- 拆分模块:
编译时:将模板变成虚拟节点;运行时:将虚拟节点变成真实节点
2. 说一下 Vue2 和 Vue3 的响应式原理?
Vue2:当创建 Vue
实例时,Vue
会遍历 data
选项的属性,利用 Object.defineProperty
为属性添加 getter
和 setter
(getter
用来依赖收集,setter
用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
Vue3:Proxy
在 Vue.2x
中,使用 Object.defineProperty()
对对象进行监听。而在 Vue3.0
中,改用 Proxy
进行监听。Proxy
比起 Object.defineProperty()
有如下优势:
- 可以监听属性的增删操作
- 可以监听数组某个索引值的变化以及数组长度的变化
reactive()
的作用是将目标对象转换为响应式的proxy
实例reactive
->createReactiveObject
方法的处理逻辑:- 如果
target
不是对象,返回target
- 如果
target
已经是proxy
实例,或者readonly
处理reactive
对象,返回target
- 通过
isReadonly
创建WeakMap
,判断是否监听过target
,存在,返回监听过后的existingProxy
,防止同一对象重复代理 - 获取
target
类型,看是否是INVALID
无效类型,返回target
.(如果是skip
跳过或不可扩展属性就返回INVALID
无效对象,否则是普通对象类型(Object
、Array
)或者集合类型(Set
、Map
、WeakSet
、WeakMap
))Object.isExtensible(target)
检查是否可以向对象添加新属性
- 生成
proxy
实例,如果是集合类型就使用collectionHandlers
,否则使用baseHandlers
- 使用
proxyMap.set(target, proxy)
存起来,供第 3 做判断
- 如果
Vue2
是深层递归,Vue3
是懒递归(如果是嵌套对象,会继续递归将子对象转为响应式对象)
proxy
实例的种类:通过函数柯里化传入不同参数创建不同的proxy
实例- 完成响应式
proxy
实例,reactive()
- 只读的
proxy
实例,readonly()
- 浅层响应式
proxy
实例(对象第一层是响应式),shallowReactive()
- 只读的浅层的响应式
proxy
,shallowReadonly()
- 完成响应式
处理器种类:
普通类型:
baseHandler.ts
,4 种proxy
实例,对应着 4 种不同的处理器mutationHandlers
readonlyHandlers
shallowReactiveHandlers
shallowReadonlyHandlers
- 处理器对五种操作进行了拦截,分别是:
get
获取属性createGetter()
set
设置属性createSetter()
deleteProperty
删除属性deleteProperty()
has
是否拥有某个属性has()
ownKeys
(可以拦截以下操作:Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.keys()
、Object.ownKeys()
)ownKeys()
注意:get
、has
、ownKeys
操作会依赖收集
-> track,set
、deleteProperty
操作会触发依赖
-> trigger
集合类型:
collectionHandlers.ts
track()
将对象中的属性与effect
关联起来trigger()
effect()
->createReactiveEffect()
高阶函数 将自己设置为activeEffect
,然后执行fn
函数,如果fn
函数里有对响应式属性进行读取会触发get
操作,从而收集依赖 注意:effectStack
函数栈来维护 属性 与 effect 的对应
reactive
内部采用 proxy,ref
中内部的是 defineProperty
Observer
(观察者):主要给对象的属性添加getter
和setter
,用于 依赖收集Dep
(依赖的管理者和派发更新):用于收集当前的响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep
实例Watcher
(订阅者):三者之间的关系?
Observer
将数据定义为响应式,每个Observer
实例都有自己的Dep
来管理依赖,实例化Watcher
的时候进行求值会触发getter
,进而执行dep.depend()
, 将当前的Watcher
加入Dep
维护的依赖列表subs
,这就是依赖收集的过程。- 数据发生变化触发
setter
,进而执行dep.notify()
,Dep
会执行所有的依赖的update()
,并加入异步更新队列,这就是触发依赖的过程。
3. Vue 实例挂载的过程发生了什么?(new Vue()发生了啥?)
src/core/instance/index.js
new Vue()
-> this._init(options)
-> initMixin(Vue)
[原型上挂载 _init
方法]
scr/core/instance/init.js
Vue.prototype._init
-> 合并配置 mergeOptions()
-> 初始化生命周期(initLifecycle
) -> 初始化事件中心(initEvents
) -> 初始化渲染(initRender
) -> 挂载 beforeCreate
钩子 -> 初始化 Inject
(initInject
) -> 初始化 initState
(data
、 props
、 computed
、 watcher
、methods
)等等 -> 初始化 Provide
(initProvide
) -> 初始化 created
钩子 -> 最后判断是否有 el
使用 $mount
挂载
src/core/instance/state.js
initState
: 会对 props
、methods
、data
、computed
、watcher
进行初始化 =》数组的基本来源
initData
: 初始化数据 -> 判断 data
是否是函数 -> 获取 data
所有的 key
和 props
、methods
判断是否有重名 -> 观测数据(observe(data)
)
src/core/observer/index.js
observe()
-> 通过 __ob__
判断数据是否被观测 -> 如果观测就返回观测的数据,若没有观测就实例化(new Observer(value)
)
src/core/observer/index.js
class Observer
类 -> 1、给每个属性添加 ob 属性;2、判断数组
还是对象
;3、数组走 observeArray()
, 对象走 walk()
【注意:此时实例化 dep
;this.dep = new Dep()
】
- 对象情况:
循环所有的 key
, 调用defineReactive() -> Object.defineProperty()
get()
[dep.depend()
依赖收集【如果有孩子深度观测 childOb.dep.depend()
;若孩子是数组走 dependArray()
(循环所有的孩子,e && e.__ob__
&& e.__ob__
.dep.depend());如果孩子里还有数组,递归调用 dependArray()
】]
1. src/observer/dep.js -> depend() :如果 Dep.target 存在,就添加依赖 Dep.target.addDep(this)
2. src/observer/watcher.js -> addDep(dep) :将 dep 添加到 watcher 的 newDeps 中;通过 addSub()将 watcher 添加到 Dep 的 subs 数组中(判断是否重复,相同的只能添加一次),不存在的话就将 watcher 添加到 dep 中 dep.addSub(this)
3. src/observer/dep.js -> addSub(sub: Watcher)将 watcher 添加到 dep 中 this.subs.push(sub)
set()
[dep.notify()
触发更新]【如果孩子是数组也要进行观测 childOb = observe(newVal)
】-> dep.notify()
1. src/observer/dep.js -> notify() : 遍历 Dep 中的 subs 里所有的 watcher,循环调用 watcher 的 update 方法;subs[i].update()
2. src/observer/watcher.js -> update(): 如果是同步 this.sync 调用 this.run() 【调用 watcher 中的 get 方法 this.get()获取最新 value,将 this.value 赋值给 oldValue; 将最新的 value 赋值给 this.value;调用 this.cb.call(this.vm, value, oldValue)】-> 如果是异步调用 queueWatcher(this)
3. src/observer/scheduler.js -> queueWatch(watcher: Watcher)方法: -> nextTick(flushSchedulerQueue)
4. src/observer/scheduler.js -> flushSchedulerQueue() 就是将更新队列中的 watcher 拿出来并依次调用他们的 callback,但重点在于为什么在 for 循环之前先对这些 watcher 进行了升序排列
src/platform/web/entry-runtime-with-compiler.js
$mount()
重写$mount
方法 -> 判断(1、是否render
函数、2、是否有template
、3、都没有则el.outerHTML
)-> 将template
转换成render
方法【compileToFunction(template)
】线路图:
new Vue
=>_init
=>$mount
=>mountComponent
src/instance/lifecycle.js
:mountComponent()
-> 挂载组件 ->callHook(vm, 'beforeMount')
-> 定义updateComponent
->new Watcher(vm, updateComponent, () => {}, true)
// ==updateComponent()
;默认vue
是通过watcher
来进行渲染 渲染watcher
_render()
函数返回的就是 虚拟 dom; _update()
方法将 虚拟节点转换成真实节点; _render
函数会读取 data
中的数据从而触发 getter
方法进行 依赖收集(会调用 watcher
中的 get
方法进行求值)
扩展:Vue2
中 runtime + compiler
和 runtime-only
的区别?
runtime-only
相比runtime-compiler
有两个优点:- 运行效率高
- 源代码量更少
runtime-compiler
模式,runtime-only
模式在src
文件里面只在main.js
里面有区别:runtime-compiler
模式里的Vue
实例的模板,和注册的组件,都被一个render
函数替换掉了h
函数是createElement
的缩写-》用于创建虚拟DOM
的
runtime-compiler 的步骤:template
-> ast
-> render
-> virtual dom
-> 真实 dom
runtime-only 的步骤:render
-> virtual dom
-> 真实 dom
问题:runtime-only
:main.js
中的 template
被替换掉了,那组件中 template
还存在,他是怎么编译的呢?
vue-template-compiler
,运行项目时,这个包会自动将组件的 template
转换成 render
函数
4. Vue2 和 Vue3 组件通信方式有哪些?
Vue2 组件通信方式:
- 父 -> 子:props; 子 -> 父:$emit【单向数据流:props 只能从上一级组件传递到下一级组件】
- 父子组件之间:$parents、$children 获取组件实例
- provide 和 inject :父组件中通过 provide 来提供变量,然后子组件中使用 inject 来注入变量【Vue2.2.0 新增】
- ref 和 refs:如果在普通的 DOM 元素上,引用指向的是 DOM 元素;如果用在子组件上,引用就指向组件是来,可以通过实例获取组件的方法和数据等。
- EventBus:事件总线($emit派发事件、$on 注册事件、$off 移除事件)
- $attrs 和 $listeners:新增了inheritAttrs 选项,默认值为 true【Vue2.4 新增】
- Vuex:专为 Vue.js 应用程序开发设计的状态管理
Vue3 组件通信方式:
- 父 -> 子:props; 子 -> 父:emit
- v-model 方式
- $refs 【defineExpose 暴露属性】
- provide 和 inject
- 事件总线(Vue3 中移除了 Event-Bus,但是可以借助第三方工具:Vue 官方推荐:mitt 和 tiny-emitter)
- Vuex4
- Pinia
5. 说一说 nextTick 的原理?
nextTick
作用:是在下次 DOM
更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
。
再回答
nextTick
实现原理之前,首先要了解JS 的执行机制
:JS
是单线程
的,一次只能干一件事,即同步。(就是说所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,这是非常影响用户体验的)。所以有了 异步 的概念- 同步任务:指排队在主线程上依次执行的任务
- 异步任务:不进入主线程,而进入任务队列的任务,又分为
宏任务
和微任务
- 宏任务: 渲染事件、请求、
script
、setTimeout
、setInterval
、Node
中的setImmediate
等 - 微任务:
Promise.then
、MutationObserver
(监听 DOM)、Node 中的Process.nextTick
等
当执行栈中的同步任务执行完后,就会去任务队列中拿一个宏任务放到执行栈中执行,执行完该宏任务中的所有微任务,再到任务队列中拿宏任务,即一个宏任务、所有微任务、渲染、一个宏任务、所有微任务、渲染...,如此形成循环,即事件循环(EventLoop)
Vue2 中 nextTick 源码解析:
源码地址:src/core/util/next-tick.js,源码版本:2.6.14
源码的主要的两个作用:
一是判断当前环境能使用的最合适的 API 并保存异步函数
主要是判断用哪个宏任务或微任务,因为宏任务耗费的时间是大于微任务的,所以成先使用微任务,判断顺序如下:
Promise
MutationObserver
setImmediate
setTimeout
环境判断结束就会得到一个延迟回调函数
timerFunc
, 然后进入核心的nextTick
二是调用异步函数 执行回调队列
nextTick 方法主要逻辑就是:
- 把传入的回调函数放进回调队列
callbacks
- 执行保存的异步任务
timeFunc
,就会遍历callbacks
执行相应的回调函数了
- 把传入的回调函数放进回调队列
注意:如果没有提供回调,并且支持 Promise,就返回一个 Promise。 -> this.$nextTick().then(()=>{ ... })
- 名词解析:
isUsingMicroTask
:是否启用微任务开关callbacks
:回调队列pending
:异步控制开关,同一时间只能执行一次flushCallbacks()
:该方法负责执行队列中的全部回调;执行之前先备份并清空回调队列,是为了防止nextTick
里有nextTick
出现的问题timerFunc
:用来保存调用异步任务方法
- Vue3 中 nextTick 源码解析:
- 源码地址:packages/runtime-core/src/scheduler.ts,源码版本:3.2.11
- vue3 中 nextTick 的队列由几个方法维护,基本执行顺序是这样的: queueJob -> queueFlush -> flushJobs -> nextTick 参数的 fn
- 【还需探索...】
扩展: Vue3 中的 nextTick 是如何实现的?
源码地址:packages/runtime-core/src/scheduler.ts
可以看出 nextTick 接受一个函数为参数,同时会创建一个微任务。在我们页面调用 nextTick 的时候,会执行该函数,把我们的参数 fn 赋值给 p.then(fn),在队列的任务完成后,fn 就执行了。由于加了几个维护队列的方法,所以执行顺序是这样的:queueJob -> queueFlush -> flushJobs -> nextTick 参数的 fn
6. 你知道 Vue 页面不刷新的情况有哪些?
Vue
无法检测实例被创建时不存在data
中的属性Vue
无法检测对象属性的添加和移除Vue
通过数组下标(索引)修改一个数据项Vue
不能直接修改数组的length
- 在异步执行之前操作
DOM
- 循环嵌套太深,视图不更新 -> 解决:
$forceUpdate()
- 路由参数变化时,页面不刷新 -> 解决:
watch
监听$route
和router-view
添加key
7. keep-alive 的原理?
keep-alive
是一个抽象组件:它自身不会渲染一个 DOM
元素,也不会出现在父组件链中;使用 keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
总的来说:keep-alive 用于保存组件的渲染状态,防止组件多次渲染。
- keep-alive 用法:
- 在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount"> <component :is="currentComponent"></component> </keep-alive>
- 在 vue-router 中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount"> <router-view></router-view> </keep-alive>
- 在动态组件中的应用
其中:include
定义缓存白名单,keep-alive
会缓存命中的组件;exclude
定义缓存黑名单,被命中的组件将不会被缓存;max
定义缓存组件上限,超出上限使用 LRU
的策略置换缓存数据。
缓存淘汰策略
LRU
(最近最少使用)源码解析过程:
路径:src/core/components/keep-alive.js
abstract
属性:true
,判断当前组件虚拟dom
是否渲染成真实dom
的关键props
属性:include
:缓存白名单exclude
:缓存黑名单max
:缓存的组件数量
keep-alive
在它生命周期内定义了三个钩子函数:created
:初始化两个对象分别缓存VNode
(虚拟DOM
)cache
和VNode
对应的键集合keys
destroyed
:删除this.cache
中缓存的VNode
实例扩展:为什么不直接将
this.cache
置为null
,而是遍历调用pruneCacheEntry
函数删除? 删除缓存的VNode
还要对应组件实例的destroy
钩子函数mounted
:对include
和exclude
参数进行监听,然后实时地更新(删除)this.cache
对象数据render
:- 第一步:获取
keep-alive
包裹着的第一个子组件对象及其组件名 - 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步
- 第三步:根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新
key
的位置是实现LRU
置换策略的关键),否则执行第四步 - 第四步:在
this.cache
对象中存储该组件实例并保存key
值,之后检查缓存的实例数量是否超过 max 设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key) - 第五步:最后并且很重要,将该组件实例的
keepAlive
属性值设置为true
- 第一步:获取
新增两个钩子函数:
activated
、deactivated
常见问题:
keep-alive
不会生成真正的DOM
节点,Vue
是如何做到的?Vue
在初始化生命周期的时候,为组件实例建立父子关系会根据abstract
属性决定是否忽略某个组件。在keep-alive
中,设置了abstract: true
,那Vue
就会跳过该组件实例。// src/core/instance/lifecycle.js if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) }
keep-alive
包裹的组件是如何使用缓存的?在
patch
阶段,会执行createComponent
函数:// src/core/vdom/patch.js function createComponent() { let i = vnode.data if (isDef(i)) { if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false) } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElem, vnode.elem, refElem) // 将缓存的DOM(vnode.elem) 插入父元素中 } } }
- 在首次加载被包裹组建时,由
keep-alive.js
中的render
函数可知,vnode.componentInstance
的值是undefined
,keepAlive
的值是true
,因为keep-alive
组件作为父组件,它的render
函数会先于被包裹组件执行;那么只执行到 i(vnode,false),后面的逻辑不执行 - 再次访问被包裹组件时,
vnode.componentInstance
的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)
逻辑,这样就直接把上一次的 DOM 插入到父元素中
- 在首次加载被包裹组建时,由
8. Vue3 中的 Composition API 和 Vue2 中的 Options API 有什么不同?
- 通常使用
Vue2
开发的项目,普遍会存在以下问题:- 代码的可读性随着组件变大而变差
- 每一种代码复用的方式,都存在缺点
TypeScript
支持有限
mixins
的缺陷:- 命名冲突
- 数据来源不清楚
- 两者的主要区别:
- 在逻辑组织和逻辑复用方面,
Composition API
是优于Options API
- 因为
Composition API
几乎是函数,会有更好的类型推断 Composition API
对tree-shaking
友好,代码也更容易压缩Composition API
中见不到this
的使用,减少了this
指向不明的情况- 如果是小型组件,可以继续使用
Options API
,也是十分友好的
- 在逻辑组织和逻辑复用方面,
9. SPA 单页面的理解以及优缺点?SSR 了解吗?
SPA
( single-page application
)仅在 Web
页面初始化时加载相应的 HTML
、JavaScript
和 CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制
实现 HTML
内容的变换,UI
与用户的交互,避免页面的重新加载。
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
- 基于上面一点,
SPA
相对对服务器压力小 - 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
初次加载耗时多
:为实现单页Web
应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载前进后退路由管理
:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理SEO 难度较大
:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
SSR
服务端渲染Nuxt.js
- 优点:
- 更利于
SEO
- 更利于首屏渲染
- 更利于
- 缺点:
- 服务端压力较大
- 开发条件受限(
created
和beforeCreate
之外的生命周期钩子不可用) - 学习成本相对较高(除了对
webpack
、Vue
要熟悉,还需要掌握node
、Express
相关技术)
- 优点:
10. set、delete 的实现原理?
set
的实现原理?- 路径:src/core/instance/observer/index.js -> set(target, key, val)
- 大致流程:核心方法:
defineReactive(ob.value, key, val)
- 判断目标值是否为有效值,不是有效值直接停止
- 判断是否为数组,并且
key
值是否为有效的key
值 如果是数组,就选择数组的长度和key
值取较大值作为数组的新的length
值,并且替换目标值splice
方法,重写了,所以执行splice
,会双向数据绑定 - 如果目标值是对象,并且
key
值是目标值存在的有效key
值,并且不是原型上的key
值 判断目标值是否为响应式的ob 如果是 vue 实例,直接不行 如果不是响应式的数据,就是普通的修改对象操作 如果是响应式数据,那就通过Object.defineProperty
进行数据劫持 - 通知
dom
更新ob.dep.notify()
delete
的实现原理?- 路径:src/core/instance/observer/index.js -> del(target, key)
- 大致流程:核心逻辑:delete target[key]
- 非生产环境下, 不允许删除一个原始数据类型, 或者
undefined
,null
- 如果
target
是数组, 并且key
是一个合法索引,通过数组的splice
方法删除值, 并且还能触发数据的响应(数组拦截器截取到变化到元素, 通知依赖更新数据) target._isVue
: 不允许删除Vue
实例对象上的属性;(ob && ob.vmCount
): 不允许删除根数据对象的属性,触发不了响应- 如果属性压根不在对象上, 什么都不做处理
- 走到这一步说明,
target
是对象, 并且key
在target
上, 直接使用delete
删除 - 如果
ob
不存在, 说明target
本身不是响应式数据 return; - 存在
ob
, 通过ob
里面存储的Dep
实例的notify
方法通知依赖更新ob.dep.notify()
- 非生产环境下, 不允许删除一个原始数据类型, 或者
扩展:$delete
和 delete
的区别?
delete
是js
原生方法,由于语言限制,此操作无法设置回调来响应$delete
是vue
提供的实例方法,核心就是在删除后通知了依赖更新
11. Vue 源码设计用了哪些设计模式?
单例模式:整个程序有且仅有一个实例
new 多次,只有一个实例
、vuex
和vue-router
的插件注册方法install
判断如果系统存在实例就直接返回掉工场模式:传入参数就可以创建实例
虚拟 DOM
根据参数的不同
返回基础标签的 Vnode
和组件 Vnode
发布订阅模式:
eventBus
、vue 事件机制
观察者模式:
watch
和dep
(响应式数据原理)代理模式:
_data
属性、proxy
、防抖、节流中介者模式:
vuex
策略模式
外观模式
扩展:发布 / 订阅模式
和 观察者模式
的区别是什么?
- 在观察者模式中,被观察者通常会维护一个观察者列表。当被观察者的状态发生改变时,就会通知观察者
- 在发布订阅模式中,具体发布者会动态维护一个订阅者的列表:可在运行时根据程序需要开始或停止发布给对应订阅者的事件通知
区别在于发布者本身并不维护订阅列表(它不会像观察者一样主动维护一个列表),它会将工作委派给具体发布者(相当于秘书,任何人想知道我的事情,直接问我的秘书就可以了);订阅者在接收到发布者的消息后,会委派具体的订阅者来进行相关的处理
12. Vue 中常见的性能优化方式?
- 编码优化
- 尽量不要将所有的数据都放在
data
中,data
中的数据都会增加getter
和setter
,会收集对应的watcher
vue
在v-for
时给每项元素绑定事件尽量用事件代理
- 拆分组件( 提高复用性、增加代码的可维护性,减少不必要的渲染 )
v-if
当值为false
时内部指令不会执行,具有阻断功能,很多情况下使用v-if
替代v-show
- 合理使用
路由懒加载
、异步组件
Object.freeze
冻结数据
- 尽量不要将所有的数据都放在
- 用户体验
app-skeleton
骨架屏pwa
service worker
【web worker
,shared worker
的区别?】
- 加载性能优化
- 第三方模块按需导入 (
babel-plugin-component
) - 滚动到可视区域动态加载 (
vue-virtual-scroll-list
) - 图片懒加载 (
vue-lazyload
)
- 第三方模块按需导入 (
- SEO 优化
- 预渲染插件
prerender-spa-plugin
- 服务端渲染
ssr
- 预渲染插件
- 打包优化
- 使用
cdn
的方式加载第三方模块 - 多线程打包
happypack
、parallel-webpack
- 控制包文件大小(
tree shaking
/splitChunksPlugin
) - 使用
DllPlugin
提高打包速度
- 使用
- 缓存/压缩
- 客户端缓存/服务端缓存
- 服务端
gzip
压缩
扩展:按需加载的实现原理?
其实就是在 visitor
对象上设置响应的方法(节点类型),然后去处理符合要求的节点,将节点上对应的属性更改为目标代码上响应的值
babel
插件 -> 函数会有个 babelTypes
参数(包含 types
) -> 规定返回一个 visitor
对象(然后在 visitor
中编写获取各个节点的方法)
Identifier
:负责处理所有节点类型为 Identifier
的 AST
节点
VariableDeclaration
:处理变量声明关键字
import
对应的 ImportDeclaration
的节点
Vue.use(ElementUI)
对应于 ExpressionStatement
类型的节点
13. Vue2 和 Vue3 中计算属性的区别?
Vue3
中计算属性也要收集依赖。而 Vue2
中计算属性不具备收集依赖的
computed
设计的初衷是: 为了使模板中的逻辑运算更简单。它有两大优势:- 使模板中的逻辑更加清晰,方便代码管理
- 计算之后的值会被缓存起来,依赖的
data
值改变后会重新计算
问题 1:
Vue2
中computed
是如何初始化的?
Vue()
-> this.\_init()
-> initState()
-> initComputed()
initComputed()
做了哪些事?- 首先使用
Object.create(null)
; 创建一个空对象, 分别赋值给watchers
; 和vm._computedWatchers
const isSSR = isServerRendering()
; 判断是否是服务器端渲染- 使用
for in
循环遍历computed
, 判断用户写的computed
是函数还是对象const userDef = computed[key]
;const getter = typeof userDef === 'function' ? userDef : userDef.get
- 会根据
computed
中的key
来实例化watcher
,因此我们可以理解为其实computed
就是watcher
的实现, 通过一个发布订阅
模式来监听的。给Watch
方法传递了四个参数, 分别为VM实列
, 上面我们获取到的getter方法
,noop
是一个回调函数。computedWatcherOptions
参数我们在源码初始化该值为:const computedWatcherOptions = { lazy: true }
- 如果
computed
中的key
没有在vm
中, 则通过defineComputed
挂载上去。第一次执行的时候,vm
中没有该属性的 - 在
defineComputed
方法中首先执行const shouldCache = !isServerRendering()
; 判断是不是服务器端渲染。该参数的作用是否需要被缓存数据, 为true
是需要被缓存的。也就是说我们的这里的computed
只要不是服务器端渲染的话, 默认会缓存数据的。 - 接着会判断
userDef
是否是一个函数, 如果是函数的话,说明是我们的computed
的用法。因此sharedPropertyDefinition.get = createComputedGetter(key);
的返回值。重新定义 getter Object.defineProperty(target, key, sharedPropertyDefinition)
; 使用Object.defineProperty
来监听对象属性值的变化;只要我们的data
对象中的某个属性发生改变的话, 我们的reversedMsg
方法中依赖了该属性的话, 也会调用sharedPropertyDefinition
方法中的get/set
方法的。
- 首先使用
重点:但是在我们的页面第一次初始化的时候, 我们要如何初始化执行computed
中的对应方法呢?
[initMixin]中的
\_init()
->vm.$mount(vm.$options.el)
该代码的作用是对我们的页面中的模板进行编译操作。[entry-runtime-with-compiler.js] 中重写
\$mount
->mount.call(this, el, hydrating);
[runtime/index.js]中定义
Vue.prototype.$mount
->mountComponent(this, el, hydrating)
[instance/lifecycle.js]中会
new
一个Watcher
进行实列化了,此时this.lazy
为false
,执行this.get()
函数, 也就是说执行了this.getter.call(vm, vm)
方法最后回到[instance/state.js] ->
createComputedGetter()
->return watcher.value
Vue3
中computed
的实现原理?
- [reactivity/computed.ts] ->
computed(getterOrOptions)
参数有可能是函数有可能包含get
和set
方法的对象 - 如果是函数,就把
getterOrOptions
赋值给getter
,setter
提示警告不让修改。如果对象,就把getterOrOptions.get
赋值给getter
,getterOrOptions.set
赋值给setter
return new ComputedRefImpl(getter, setter)
重点:ComputedRefImpl
的实现?
- 在构造函数中创建一个
effect
,响应式的计算属性。将getter
作为effect
的回调函数,并传入lazy: true
默认不执行和scheduler
函数的选项 - 在类中定义
get value()
,set value(newValue)
访问器。用来获取和设置。在get value()
中如果this.\_dirty
为真(默认不执行,脏的)时,将会将用户的返回值返回this.\_value = this.effect()
;并将this.\_dirty=false
,下次调用的时候不重新执行。最后返回return this.\_value
;在设置值时,set value()
会执行this.setter(newValue)
。
如果 scheduler
存在就执行scheduler
,不存在就执行effect
注意:当计算属性直接在 effect
中访问的时候,我们需要监听 value
属性,做依赖收集 track(this, 'value')
14. Vue 和 React 的区别?
15. 你知道 Vue2 的模板编译原理吗?和 Vue3 做了哪些改进?
16. 有写过自定义指令吗?自定义指令的应用场景?
17. 前端路由的原理?
18. 说一下 Vue2 中 Diff 算法?Vue3 做了哪些改进?
虚拟 DOM
是一个 JS 对象
. 虚拟 DOM
算法 = 虚拟 DOM
+ Diff
算法
使用虚拟
DOM
算法的损耗计算: 总损耗 = 虚拟 DOM 增删改 +(与 Diff 算法效率有关)真实 DOM 差异增删改 +(较少的节点)排版与重绘直接操作真实 DOM 的损耗计算: 总损耗 = 真实 DOM 完全增删改 +(可能较多的节点)排版与重绘
Diff 算法的目的是什么? 为了减少 DOM 操作的性能开销,我们要尽可能的复用 DOM 元素。所以我们需要判断出是否有节点需要移动,应该如何移动以及找出那些需要被添加或删除的节点。
diff 算法: 是一种通过同层
的树节点进行比较的高效算法, diff 整体策略为:深度优先
,同层比较
. 其有两个特点:
- 比较只会在同层级进行, 不会跨层级比较
- 在 diff 比较的过程中,循环从两边向中间比较
原理分析 源码位置:src/core/vdom/patch.js
当数据发生改变时,set
方法会调用 dep.notify
通知所有订阅者 Watcher
,订阅者就会调用 patch
给真实的 DOM
打补丁,更新相应的视图
el
: 真的的 DOM
,oldVnode
: 旧节点,newVnode
: 新节点
对比流程:
set
->dep.notify
->patch(oldVnode, newVnode)
->isSameVnode()
- 不是, 直接替换
- 是走
patchVnode
oldVnode
有子节点,newVnode
没有
oldVnode
没有子节点,newVnode
有
- 都是文本节点
- 都有子节点
updateChildren
sameVnode
方法判断是否为同一类型
的节点。如何才算同一类型的节点?sameVnode(oldVnode, newVnode)
几种情况:key
值是否一样tagName
标签名是否一样isComment
是否都是注释节点- 是否都定义了
data(class, style)
是否一样 sameInputType()
当前节点为input
时,type
必须相同
patchVnode
函数做了哪些事情?- 获取对应的
真实 DOM
->el
- 判断
newVnode
和oldVnode
是否指向同一个对象
,如果是,直接返回 - 如果他们都有文本节点并且不相同,那么将
el
的文本节点设置为newVnode
的文本节点 - 如果
oldVnode
有子节点而新节点没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而新节点有,则将newVnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点
- 获取对应的
updateChildren 函数五种比较情况:
- 旧头 & 新头
- 旧头 & 新尾
- 旧尾 & 新头
- 旧尾 & 新尾
- 如果以上逻辑都不匹配,再把所有旧子节点的
key
做一个映射到旧节点下标的key -> index
表,然后用新vnode
的key
去找出在旧节点中可以复用
的位置
19. 描述一下 Vue2 以及 Vue3 组件的渲染和更新的过程?
20. 简述一下 Vuex 工作原理?
21. Vue 中事件绑定原理?
22. Vue2 和 Vue3 中 v-model 的实现原理?
23. Vue3.0 是如何变得更快的?
diff
方法优化Vue2.x
中的虚拟dom
是进行全量的对比Vue3.0
中新增了静态标记(PatchFlag
) 在与上次虚拟节点进行对比的时候,只对比带有patch flag
的节点,并且可以通过flag
的信息得知当前节点要对比的具体内容。
hoistStatic
静态提升Vue2.x
: 无论元素是否参与更新,每次都会重新创建Vue3.0
: 对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用
cacheHandlers
事件侦听器缓存 默认情况下onClick
会被视为动态绑定,所以每次都会去追踪它的变化但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
Vue 基本原理
数据双向绑定,需要三大模块:
Observer
: 能够对数据对象的所有数据进行监听,如有变动可拿到最新值并通知订阅者。Compile
: 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。Watcher
: 作为连接Observer
和Compiler
的乔辽,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
Observer
的核心是通过Object.defineProperty()
来监听数据的变动,这个函数内部可以定义setter
和getter
,每当数据发生变化,就会触发setter
。这时候Observer
就要通知订阅者。订阅者就是Watcher
Watcher
订阅者作为Observer
和Compiler
之间通信的桥梁,主要做的事情是:- 在自身实例化时往属性订阅器(
dep
)里面添加自己 - 自身必须有一个
update()
方法 - 待属性变动
dep.notify()
通知时,能调用自身的update()
方法,并触发Compiler
中绑定的回调
- 在自身实例化时往属性订阅器(
Compiler
主要做的是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Vue 生命周期
每个
Vue
实例在被创建之前都要经过一系列的初始化过程,这个过程就是 vue 的生命周期。下面是官方文档提供的一张图:
每个 Vue 组件完整的生命周期都会有很多钩子函数,例如:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
注意:Vue
为了结合<keep-alive></keep-alive>
组件,提供了两个额外的钩子函数
activated
deactivated
下面我们通过详细的代码来分析
Vue
组件生命周期
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>vue生命周期学习</title>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
</head>
<body>
<div id="app">
<h1>{{msg}}</h1>
</div>
</body>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'Vue的生命周期',
},
beforeCreate() {
console.group('------beforeCreate创建前状态------')
console.log('%c%s', 'color:red', 'el : ' + this.$el) //undefined
console.log('%c%s', 'color:red', 'data : ' + this.$data) //undefined
console.log('%c%s', 'color:red', 'msg: ' + this.msg)
},
created() {
console.group('------created创建完毕状态------')
console.log('%c%s', 'color:red', 'el : ' + this.$el) //undefined
console.log('%c%s', 'color:red', 'data : ' + this.$data) //已被初始化
console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
},
beforeMount() {
console.group('------beforeMount挂载前状态------')
console.log('%c%s', 'color:red', 'el : ' + this.$el) //已被初始化
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data) //已被初始化
console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
},
mounted() {
console.group('------mounted 挂载结束状态------')
console.log('%c%s', 'color:red', 'el : ' + this.$el) //已被初始化
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data) //已被初始化
console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
},
beforeUpdate() {
console.group('beforeUpdate 更新前状态===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el)
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data)
console.log('%c%s', 'color:red', 'msg: ' + this.msg)
},
updated() {
console.group('updated 更新完成状态===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el)
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data)
console.log('%c%s', 'color:red', 'msg: ' + this.msg)
},
beforeDestroy() {
console.group('beforeDestroy 销毁前状态===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el)
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data)
console.log('%c%s', 'color:red', 'msg: ' + this.msg)
},
destroyed() {
console.group('destroyed 销毁完成状态===============》')
console.log('%c%s', 'color:red', 'el : ' + this.$el)
console.log(this.$el)
console.log('%c%s', 'color:red', 'data : ' + this.$data)
console.log('%c%s', 'color:red', 'msg: ' + this.msg)
},
})
</script>
</html>
可以看到一个Vue
实例在创建到渲染会执行四个钩子函数
- 在
beforeCreate
和created
阶段
进行初始化事件,进行数据的观测,可以看到在 created 的时候数据已经和 data 属性进行绑定
注意:此时还获取不到dom
- 在
created
和beforeMount
阶段
主要做以下事情:
判断对象是否有el
选项;若有,则继续向下编译;若没有el
选项,则停止编译,也就意味着停止了生命周期,直到在该vue
实例上调用vm.$mount(el)
。
注意:要注意template
选项的有无对生命周期的影响 1)、若vue
实例对象中有template
参数选项,则将其作为模板编译成render函数
。 2)、若没有template
选项,则将外部 HTML 作为模板编译。 3)、template
中的模板优先级要高于outer HTML
的优先级。
注意:优先级
render函数 > template > outer HTML
- 在
beforeMount
和mounted
阶段
此时vue实例
对象添加$el
,并且替换掉挂在的DOM
元素。注意:beforeMount
之前el
上还是undefined
。
- 在
mounted
阶段
在beforeMount
时,此时还是虚拟DOM
存在;在mounted
阶段,就可以看到真实的DOM
;注意:此时经常是调接口的地方。
- 在
beforeUpdate
和updated
阶段
当组件的data
选项里数据发生变化,会触发对应组件重新渲染,系统会先后调用beforeUpdate
和updated
钩子函数。
- 在
beforeDestroy
和destroyed
阶段
beforeDestroy
钩子函数在实例销毁之前调用。此时,实例仍然完全可用。
destroyed
钩子函数在Vue 实例
销毁后调用。调用后,Vue 实例
指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
Vue 组件间通信
Vuex 的原理及理解
Vue-Router 的原理及理解
问题: vue-router 中路由方法 pushState 和 replaceState 能否触发 popState 事件?
答案是:不能
pushState 和 replaceState 是 HTML5 新接口,可以改变网址(存在跨域限制)而不刷新页面,这个强大的特性后来用到了单页面应用如:vue-router,react-router-dom 中。
注意: 仅改变网址,网页不会真的跳转,也不会获取到新的内容,本质上网页还停留在原页面
window.history.pushState(state, title, targetURL);
@状态对象:传给目标路由的信息,可为空
@页面标题:目前所有浏览器都不支持,填空字符串即可
@可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data
window.history.replaceState(state, title, targetURL);
@类似于pushState,但是会直接替换掉当前url,而不会在history中留下记录
总结:popstate
事件会在点击 后退
、前进
按钮(或调用 history.back()
、history.forward()
、history.go()
方法)时触发
Vue 组件间生命周期的调用顺序
Vue 如何监听数组
Provide 与 inject 用法
EventBus
Watch 高级用法
路由懒加载
Vue 的优缺点
Assets 和 Static 的区别
Vue 路由钩子函数
Vue v-model 原理
内部会根据标签的不同解析出,不同的语法
- 如: 文本框会被解析成 value + input 事件
- 如: 复选框会被解析成 checked + change 事件
- ...