vue 面试汇总(更新中...)

1.说说对双向绑定的理解

1.1、双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

数据层(Model):应用的数据及业务逻辑

视图层(View):应用的展示效果,各类UI组件

业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

数据变化后更新视图

视图变化后更新数据

当然,它还有两个主要部分组成

监听器(Observer):对所有数据的属性进行监听

解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

1.2、实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中

同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中

同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher

将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数


2.单页应用与多页应用的区别

单页应用 ---SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面举个例子来讲就是一个杯子,早上装的牛奶,中午装的是开水,晚上装的是茶,我们发现,变的始终是杯子里的内容,而杯子始终是那个杯子


单页面应用(SPA) | 多页面应用(MPA) 

组成 | 一个主页面和多个页面片段 | 多个主页面 |

刷新方式 | 局部刷新 | 整页刷新 |

url模式 | 哈希模式 | 历史模式 |

SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |

数据传递 | 容易 | 通过url、cookie、localStorage等传递 |

页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |

 维护成本 | 相对容易 | 相对复杂 |

1.1单页应用优缺点

优点:

具有桌面应用的即时性、网站的可移植性和可访问性

用户体验好、快,内容的改变不需要重新加载整个页面

良好的前后端分离,分工更明确

缺点:

不利于搜索引擎的抓取

首次渲染速度相对较慢

3.v-show与v-if的区别

控制手段不同

编译过程不同

编译条件不同

控制手段:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

v-show 由false变为true的时候不会触发组件的生命周期

v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法

性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

4.vue挂载都干了什么

1.1 分析

首先找到vue的构造函数

源码位置:src\core\instance\index.js

functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)}

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法

initMixin(Vue);// 定义 _initstateMixin(Vue);// 定义 $set $get $delete $watch 等eventsMixin(Vue);// 定义事件  $on  $once $off $emitlifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroyrenderMixin(Vue);// 定义 _render 返回虚拟dom

首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法

源码位置:src\core\instance\init.js

