声明:本篇文章纯属笔记性文章,非整体原创,是对vue知识的整理, 参考文章:1.vue官网 2.掘金Vue系列文章 3其它优秀微信公众号文章
基础篇
那首先谈谈你对 Vue 的理解吧?❗
答案
官网介绍:渐进式 JavaScript 框架、核心库加插件、动态创建用户界面(异步获取后台数据,数据展示在页面) 特点: MVVM 模式;代码简介体积小,运行效率高,适合移动PC端开发;本身只关注 UI (和 React 相似),可以轻松引入 Vue 插件或其他的第三方库进行开发。
说说你对MVVM的理解?
答案
-
Model-View-ViewModel的缩写,Model代表数据模型,View代表UI组件,ViewModel将Model和View关联起来
-
数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据
说说computed与watch的区别?
答案
计算属性computed和监听器watch都可以观察属性的变化从而做出响应,不同的是:
计算属性computed更多是作为缓存功能的观察者,它可以将一个或者多个data的属性进行复杂的计算生成一个新的值,提供给渲染函数使用,当依赖的属性变化时,computed不会立即重新计算生成新的值,而是先标记为脏数据,当下次computed被获取时候,才会进行重新计算并返回。
而监听器watch并不具备缓存性,监听器watch提供一个监听函数,当监听的属性发生变化时,会立即执行该函数。
小伙子说说 Vue 的渲染过程?
答案
- 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
-
parse 函数解析 template,生成 ast(抽象语法树)
-
optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
-
generate 函数生成 render 函数字符串
-
调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
-
调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
简单介绍下Vue的生命周期?
答案
beforeCreated :是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。
created :在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。
beforeMount :发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。
mounted :在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
beforeUpdate :发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
updated :发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
beforeDestory :发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
destoryed :发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
每个生命周期内部可以做什么?❗
答案
-
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
-
mounted:实例已经挂载完成,可以进行一些 DOM 操作。
-
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
-
updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。
-
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
为什么组件的data必须是一个函数?
答案
一个组件可能在很多地方使用,也就是会创建很多个实例,如果data是一个对象的话,对象是引用类型,一个实例修改了data会影响到其他实例,所以data必须使用函数,为每一个实例创建一个属于自己的data,使其同一个组件的不同实例互不影响,避免污染实例。
Vue组件生命周期调用顺序?
答案
渲染顺序:先父后子,完成顺序:先子后父更新顺序:父更新导致子更新,子更新完成后父销毁顺序:先父后子,完成顺序:先子后父
Key属性的作用是什么?
答案
在对节点进行diff的过程中,判断是否为相同节点的一个很重要的条件是key是否相等,如果是相同节点,则会尽可能的复用原有的DOM节点。所以key属性是提供给框架在diff的时候使用的,而非开发者。
了解nextTick吗?
答案
异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeout、 promise 那些),定义了一个异步方法,多次调用nextTiCKt
会将方法存入队列,通过异步方法清空当前队列。
v-for 和 v-if 为什么不能连用?
答案
v-for 会比 v-if 的优先级更高,连用的话会把 v-if
的每个元素都添加一下,造成性能问题。
为什么要使用异步组件?
答案
-
节省打包出的结果,异步组件分开打包,采用 jsonp 的方式进行加载,有效解决文件过大的问题。
-
核心就是包组件定义变成一个函数,依赖 import () 语法,可以实现文件的分割加载。
说说Vue组件通信方式有哪些?
答案
-
父子间通信:父亲提供数据通过属性 props 传给儿子;儿子通过
$on
绑父亲的事件,再通过 $emit 触发自己的事件(发布订阅) -
利用父子关系 children
获取父子组件实例的方法。
-
父组件提供数据,子组件注入。 provide 、 inject ,插件用得多。
-
ref 获取组件实例,调用组件的属性、方法
-
跨组件通信 Event Bus (Vue.prototype.bus=newVue)其实基于 bus = new Vue)其实基于 bus=newVue)其实基于 on 与$emit
-
vuex 状态管理实现通信
谈谈对keep-alive的了解?(简单)
答案
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的 2 个属性 include/exclude **,**2 个生命周期 activated
, deactived
简单说下Vue事件绑定原理?(简单)
答案
每一个Vue实例都是一个Event Bus,当子组件被创建的时候,父组件将事件传递给子组件,子组件初始化的时候是有 emit 触发函数,而对于原生native事件,使用addEventListener绑定到真实的DOM元素上。
简单说下 v-if 和 v-show的区别?(简单)
答案
-
当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。
-
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;
-
v-show 则适用于需要非常频繁切换条件的场景。
说说你对 SPA 单页面的理解,它的优缺点分别是什么?
答案
-
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
-
优点:
-
用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
-
基于上面一点,SPA 相对对服务器压力小;
-
前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
-
缺点:
-
初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
-
前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
-
SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
原理篇
Vue模板渲染的原理是什么?
答案
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
-
parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
-
optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
-
generate阶段:将最终的AST转化为render函数字符串。
Vue 模板 template预编译是什么?
答案
对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime 是一种性能损耗。
而模板编译的目的仅仅是将template转化为 render function ,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime 时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template的过程,就是预编译。
vue 是如何实现响应式数据的呢?(响应式数据原理)
答案
Vue2:Object.definProperty
重新定义 data
中所有的属性, Object.definProperty
可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。具体的过程:首先 Vue 使用 initData
初始化用户传入的参数,然后使用 new Observer
对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value)
对对象进行处理,内部使用 defineReactive
循环对象属性定义响应式变化,核心就是使用 Object.definProperty
重新定义数据。
那 vue 中是如何检测数组变化的呢?
答案
数组就是使用 object.defineProperty
重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, pop
、 push
、 shift
、 unshift
、 splice
、 sort
、 reverse
这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。
-
是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。
-
数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
vue3:改用 proxy
,可直接监听对象数组的变化。
v-model 中的实现原理及如何自定义 v-model ?
答案
v-model
可以看成是 value+input
方法的语法糖(组件)。原生的 v-model
,会根据标签的不同生成不同的事件与属性。解析一个指令来。自定义:自己写 model
属性,里面放上 prop
和 event
说说diff 算法?(重点)
答案
时间复杂度: 个树的完全 diff
算法是一个时间复杂度为 O(n*3)
,vue 进行优化转化成 O(n)
。理解:
-
最小量更新,
key
很重要。这个可以是这个节点的唯一标识,告诉diff
算法,在更改前后它们是同一个 DOM 节点 -
扩展
v-for
为什么要有key
,没有key
会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加key
只会移动减少操作 DOM。 -
只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
-
只进行同层比较,不会进行跨层比较。
diff 算法的优化策略:四种命中查找,四个指针
-
旧前与新前(先比开头,后插入和删除节点的这种情况)
-
旧后与新后(比结尾,前插入或删除的情况)
-
旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
-
旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
描述组件渲染和更新过程?
答案
渲染组件时,会通过 vue.extend()
方法构建子组件的构造函数,并进行实例化。最终手动调用 $mount()
进行挂载。更新组件时会进行 patchVnode
流程,核心就是 diff
算法。
说说 Vue 的事件绑定原理?
答案
-
原生
DOM
的绑定:Vue 在创建真实 DOM 时会调用createElm
,默认会调用invokeCreateHooks
。会遍历当前平台下相对的属性处理代码,其中就有updateDOMLListeners
方法,内部会传入add()
方法 -
组件绑定事件,原生事件,自定义事件;组件绑定之间是通过 Vue 中自定义的
$on
方法实现的。
说说 Vue2.0 和Vue3.0 有什么区别?
答案
- 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
-
可直接监听数组类型的数据变化
-
监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
-
可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行
-
直接实现对象属性的新增/删除
-
新增Composition API,更好的逻辑复用和代码组织
-
重构 Virtual DOM
-
模板编译时的优化,将一些静态节点编译成常量
-
slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
-
模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
-
代码结构调整,更便于Tree shaking,使得体积更小
-
使用Typescript替换Flow
nextTick的实现原理是什么?
答案
1、在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。
2、nextTick主要使用了宏任务和微任务。
根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
vm.$set 的实现原理?
答案
-
如果目标是数组,直接使用数组的 splice 方法触发相应式;
-
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
computed 的实现原理?
答案
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。(Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
聊聊 keep-alive 的实现原理和缓存策略?
答案
export default { name: "keep-alive", abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中 props: { include: patternTypes, // 被缓存组件 exclude: patternTypes, // 不被缓存组件 max: [String, Number] // 指定缓存大小 }, created() { this.cache = Object.create(null); // 缓存 this.keys = []; // 缓存的VNode的键 }, destroyed() { for (const key in this.cache) { // 删除所有缓存 pruneCacheEntry(this.cache, key, this.keys); } }, mounted() { // 监听缓存/不缓存组件 this.$watch("include", val => { pruneCache(this, name => matches(val, name)); }); this.$watch("exclude", val => { pruneCache(this, name => !matches(val, name)); }); }, render() { // 获取第一个子元素的 vnode const slot = this.$slots.default; const vnode: VNode = getFirstComponentChild(slot); const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // name不在inlcude中或者在exlude中 直接返回vnode // check pattern const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode; } const { cache, keys } = this; // 获取键,优先获取组件的name字段,否则是组件的tag const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); keys.push(key); } // 不命中缓存,把 vnode 设置进缓存 else { cache[key] = vnode; keys.push(key); // prune oldest entry // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } } // keepAlive标记位 vnode.data.keepAlive = true; } return vnode || (slot && slot[0]); }};
原理
-
获取 keep-alive 包裹着的第一个子组件对象及其组件名
-
根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
-
根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
-
在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
-
最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说
LRU 缓存淘汰算法
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
Vue 性能优化
答案
编码优化:
-
事件代理
-
keep-alive
-
拆分组件
-
key
保证唯一性 -
路由懒加载、异步组件
-
防抖节流
Vue 加载性能优化
-
第三方模块按需导入(
babel-plugin-component
) -
图片懒加载
用户体验
-
app-skeleton
骨架屏 -
shellap
p 壳 -
pwa
SEO 优化
- 预渲染
最后的话
🚀🚀觉得不错的朋友可以 ⭐️ 关注我 < 微信公众号 前端互助圈 >
参考资料
[1]
GitHub 仓库:
[2]
cn.vuejs.org/index.html:
[3]
vuex.vuejs.org/zh/: