vue面试中常见的面试题 您所在的位置:网站首页 阿里vue面试题 vue面试中常见的面试题

vue面试中常见的面试题

2024-06-01 03:22| 来源: 网络整理| 查看: 265

1.谈一下你对MVVM 原理的理解 传统的MVC指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染 MVVM:传统的前端会将数据手动渲染到页面上,MVVM模式不需要用户收到操作dom元素,将数据绑定到viewModel层上,会自动将数据渲染到页面中,视图变化会通知viewModel层更新数据。ViewModel就是我们MVVM模式中的桥梁. 2.请说一下响应式数据的原理? 1.核心点:Object.defineProperty 2.默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // ** 收集依赖 ** / if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } val = newVal childOb = !shallow && observe(newVal) dep.notify() /**通知相关依赖进行更新**/ } }) 3.Vue中是如何检测数组变化? 使用函数劫持的方式,重写了数组的方法 Vue将data中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) 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 change ob.dep.notify() // 当调用数组方法后,手动通知视图更新 return result }) }) this.observeArray(value) // 进行深度监控 4.为何Vue采用异步渲染?

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染.所以为了性能考虑。Vue会在本轮数据更新后,再去异步更新视图!

update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新 } } export function queueWatcher (watcher: Watcher) { const id = watcher.id // 会对相同的watcher进行过滤 if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新 } } } 5.nextTick实现原理

nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。 所以这个nextTick方法就是异步方法

let timerFunc // 会定义一个异步方法 if (typeof Promise !== 'undefined' && isNative(Promise)) { // promise const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' ) { // setImmediate timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { // setTimeout setTimeout(flushCallbacks, 0) } } // nextTick实现 export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } } 6.Vue中Computed的特点 默认computed也是一个watcher是具备缓存的,只要当依赖的属性发生变化时才会更新视图 function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } } function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // 如果依赖的值没发生变化,就不会重新求值 watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } } 7.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() } return value } function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } } 8.Vue组件的生命周期

在这里插入图片描述

要掌握每个生命周期什么时候被调用 beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。 created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。 mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。 beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。 destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。 要掌握每个生命周期内部可以做什么事 created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。 mounted 实例已经挂载完成,可以进行一些DOM操作 beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。 updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。 destroyed 可以执行一些优化操作,清空定时器,解除绑定事件 9.ajax请求放在哪个生命周期中 在created的时候,视图中的dom并没有渲染出来,所以此时如果直接去操dom节点,无法找到相关的元素 在mounted中,由于此时dom已经渲染出来了,所以可以直接操作dom节点

一般情况下都放到mounted中,保证逻辑的统一性,因为生命周期是同步执行的,ajax是异步执行的

服务端渲染不支持mounted方法,所以在服务端渲染的情况下统一放到created中 10.何时需要使用beforeDestroy 可能在当前页面中使用了$on方法,那需要在组件销毁前解绑。 清除自己定义的定时器 解除事件的绑定 scroll mousemove .... 11.Vue中模板编译原理 将template转化成render函数 function baseCompile ( template: string, options: CompilerOptions ) { const ast = parse(template.trim(), options) // 1.将模板转化成ast语法树 if (options.optimize !== false) { // 2.优化树 optimize(ast, options) } const code = generate(ast, options) // 3.生成树 return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; const qnameCapture = `((?:${ncname}\\:)?${ncname})`; const startTagOpen = new RegExp(`^`); // 匹配标签结尾的 const attribute = /^\s*([^\s"'\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=`]+)))?/; // 匹配属性的 const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 > let root; let currentParent; let stack = [] function createASTElement(tagName,attrs){ return { tag:tagName, type:1, children:[], attrs, parent:null } } function start(tagName,attrs){ let element = createASTElement(tagName,attrs); if(!root){ root = element; } currentParent = element; stack.push(element); } function chars(text){ currentParent.children.push({ type:3, text }) } function end(tagName){ const element = stack[stack.length-1]; stack.length --; currentParent = stack[stack.length-1]; if(currentParent){ element.parent = currentParent; currentParent.children.push(element) } } function parseHTML(html){ while(html){ let textEnd = html.indexOf('`) // with(this){return _c('div',{domProps:{"innerHTML":_s('hello')}})} console.log(r.render); // _c 定义在core/instance/render.js // _s 定义在core/instance/render-helpers/index,js if (key === 'textContent' || key === 'innerHTML') { if (vnode.children) vnode.children.length = 0 if (cur === oldProps[key]) continue // #6601 work around Chrome version 子通过props、子-> 父$on、$emit 获取父子组件实例的方式$parent、$children 在父组件中提供数据子组件进行消费 Provide、inject Ref获取实例的方式调用组件的属性或者方法 Event Bus 实现跨组件通信 Vuex 状态管理实现通信 25.Vue中相同逻辑如何抽离? Vue.mixin用法 给组件每个生命周期,函数等都混入一些公共逻辑 Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin); // 将当前定义的属性合并到每个组件中 return this } export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (!child._base) { if (child.extends) { // 递归合并extends parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { // 递归合并mixin 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 } 26.为什么要使用异步组件? 理解:

如果组件功能多打包出的结果会变大,我可以采用异步的方式来加载组件。主要依赖import()这个语法,可以实现文件的分割加载。

components:{ AddCustomerSchedule(resolve) { require(["../components/AddCustomer"], resolve); } } 原理: export function createComponent ( Ctor: Class | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array, tag?: string ): VNode | Array | void { // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回undefiend // 第二次渲染时Ctor不为undefined if (Ctor === undefined) { return createAsyncPlaceholder( // 渲染占位符 空虚拟节点 asyncFactory, data, context, children, tag ) } } } function resolveAsyncComponent ( factory: Function, baseCtor: Class ): Class | void { if (isDef(factory.resolved)) { // 3.在次渲染时可以拿到获取的最新组件 return factory.resolved } const resolve = once((res: Object | Class) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender(true) //2. 强制更新视图重新渲染 } else { owners.length = 0 } }) const reject = once(reason => { if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } }) const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用resolve方法后 sync = false return factory.resolved } 27.什么是作用域插槽? 1.插槽: 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类 {a:[vnode],b[vnode]} 渲染组件时会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件) 2.作用域插槽: 作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件) const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` node react vue `) const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` `); 作用域插槽: let ele = VueTemplateCompiler.compile(` {{msg.a}} `); const VueTemplateCompiler = require('vue-template-compiler'); VueTemplateCompiler.compile(` `); 28.谈谈你对 keep-alive 的了解?

keep-alive可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性include/exclude,2个生命周期activated,deactivated

core/components/keep-alive.js export default { name: 'keep-alive', abstract: true, // 抽象组件 props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, created () { this.cache = Object.create(null) // 创建缓存列表 this.keys = [] // 创建缓存组件的key列表 }, destroyed () { // keep-alive销毁时 会清空所有的缓存和key for (const key in this.cache) { // 循环销毁 pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // 会监控include 和 include属性 进行组件的缓存处理 this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render () { const slot = this.$slots.default // 会默认拿插槽 const vnode: VNode = getFirstComponentChild(slot) // 只缓存第一个组件 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // 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 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 // 如果组件没key 就自己通过 组件的标签和key和cid 拼接一个key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // 直接拿到组件实例 // make current key freshest remove(keys, key) // 删除当前的 [b,c,d,e,a] // LRU 最近最久未使用法 keys.push(key) // 并将key放到后面[b,a] } else { cache[key] = vnode // 缓存vnode keys.push(key) // 将key 存入 // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { // 缓存的太多超过了max 就需要删除掉 pruneCacheEntry(cache, keys[0], keys, this._vnode) // 要删除第0个 但是现在渲染的就是第0个 } } vnode.data.keepAlive = true // 并且标准keep-alive下的组件是一个缓存组件 } return vnode || (slot && slot[0]) // 返回当前的虚拟节点 } } 29.Vue中常见性能优化 1.编码优化: 1.不要将所有的数据都放在data中,data中的数据都会增加getter和setter,会收集对应的watcher 2.vue 在 v-for 时给每项元素绑定事件需要用事件代理 3.SPA页面采用keep-alive缓存组件 4.拆分组件( 提高复用性、增加代码的可维护性,减少不必要的渲染 ) 5.v-if 当值为false时内部指令不会执行,具有阻断功能,很多情况下使用v-if替代v-show 6.key保证唯一性 ( 默认vue会采用就地复用策略 ) 7.Object.freeze 冻结数据 8.合理使用路由懒加载、异步组件 9.尽量采用runtime运行时版本 10.数据持久化的问题 (防抖、节流) 2.Vue加载性能优化: 第三方模块按需导入 (babel-plugin-component) 滚动到可视区域动态加载 ( https://tangbc.github.io/vue-virtual-scroll-list ) 图片懒加载 (https://github.com/hilongjw/vue-lazyload.git) 3.用户体验: app-skeleton骨架屏 app-shellapp壳 pwa 4.SEO优化: 预渲染插件 prerender-spa-plugin 服务端渲染ssr 5.打包优化: 使用cdn的方式加载第三方模块 多线程打包 happypack splitChunks 抽离公共文件 sourceMap生成 6.缓存,压缩 客户端缓存、服务端缓存 服务端gzip压缩 30.Vue3.0你知道有哪些改进? Vue3采用了TS来编写 支持 Composition API Vue3中响应式数据原理改成proxy vdom的对比算法更新,只更新vdom的绑定了动态数据的部分 31.实现hash路由和history路由 onhashchange history.pushState 32.Vue-Router中导航守卫有哪些? 导航被触发。 在失活的组件里调用离开守卫。 调用全局的 beforeEach 守卫。 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。 在路由配置里调用 beforeEnter。 解析异步路由组件。 在被激活的组件里调用 beforeRouteEnter。 调用全局的 beforeResolve 守卫 (2.5+)。 导航被确认。 调用全局的 afterEach 钩子。 触发 DOM 更新。 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。 33.action 和 mutation区别 mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测) action 异步操作,可以获取数据后调佣mutation提交最终数据 34.简述Vuex工作原理



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有