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 <html> or <body> - 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
<divid="app"><pv-if="isShow"v-for="item in items">{{ item.title }}</p></div>
创建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置于不同标签
<divid="app"><templatev-if="isShow"><pv-for="item in items">{{item.title}}</p></template></div>
再输出下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循环
<templatev-if="isShow"><pv-for="item in items"></template>
如果条件出现在循环内部,可通过计算属性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:`<div>组件</div>`,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结构
<divid="app">{{ message }}</div>
构建一个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<copies.length;i++){copies[i]()}}
小结:
把回调函数放入callbacks等待执行
将执行函数放到微任务或者宏任务中
事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
10.修饰符是什么
在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号
在Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理
vue中修饰符分为以下五种:
表单修饰符
事件修饰符
鼠标按键修饰符
键值修饰符
v-bind修饰符
1.修饰符的作用
表单修饰符
在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model
关于表单的修饰符有如下:
lazy
trim
number
lazy
在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
<inputtype="text"v-model.lazy="value"><p>{{value}}</p>
trim
自动过滤用户输入的首空格字符,而中间的空格不会过滤
<inputtype="text"v-model.trim="value">
number
自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值
<inputv-model.number="age"type="number">
事件修饰符
事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:
stop
prevent
self
once
capture
passive
native
stop
阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div@click="shout(2)"><button@click.stop="shout(1)">ok</button></div>//只输出1
prevent
阻止了事件的默认行为,相当于调用了event.preventDefault方法
<formv-on:submit.prevent="onSubmit"></form>
self
只当在 event.target 是当前元素自身时触发处理函数
<divv-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击
once
绑定了事件以后只能触发一次,第二次就不会触发
<button@click.once="shout(1)">ok
capture
使事件触发从包含这个元素的顶层开始往下触发
<div@click.capture="shout(1)"> obj1<div @click.capture="shout(2)"> obj2<div @click="shout(3)"> obj3<div @click="shout(4)"> obj4// 输出结构: 1 2 4 3
passive
在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<!--滚动事件的默认行为(即滚动行为)将会立即触发--><!--而不会等待`onScroll`完成--><!--这其中包含`event.preventDefault()`的情况--><divv-on:scroll.passive="onScroll">...</div>
不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。
passive 会告诉浏览器你不想阻止事件的默认行为
native
让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-componentv-on:click.native="doSomething"></my-component>
使用.native修饰符来操作普通HTML标签是会令事件失效的
鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
left 左键点击
right 右键点击
middle 中键点击
<button@click.left="shout(1)">ok</button><button @click.right="shout(1)">ok</button><button @click.middle="shout(1)">ok
键盘修饰符
键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的,有如下:
keyCode存在很多,但vue为我们提供了别名,分为以下两种:
普通键(enter、tab、delete、space、esc、up...)
系统修饰键(ctrl、alt、meta、shift...)
// 只有按键为keyCode的时候才触发<inputtype="text"@keyup.keyCode="shout()">
还可以通过以下方式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2=113
v-bind修饰符
v-bind修饰符主要是为属性进行操作,用来分别有如下:
async
prop
camel
async
能对props进行一个双向绑定
//父组件<comp:myMessage.sync="bar"></comp>//子组件this.$emit('update:myMessage',params);
以上这种方法相当于以下的简写
//父亲组件<comp:myMessage="bar"@update:myMessage="func"></comp>func(e){this.bar=e;}//子组件jsfunc2(){this.$emit('update:myMessage',params);}
使用async需要注意以下两点:
使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
props
设置自定义标签属性,避免暴露数据,防止污染HTML结构
<inputid="uid"title="title1"value="1":index.prop="index">
camel
将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox
<svg:viewBox="viewBox"></svg>
2.应用场景
根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:
.stop:阻止事件冒泡
.native:绑定原生事件
.once:事件只执行一次
.self :将事件绑定在自身身上,相当于阻止事件冒泡
.prevent:阻止默认事件
.caption:用于事件捕获
.once:只触发一次
.keyCode:监听特定键盘按下
.right:右键
11.自定义指令
1.什么是指令
开始之前我们先学习一下指令系统这个词
指令系统是计算机硬件的语言系统,也叫机器语言,它是系统程序员看到的计算机的主要属性。因此指令系统表征了计算机的基本功能决定了机器所要求的能力
在vue中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系统
我们看到的v- 开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令
指令使用的几种方式:
//会实例化一个指令,但这个指令没有参数 `v-xxx`// -- 将值传到指令中`v-xxx="value"`// -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"``v-xxx="'string'"`// -- 传参数(`arg`),如`v-bind:class="className"``v-xxx:arg="value"`// -- 使用修饰符(`modifier`)`v-xxx:arg.modifier="value"`
2.如何实现
注册一个自定义指令有全局注册与局部注册
全局注册注册主要是用过Vue.directive方法进行注册
Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数
// 注册一个全局自定义指令 `v-focus`Vue.directive('focus',{// 当被绑定的元素插入到 DOM 中时……inserted:function(el){// 聚焦元素el.focus()// 页面加载完成之后自动让输入框获取到焦点的小功能}})
局部注册通过在组件options选项中设置directive属性
directives:{focus:{// 指令的定义inserted:function(el){el.focus()// 页面加载完成之后自动让输入框获取到焦点的小功能}}}
然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<inputv-focus/>
自定义指令也像组件那样存在钩子函数:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
unbind:只调用一次,指令与元素解绑时调用
所有的钩子函数的参数都有以下:
el:指令所绑定的元素,可以用来直接操作 DOM
binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
vnode:Vue 编译生成的虚拟节点
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用
除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行
举个例子:
<divv-demo="{ color: 'white', text: 'hello!' }"></div><script>Vue.directive('demo',function(el,binding){console.log(binding.value.color)// "white"console.log(binding.value.text)// "hello!"})</script>
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自定义指令<button@click="sayHello"v-throttle>提交</button>
图片懒加载
设置一个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}}<!--在`v-bind`中--><divv-bind:id="rawId | formatId"></div>
定义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 的值作为第三个参数
举个例子:
<divid="app"><p>{{ msg | msgFormat('疯狂','--')}}</p></div><script>// 定义一个 Vue 全局的过滤器,名字叫做 msgFormatVue.filter('msgFormat',function(msg,arg,arg2){// 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则returnmsg.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'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<filters.length;i++){experssion=warpFilter(expression,filters[i].trim())// 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数}}returnexpression}// warpFilter函数实现functionwarpFilter(exp,filter){// 首先判断过滤器是否有其他参数consti=filter.indexof('(')if(i<0){// 不含其他参数,直接进行过滤器表达式字符串的拼接return`_f("${filter}")(${exp})`}else{constname=filter.slice(0,i)// 过滤器名称constargs=filter.slice(i+1)// 参数,但还多了 ‘)’return`_f('${name}')(${exp},${args}`// 注意这一步少给了一个 ')'}}
小结:
在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
编译后通过调用resolveFilter函数找到对应过滤器并返回结果
执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnode的text属性中,渲染到视图