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屬性中,渲染到視圖