vue面试题 ゝ一纸荒年。 2024-03-17 13:17 7阅读 0赞 #### Vue-router 路由模式有几种 #### `vue-router` 有 `3` 种路由模式:`hash`、`history`、`abstract`,对应的源码如下所示 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } 复制 其中,3 种路由模式的说明如下: * `hash`: 使用 `URL hash` 值来作路由,支持所有浏览器 * `history` : 依赖 `HTML5 History API` 和[服务器][Link 1]配置 * `abstract` : 支持所有 `JavaScript` 运行环境,如 `Node.js` 服务器端。如果发现没有浏览器的 `API`,路由会自动强制进入这个模式. #### mixin 和 mixins 区别 #### `mixin` 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。 Vue.mixin({ beforeCreate() { // ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数 }, }); 复制 虽然文档不建议在应用中直接使用 `mixin`,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 `ajax` 或者一些工具函数等等。 `mixins` 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 `mixins` 混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 `mixins` 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。 #### 虚拟 DOM 的优缺点? #### **优点:** * **保证性能下限:** 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限; * **无需手动操作 DOM:** 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率; * **跨平台:** 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。 **缺点:** * **无法进行极致优化:** 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。 #### Vue中的过滤器了解吗?过滤器的应用场景有哪些? #### 过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数 Vue 允许你自定义过滤器,可被用于一些常见的文本格式化 ps: `Vue3`中已废弃`filter` ##### 如何用 ##### vue中的过滤器可以用在两个地方:双花括号插值和 `v-bind` 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示: <!-- 在双花括号中 --> { message | capitalize } <!-- 在 `v-bind` 中 --> <div v-bind:id="rawId | formatId"></div> 复制 ##### 定义filter ##### 在组件的选项中定义本地的过滤器 filters: { capitalize: function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) } } 复制 定义全局过滤器: Vue.filter('capitalize', function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) }) new Vue({ // ... }) 复制 注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,`capitalize` 过滤器函数将会收到 `message` 的值作为第一个参数 过滤器可以串联: { message | filterA | filterB } 复制 在这个例子中,`filterA` 被定义为接收单个参数的过滤器函数,表达式 `message` 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 `filterB`,将 `filterA` 的结果传递到 `filterB` 中。 过滤器是 `JavaScript`函数,因此可以接收参数: { { message | filterA('arg1', arg2) }} 复制 这里,`filterA` 被定义为接收三个参数的过滤器函数。 其中 `message` 的值作为第一个参数,普通字符串 `'arg1'` 作为第二个参数,表达式 `arg2` 的值作为第三个参数 举个例子: <div id="app"> <p>{ { msg | msgFormat('疯狂','--')}}</p> </div> <script> // 定义一个 Vue 全局的过滤器,名字叫做 msgFormat Vue.filter('msgFormat', function(msg, arg, arg2) { // 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则 return msg.replace(/单纯/g, arg+arg2) }) </script> 复制 **小结:** * 部过滤器优先于全局过滤器被调用 * 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右 ##### 应用场景 ##### 平时开发中,需要用到过滤器的地方有很多,比如`单位转换`、`数字打点`、`文本格式化`、`时间格式化`之类的等 比如我们要实现将`30000 => 30,000`,这时候我们就需要使用过滤器 Vue.filter('toThousandFilter', function (value) { if (!value) return '' value = value.toString() return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,') }) 复制 ##### 原理分析 ##### 使用过滤器 { { message | capitalize }} 复制 在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过`parseFilters`,我们放到最后讲 _s(_f('filterFormat')(message)) 复制 首先分析一下`_f`: `_f` 函数全名是:`resolveFilter`,这个函数的作用是从`this.$options.filters`中找出注册的过滤器并返回 // 变为 this.$options.filters['filterFormat'](message) // message为参数 复制 关于`resolveFilter` import { indentity,resolveAsset } from 'core/util/index' export function resolveFilter(id){ return resolveAsset(this.$options,'filters',id,true) || identity } 复制 内部直接调用`resolveAsset`,将`option`对象,类型,过滤器`id`,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器; `resolveAsset`的代码如下: export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西 if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回 return } const assets = options[type] // 将我们注册的所有过滤器保存在变量中 // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配 if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器 // 没有找到,代码继续执行 const camelizedId = camelize(id) // 万一你是驼峰的呢 if(hasOwn(assets,camelizedId)) return assets[camelizedId] // 没找到,继续执行 const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢 if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId] // 如果还是没找到,则检查原型链(即访问属性) const result = assets[id] || assets[camelizedId] || assets[PascalCaseId] // 如果依然没找到,则在非生产环境的控制台打印警告 if(process.env.NODE_ENV !== 'production' && warnMissing && !result){ warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options) } // 无论是否找到,都返回查找结果 return result } 复制 下面再来分析一下`_s`: `_s` 函数的全称是 `toString`,过滤器处理后的结果会当作参数传递给 `toString`函数,最终 `toString`函数执行后的结果会保存到`Vnode`中的text属性中,渲染到视图中 function toString(value){ return value == null ? '' : typeof value === 'object' ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距 : String(value) } 复制 最后,在分析下`parseFilters`,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式 function parseFilters (filter) { let filters = filter.split('|') let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组 let i if (filters) { for(i = 0;i < filters.length;i++){ experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数 } } return expression } // warpFilter函数实现 function warpFilter(exp,filter){ // 首先判断过滤器是否有其他参数 const i = filter.indexof('(') if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接 return `_f("${filter}")(${exp})` }else{ const name = filter.slice(0,i) // 过滤器名称 const args = filter.slice(i+1) // 参数,但还多了 ‘)’ return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')' } } 复制 **小结:** * 在编译阶段通过`parseFilters`将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数) * 编译后通过调用`resolveFilter`函数找到对应过滤器并返回结果 * 执行结果作为参数传递给`toString`函数,而`toString`执行后,其结果会保存在`Vnode`的`text`属性中,渲染到视图 ### 如何理解Vue中模板编译原理 ### > `Vue` 的编译过程就是将 `template` 转化为 `render` 函数的过程 * **解析生成AST树** 将`template`模板转化成`AST`语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理 * **标记优化** 对静态语法做静态标记 `markup`(静态节点如`div`下有`p`标签内容不会变化) `diff`来做优化 静态节点跳过`diff`操作 * `Vue`的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的`DOM`也不会变化。那么优化过程就是深度遍历`AST`树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用 * 等待后续节点更新,如果是静态的,不会在比较`children`了 * **代码生成** 编译的最后一步是将优化后的`AST`树转换为可执行的代码 回答范例 **思路** * 引入`vue`编译器概念 * 说明编译器的必要性 * 阐述编译器工作流程 **回答范例** 1. `Vue`中有个独特的编译器模块,称为`compiler`,它的主要作用是将用户编写的`template`编译为`js`中可执行的`render`函数。 2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用`HTML`来编写视图,直观且高效。手写`render`函数不仅效率底下,而且失去了编译期的优化能力。 3. 在`Vue`中编译器会先对`template`进行解析,这一步称为`parse`,结束之后会得到一个`JS`对象,我们称为 **抽象语法树AST** ,然后是对`AST`进行深加工的转换过程,这一步成为`transform`,最后将前面得到的`AST`生成为`JS`代码,也就是`render`函数 **可能的追问** 1. `Vue`中编译器何时执行? ![b71d0e754854a220735666e3b2763d7a.png][] > 在 `new Vue()`之后。 `Vue` 会调用 `_init` 函数进行初始化,也就是这里的 i`nit` 过程,它会初始化生命周期、事件、 `props`、 `methods`、 `data`、 `computed` 与 `watch`等。其中最重要的是通过 `Object.defineProperty` 设置 `setter` 与 `getter` 函数,用来实现「响应式」以及「依赖收集」 * 初始化之后调用 `$mount` 会挂载组件,如果是运行时编译,即不存在 `render function` 但是存在 `template` 的情况,需要进行「编译」步骤 * `compile`编译可以分成 `parse`、`optimize` 与 `generate` 三个阶段,最终需要得到 `render function` * `React`有没有编译器? `react` 使用`babel`将`JSX`语法解析 <div id="app"></div> <script> let vm = new Vue({ el: '#app', template: `<div> // <span>hello world</span> 是静态节点 <span>hello world</span> // <p>{ {name}}</p> 是动态节点 <p>{ {name}}</p> </div>`, data() { return { name: 'test' } } }); </script> 复制 源码分析 export function compileToFunctions(template) { // 我们需要把html字符串变成render函数 // 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法 // 很多库都运用到了ast 比如 webpack babel eslint等等 let ast = parse(template); // 2.优化静态节点:对ast树进行标记,标记静态节点 if (options.optimize !== false) { optimize(ast, options); } // 3.通过ast 重新生成代码 // 我们最后生成的代码需要和render函数一样 // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn; } 复制 #### Vue 中的 key 到底有什么用? #### key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。) diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点. 更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。 更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下: function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key; const map = {}; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (isDef(key)) map[key] = i; } return map; } 复制 #### v-model实现原理 #### > 我们在 `vue` 项目中主要使用 `v-model` 指令在表单 `input`、`textarea`、`select` 等元素上创建双向数据绑定,我们知道 `v-model` 本质上不过是语法糖(可以看成是`value + input`方法的语法糖),`v-model` 在内部为不同的输入元素使用不同的属性并抛出不同的事件: * `text` 和 `textarea` 元素使用 `value` 属性和 `input` 事件 * `checkbox` 和 `radio` 使用 `checked` 属性和 `change` 事件 * `select` 字段将 `value` 作为 `prop` 并将 `change` 作为事件 **所以我们可以v-model进行如下改写:** <input v-model="sth" /> <!-- 等同于 --> <input :value="sth" @input="sth = $event.target.value" /> 复制 > 当在`input`元素中使用`v-model`实现双数据绑定,其实就是在输入的时候触发元素的`input`事件,通过这个语法糖,实现了数据的双向绑定 * 这个语法糖必须是固定的,也就是说属性必须为`value`,方法名必须为:`input`。 * 知道了`v-model`的原理,我们可以在自定义组件上实现`v-model` //Parent <template> { {num}} <Child v-model="num"> </template> export default { data(){ return { num: 0 } } } //Child <template> <div @click="add">Add</div> </template> export default { props: ['value'], // 属性必须为value methods:{ add(){ // 方法名为input this.$emit('input', this.value + 1) } } } 复制 **原理** 会将组件的 `v-model` 默认转化成`value+input` const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 观察输出的渲染函数: // with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // } 复制 // 源码位置 core/vdom/create-component.js line:155 function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } } 复制 原生的 `v-model`,会根据标签的不同生成不同的事件和属性 const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>'); // with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) }, // on: {"input": function ($event) { // if ($event.target.composing) return; // value = $event.target.value // } // } // }) // } 复制 > 编译时:不同的标签解析出的内容不一样 `platforms/web/compiler/directives/model.js` if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } else if (tag === 'select') { genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } 复制 > 运行时:会对元素处理一些关于输入法的问题 `platforms/web/runtime/directives/model.js` inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) } el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true } } } } 复制 #### scoped样式穿透 #### > `scoped`虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除`scoped`属性 1. 使用`/deep/` <!-- Parent --> <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> .wrap /deep/ .box{ background: red; } </style> <!-- Child --> <template> <div class="box"></div> </template> 复制 1. 使用两个`style`标签 <!-- Parent --> <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> /* 其他样式 */ </style> <style lang="scss"> .wrap .box{ background: red; } </style> <!-- Child --> <template> <div class="box"></div> </template> 复制 ### Vue 是如何实现数据双向绑定的 ### `Vue` 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示: ![9b4737d13126aa3da427d4b5ae4ff5e0.png][] * 输入框内容变化时,`Data` 中的数据同步变化。即 `View => Data` 的变化。 * `Data` 中的数据变化时,文本节点的内容同步变化。即 `Data => View` 的变化 **Vue 主要通过以下 4 个步骤来实现数据双向绑定的** * **实现一个监听器 Observer** :对数据对象进行遍历,包括子属性对象的属性,利用 `Object.defineProperty()` 对属性都加上 `setter` 和 `getter`。这样的话,给这个对象的某个值赋值,就会触发 `setter`,那么就能监听到了数据变化 * **实现一个解析器 Compile** :解析 `Vue` 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新 * **实现一个订阅者 Watcher** :`Watcher` 订阅者是 `Observer` 和 `Compile` 之间通信的桥梁 ,主要的任务是订阅 `Observer` 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 `Compile` 中对应的更新函数 * **实现一个订阅器 Dep** :订阅器采用 发布-订阅 设计模式,用来收集订阅者 `Watcher`,对监听器 `Observer` 和 订阅者 `Watcher` 进行统一管理 ![336688c961e952ad64c7fbd478b4cc54.png][] **Vue 数据双向绑定原理图** ![6397dcf2f532bdb98378967820b1ce2e.png][] #### diff算法 #### **时间复杂度:** 个树的完全`diff` 算法是一个时间复杂度为`O(n*3)` ,vue进行优化转化成`O(n)` 。 **理解:** * 最小量更新,`key` 很重要。这个可以是这个节点的唯一标识,告诉`diff` 算法,在更改前后它们是同一个DOM节点 * 扩展`v-for` 为什么要有`key` ,没有`key` 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加`key` 只会移动减少操作DOM。 * 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。 * 只进行同层比较,不会进行跨层比较。 **diff算法的优化策略**:四种命中查找,四个指针 1. 旧前与新前(先比开头,后插入和删除节点的这种情况) 2. 旧后与新后(比结尾,前插入或删除的情况) 3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后) 4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前) #### Watch中的deep:true是如何实现的 #### > 当用户指定了 `watch` 中的deep属性为 `true` 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 `watcher`存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新 **源码相关** get () { pushTarget(this) // 先将当前依赖放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果需要深度监控 traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法 }popTarget() } 复制 #### Vue性能优化 #### **编码优化**: * 事件代理 * `keep-alive` * 拆分组件 * `key` 保证唯一性 * 路由懒加载、异步组件 * 防抖节流 **Vue加载性能优化** * 第三方模块按需导入(`babel-plugin-component` ) * 图片懒加载 **用户体验** * `app-skeleton` 骨架屏 * `shellap` p壳 * `pwa` **SEO优化** * 预渲染 #### Vue.js的template编译 #### 简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下: > 首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。 > > 然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等) #### 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异 #### * 响应式数据变化,`Vue`确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加`watcher`用于更新的话,会产生大量的`watcher`从而降低性能 * 而且粒度过细也得导致更新不准确的问题,所以`vue`采用了组件级的`watcher`配合`diff`来检测差异 #### Vue组件渲染和更新过程 #### > 渲染组件时,会通过 `Vue.extend` 方法构建子组件的构造函数,并进行实例化。最终手动调用`$mount()` 进行挂载。更新组件时会进行 `patchVnode` 流程,核心就是`diff`算法 ![6f60d5b59f74763ae89c56bf52d3d7c7.png][] #### 什么是 mixin ? #### * Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。 * 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。 * 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。 #### Vue的性能优化有哪些 #### **(1)编码阶段** * 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher * v-if和v-for不能连用 * 如果需要使用v-for给每项元素绑定事件时使用事件代理 * SPA 页面采用keep-alive缓存组件 * 在更多的情况下,使用v-if替代v-show * key保证唯一 * 使用路由懒加载、异步组件 * 防抖、节流 * 第三方模块按需导入 * 长列表滚动到可视区域动态加载 * 图片懒加载 **(2)SEO优化** * 预渲染 * 服务端渲染SSR **(3)打包优化** * 压缩代码 * Tree Shaking/Scope Hoisting * 使用cdn加载第三方模块 * 多线程打包happypack * splitChunks抽离公共文件 * sourceMap优化 **(4)用户体验** * 骨架屏 * PWA * 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。 #### 了解history有哪些方法吗?说下它们的区别 #### > `history` 这个对象在`html5`的时候新加入两个`api` `history.pushState()` 和 `history.repalceState()` 这两个`API`可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。 从参数上来说: window.history.pushState(state,title,url) //state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取 //title:标题,基本没用,一般传null //url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。 //如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/, //执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/ window.history.replaceState(state,title,url) //与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录 复制 另外还有: * `window.history.back()` 后退 * `window.history.forward()`前进 * `window.history.go(1)` 前进或者后退几步 从触发事件的监听上来说: * `pushState()`和`replaceState()`不能被`popstate`事件所监听 * 而后面三者可以,且用户点击浏览器前进后退键时也可以 #### Vue.extend 作用和原理 #### > 官方解释:`Vue.extend` 使用基础 `Vue` 构造器,创建一个“子类”。参数是一个包含组件选项的对象。 其实就是一个子类构造器 是 `Vue` 组件的核心 `api` 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 `mergeOptions` 把传入组件的 `options` 和父类的 `options` 进行了合并 * `extend`是构造一个组件的语法器。然后这个组件你可以作用到`Vue.component`这个全局注册方法里还可以在任意`vue`模板里使用组件。 也可以作用到`vue`实例或者某个组件中的`components`属性中并在内部使用`apple`组件。 * `Vue.component`你可以创建 ,也可以取组件。 相关代码如下 export default function initExtend(Vue) { let cid = 0; //组件的唯一标识 // 创建子类继承Vue父类 便于属性扩展 Vue.extend = function (extendOptions) { // 创建子类的构造函数 并且调用初始化方法 const Sub = function VueComponent(options) { this._init(options); //调用Vue初始化方法 }; Sub.cid = cid++; Sub.prototype = Object.create(this.prototype); // 子类原型指向父类 Sub.prototype.constructor = Sub; //constructor指向自己 Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options return Sub; }; } 复制 #### Vue组件之间通信方式有哪些 #### > Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 **Vue 组件间通信只要指以下 3 类通信** :`父子组件通信`、`隔代组件通信`、`兄弟组件通信`,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信 **组件传参的各种方式** ![f83e91f83ae6eb21fab3e7a236d1b65e.png][] **组件通信常用方式有以下几种** * `props / $emit` **适用 父子组件通信** * 父组件向子组件传递数据是通过 `prop` 传递的,子组件传递数据给父组件是通过`$emit` 触发事件来做到的 * `ref` 与 `$parent / $children(vue3废弃)` **适用 父子组件通信** * `ref`:如果在普通的 `DOM` 元素上使用,引用指向的就是 `DOM` 元素;如果用在子组件上,引用就指向组件实例 * `$parent / $children`:访问访问父组件的属性或方法 / 访问子组件的属性或方法 * `EventBus ($emit / $on)` **适用于 父子、隔代、兄弟组件通信** * 这种方法通过一个空的 `Vue` 实例作为中央事件总线([事件中心][Link 2]),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件 * `$attrs / $listeners(vue3废弃)` **适用于 隔代组件通信** * `$attrs`:包含了父作用域中不被 `prop` 所识别 (且获取) 的特性绑定 ( `class` 和 `style` 除外 )。当一个组件没有声明任何 `prop`时,这里会包含所有父作用域的绑定 ( `class` 和 `style` 除外 ),并且可以通过 `v-bind="$attrs"` 传入内部组件。通常配合 `inheritAttrs` 选项一起使用 * `$listeners`:包含了父作用域中的 (不含 `.native` 修饰器的) `v-on` 事件监听器。它可以通过 `v-on="$listeners"` 传入内部组件 * `provide / inject` **适用于 隔代组件通信** * 祖先组件中通过 `provider` 来提供变量,然后在子孙组件中通过 `inject` 来注入变量。 `provide / inject` API 主要解决了跨级组件间的通信问题, **不过它的使用场景,主要是子组件获取上级组件的状态** ,跨级组件间建立了一种主动提供与依赖注入的关系 * `$root` **适用于 隔代组件通信** 访问根组件中的属性或方法,是根组件,不是父组件。`$root`只对根组件有用 * `Vuex` **适用于 父子、隔代、兄弟组件通信** * `Vuex` 是一个专为 `Vue.js` 应用程序开发的状态管理模式。每一个 `Vuex` 应用的核心就是 `store`(仓库)。“store” 基本上就是一个[容器][Link 3],它包含着你的应用中大部分的状态 ( `state` ) * `Vuex` 的状态存储是响应式的。当 `Vue` 组件从 `store` 中读取状态的时候,若 `store` 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 * 改变 `store` 中的状态的唯一途径就是显式地提交 (`commit`) `mutation`。这样使得我们可以方便地跟踪每一个状态的变化。 **根据组件之间关系讨论组件通信最为清晰有效** * 父子组件:`props`/`$emit`/`$parent`/`ref` * 兄弟组件:`$parent`/`eventbus`/`vuex` * 跨层级关系:`eventbus`/`vuex`/`provide+inject`/`$attrs + $listeners`/`$root` > 下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯 **1. 父子组件通信** > 使用`props`,父组件可以使用`props`向子组件传递数据。 父组件`vue`模板`father.vue`: <template> <child :msg="message"></child> </template> <script> import child from './child.vue'; export default { components: { child }, data () { return { message: 'father message'; } } } </script> 复制 子组件`vue`模板`child.vue`: <template> <div>{ {msg}}</div> </template> <script> export default { props: { msg: { type: String, required: true } } } </script> 复制 **回调函数(callBack)** 父传子:将父组件里定义的`method`作为`props`传入子组件 // 父组件Parent.vue: <Child :changeMsgFn="changeMessage"> methods: { changeMessage(){ this.message = 'test' } } 复制 // 子组件Child.vue: <button @click="changeMsgFn"> props:['changeMsgFn'] 复制 **子组件向父组件通信** > 父组件向子组件传递事件方法,子组件通过`$emit`触发事件,回调给父组件 父组件`vue`模板`father.vue`: <template> <child @msgFunc="func"></child> </template> <script> import child from './child.vue'; export default { components: { child }, methods: { func (msg) { console.log(msg); } } } </script> 复制 子组件`vue`模板`child.vue`: <template> <button @click="handleClick">点我</button> </template> <script> export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } } } </script> 复制 **2. provide / inject 跨级访问祖先组件的数据** 父组件通过使用`provide(){return{}}`提供需要传递的数据 export default { data() { return { title: '我是父组件', name: 'poetry' } }, methods: { say() { alert(1) } }, // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法 provide() { return { message: '我是祖先组件提供的数据', name: this.name, // 传递属性 say: this.say } } } 复制 子组件通过使用`inject:[“参数1”,”参数2”,…]`接收父组件传递的参数 <template> <p>曾孙组件</p> <p>{ {message}}</p> </template> <script> export default { // inject 注入/接收祖先组件传递的所需要的数据即可 //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 { {}} inject: [ "message","say"], mounted() { this.say(); }, }; </script> 复制 **3. $parent + $children 获取父组件实例和子组件实例的集合** * `this.$parent` 可以直接访问该组件的父实例或组件 * 父组件也可以通过 `this.$children` 访问它所有的子组件;需要注意 `$children` 并不保证顺序,也不是响应式的 <!-- parent.vue --> <template> <div> <child1></child1> <child2></child2> <button @click="clickChild">$children方式获取子组件值</button> </div> </template> <script> import child1 from './child1' import child2 from './child2' export default { data(){ return { total: 108 } }, components: { child1, child2 }, methods: { funa(e){ console.log("index",e) }, clickChild(){ console.log(this.$children[0].msg); console.log(this.$children[1].msg); } } } </script> 复制 <!-- child1.vue --> <template> <div> <button @click="parentClick">点击访问父组件</button> </div> </template> <script> export default { data(){ return { msg:"child1" } }, methods: { // 访问父组件数据 parentClick(){ this.$parent.funa("xx") console.log(this.$parent.total); } } } </script> 复制 <!-- child2.vue --> <template> <div> child2 </div> </template> <script> export default { data(){ return { msg: 'child2' } } } </script> 复制 **4. $attrs + $listeners多级组件通信** > `$attrs` 包含了从父组件传过来的所有`props`属性 // 父组件Parent.vue: <Child :name="name" :age="age"/> // 子组件Child.vue: <GrandChild v-bind="$attrs" /> // 孙子组件GrandChild <p>姓名:{ {$attrs.name}}</p> <p>年龄:{ {$attrs.age}}</p> 复制 > `$listeners`包含了父组件监听的所有事件 // 父组件Parent.vue: <Child :name="name" :age="age" @changeNameFn="changeName"/> // 子组件Child.vue: <button @click="$listeners.changeNameFn"></button> 复制 **5. ref 父子组件通信** // 父组件Parent.vue: <Child ref="childComp"/> <button @click="changeName"></button> changeName(){ console.log(this.$refs.childComp.age); this.$refs.childComp.changeAge() } // 子组件Child.vue: data(){ return{ age:20 } }, methods(){ changeAge(){ this.age=15 } } 复制 **6. 非父子, 兄弟组件之间通信** > `vue2`中废弃了`broadcast`广播和分发事件的方法。父子组件中可以用`props`和`$emit()`。如何实现非父子组件间的通信,可以通过实例一个`vue`实例`Bus`作为媒介,要相互通信的兄弟组件之中,都引入`Bus`,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。`Bus.js`可以是这样: // Bus.js // 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能 复制 <template> <button @click="toBus">子组件传给兄弟组件</button> </template> <script> export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } } } </script> 复制 另一个组件也在钩子函数中监听`on`事件 export default { data() { return { message: '' } }, mounted() { this.$bus.$on('foo', (msg) => { this.message = msg }) } } 复制 **7. $root 访问根组件中的属性或方法** * 作用:访问根组件中的属性或方法 * 注意:是根组件,不是父组件。`$root`只对根组件有用 var vm = new Vue({ el: "#app", data() { return { rootInfo:"我是根元素的属性" } }, methods: { alerts() { alert(111) } }, components: { com1: { data() { return { info: "组件1" } }, template: "<p>{ { info }} <com2></com2></p>", components: { com2: { template: "<p>我是组件1的子组件</p>", created() { this.$root.alerts()// 根组件方法 console.log(this.$root.rootInfo)// 我是根元素的属性 } } } } } }); 复制 **8. vuex** * 适用场景: 复杂关系的组件数据传递 * Vuex作用相当于一个用来存储共享变量的容器 ![3c0a36d153508a582a4845b44c6dba22.png][] * `state`用来存放共享变量的地方 * `getter`,可以增加一个`getter`派生状态,(相当于`store`中的计算属性),用来获得共享变量的值 * `mutations`用来存放修改`state`的方法。 * `actions`也是用来存放修改state的方法,不过`action`是在`mutations`的基础上进行。常用来做一些异步操作 [Link 1]: https://cloud.tencent.com/product/cvm?from=20065&from_column=20065 [b71d0e754854a220735666e3b2763d7a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/49c92514ab3d4732887e2869d39d176a.png [9b4737d13126aa3da427d4b5ae4ff5e0.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/746c4588dc964978a8fb05fff207e206.png [336688c961e952ad64c7fbd478b4cc54.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/e1e6d33000f048c58560b29d4e6dc2de.png [6397dcf2f532bdb98378967820b1ce2e.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/26faa14d5d4142cba78cedda66c031ef.png [6f60d5b59f74763ae89c56bf52d3d7c7.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/083c3b7181ee4d54826e3b1ff674a389.png [f83e91f83ae6eb21fab3e7a236d1b65e.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/2e9dfb5dcdbd42968701534dbdd1f680.png [Link 2]: https://cloud.tencent.com/product/eb?from=20065&from_column=20065 [Link 3]: https://cloud.tencent.com/product/tke?from=20065&from_column=20065 [3c0a36d153508a582a4845b44c6dba22.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/15/5c1a09d1bb4d4fe79427dc4e9aaa4255.png
相关 面试题——vue面试题总结 vue面试题总结 1.vue的特点是什么 1.国人开发的轻量级框架 2.双向数据绑定,在数据操作方面更为简单 3.视图,数据,结构分析,不需要进行逻辑代码的修改 た 入场券/ 2023年01月18日 11:29/ 0 赞/ 428 阅读
相关 Vue面试题 > 1.对MVVM的理解 M:model层,在model层对数据进行操作和修改数据 V:视图层 VM:监听模型数据的改变和控制视图行为。相当于模型层和视图层 女爷i/ 2022年12月22日 11:17/ 0 赞/ 213 阅读
相关 vue面试题 为什么props是单向绑定的? > 所有prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。每次父级组件发生变更 た 入场券/ 2022年11月17日 10:18/ 0 赞/ 212 阅读
相关 vue面试题 vue.js是什么? 是一套构建用户界面的 渐进式框架 vuex是什么? Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用 曾经终败给现在/ 2022年05月10日 01:03/ 0 赞/ 157 阅读
相关 Vue面试题 1. Vue中的MVVM模式 ![在这里插入图片描述][70] 即 Model-View-ViewModel Model是数据层,View是视图层,ViewMod 水深无声/ 2022年05月07日 05:56/ 0 赞/ 404 阅读
相关 Vue 面试题 Vue面试题 1、Vue总结: vue的使用方式有两种 方式一:像jQuery一样引入使用--vue的特性都可以使用,双向数据绑定 妖狐艹你老母/ 2022年04月13日 15:15/ 0 赞/ 283 阅读
相关 Vue面试题 VUE 面试题 一.对于MVVM的理解? MVVM 是 Model-View-ViewModel 的缩写。 Model代表数据模型,也可以在Model中定义数据修改 超、凢脫俗/ 2022年02月28日 06:18/ 0 赞/ 337 阅读
相关 VUE面试题 VUE 1. MVVM如何实现模板绑定,依赖是如何收集的? 2. vue2中的diff算法是怎样实现的? 3. 请详细说出vue生命周期的执行过程 4 ╰半橙微兮°/ 2022年01月30日 15:34/ 0 赞/ 276 阅读
相关 vue面试题 vue面试题 1.Vue和react的相同与不同 相同点: 都支持服务器端渲染 都有virtual DOM,组件化开发,通过props参数进行父子组件数据的 太过爱你忘了你带给我的痛/ 2021年12月08日 16:03/ 0 赞/ 385 阅读
相关 vue面试题 1.vuex中异步在哪里写,可以在mutation里面吗,为什么? Mutation 必须是同步函数 一条重要的原则就是要记住 mutation 必须是同步函数。为什么 ゞ 浴缸里的玫瑰/ 2021年12月05日 02:31/ 0 赞/ 361 阅读
还没有评论,来说两句吧...