Vue.prototype._init=function(options?:Object){constvm:Component=this// a uidvm._uid=uid++letstartTag,endTag/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){startTag=`vue-perf-start:${vm._uid}`endTag=`vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue=true// merge options// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法if(options&&options._isComponent){// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm,options)}else{// 合并vue属性vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}/* istanbul ignore else */if(process.env.NODE_ENV!=='production'){// 初始化proxy拦截器initProxy(vm)}else{vm._renderProxy=vm}// expose real selfvm._self=vm// 初始化组件生命周期标志位initLifecycle(vm)// 初始化组件事件侦听initEvents(vm)// 初始化渲染方法initRender(vm)callHook(vm,'beforeCreate')// 初始化依赖注入内容,在初始化data、props之前initInjections(vm)// resolve injections before data/props// 初始化props/data/method/watch/methodsinitState(vm)initProvide(vm)// resolve provide after data/propscallHook(vm,'created')/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){vm._name=formatComponentName(vm,false)mark(endTag)measure(`vue${vm._name}init`,startTag,endTag)}// 挂载元素if(vm.$options.el){vm.$mount(vm.$options.el)}}

仔细阅读上面的代码,我们得到以下结论:

在调用beforeCreate之前,数据初始化并未完成,像data、props这些属性无法访问到

到了created的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

挂载方法是调用vm.$mount方法

initState方法是完成props/data/method/watch/methods的初始化

源码位置:src\core\instance\state.js

exportfunctioninitState(vm:Component){// 初始化组件的watcher列表vm._watchers=[]constopts=vm.$options// 初始化propsif(opts.props)initProps(vm,opts.props)// 初始化methods方法if(opts.methods)initMethods(vm,opts.methods)if(opts.data){// 初始化data  initData(vm)}else{observe(vm._data={},true/* asRootData */)}if(opts.computed)initComputed(vm,opts.computed)if(opts.watch&&opts.watch!==nativeWatch){initWatch(vm,opts.watch)}}

我们和这里主要看初始化data的方法为initData,它与initState在同一文件上

functioninitData(vm:Component){letdata=vm.$options.data// 获取到组件上的datadata=vm._data=typeofdata==='function'?getData(data,vm):data||{}if(!isPlainObject(data)){data={}process.env.NODE_ENV!=='production'&&warn('data functions should return an object:\n'+'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconstkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]if(process.env.NODE_ENV!=='production'){// 属性名不能与方法名重复if(methods&&hasOwn(methods,key)){warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 属性名不能与state名称重复if(props&&hasOwn(props,key)){process.env.NODE_ENV!=='production'&&warn(`The data property "${key}" is already declared as a prop. `+`Use prop default value instead.`,vm)}elseif(!isReserved(key)){// 验证key值的合法性// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据proxy(vm,`_data`,key)}}// observe data// 响应式监听data是数据的变化observe(data,true/* asRootData */)}

仔细阅读上面的代码,我们可以得到以下结论:

初始化顺序:props、methods、data

data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

关于数据响应式在这就不展开详细说明

上文提到挂载方法是调用vm.$mount方法

源码位置:

Vue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{// 获取或查询元素el=el&&query(el)/* istanbul ignore if */// vue 不允许直接挂载到body或页面文档上if(el===document.body||el===document.documentElement){process.env.NODE_ENV!=='production'&&warn(`Do not mount Vue to or - mount to normal elements instead.`)returnthis}constoptions=this.$options// resolve template/el and convert to render functionif(!options.render){lettemplate=options.template// 存在template模板,解析vue模板文件if(template){if(typeoftemplate==='string'){if(template.charAt(0)==='#'){template=idToTemplate(template)/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&!template){warn(`Template element not found or is empty:${options.template}`,this)}}}elseif(template.nodeType){template=template.innerHTML}else{if(process.env.NODE_ENV!=='production'){warn('invalid template option:'+template,this)}returnthis}}elseif(el){// 通过选择器获取元素内容template=getOuterHTML(el)}if(template){/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile')}/**      *  1.将temmplate解析ast tree      *  2.将ast tree转换成render语法字符串      *  3.生成render方法      */const{render,staticRenderFns}=compileToFunctions(template,{outputSourceRange:process.env.NODE_ENV!=='production',shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters:options.delimiters,comments:options.comments},this)options.render=renderoptions.staticRenderFns=staticRenderFns/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile end')measure(`vue${this._name}compile`,'compile','compile end')}}}returnmount.call(this,el,hydrating)}

阅读上面代码,我们能得到以下结论:

不要将根元素放到body或者html上

可以在对象中定义template/render或者直接使用template、el表示元素选择器

最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

对template的解析步骤大致分为以下几步:

将html文档片段解析成ast描述符

将ast描述符解析成字符串

生成render函数

生成render函数,挂载到vm上后,会再次调用mount方法

源码位置:src\platforms\web\runtime\index.js

// public mount methodVue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{el=el&&inBrowser?query(el):undefined// 渲染组件returnmountComponent(this,el,hydrating)}

调用mountComponent渲染组件

exportfunctionmountComponent(vm:Component,el: ?Element,hydrating?:boolean):Component{vm.$el=el// 如果没有获取解析的render函数,则会抛出警告// render是解析模板文件生成的if(!vm.$options.render){vm.$options.render=createEmptyVNodeif(process.env.NODE_ENV!=='production'){/* istanbul ignore if */if((vm.$options.template&&vm.$options.template.charAt(0)!=='#')||vm.$options.el||el){warn('You are using the runtime-only build of Vue where the template '+'compiler is not available. Either pre-compile the templates into '+'render functions, or use the compiler-included build.',vm)}else{// 没有获取到vue的模板文件warn('Failed to mount component: template or render function not defined.',vm)}}}// 执行beforeMount钩子callHook(vm,'beforeMount')letupdateComponent/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){updateComponent=()=>{constname=vm._nameconstid=vm._uidconststartTag=`vue-perf-start:${id}`constendTag=`vue-perf-end:${id}`mark(startTag)constvnode=vm._render()mark(endTag)measure(`vue${name}render`,startTag,endTag)mark(startTag)vm._update(vnode,hydrating)mark(endTag)measure(`vue${name}patch`,startTag,endTag)}}else{// 定义更新函数updateComponent=()=>{// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_rendervm._update(vm._render(),hydrating)}}// we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already defined// 监听当前组件状态,当有数据变化时,更新组件newWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted&&!vm._isDestroyed){// 数据更新引发的组件更新callHook(vm,'beforeUpdate')}}},true/* isRenderWatcher */)hydrating=false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvm}

阅读上面代码,我们得到以下结论:

会触发boforeCreate钩子

定义updateComponent渲染页面视图的方法

监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent方法主要执行在vue初始化时声明的render,update方法

render的作用主要是生成vnode

源码位置:src\core\instance\render.js

// 定义vue 原型上的render方法Vue.prototype._render=function():VNode{constvm:Component=this// render函数来自于组件的optionconst{render,_parentVnode}=vm.$optionsif(_parentVnode){vm.$scopedSlots=normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)}// set parent vnode. this allows render functions to have access// to the data on the placeholder node.vm.$vnode=_parentVnode// render selfletvnodetry{// There's no need to maintain a stack because all render fns are called// separately from one another. Nested component's render fns are called// when parent component is patched.currentRenderingInstance=vm// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNodevnode=render.call(vm._renderProxy,vm.$createElement)}catch(e){handleError(e,vm,`render`)// return error render result,// or previous vnode to prevent render error causing blank component/* istanbul ignore else */if(process.env.NODE_ENV!=='production'&&vm.$options.renderError){try{vnode=vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e)}catch(e){handleError(e,vm,`renderError`)vnode=vm._vnode}}else{vnode=vm._vnode}}finally{currentRenderingInstance=null}// if the returned array contains only a single node, allow itif(Array.isArray(vnode)&&vnode.length===1){vnode=vnode[0]}// return empty vnode in case the render function errored outif(!(vnodeinstanceofVNode)){if(process.env.NODE_ENV!=='production'&&Array.isArray(vnode)){warn('Multiple root nodes returned from render function. Render function '+'should return a single root node.',vm)}vnode=createEmptyVNode()}// set parentvnode.parent=_parentVnodereturnvnode}

_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中

源码位置:src\core\instance\lifecycle.js

Vue.prototype._update=function(vnode:VNode,hydrating?:boolean){constvm:Component=thisconstprevEl=vm.$elconstprevVnode=vm._vnode// 设置当前激活的作用域constrestoreActiveInstance=setActiveInstance(vm)vm._vnode=vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if(!prevVnode){// initial render// 执行具体的挂载逻辑vm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/* removeOnly */)}else{// updatesvm.$el=vm.__patch__(prevVnode,vnode)}restoreActiveInstance()// update __vue__ referenceif(prevEl){prevEl.__vue__=null}if(vm.$el){vm.$el.__vue__=vm}// if parent is an HOC, update its $el as wellif(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){vm.$parent.$el=vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}


new Vue的时候调用会调用_init方法

定义 $set、 $get 、$delete、$watch 等方法

定义 $on、$off、$emit、$off 等事件

定义 _update、$forceUpdate、$destroy生命周期

调用$mount进行页面的挂载

挂载的时候主要是通过mountComponent方法

定义updateComponent更新函数

执行render生成虚拟DOM

_update将虚拟DOM生成真实DOM结构,并且渲染到页面中

5.vue 生命周期

1.1生命周期是什么

生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())

1.2生命周期有哪些

Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期

生命周期描述

beforeCreate组件实例被创建之初

created组件实例已经完全创建

beforeMount组件挂载之前

mounted组件挂载到实例上去之后

beforeUpdate组件数据发生变化,更新之前

updated数据数据更新之后

beforeDestroy组件实例销毁之前

destroyed组件实例销毁之后

activatedkeep-alive 缓存的组件激活时

deactivatedkeep-alive 缓存的组件停用时调用

errorCaptured捕获一个来自子孙组件的错误时被调用

1.3生命周期整体流程

Vue生命周期流程图

具体分析

beforeCreate -> created

初始化vue实例,进行数据观测

created

完成数据观测,属性与方法的运算,watch、event事件回调的配置

可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算

此时vm.$el 并没有被创建

created -> beforeMount

判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译

优先级:render > template > outerHTML

vm.el获取到的是挂载DOM的

beforeMount

在此阶段可获取到vm.el

此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

beforeMount -> mounted

此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM

mounted

vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM

beforeUpdate

更新的数据必须是被渲染在模板上的(el、template、render之一)

此时view层还未更新

若在beforeUpdate中再次修改数据,不会再次触发更新方法

updated

完成view层的更新

若在updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)

beforeDestroy

实例被销毁前调用,此时实例属性与方法仍可访问

destroyed

完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

并不能清除DOM,仅仅销毁实例

使用场景分析

生命周期描述

beforeCreate执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务

created组件初始化完毕,各种数据可以使用,常用于异步数据获取

beforeMount未执行渲染、更新,dom未创建

mounted初始化结束,dom已创建,可用于获取访问数据和dom元素

beforeUpdate更新前,可用于获取更新前各种状态

updated更新后,所有状态已是最新

beforeDestroy销毁前,可用于一些定时器或订阅的取消

destroyed组件已销毁,作用同上

1.4题外话:数据请求在created和mouted的区别

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成mounted是在页面dom节点渲染完毕之后就立刻执行的触发时机上created是比mounted要更早的两者相同点:都能拿到实例对象的属性和方法讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况建议:放在create生命周期当中

6.v-if 和v-for

1.1优先级

v-if与v-for都是vue模板系统中的指令

在vue模板编译的时候,会将指令系统转化成可执行的render函数

示例

编写一个p标签,同时使用v-if与 v-for

{{ item.title }}

创建vue实例,存放isShow与items数据

constapp=newVue({el:"#app",data(){return{items:[{title:"foo"},{title:"baz"}]}},computed:{isShow(){returnthis.items&&this.items.length>0}}})

模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数

ƒanonymous(){with(this){return_c('div',{attrs:{"id":"app"}},_l((items),function(item){return(isShow)?_c('p',[_v("\n"+_s(item.title)+"\n")]):_e()}),0)}}

_l是vue的列表渲染函数,函数内部都会进行一次if判断

初步得到结论:v-for优先级是比v-if高

再将v-for与v-if置于不同标签

{{item.title}}

再输出下render函数

ƒanonymous(){with(this){return_c('div',{attrs:{"id":"app"}},[(isShow)?[_v("\n"),_l((items),function(item){return_c('p',[_v(_s(item.title))])})]:_e()],2)}}

这时候我们可以看到,v-for与v-if作用在不同标签时候,是先进行判断,再进行列表的渲染

我们再在查看下vue源码

源码位置: \vue-dev\src\compiler\codegen\index.js

exportfunctiongenElement(el:ASTElement,state:CodegenState):string{if(el.parent){el.pre=el.pre||el.parent.pre}if(el.staticRoot&&!el.staticProcessed){returngenStatic(el,state)}elseif(el.once&&!el.onceProcessed){returngenOnce(el,state)}elseif(el.for&&!el.forProcessed){returngenFor(el,state)}elseif(el.if&&!el.ifProcessed){returngenIf(el,state)}elseif(el.tag==='template'&&!el.slotTarget&&!state.pre){returngenChildren(el,state)||'void 0'}elseif(el.tag==='slot'){returngenSlot(el,state)}else{// component or element...}

在进行if判断的时候,v-for是比v-if先进行判断

最终结论:v-for优先级比v-if高

1.2注意事项

永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)

如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环

如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项

computed:{items:function(){returnthis.list.filter(function(item){returnitem.isShow})}}

7.spa首屏加载慢

一、什么是首屏加载

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验中最重要的环节

关于计算首屏时间

利用performance.timing提供的数据:

通过DOMContentLoad或者performance来计算出首屏时间

// 方案一:document.addEventListener('DOMContentLoaded',(event)=>{console.log('first contentful painting');});

// 方案二:performance.getEntriesByName("first-contentful-paint")[0].startTime// performance.getEntriesByName("first-contentful-paint")[0]// 会返回一个 PerformancePaintTiming的实例,

结构如下:{name:"first-contentful-paint",entryType:"paint",startTime:507.80000002123415,duration:0,};

二、加载慢的原因

在页面渲染的过程,导致加载速度慢的因素可能如下:

网络延时问题

资源文件体积是否过大

资源是否重复发送请求去加载了

加载脚本的时候,渲染内容堵塞了

三、解决方案

常见的几种SPA首屏优化方式

减小入口文件积

静态资源本地缓存

UI框架按需加载

图片资源的压缩

组件重复打包

开启GZip压缩

使用SSR

8.为什么组件data必须是函数不能是对象

一、实例和组件定义data的区别

vue实例的时候定义data属性既可以是一个对象,也可以是一个函数

constapp=newVue({el:"#app",// 对象格式data:{foo:"foo"},// 函数格式data(){return{foo:"foo"}}})

组件中定义data属性,只能是一个函数

如果为组件data直接定义为一个对象

Vue.component('component1',{template:`

组件
`,data:{foo:"foo"}})

则会得到警告信息

警告说明:返回的data应该是一个函数在每一个组件实例中

二、组件data定义函数与对象的区别

上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?

在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例

这里我们模仿组件构造函数,定义data属性,采用对象的形式

functionComponent(){}Component.prototype.data={count:0}

创建两个组件实例

const componentA = new Component()

const componentB = new Component()

修改componentA组件data属性的值,componentB中的值也发生了改变

console.log(componentB.data.count)// 0componentA.data.count=1console.log(componentB.data.count)// 1

产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响

如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)

functionComponent(){this.data=this.data()}Component.prototype.data=function(){return{count:0}}

修改componentA组件data属性的值,componentB中的值不受影响

console.log(componentB.data.count)// 0componentA.data.count=1console.log(componentB.data.count)// 0

vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染

三、原理分析

首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象

源码位置:/vue-dev/src/core/instance/state.js

functioninitData(vm:Component){letdata=vm.$options.datadata=vm._data=typeofdata==='function'?getData(data,vm):data||{}...}

data既能是object也能是function,那为什么还会出现上文警告呢?

别急,继续看下文

组件在创建的时候,会进行选项的合并

源码位置:/vue-dev/src/core/util/options.js

自定义组件会进入mergeOptions进行选项合并

Vue.prototype._init=function(options?:Object){...// merge optionsif(options&&options._isComponent){// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm,options)}else{vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}...}

定义data会进行数据校验

源码位置:/vue-dev/src/core/instance/init.js

这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示

strats.data=function(parentVal:any,childVal:any,vm?:Component): ?Function{if(!vm){if(childVal&&typeofchildVal!=="function"){process.env.NODE_ENV!=="production"&&warn('The "data" option should be a function '+"that returns a per-instance value in component "+"definitions.",vm);returnparentVal;}returnmergeDataOrFn(parentVal,childVal);}returnmergeDataOrFn(parentVal,childVal,vm);};

四、结论

根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况

组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

9.NextTick是什么

官方对其的定义

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

什么意思呢?

我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

举例一下

Html结构

{{ message }}

构建一个vue实例

constvm=newVue({el:'#app',data:{message:'原始值'}})

修改message

this.message='修改后的值1'this.message='修改后的值2'this.message='修改后的值3'

这时候想获取页面最新的DOM节点,却发现获取到的是旧值

console.log(vm.$el.textContent)// 原始值

这是因为message数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中

如果我们一直修改相同数据,异步操作队列还会进行去重

等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新

为什么要有nexttick

举个例子

{{num}}for(leti=0;i<100000;i++){num=i}

如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略

二、使用场景

如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()

第一个参数为:回调函数(可以获取最近的DOM结构)

第二个参数为:执行函数上下文

// 修改数据vm.message='修改后的值'// DOM 还没有更新console.log(vm.$el.textContent)// 原始的值Vue.nextTick(function(){// DOM 更新了console.log(vm.$el.textContent)// 修改后的值})

组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上

this.message='修改后的值'console.log(this.$el.textContent)// => '原始的值'this.$nextTick(function(){console.log(this.$el.textContent)// => '修改后的值'})

$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情

this.message='修改后的值'console.log(this.$el.textContent)// => '原始的值'awaitthis.$nextTick()console.log(this.$el.textContent)// => '修改后的值'

三、实现原理

源码位置:/src/core/util/next-tick.js

callbacks也就是异步操作队列

callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次

exportfunctionnextTick(cb?:Function,ctx?:Object){let_resolve;// cb 回调函数会经统一处理压入 callbacks 数组callbacks.push(()=>{if(cb){// 给 cb 回调函数执行加上了 try-catch 错误处理try{cb.call(ctx);}catch(e){handleError(e,ctx,'nextTick');}}elseif(_resolve){_resolve(ctx);}});// 执行异步延迟函数 timerFuncif(!pending){pending=true;timerFunc();}// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用if(!cb&&typeofPromise!=='undefined'){returnnewPromise(resolve=>{_resolve=resolve;});}}

timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:

Promise.then、MutationObserver、setImmediate、setTimeout

通过上面任意一种方法,进行降级操作

exportletisUsingMicroTask=falseif(typeofPromise!=='undefined'&&isNative(Promise)){//判断1:是否原生支持Promiseconstp=Promise.resolve()timerFunc=()=>{p.then(flushCallbacks)if(isIOS)setTimeout(noop)}isUsingMicroTask=true}elseif(!isIE&&typeofMutationObserver!=='undefined'&&(isNative(MutationObserver)||MutationObserver.toString()==='[object MutationObserverConstructor]')){//判断2:是否原生支持MutationObserverletcounter=1constobserver=newMutationObserver(flushCallbacks)consttextNode=document.createTextNode(String(counter))observer.observe(textNode,{characterData:true})timerFunc=()=>{counter=(counter+1)%2textNode.data=String(counter)}isUsingMicroTask=true}elseif(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){//判断3:是否原生支持setImmediatetimerFunc=()=>{setImmediate(flushCallbacks)}}else{//判断4:上面都不行,直接用setTimeouttimerFunc=()=>{setTimeout(flushCallbacks,0)}}

无论是微任务还是宏任务,都会放到flushCallbacks使用

这里将callbacks里面的函数复制一份,同时callbacks置空

依次执行callbacks里面的函数

functionflushCallbacks(){pending=falseconstcopies=callbacks.slice(0)callbacks.length=0for(leti=0;i

小结:

把回调函数放入callbacks等待执行

将执行函数放到微任务或者宏任务中

事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

10.修饰符是什么

在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号

在Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理

vue中修饰符分为以下五种:

表单修饰符

事件修饰符

鼠标按键修饰符

键值修饰符

v-bind修饰符

1.修饰符的作用

表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

lazy

trim

number

lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步

{{value}}

trim

自动过滤用户输入的首空格字符,而中间的空格不会过滤

number

自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值

事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

stop

prevent

self

once

capture

passive

native

stop

阻止了事件冒泡,相当于调用了event.stopPropagation方法

ok

//只输出1

prevent

阻止了事件的默认行为,相当于调用了event.preventDefault方法

self

只当在 event.target 是当前元素自身时触发处理函数

...

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

once

绑定了事件以后只能触发一次,第二次就不会触发

ok

capture

使事件触发从包含这个元素的顶层开始往下触发

    obj1

    obj2
    obj3
    obj4// 输出结构: 1 2 4 3

passive

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符

...

不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。

passive 会告诉浏览器你不想阻止事件的默认行为

native

让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件

使用.native修饰符来操作普通HTML标签是会令事件失效的

鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

left 左键点击

right 右键点击

middle 中键点击

ok

3.应用场景

使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:

防抖

图片懒加载

一键 Copy的功能

输入框防抖

防抖这种情况设置一个v-throttle自定义指令来实现

举个例子:

// 1.设置v-throttle自定义指令Vue.directive('throttle',{bind:(el,binding)=>{letthrottleTime=binding.value;// 防抖时间if(!throttleTime){// 用户若不设置防抖时间,则默认2sthrottleTime=2000;}letcbFun;el.addEventListener('click',event=>{if(!cbFun){// 第一次执行cbFun=setTimeout(()=>{cbFun=null;},throttleTime);}else{event&&event.stopImmediatePropagation();}},true);},});// 2.为button标签设置v-throttle自定义指令提交

图片懒加载

设置一个v-lazy自定义组件完成图片懒加载

constLazyLoad={// install方法install(Vue,options){// 代替图片的loading图letdefaultSrc=options.default;Vue.directive('lazy',{bind(el,binding){LazyLoad.init(el,binding.value,defaultSrc);},inserted(el){// 兼容处理if('IntersectionObserver'inwindow){LazyLoad.observe(el);}else{LazyLoad.listenerScroll(el);}},})},// 初始化init(el,val,def){// data-src 储存真实srcel.setAttribute('data-src',val);// 设置src为loading图el.setAttribute('src',def);},// 利用IntersectionObserver监听elobserve(el){letio=newIntersectionObserver(entries=>{letrealSrc=el.dataset.src;if(entries[0].isIntersecting){if(realSrc){el.src=realSrc;el.removeAttribute('data-src');}}});io.observe(el);},// 监听scroll事件listenerScroll(el){lethandler=LazyLoad.throttle(LazyLoad.load,300);LazyLoad.load(el);window.addEventListener('scroll',()=>{handler(el);});},// 加载真实图片load(el){letwindowHeight=document.documentElement.clientHeightletelTop=el.getBoundingClientRect().top;letelBtm=el.getBoundingClientRect().bottom;letrealSrc=el.dataset.src;if(elTop-windowHeight<0&&elBtm>0){if(realSrc){el.src=realSrc;el.removeAttribute('data-src');}}},// 节流throttle(fn,delay){lettimer;letprevTime;returnfunction(...args){letcurrTime=Date.now();letcontext=this;if(!prevTime)prevTime=currTime;clearTimeout(timer);if(currTime-prevTime>delay){prevTime=currTime;fn.apply(context,args);clearTimeout(timer);return;}timer=setTimeout(function(){prevTime=Date.now();timer=null;fn.apply(context,args);},delay);}}}exportdefaultLazyLoad;

一键 Copy的功能

import{Message}from'ant-design-vue';constvCopy={///*    bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置    el: 作用的 dom 对象    value: 传给指令的值,也就是我们要 copy 的值  */bind(el,{value}){el.$value=value;// 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到el.handler=()=>{if(!el.$value){// 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意Message.warning('无复制内容');return;}// 动态创建 textarea 标签consttextarea=document.createElement('textarea');// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域textarea.readOnly='readonly';textarea.style.position='absolute';textarea.style.left='-9999px';// 将要 copy 的值赋给 textarea 标签的 value 属性textarea.value=el.$value;// 将 textarea 插入到 body 中document.body.appendChild(textarea);// 选中值并复制textarea.select();// textarea.setSelectionRange(0, textarea.value.length);constresult=document.execCommand('Copy');if(result){Message.success('复制成功');}document.body.removeChild(textarea);};// 绑定点击事件,就是所谓的一键 copy 啦el.addEventListener('click',el.handler);},// 当传进来的值更新的时候触发componentUpdated(el,{value}){el.$value=value;},// 指令与元素解绑的时候,移除事件绑定unbind(el){el.removeEventListener('click',el.handler);},};exportdefaultvCopy;

12.过滤器

一、是什么

过滤器(filter)是输送介质管道上不可缺少的一种装置

大白话,就是把一些不必要的东西过滤掉

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

二、如何用

vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

{{message|capitalize}}

定义filter

在组件的选项中定义本地的过滤器

filters:{capitalize:function(value){if(!value)return''value=value.toString()returnvalue.charAt(0).toUpperCase()+value.slice(1)}}

定义全局过滤器:

Vue.filter('capitalize',function(value){if(!value)return''value=value.toString()returnvalue.charAt(0).toUpperCase()+value.slice(1)})newVue({// ...})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器可以串联:

{{ message | filterA | filterB }}

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

过滤器是 JavaScript 函数,因此可以接收参数:

{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。

其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

{{ msg | msgFormat('疯狂','--')}}

小结:

部过滤器优先于全局过滤器被调用

一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

三、应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换、数字打点、文本格式化、时间格式化之类的等

比如我们要实现将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'exportfunctionresolveFilter(id){returnresolveAsset(this.$options,'filters',id,true)||identity}

内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

exportfunctionresolveAsset(options,type,id,warnMissing){// 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西if(typeofid!=='string'){// 判断传递的过滤器id 是不是字符串,不是则直接返回return}constassets=options[type]// 将我们注册的所有过滤器保存在变量中// 接下来的逻辑便是判断id是否在assets中存在,即进行匹配if(hasOwn(assets,id))returnassets[id]// 如找到,直接返回过滤器// 没有找到,代码继续执行constcamelizedId=camelize(id)// 万一你是驼峰的呢if(hasOwn(assets,camelizedId))returnassets[camelizedId]// 没找到,继续执行constPascalCaseId=capitalize(camelizedId)// 万一你是首字母大写的驼峰呢if(hasOwn(assets,PascalCaseId))returnassets[PascalCaseId]// 如果还是没找到,则检查原型链(即访问属性)constresult=assets[id]||assets[camelizedId]||assets[PascalCaseId]// 如果依然没找到,则在非生产环境的控制台打印警告if(process.env.NODE_ENV!=='production'&&warnMissing&&!result){warn('Failed to resolve '+type.slice(0,-1)+': '+id,options)}// 无论是否找到,都返回查找结果returnresult}

下面再来分析一下_s:

_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的text属性中,渲染到视图中

functiontoString(value){returnvalue==null?'':typeofvalue==='object'?JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距:String(value)}

最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式

functionparseFilters(filter){letfilters=filter.split('|')letexpression=filters.shift().trim()// shift()删除数组第一个元素并将其返回,该方法会更改原数组letiif(filters){for(i=0;i

小结:

在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)

编译后通过调用resolveFilter函数找到对应过滤器并返回结果

执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnode的text属性中,渲染到视图

上述均转自: https://github.com/febobo/web-interview

你可能感兴趣的:(vue 面试汇总(更新中...))