網(wǎng)站建設(shè)入門教程對(duì)網(wǎng)絡(luò)營(yíng)銷的認(rèn)識(shí)800字
action 與 mutation 的區(qū)別
mutation
是同步更新,$watch
嚴(yán)格模式下會(huì)報(bào)錯(cuò)action
是異步操作,可以獲取數(shù)據(jù)后調(diào)用mutation
提交最終數(shù)據(jù)
MVVM的優(yōu)缺點(diǎn)?
優(yōu)點(diǎn):
- 分離視圖(View)和模型(Model),降低代碼耦合,提?視圖或者邏輯的重?性: ?如視圖(View)可以獨(dú)?于Model變化和修改,?個(gè)ViewModel可以綁定不同的"View"上,當(dāng)View變化的時(shí)候Model不可以不變,當(dāng)Model變化的時(shí)候View也可以不變。你可以把?些視圖邏輯放在?個(gè)ViewModel??,讓很多view重?這段視圖邏輯
- 提?可測(cè)試性: ViewModel的存在可以幫助開發(fā)者更好地編寫測(cè)試代碼
- ?動(dòng)更新dom: 利?雙向綁定,數(shù)據(jù)更新后視圖?動(dòng)更新,讓開發(fā)者從繁瑣的?動(dòng)dom中解放
缺點(diǎn):
- Bug很難被調(diào)試: 因?yàn)槭?雙向綁定的模式,當(dāng)你看到界?異常了,有可能是你View的代碼有Bug,也可能是Model的代碼有問題。數(shù)據(jù)綁定使得?個(gè)位置的Bug被快速傳遞到別的位置,要定位原始出問題的地?就變得不那么容易了。另外,數(shù)據(jù)綁定的聲明是指令式地寫在View的模版當(dāng)中的,這些內(nèi)容是沒辦法去打斷點(diǎn)debug的
- ?個(gè)?的模塊中model也會(huì)很?,雖然使??便了也很容易保證了數(shù)據(jù)的?致性,當(dāng)時(shí)?期持有,不釋放內(nèi)存就造成了花費(fèi)更多的內(nèi)存
- 對(duì)于?型的圖形應(yīng)?程序,視圖狀態(tài)較多,ViewModel的構(gòu)建和維護(hù)的成本都會(huì)?較?。
描述下Vue自定義指令
在 Vue2.0 中,代碼復(fù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對(duì)普通 DOM 元素進(jìn)行底層操作,這時(shí)候就會(huì)用到自定義指令。
一般需要對(duì)DOM元素進(jìn)行底層操作時(shí)使用,盡量只用來操作 DOM展示,不修改內(nèi)部的值。當(dāng)使用自定義指令直接修改 value 值時(shí)綁定v-model的值也不會(huì)同步更新;如必須修改可以在自定義指令中使用keydown事件,在vue組件中使用 change事件,回調(diào)中修改vue數(shù)據(jù);
(1)自定義指令基本內(nèi)容
-
全局定義:
Vue.directive("focus",{})
-
局部定義:
directives:{focus:{}}
-
鉤子函數(shù):指令定義對(duì)象提供鉤子函數(shù)
o bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。
o inSerted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用(僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。
o update:所在組件的VNode更新時(shí)調(diào)用,但是可能發(fā)生在其子VNode更新之前調(diào)用。指令的值可能發(fā)生了改變,也可能沒有。但是可以通過比較更新前后的值來忽略不必要的模板更新。
o ComponentUpdate:指令所在組件的 VNode及其子VNode全部更新后調(diào)用。
o unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。
-
鉤子函數(shù)參數(shù)
o el:綁定元素o bing: 指令核心對(duì)象,描述指令全部信息屬性
o name
o value
o oldValue
o expression
o arg
o modifers
o vnode 虛擬節(jié)點(diǎn)
o oldVnode:上一個(gè)虛擬節(jié)點(diǎn)(更新鉤子函數(shù)中才有用)
(2)使用場(chǎng)景
-
普通DOM元素進(jìn)行底層操作的時(shí)候,可以使用自定義指令
-
自定義指令是用來操作DOM的。盡管Vue推崇數(shù)據(jù)驅(qū)動(dòng)視圖的理念,但并非所有情況都適合數(shù)據(jù)驅(qū)動(dòng)。自定義指令就是一種有效的補(bǔ)充和擴(kuò)展,不僅可用于定義任何的DOM操作,并且是可復(fù)用的。
(3)使用案例
初級(jí)應(yīng)用:
- 鼠標(biāo)聚焦
- 下拉菜單
- 相對(duì)時(shí)間轉(zhuǎn)換
- 滾動(dòng)動(dòng)畫
高級(jí)應(yīng)用:
- 自定義指令實(shí)現(xiàn)圖片懶加載
- 自定義指令集成第三方插件
Vue-Router 的懶加載如何實(shí)現(xiàn)
非懶加載:
import List from '@/components/list.vue'
const router = new VueRouter({routes: [{ path: '/list', component: List }]
})
(1)方案一(常用):使用箭頭函數(shù)+import動(dòng)態(tài)加載
const List = () => import('@/components/list.vue')
const router = new VueRouter({routes: [{ path: '/list', component: List }]
})
(2)方案二:使用箭頭函數(shù)+require動(dòng)態(tài)加載
const router = new Router({routes: [{path: '/list',component: resolve => require(['@/components/list'], resolve)}]
})
(3)方案三:使用webpack的require.ensure技術(shù),也可以實(shí)現(xiàn)按需加載。 這種情況下,多個(gè)路由指定相同的chunkName,會(huì)合并打包成一個(gè)js文件。
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的寫法 這種是官方推薦的寫的 按模塊劃分懶加載
const router = new Router({routes: [{path: '/list',component: List,name: 'list'}]
}))
Vue 3.0 中的 Vue Composition API?
在 Vue2 中,代碼是 Options API 風(fēng)格的,也就是通過填充 (option) data、methods、computed 等屬性來完成一個(gè) Vue 組件。這種風(fēng)格使得 Vue 相對(duì)于 React極為容易上手,同時(shí)也造成了幾個(gè)問題:
- 由于 Options API 不夠靈活的開發(fā)方式,使得Vue開發(fā)缺乏優(yōu)雅的方法來在組件間共用代碼。
- Vue 組件過于依賴
this
上下文,Vue 背后的一些小技巧使得 Vue 組件的開發(fā)看起來與 JavaScript 的開發(fā)原則相悖,比如在methods
中的this
竟然指向組件實(shí)例來不指向methods
所在的對(duì)象。這也使得 TypeScript 在Vue2 中很不好用。
于是在 Vue3 中,舍棄了 Options API,轉(zhuǎn)而投向 Composition API。Composition API本質(zhì)上是將 Options API 背后的機(jī)制暴露給用戶直接使用,這樣用戶就擁有了更多的靈活性,也使得 Vue3 更適合于 TypeScript 結(jié)合。
如下,是一個(gè)使用了 Vue Composition API 的 Vue3 組件:
<template><button @click="increment">Count: {{ count }} </button>
</template><script>
// Composition API 將組件屬性暴露為函數(shù),因此第一步是導(dǎo)入所需的函數(shù)
import { ref, computed, onMounted } from 'vue'export default { setup() {
// 使用 ref 函數(shù)聲明了稱為 count 的響應(yīng)屬性,對(duì)應(yīng)于Vue2中的data函數(shù)const count = ref(0)
// Vue2中需要在methods option中聲明的函數(shù),現(xiàn)在直接聲明function increment() { count.value++ } // 對(duì)應(yīng)于Vue2中的mounted聲明周期onMounted(() => console.log('component mounted!')) return { count, increment } }}
</script>
顯而易見,Vue Composition API 使得 Vue3 的開發(fā)風(fēng)格更接近于原生 JavaScript,帶給開發(fā)者更多地靈活性
SPA、SSR的區(qū)別是什么
我們現(xiàn)在編寫的Vue
、React
和Angular
應(yīng)用大多數(shù)情況下都會(huì)在一個(gè)頁面中,點(diǎn)擊鏈接跳轉(zhuǎn)頁面通常是內(nèi)容切換而非頁面跳轉(zhuǎn),由于良好的用戶體驗(yàn)逐漸成為主流的開發(fā)模式。但同時(shí)也會(huì)有首屏加載時(shí)間長(zhǎng),SEO
不友好的問題,因此有了SSR
,這也是為什么面試中會(huì)問到兩者的區(qū)別
SPA
(Single Page Application)即單頁面應(yīng)用。一般也稱為 客戶端渲染(Client Side Render), 簡(jiǎn)稱CSR
。SSR
(Server Side Render)即 服務(wù)端渲染。一般也稱為 多頁面應(yīng)用(Mulpile Page Application),簡(jiǎn)稱MPA
SPA
應(yīng)用只會(huì)首次請(qǐng)求html
文件,后續(xù)只需要請(qǐng)求JSON
數(shù)據(jù)即可,因此用戶體驗(yàn)更好,節(jié)約流量,服務(wù)端壓力也較小。但是首屏加載的時(shí)間會(huì)變長(zhǎng),而且SEO
不友好。為了解決以上缺點(diǎn),就有了SSR
方案,由于HTML
內(nèi)容在服務(wù)器一次性生成出來,首屏加載快,搜索引擎也可以很方便的抓取頁面信息。但同時(shí)SSR方案也會(huì)有性能,開發(fā)受限等問題- 在選擇上,如果我們的應(yīng)用存在首屏加載優(yōu)化需求,
SEO
需求時(shí),就可以考慮SSR
- 但并不是只有這一種替代方案,比如對(duì)一些不常變化的靜態(tài)網(wǎng)站,SSR反而浪費(fèi)資源,我們可以考慮預(yù)渲染(
prerender
)方案。另外nuxt.js/next.js
中給我們提供了SSG(Static Site Generate)
靜態(tài)網(wǎng)站生成方案也是很好的靜態(tài)站點(diǎn)解決方案,結(jié)合一些CI
手段,可以起到很好的優(yōu)化效果,且能節(jié)約服務(wù)器資源
內(nèi)容生成上的區(qū)別:
SSR
SPA
部署上的區(qū)別
參考 前端進(jìn)階面試題詳細(xì)解答
delete和Vue.delete刪除數(shù)組的區(qū)別?
delete
只是被刪除的元素變成了empty/undefined
其他的元素的鍵值還是不變。Vue.delete
直接刪除了數(shù)組 改變了數(shù)組的鍵值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
說說你對(duì)slot的理解?slot使用場(chǎng)景有哪些
一、slot是什么
在HTML中 slot
元素 ,作為 Web Components
技術(shù)套件的一部分,是Web組件內(nèi)的一個(gè)占位符
該占位符可以在后期使用自己的標(biāo)記語言填充
舉個(gè)栗子
<template id="element-details-template"><slot name="element-name">Slot template</slot>
</template>
<element-details><span slot="element-name">1</span>
</element-details>
<element-details><span slot="element-name">2</span>
</element-details>
template
不會(huì)展示到頁面中,需要用先獲取它的引用,然后添加到DOM
中,
customElements.define('element-details',class extends HTMLElement {constructor() {super();const template = document.getElementById('element-details-template').content;const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(template.cloneNode(true));}
})
在Vue
中的概念也是如此
Slot
藝名插槽,花名“占坑”,我們可以理解為solt
在組件模板中占好了位置,當(dāng)使用該組件標(biāo)簽時(shí)候,組件標(biāo)簽里面的內(nèi)容就會(huì)自動(dòng)填坑(替換組件模板中slot
位置),作為承載分發(fā)內(nèi)容的出口
二、使用場(chǎng)景
通過插槽可以讓用戶可以拓展組件,去更好地復(fù)用組件和對(duì)其做定制化處理
如果父組件在使用到一個(gè)復(fù)用組件的時(shí)候,獲取這個(gè)組件在不同的地方有少量的更改,如果去重寫組件是一件不明智的事情
通過slot
插槽向組件內(nèi)部指定位置傳遞內(nèi)容,完成這個(gè)復(fù)用組件在不同場(chǎng)景的應(yīng)用
比如布局組件、表格列、下拉選、彈框顯示內(nèi)容等
如果讓你從零開始寫一個(gè)vuex,說說你的思路
思路分析
這個(gè)題目很有難度,首先思考vuex
解決的問題:存儲(chǔ)用戶全局狀態(tài)并提供管理狀態(tài)API。
vuex
需求分析- 如何實(shí)現(xiàn)這些需求
回答范例
- 官方說
vuex
是一個(gè)狀態(tài)管理模式和庫(kù),并確保這些狀態(tài)以可預(yù)期的方式變更。可見要實(shí)現(xiàn)一個(gè)vuex
- 要實(shí)現(xiàn)一個(gè)
Store
存儲(chǔ)全局狀態(tài) - 要提供修改狀態(tài)所需API:
commit(type, payload), dispatch(type, payload)
- 實(shí)現(xiàn)
Store
時(shí),可以定義Store
類,構(gòu)造函數(shù)接收選項(xiàng)options
,設(shè)置屬性state
對(duì)外暴露狀態(tài),提供commit
和dispatch
修改屬性state
。這里需要設(shè)置state
為響應(yīng)式對(duì)象,同時(shí)將Store
定義為一個(gè)Vue
插件 commit(type, payload)
方法中可以獲取用戶傳入mutations
并執(zhí)行它,這樣可以按用戶提供的方法修改狀態(tài)。dispatch(type, payload)
類似,但需要注意它可能是異步的,需要返回一個(gè)Promise
給用戶以處理異步結(jié)果
實(shí)踐
Store
的實(shí)現(xiàn):
class Store {constructor(options) {this.state = reactive(options.state)this.options = options}commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)}
}
vuex簡(jiǎn)易版
/*** 1 實(shí)現(xiàn)插件,掛載$store* 2 實(shí)現(xiàn)store*/let Vue;class Store {constructor(options) {// state響應(yīng)式處理// 外部訪問: this.$store.state.***// 第一種寫法// this.state = new Vue({// data: options.state// })// 第二種寫法:防止外界直接接觸內(nèi)部vue實(shí)例,防止外部強(qiáng)行變更this._vm = new Vue({data: {$$state: options.state}})this._mutations = options.mutationsthis._actions = options.actionsthis.getters = {}options.getters && this.handleGetters(options.getters)this.commit = this.commit.bind(this)this.dispatch = this.dispatch.bind(this)}get state () {return this._vm._data.$$state}set state (val) {return new Error('Please use replaceState to reset state')}handleGetters (getters) {Object.keys(getters).map(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)})})}commit (type, payload) {let entry = this._mutations[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this.state, payload)}dispatch (type, payload) {let entry = this._actions[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this, payload)}
}const install = (_Vue) => {Vue = _VueVue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}},})
}export default { Store, install }
驗(yàn)證方式
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)export default new Vuex.Store({state: {counter: 0},mutations: {// state從哪里來的add (state) {state.counter++}},getters: {doubleCounter (state) {return state.counter * 2}},actions: {add ({ commit }) {setTimeout(() => {commit('add')}, 1000)}},modules: {}
})
Vue中v-html會(huì)導(dǎo)致哪些問題
- 可能會(huì)導(dǎo)致
xss
攻擊 v-html
會(huì)替換掉標(biāo)簽內(nèi)部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) // with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);// _c 定義在core/instance/render.js
// _s 定義在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') { if (vnode.children) vnode.children.length = 0 if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property if (elm.childNodes.length === 1) { elm.removeChild(elm.childNodes[0]) }
}
v-model實(shí)現(xiàn)原理
我們?cè)?
vue
項(xiàng)目中主要使用v-model
指令在表單input
、textarea
、select
等元素上創(chuàng)建雙向數(shù)據(jù)綁定,我們知道v-model
本質(zhì)上不過是語法糖(可以看成是value + input
方法的語法糖),v-model
在內(nèi)部為不同的輸入元素使用不同的屬性并拋出不同的事件:
text
和textarea
元素使用value
屬性和input
事件checkbox
和radio
使用checked
屬性和change
事件select
字段將value
作為prop
并將change
作為事件
所以我們可以v-model進(jìn)行如下改寫:
<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />
當(dāng)在
input
元素中使用v-model
實(shí)現(xiàn)雙數(shù)據(jù)綁定,其實(shí)就是在輸入的時(shí)候觸發(fā)元素的input
事件,通過這個(gè)語法糖,實(shí)現(xiàn)了數(shù)據(jù)的雙向綁定
- 這個(gè)語法糖必須是固定的,也就是說屬性必須為
value
,方法名必須為:input
。 - 知道了
v-model
的原理,我們可以在自定義組件上實(shí)現(xiàn)v-model
//Parent
<template>{{num}}<Child v-model="num">
</template>
export default {data(){return {num: 0}}
}//Child
<template><div @click="add">Add</div>
</template>
export default {props: ['value'], // 屬性必須為valuemethods:{add(){// 方法名為inputthis.$emit('input', this.value + 1)}}
}
原理
會(huì)將組件的 v-model
默認(rèn)轉(zhuǎn)化成value+input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 觀察輸出的渲染函數(shù):
// with(this) {
// return _c('el-checkbox', {
// model: {
// value: (check),
// callback: function ($$v) { check = $$v },
// expression: "check"
// }
// })
// }
// 源碼位置 core/vdom/create-component.js line:155function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing) } } else { on[event] = callback }
}
原生的 v-model
,會(huì)根據(jù)標(biāo)簽的不同生成不同的事件和屬性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');// with(this) {
// return _c('input', {
// directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }],
// domProps: { "value": (value) },
// on: {"input": function ($event) {
// if ($event.target.composing) return;
// value = $event.target.value
// }
// }
// })
// }
編譯時(shí):不同的標(biāo)簽解析出的內(nèi)容不一樣
platforms/web/compiler/directives/model.js
if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false
} else if (tag === 'select') { genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false
}
運(yùn)行時(shí):會(huì)對(duì)元素處理一些關(guān)于輸入法的問題
platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) }el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome// fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true }}}
}
子組件可以直接改變父組件的數(shù)據(jù)么,說明原因
這是一個(gè)實(shí)踐知識(shí)點(diǎn),組件化開發(fā)過程中有個(gè)單項(xiàng)數(shù)據(jù)流原則
,不在子組件中修改父組件是個(gè)常識(shí)問題
思路
- 講講單項(xiàng)數(shù)據(jù)流原則,表明為何不能這么做
- 舉幾個(gè)常見場(chǎng)景的例子說說解決方案
- 結(jié)合實(shí)踐講講如果需要修改父組件狀態(tài)應(yīng)該如何做
回答范例
- 所有的
prop
都使得其父子之間形成了一個(gè)單向下行綁定:父級(jí)prop
的更新會(huì)向下流動(dòng)到子組件中,但是反過來則不行。這樣會(huì)防止從子組件意外變更父級(jí)組件的狀態(tài),從而導(dǎo)致你的應(yīng)用的數(shù)據(jù)流向難以理解。另外,每次父級(jí)組件發(fā)生變更時(shí),子組件中所有的prop
都將會(huì)刷新為最新的值。這意味著你不應(yīng)該在一個(gè)子組件內(nèi)部改變prop
。如果你這樣做了,Vue
會(huì)在瀏覽器控制臺(tái)中發(fā)出警告
const props = defineProps(['foo'])
// ? 下面行為會(huì)被警告, props是只讀的!
props.foo = 'bar'
- 實(shí)際開發(fā)過程中有兩個(gè)場(chǎng)景會(huì)想要修改一個(gè)屬性:
這個(gè) prop 用來傳遞一個(gè)初始值;這個(gè)子組件接下來希望將其作為一個(gè)本地的 prop 數(shù)據(jù)來使用。 在這種情況下,最好定義一個(gè)本地的 data
,并將這個(gè) prop
用作其初始值:
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
這個(gè) prop 以一種原始的值傳入且需要進(jìn)行轉(zhuǎn)換。 在這種情況下,最好使用這個(gè) prop
的值來定義一個(gè)計(jì)算屬性:
const props = defineProps(['size'])
// prop變化,計(jì)算屬性自動(dòng)更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 實(shí)踐中如果確實(shí)想要改變父組件屬性應(yīng)該
emit
一個(gè)事件讓父組件去做這個(gè)變更。注意雖然我們不能直接修改一個(gè)傳入的對(duì)象或者數(shù)組類型的prop
,但是我們還是能夠直接改內(nèi)嵌的對(duì)象或?qū)傩?/li>
怎么緩存當(dāng)前的組件?緩存后怎么更新
緩存組件使用keep-alive
組件,這是一個(gè)非常常見且有用的優(yōu)化手段,vue3
中keep-alive
有比較大的更新,能說的點(diǎn)比較多
思路
- 緩存用
keep-alive
,它的作用與用法 - 使用細(xì)節(jié),例如緩存指定/排除、結(jié)合
router
和transition
- 組件緩存后更新可以利用
activated
或者beforeRouteEnter
- 原理闡述
回答范例
- 開發(fā)中緩存組件使用
keep-alive
組件,keep-alive
是vue
內(nèi)置組件,keep-alive
包裹動(dòng)態(tài)組件component
時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例,而不是銷毀它們,這樣在組件切換過程中將狀態(tài)保留在內(nèi)存中,防止重復(fù)渲染DOM
<keep-alive><component :is="view"></component>
</keep-alive>
- 結(jié)合屬性
include
和exclude
可以明確指定緩存哪些組件或排除緩存指定組件。vue3
中結(jié)合vue-router
時(shí)變化較大,之前是keep-alive
包裹router-view
,現(xiàn)在需要反過來用router-view
包裹keep-alive
<router-view v-slot="{ Component }"><keep-alive><component :is="Component"></component></keep-alive>
</router-view>
- 緩存后如果要獲取數(shù)據(jù),解決方案可以有以下兩種
beforeRouteEnter
:在有vue-router的
項(xiàng)目,每次進(jìn)入路由的時(shí)候,都會(huì)執(zhí)行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{console.log(vm)// 每次進(jìn)入路由執(zhí)行vm.getData() // 獲取數(shù)據(jù)})
},
actived
:在keep-alive
緩存的組件被激活的時(shí)候,都會(huì)執(zhí)行actived
鉤子
activated(){this.getData() // 獲取數(shù)據(jù)
},
keep-alive
是一個(gè)通用組件,它內(nèi)部定義了一個(gè)map
,緩存創(chuàng)建過的組件實(shí)例,它返回的渲染函數(shù)內(nèi)部會(huì)查找內(nèi)嵌的component
組件對(duì)應(yīng)組件的vnode
,如果該組件在map
中存在就直接返回它。由于component
的is
屬性是個(gè)響應(yīng)式數(shù)據(jù),因此只要它變化,keep-alive
的render
函數(shù)就會(huì)重新執(zhí)行
Vue中修飾符.sync與v-model的區(qū)別
sync
的作用
.sync
修飾符可以實(shí)現(xiàn)父子組件之間的雙向綁定,并且可以實(shí)現(xiàn)子組件同步修改父組件的值,相比較與v-model
來說,sync
修飾符就簡(jiǎn)單很多了- 一個(gè)組件上可以有多個(gè)
.sync
修飾符
<!-- 正常父?jìng)髯?-->
<Son :a="num" :b="num2" /><!-- 加上sync之后的父?jìng)髯?-->
<Son :a.sync="num" :b.sync="num2" /><!-- 它等價(jià)于 -->
<Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val"
/><!-- 相當(dāng)于多了一個(gè)事件監(jiān)聽,事件名是update:a, -->
<!-- 回調(diào)函數(shù)中,會(huì)把接收到的值賦值給屬性綁定的數(shù)據(jù)項(xiàng)中。 -->
v-model
的工作原理
<com1 v-model="num"></com1>
<!-- 等價(jià)于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
- 相同點(diǎn)
- 都是語法糖,都可以實(shí)現(xiàn)父子組件中的數(shù)據(jù)的雙向通信
- 區(qū)別點(diǎn)
- 格式不同:
v-model="num"
,:num.sync="num"
v-model
:@input + value
:num.sync
:@update:num
v-model
只能用一次;.sync
可以有多個(gè)
- 格式不同:
Vue組件之間通信方式有哪些
Vue 組件間通信是面試??嫉闹R(shí)點(diǎn)之一,這題有點(diǎn)類似于開放題,你回答出越多方法當(dāng)然越加分,表明你對(duì) Vue 掌握的越熟練。 Vue 組件間通信只要指以下 3 類通信 :
父子組件通信
、隔代組件通信
、兄弟組件通信
,下面我們分別介紹每種通信方式且會(huì)說明此種方法可適用于哪類組件間通信
組件傳參的各種方式
組件通信常用方式有以下幾種
props / $emit
適用 父子組件通信- 父組件向子組件傳遞數(shù)據(jù)是通過
prop
傳遞的,子組件傳遞數(shù)據(jù)給父組件是通過$emit
觸發(fā)事件來做到的
- 父組件向子組件傳遞數(shù)據(jù)是通過
ref
與$parent / $children(vue3廢棄)
適用 父子組件通信ref
:如果在普通的DOM
元素上使用,引用指向的就是DOM
元素;如果用在子組件上,引用就指向組件實(shí)例$parent / $children
:訪問訪問父組件的屬性或方法 / 訪問子組件的屬性或方法
EventBus ($emit / $on)
適用于 父子、隔代、兄弟組件通信- 這種方法通過一個(gè)空的
Vue
實(shí)例作為中央事件總線(事件中心),用它來觸發(fā)事件和監(jiān)聽事件,從而實(shí)現(xiàn)任何組件間的通信,包括父子、隔代、兄弟組件
- 這種方法通過一個(gè)空的
$attrs / $listeners(vue3廢棄)
適用于 隔代組件通信$attrs
:包含了父作用域中不被prop
所識(shí)別 (且獲取) 的特性綁定 (class
和style
除外 )。當(dāng)一個(gè)組件沒有聲明任何prop
時(shí),這里會(huì)包含所有父作用域的綁定 (class
和style
除外 ),并且可以通過v-bind="$attrs"
傳入內(nèi)部組件。通常配合inheritAttrs
選項(xiàng)一起使用$listeners
:包含了父作用域中的 (不含.native
修飾器的)v-on
事件監(jiān)聽器。它可以通過v-on="$listeners"
傳入內(nèi)部組件
provide / inject
適用于 隔代組件通信- 祖先組件中通過
provider
來提供變量,然后在子孫組件中通過inject
來注入變量。provide / inject
API 主要解決了跨級(jí)組件間的通信問題, 不過它的使用場(chǎng)景,主要是子組件獲取上級(jí)組件的狀態(tài) ,跨級(jí)組件間建立了一種主動(dòng)提供與依賴注入的關(guān)系
- 祖先組件中通過
$root
適用于 隔代組件通信 訪問根組件中的屬性或方法,是根組件,不是父組件。$root
只對(duì)根組件有用Vuex
適用于 父子、隔代、兄弟組件通信Vuex
是一個(gè)專為Vue.js
應(yīng)用程序開發(fā)的狀態(tài)管理模式。每一個(gè)Vuex
應(yīng)用的核心就是store
(倉(cāng)庫(kù))?!皊tore” 基本上就是一個(gè)容器,它包含著你的應(yīng)用中大部分的狀態(tài) (state
)Vuex
的狀態(tài)存儲(chǔ)是響應(yīng)式的。當(dāng)Vue
組件從store
中讀取狀態(tài)的時(shí)候,若store
中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會(huì)相應(yīng)地得到高效更新。- 改變
store
中的狀態(tài)的唯一途徑就是顯式地提交 (commit
)mutation
。這樣使得我們可以方便地跟蹤每一個(gè)狀態(tài)的變化。
根據(jù)組件之間關(guān)系討論組件通信最為清晰有效
- 父子組件:
props
/$emit
/$parent
/ref
- 兄弟組件:
$parent
/eventbus
/vuex
- 跨層級(jí)關(guān)系:
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
下面演示組件之間通訊三種情況: 父?jìng)髯?、子傳父、兄弟組件之間的通訊
1. 父子組件通信
使用
props
,父組件可以使用props
向子組件傳遞數(shù)據(jù)。
父組件vue
模板father.vue
:
<template><child :msg="message"></child>
</template><script>
import child from './child.vue';
export default {components: {child},data () {return {message: 'father message';}}
}
</script>
子組件vue
模板child.vue
:
<template><div>{{msg}}</div>
</template><script>
export default {props: {msg: {type: String,required: true}}
}
</script>
回調(diào)函數(shù)(callBack)
父?jìng)髯?#xff1a;將父組件里定義的method
作為props
傳入子組件
// 父組件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子組件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']
子組件向父組件通信
父組件向子組件傳遞事件方法,子組件通過
$emit
觸發(fā)事件,回調(diào)給父組件
父組件vue
模板father.vue
:
<template><child @msgFunc="func"></child>
</template><script>
import child from './child.vue';
export default {components: {child},methods: {func (msg) {console.log(msg);}}
}
</script>
子組件vue
模板child.vue
:
<template><button @click="handleClick">點(diǎn)我</button>
</template><script>
export default {props: {msg: {type: String,required: true}},methods () {handleClick () {//........this.$emit('msgFunc');}}
}
</script>
2. provide / inject 跨級(jí)訪問祖先組件的數(shù)據(jù)
父組件通過使用provide(){return{}}
提供需要傳遞的數(shù)據(jù)
export default {data() {return {title: '我是父組件',name: 'poetry'}},methods: {say() {alert(1)}},// provide屬性 能夠?yàn)楹竺娴暮蟠M件/嵌套的組件提供所需要的變量和方法provide() {return {message: '我是祖先組件提供的數(shù)據(jù)',name: this.name, // 傳遞屬性say: this.say}}
}
子組件通過使用inject:[“參數(shù)1”,”參數(shù)2”,…]
接收父組件傳遞的參數(shù)
<template><p>曾孫組件</p><p>{{message}}</p>
</template>
<script>
export default {// inject 注入/接收祖先組件傳遞的所需要的數(shù)據(jù)即可 //接收到的數(shù)據(jù) 變量 跟data里面的變量一樣 可以直接綁定到頁面 {{}}inject: [ "message","say"],mounted() {this.say();},
};
</script>
3. $parent + $children 獲取父組件實(shí)例和子組件實(shí)例的集合
this.$parent
可以直接訪問該組件的父實(shí)例或組件- 父組件也可以通過
this.$children
訪問它所有的子組件;需要注意$children
并不保證順序,也不是響應(yīng)式的
<!-- parent.vue -->
<template>
<div><child1></child1> <child2></child2> <button @click="clickChild">$children方式獲取子組件值</button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){return {total: 108}},components: {child1,child2 },methods: {funa(e){console.log("index",e)},clickChild(){console.log(this.$children[0].msg);console.log(this.$children[1].msg);}}
}
</script>
<!-- child1.vue -->
<template><div><button @click="parentClick">點(diǎn)擊訪問父組件</button></div>
</template>
<script>
export default {data(){return {msg:"child1"}},methods: {// 訪問父組件數(shù)據(jù)parentClick(){this.$parent.funa("xx")console.log(this.$parent.total);}}
}
</script>
<!-- child2.vue -->
<template><div>child2</div>
</template>
<script>
export default {data(){return {msg: 'child2'}}
}
</script>
4. $attrs + $listeners多級(jí)組件通信
$attrs
包含了從父組件傳過來的所有props
屬性
// 父組件Parent.vue:
<Child :name="name" :age="age"/>// 子組件Child.vue:
<GrandChild v-bind="$attrs" />// 孫子組件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年齡:{{$attrs.age}}</p>
$listeners
包含了父組件監(jiān)聽的所有事件
// 父組件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>// 子組件Child.vue:
<button @click="$listeners.changeNameFn"></button>
5. ref 父子組件通信
// 父組件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);this.$refs.childComp.changeAge()
}// 子組件Child.vue:
data(){return{age:20}
},
methods(){changeAge(){this.age=15}
}
6. 非父子, 兄弟組件之間通信
vue2
中廢棄了broadcast
廣播和分發(fā)事件的方法。父子組件中可以用props
和$emit()
。如何實(shí)現(xiàn)非父子組件間的通信,可以通過實(shí)例一個(gè)vue
實(shí)例Bus
作為媒介,要相互通信的兄弟組件之中,都引入Bus
,然后通過分別調(diào)用Bus事件觸發(fā)和監(jiān)聽來實(shí)現(xiàn)通信和參數(shù)傳遞。Bus.js
可以是這樣:
// Bus.js// 創(chuàng)建一個(gè)中央時(shí)間總線類
class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } }
} // main.js
Vue.prototype.$bus = new Bus() // 將$bus掛載到vue實(shí)例的原型上
// 另一種方式
Vue.prototype.$bus = new Vue() // Vue已經(jīng)實(shí)現(xiàn)了Bus的功能
<template><button @click="toBus">子組件傳給兄弟組件</button>
</template><script>
export default{methods: {toBus () {this.$bus.$emit('foo', '來自兄弟組件')}}
}
</script>
另一個(gè)組件也在鉤子函數(shù)中監(jiān)聽on
事件
export default {data() {return {message: ''}},mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})}
}
7. $root 訪問根組件中的屬性或方法
- 作用:訪問根組件中的屬性或方法
- 注意:是根組件,不是父組件。
$root
只對(duì)根組件有用
var vm = new Vue({el: "#app",data() {return {rootInfo:"我是根元素的屬性"}},methods: {alerts() {alert(111)}},components: {com1: {data() {return {info: "組件1"}},template: "<p>{{ info }} <com2></com2></p>",components: {com2: {template: "<p>我是組件1的子組件</p>",created() {this.$root.alerts()// 根組件方法console.log(this.$root.rootInfo)// 我是根元素的屬性}}}}}
});
8. vuex
- 適用場(chǎng)景: 復(fù)雜關(guān)系的組件數(shù)據(jù)傳遞
- Vuex作用相當(dāng)于一個(gè)用來存儲(chǔ)共享變量的容器
state
用來存放共享變量的地方getter
,可以增加一個(gè)getter
派生狀態(tài),(相當(dāng)于store
中的計(jì)算屬性),用來獲得共享變量的值mutations
用來存放修改state
的方法。actions
也是用來存放修改state的方法,不過action
是在mutations
的基礎(chǔ)上進(jìn)行。常用來做一些異步操作
小結(jié)
- 父子關(guān)系的組件數(shù)據(jù)傳遞選擇
props
與$emit
進(jìn)行傳遞,也可選擇ref
- 兄弟關(guān)系的組件數(shù)據(jù)傳遞可選擇
$bus
,其次可以選擇$parent
進(jìn)行傳遞 - 祖先與后代組件數(shù)據(jù)傳遞可選擇
attrs
與listeners
或者Provide
與Inject
- 復(fù)雜關(guān)系的組件數(shù)據(jù)傳遞可以通過
vuex
存放共享的變量
vue-loader是什么?它有什么作用?
回答范例
vue-loader
是用于處理單文件組件(SFC
,Single-File Component
)的webpack loader
- 因?yàn)橛辛?code>vue-loader,我們就可以在項(xiàng)目中編寫
SFC
格式的Vue
組件,我們可以把代碼分割為<template>
、<script>
和<style>
,代碼會(huì)異常清晰。結(jié)合其他loader
我們還可以用Pug
編寫<template>
,用SASS
編寫<style>
,用TS
編寫<script>
。我們的<style>
還可以單獨(dú)作用當(dāng)前組件 webpack
打包時(shí),會(huì)以loader
的方式調(diào)用vue-loader
vue-loader
被執(zhí)行時(shí),它會(huì)對(duì)SFC
中的每個(gè)語言塊用單獨(dú)的loader
鏈處理。最后將這些單獨(dú)的塊裝配成最終的組件模塊
原理
vue-loader
會(huì)調(diào)用@vue/compiler-sfc
模塊解析SFC
源碼為一個(gè)描述符(Descriptor
),然后為每個(gè)語言塊生成import
代碼,返回的代碼類似下面
// source.vue被vue-loader處理之后返回的代碼
?
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
?
script.render = render
export default script
我們想要script
塊中的內(nèi)容被作為js
處理(當(dāng)然如果是<script lang="ts">
被作為ts
理),這樣我們想要webpack
把配置中跟.js
匹配的規(guī)則都應(yīng)用到形如source.vue?vue&type=script
的這個(gè)請(qǐng)求上。例如我們對(duì)所有*.js
配置了babel-loader
,這個(gè)規(guī)則將被克隆并應(yīng)用到所在Vue SFC
import script from 'source.vue?vue&type=script
將被展開為:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
類似的,如果我們對(duì).sass
文件配置了style-loader + css-loader + sass-loader
,對(duì)下面的代碼
<style scoped lang="scss">
vue-loader
將會(huì)返回給我們下面結(jié)果:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
然后webpack
會(huì)展開如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
- 當(dāng)處理展開請(qǐng)求時(shí),
vue-loader
將被再次調(diào)用。這次,loader
將會(huì)關(guān)注那些有查詢串的請(qǐng)求,且僅針對(duì)特定塊,它會(huì)選中特定塊內(nèi)部的內(nèi)容并傳遞給后面匹配的loader
- 對(duì)于
<script>
塊,處理到這就可以了,但是<template>
和<style>
還有一些額外任務(wù)要做,比如- 需要用
Vue
模板編譯器編譯template
,從而得到render
函數(shù) - 需要對(duì)
<style scoped>
中的CSS
做后處理(post-process
),該操作在css-loader
之后但在style-loader
之前
- 需要用
實(shí)現(xiàn)上這些附加的loader
需要被注入到已經(jīng)展開的loader
鏈上,最終的請(qǐng)求會(huì)像下面這樣:
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
?
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
diff算法
時(shí)間復(fù)雜度: 個(gè)樹的完全 diff
算法是一個(gè)時(shí)間復(fù)雜度為 O(n*3)
,vue進(jìn)行優(yōu)化轉(zhuǎn)化成 O(n)
。
理解:
- 最小量更新,
key
很重要。這個(gè)可以是這個(gè)節(jié)點(diǎn)的唯一標(biāo)識(shí),告訴diff
算法,在更改前后它們是同一個(gè)DOM節(jié)點(diǎn)- 擴(kuò)展
v-for
為什么要有key
,沒有key
會(huì)暴力復(fù)用,舉例子的話隨便說一個(gè)比如移動(dòng)節(jié)點(diǎn)或者增加節(jié)點(diǎn)(修改DOM),加key
只會(huì)移動(dòng)減少操作DOM。
- 擴(kuò)展
- 只有是同一個(gè)虛擬節(jié)點(diǎn)才會(huì)進(jìn)行精細(xì)化比較,否則就是暴力刪除舊的,插入新的。
- 只進(jìn)行同層比較,不會(huì)進(jìn)行跨層比較。
diff算法的優(yōu)化策略:四種命中查找,四個(gè)指針
- 舊前與新前(先比開頭,后插入和刪除節(jié)點(diǎn)的這種情況)
- 舊后與新后(比結(jié)尾,前插入或刪除的情況)
- 舊前與新后(頭與尾比,此種發(fā)生了,涉及移動(dòng)節(jié)點(diǎn),那么新前指向的節(jié)點(diǎn),移動(dòng)到舊后之后)
- 舊后與新前(尾與頭比,此種發(fā)生了,涉及移動(dòng)節(jié)點(diǎn),那么新前指向的節(jié)點(diǎn),移動(dòng)到舊前之前)
vue3中 watch、watchEffect區(qū)別
watch
是惰性執(zhí)行,也就是只有監(jiān)聽的值發(fā)生變化的時(shí)候才會(huì)執(zhí)行,但是watchEffect
不同,每次代碼加載watchEffect
都會(huì)執(zhí)行(忽略watch
第三個(gè)參數(shù)的配置,如果修改配置項(xiàng)也可以實(shí)現(xiàn)立即執(zhí)行)watch
需要傳遞監(jiān)聽的對(duì)象,watchEffect
不需要watch
只能監(jiān)聽響應(yīng)式數(shù)據(jù):ref
定義的屬性和reactive
定義的對(duì)象,如果直接監(jiān)聽reactive
定義對(duì)象中的屬性是不允許的(會(huì)報(bào)警告),除非使用函數(shù)轉(zhuǎn)換一下。其實(shí)就是官網(wǎng)上說的監(jiān)聽一個(gè)getter
watchEffect
如果監(jiān)聽reactive
定義的對(duì)象是不起作用的,只能監(jiān)聽對(duì)象中的屬性
看一下watchEffect
的代碼
<template>
<div>請(qǐng)輸入firstName:<input type="text" v-model="firstName">
</div>
<div>請(qǐng)輸入lastName:<input type="text" v-model="lastName">
</div>
<div>請(qǐng)輸入obj.text:<input type="text" v-model="obj.text">
</div><div>【obj.text】 {{obj.text}}</div>
</template><script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {name: "HelloWorld",props: {msg: String,},setup(props,content){let firstName = ref('')let lastName = ref('')let obj= reactive({text:'hello'})watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(`組合后的名稱為:${firstName.value}${lastName.value}`)})return{obj,firstName,lastName}}
};
</script>
改造一下代碼
watchEffect(()=>{console.log('觸發(fā)了watchEffect');// 這里我們不使用firstName.value/lastName.value ,相當(dāng)于是監(jiān)控整個(gè)ref,對(duì)應(yīng)第四點(diǎn)上面的結(jié)論console.log(`組合后的名稱為:${firstName}${lastName}`)
})
watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(obj);
})
稍微改造一下
let obj = reactive({text:'hello'
})
watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(obj.text);
})
再看一下watch的代碼,驗(yàn)證一下
let obj= reactive({text:'hello'
})
// watch是惰性執(zhí)行, 默認(rèn)初始化之后不會(huì)執(zhí)行,只有值有變化才會(huì)觸發(fā),可通過配置參數(shù)實(shí)現(xiàn)默認(rèn)執(zhí)行
watch(obj, (newValue, oldValue) => {// 回調(diào)函數(shù)console.log('觸發(fā)監(jiān)控更新了new', newValue);console.log('觸發(fā)監(jiān)控更新了old', oldValue);
},{// 配置immediate參數(shù),立即執(zhí)行,以及深層次監(jiān)聽immediate: true,deep: true
})
- 監(jiān)控整個(gè)
reactive
對(duì)象,從上面的圖可以看到deep
實(shí)際默認(rèn)是開啟的,就算我們?cè)O(shè)置為false
也還是無效。而且舊值獲取不到。 - 要獲取舊值則需要監(jiān)控對(duì)象的屬性,也就是監(jiān)聽一個(gè)
getter
,看下圖
總結(jié)
- 如果定義了
reactive
的數(shù)據(jù),想去使用watch
監(jiān)聽數(shù)據(jù)改變,則無法正確獲取舊值,并且deep
屬性配置無效,自動(dòng)強(qiáng)制開啟了深層次監(jiān)聽。 - 如果使用
ref
初始化一個(gè)對(duì)象或者數(shù)組類型的數(shù)據(jù),會(huì)被自動(dòng)轉(zhuǎn)成reactive
的實(shí)現(xiàn)方式,生成proxy
代理對(duì)象。也會(huì)變得無法正確取舊值。 - 用任何方式生成的數(shù)據(jù),如果接收的變量是一個(gè)
proxy
代理對(duì)象,就都會(huì)導(dǎo)致watch
這個(gè)對(duì)象時(shí),watch
回調(diào)里無法正確獲取舊值。 - 所以當(dāng)大家使用
watch
監(jiān)聽對(duì)象時(shí),如果在不需要使用舊值的情況,可以正常監(jiān)聽對(duì)象沒關(guān)系;但是如果當(dāng)監(jiān)聽改變函數(shù)里面需要用到舊值時(shí),只能監(jiān)聽 對(duì)象.xxx`屬性 的方式才行
watch和watchEffect異同總結(jié)
體驗(yàn)
watchEffect
立即運(yùn)行一個(gè)函數(shù),然后被動(dòng)地追蹤它的依賴,當(dāng)這些依賴改變時(shí)重新執(zhí)行該函數(shù)
const count = ref(0)
?
watchEffect(() => console.log(count.value))
// -> logs 0
?
count.value++
// -> logs 1
watch
偵測(cè)一個(gè)或多個(gè)響應(yīng)式數(shù)據(jù)源并在數(shù)據(jù)源變化時(shí)調(diào)用一個(gè)回調(diào)函數(shù)
const state = reactive({ count: 0 })
watch(() => state.count,(count, prevCount) => {/* ... */}
)
回答范例
watchEffect
立即運(yùn)行一個(gè)函數(shù),然后被動(dòng)地追蹤它的依賴,當(dāng)這些依賴改變時(shí)重新執(zhí)行該函數(shù)。watch
偵測(cè)一個(gè)或多個(gè)響應(yīng)式數(shù)據(jù)源并在數(shù)據(jù)源變化時(shí)調(diào)用一個(gè)回調(diào)函數(shù)watchEffect(effect)
是一種特殊watch
,傳入的函數(shù)既是依賴收集的數(shù)據(jù)源,也是回調(diào)函數(shù)。如果我們不關(guān)心響應(yīng)式數(shù)據(jù)變化前后的值,只是想拿這些數(shù)據(jù)做些事情,那么watchEffect
就是我們需要的。watch
更底層,可以接收多種數(shù)據(jù)源,包括用于依賴收集的getter
函數(shù),因此它完全可以實(shí)現(xiàn)watchEffect
的功能,同時(shí)由于可以指定getter
函數(shù),依賴可以控制的更精確,還能獲取數(shù)據(jù)變化前后的值,因此如果需要這些時(shí)我們會(huì)使用watch
watchEffect
在使用時(shí),傳入的函數(shù)會(huì)立刻執(zhí)行一次。watch
默認(rèn)情況下并不會(huì)執(zhí)行回調(diào)函數(shù),除非我們手動(dòng)設(shè)置immediate
選項(xiàng)- 從實(shí)現(xiàn)上來說,
watchEffect(fn)
相當(dāng)于watch(fn,fn,{immediate:true})
watchEffect
定義如下
export function watchEffect(effect: WatchEffect,options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}
watch
定義如下
export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>
): WatchStopHandle {return doWatch(source as any, cb, options)
}
很明顯watchEffect
就是一種特殊的watch
實(shí)現(xiàn)。
vue要做權(quán)限管理該怎么做?如果控制到按鈕級(jí)別的權(quán)限怎么做
一、是什么
權(quán)限是對(duì)特定資源的訪問許可,所謂權(quán)限控制,也就是確保用戶只能訪問到被分配的資源
而前端權(quán)限歸根結(jié)底是請(qǐng)求的發(fā)起權(quán),請(qǐng)求的發(fā)起可能有下面兩種形式觸發(fā)
- 頁面加載觸發(fā)
- 頁面上的按鈕點(diǎn)擊觸發(fā)
總的來說,所有的請(qǐng)求發(fā)起都觸發(fā)自前端路由或視圖
所以我們可以從這兩方面入手,對(duì)觸發(fā)權(quán)限的源頭進(jìn)行控制,最終要實(shí)現(xiàn)的目標(biāo)是:
- 路由方面,用戶登錄后只能看到自己有權(quán)訪問的導(dǎo)航菜單,也只能訪問自己有權(quán)訪問的路由地址,否則將跳轉(zhuǎn)
4xx
提示頁 - 視圖方面,用戶只能看到自己有權(quán)瀏覽的內(nèi)容和有權(quán)操作的控件
- 最后再加上請(qǐng)求控制作為最后一道防線,路由可能配置失誤,按鈕可能忘了加權(quán)限,這種時(shí)候請(qǐng)求控制可以用來兜底,越權(quán)請(qǐng)求將在前端被攔截
二、如何做
前端權(quán)限控制可以分為四個(gè)方面:
- 接口權(quán)限
- 按鈕權(quán)限
- 菜單權(quán)限
- 路由權(quán)限
接口權(quán)限
接口權(quán)限目前一般采用jwt
的形式來驗(yàn)證,沒有通過的話一般返回401
,跳轉(zhuǎn)到登錄頁面重新進(jìn)行登錄
登錄完拿到token
,將token
存起來,通過axios
請(qǐng)求攔截器進(jìn)行攔截,每次請(qǐng)求的時(shí)候頭部攜帶token
axios.interceptors.request.use(config => {config.headers['token'] = cookie.get('token')return config
})
axios.interceptors.response.use(res=>{},{response}=>{if (response.data.code === 40099 || response.data.code === 40098) { //token過期或者錯(cuò)誤router.push('/login')}
})
路由權(quán)限控制
方案一
初始化即掛載全部路由,并且在路由上標(biāo)記相應(yīng)的權(quán)限信息,每次路由跳轉(zhuǎn)前做校驗(yàn)
const routerMap = [{path: '/permission',component: Layout,redirect: '/permission/index',alwaysShow: true, // will always show the root menumeta: {title: 'permission',icon: 'lock',roles: ['admin', 'editor'] // you can set roles in root nav},children: [{path: 'page',component: () => import('@/views/permission/page'),name: 'pagePermission',meta: {title: 'pagePermission',roles: ['admin'] // or you can only set roles in sub nav}}, {path: 'directive',component: () => import('@/views/permission/directive'),name: 'directivePermission',meta: {title: 'directivePermission'// if do not set roles, means: this page does not require permission}}]}]
這種方式存在以下四種缺點(diǎn):
- 加載所有的路由,如果路由很多,而用戶并不是所有的路由都有權(quán)限訪問,對(duì)性能會(huì)有影響。
- 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做權(quán)限判斷。
- 菜單信息寫死在前端,要改個(gè)顯示文字或權(quán)限信息,需要重新編譯
- 菜單跟路由耦合在一起,定義路由的時(shí)候還有添加菜單顯示標(biāo)題,圖標(biāo)之類的信息,而且路由不一定作為菜單顯示,還要多加字段進(jìn)行標(biāo)識(shí)
方案二
初始化的時(shí)候先掛載不需要權(quán)限控制的路由,比如登錄頁,404等錯(cuò)誤頁。如果用戶通過URL進(jìn)行強(qiáng)制訪問,則會(huì)直接進(jìn)入404,相當(dāng)于從源頭上做了控制
登錄后,獲取用戶的權(quán)限信息,然后篩選有權(quán)限訪問的路由,在全局路由守衛(wèi)里進(jìn)行調(diào)用addRoutes
添加路由
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookieNProgress.configure({ showSpinner: false })// NProgress Configuration// permission judge function
function hasPermission(roles, permissionRoles) {if (roles.indexOf('admin') >= 0) return true // admin permission passed directlyif (!permissionRoles) return truereturn roles.some(role => permissionRoles.indexOf(role) >= 0)
}const whiteList = ['/login', '/authredirect']// no redirect whitelistrouter.beforeEach((to, from, next) => {NProgress.start() // start progress barif (getToken()) { // determine if there has token/* has token*/if (to.path === '/login') {next({ path: '/' })NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it} else {if (store.getters.roles.length === 0) { // 判斷當(dāng)前用戶是否已拉取完user_info信息store.dispatch('GetUserInfo').then(res => { // 拉取user_infoconst roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']store.dispatch('GenerateRoutes', { roles }).then(() => { // 根據(jù)roles權(quán)限生成可訪問的路由表router.addRoutes(store.getters.addRouters) // 動(dòng)態(tài)添加可訪問路由表next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record})}).catch((err) => {store.dispatch('FedLogOut').then(() => {Message.error(err || 'Verification failed, please login again')next({ path: '/' })})})} else {// 沒有動(dòng)態(tài)改變權(quán)限的需求可直接next() 刪除下方權(quán)限判斷 ↓if (hasPermission(store.getters.roles, to.meta.roles)) {next()//} else {next({ path: '/401', replace: true, query: { noGoBack: true }})}// 可刪 ↑}}} else {/* has no token*/if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login') // 否則全部重定向到登錄頁NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it}}
})router.afterEach(() => {NProgress.done() // finish progress bar
})
按需掛載,路由就需要知道用戶的路由權(quán)限,也就是在用戶登錄進(jìn)來的時(shí)候就要知道當(dāng)前用戶擁有哪些路由權(quán)限
這種方式也存在了以下的缺點(diǎn):
- 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷
- 菜單信息寫死在前端,要改個(gè)顯示文字或權(quán)限信息,需要重新編譯
- 菜單跟路由耦合在一起,定義路由的時(shí)候還有添加菜單顯示標(biāo)題,圖標(biāo)之類的信息,而且路由不一定作為菜單顯示,還要多加字段進(jìn)行標(biāo)識(shí)
菜單權(quán)限
菜單權(quán)限可以理解成將頁面與理由進(jìn)行解耦
方案一
菜單與路由分離,菜單由后端返回
前端定義路由信息
{name: "login",path: "/login",component: () => import("@/pages/Login.vue")
}
name
字段都不為空,需要根據(jù)此字段與后端返回菜單做關(guān)聯(lián),后端返回的菜單信息中必須要有name
對(duì)應(yīng)的字段,并且做唯一性校驗(yàn)
全局路由守衛(wèi)里做判斷
function hasPermission(router, accessMenu) {if (whiteList.indexOf(router.path) !== -1) {return true;}let menu = Util.getMenuByName(router.name, accessMenu);if (menu.name) {return true;}return false;}Router.beforeEach(async (to, from, next) => {if (getToken()) {let userInfo = store.state.user.userInfo;if (!userInfo.name) {try {await store.dispatch("GetUserInfo")await store.dispatch('updateAccessMenu')if (to.path === '/login') {next({ name: 'home_index' })} else {//Util.toDefaultPage([...routers], to.name, router, next);next({ ...to, replace: true })//菜單權(quán)限更新完成,重新進(jìn)一次當(dāng)前路由}} catch (e) {if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login')}}} else {if (to.path === '/login') {next({ name: 'home_index' })} else {if (hasPermission(to, store.getters.accessMenu)) {Util.toDefaultPage(store.getters.accessMenu,to, routes, next);} else {next({ path: '/403',replace:true })}}}} else {if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login')}}let menu = Util.getMenuByName(to.name, store.getters.accessMenu);Util.title(menu.title);
});Router.afterEach((to) => {window.scrollTo(0, 0);
});
每次路由跳轉(zhuǎn)的時(shí)候都要判斷權(quán)限,這里的判斷也很簡(jiǎn)單,因?yàn)椴藛蔚?code>name與路由的name
是一一對(duì)應(yīng)的,而后端返回的菜單就已經(jīng)是經(jīng)過權(quán)限過濾的
如果根據(jù)路由name
找不到對(duì)應(yīng)的菜單,就表示用戶有沒權(quán)限訪問
如果路由很多,可以在應(yīng)用初始化的時(shí)候,只掛載不需要權(quán)限控制的路由。取得后端返回的菜單后,根據(jù)菜單與路由的對(duì)應(yīng)關(guān)系,篩選出可訪問的路由,通過addRoutes
動(dòng)態(tài)掛載
這種方式的缺點(diǎn):
- 菜單需要與路由做一一對(duì)應(yīng),前端添加了新功能,需要通過菜單管理功能添加新的菜單,如果菜單配置的不對(duì)會(huì)導(dǎo)致應(yīng)用不能正常使用
- 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷
方案二
菜單和路由都由后端返回
前端統(tǒng)一定義路由組件
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {home: Home,userInfo: UserInfo
};
后端路由組件返回以下格式
[{name: "home",path: "/",component: "home"},{name: "home",path: "/userinfo",component: "userInfo"}
]
在將后端返回路由通過addRoutes
動(dòng)態(tài)掛載之間,需要將數(shù)據(jù)處理一下,將component
字段換為真正的組件
如果有嵌套路由,后端功能設(shè)計(jì)的時(shí)候,要注意添加相應(yīng)的字段,前端拿到數(shù)據(jù)也要做相應(yīng)的處理
這種方法也會(huì)存在缺點(diǎn):
- 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷
- 前后端的配合要求更高
按鈕權(quán)限
方案一
按鈕權(quán)限也可以用v-if
判斷
但是如果頁面過多,每個(gè)頁面頁面都要獲取用戶權(quán)限role
和路由表里的meta.btnPermissions
,然后再做判斷
這種方式就不展開舉例了
方案二
通過自定義指令進(jìn)行按鈕權(quán)限的判斷
首先配置路由
{path: '/permission',component: Layout,name: '權(quán)限測(cè)試',meta: {btnPermissions: ['admin', 'supper', 'normal']},//頁面需要的權(quán)限children: [{path: 'supper',component: _import('system/supper'),name: '權(quán)限測(cè)試頁',meta: {btnPermissions: ['admin', 'supper']} //頁面需要的權(quán)限},{path: 'normal',component: _import('system/normal'),name: '權(quán)限測(cè)試頁',meta: {btnPermissions: ['admin']} //頁面需要的權(quán)限}]
}
自定義權(quán)限鑒定指令
import Vue from 'vue'
/**權(quán)限指令**/
const has = Vue.directive('has', {bind: function (el, binding, vnode) {// 獲取頁面按鈕權(quán)限let btnPermissionsArr = [];if(binding.value){// 如果指令傳值,獲取指令參數(shù),根據(jù)指令參數(shù)和當(dāng)前登錄人按鈕權(quán)限做比較。btnPermissionsArr = Array.of(binding.value);}else{// 否則獲取路由中的參數(shù),根據(jù)路由的btnPermissionsArr和當(dāng)前登錄人按鈕權(quán)限做比較。btnPermissionsArr = vnode.context.$route.meta.btnPermissions;}if (!Vue.prototype.$_has(btnPermissionsArr)) {el.parentNode.removeChild(el);}}
});
// 權(quán)限檢查方法
Vue.prototype.$_has = function (value) {let isExist = false;// 獲取用戶按鈕權(quán)限let btnPermissionsStr = sessionStorage.getItem("btnPermissions");if (btnPermissionsStr == undefined || btnPermissionsStr == null) {return false;}if (value.indexOf(btnPermissionsStr) > -1) {isExist = true;}return isExist;
};
export {has}
在使用的按鈕中只需要引用v-has
指令
<el-button @click='editClick' type="primary" v-has>編輯</el-button>
小結(jié)
關(guān)于權(quán)限如何選擇哪種合適的方案,可以根據(jù)自己項(xiàng)目的方案項(xiàng)目,如考慮路由與菜單是否分離
權(quán)限需要前后端結(jié)合,前端盡可能的去控制,更多的需要后臺(tái)判斷
SPA首屏加載速度慢的怎么解決
一、什么是首屏加載
首屏?xí)r間(First Contentful Paint
),指的是瀏覽器從響應(yīng)用戶輸入網(wǎng)址地址,到首屏內(nèi)容渲染完成的時(shí)間,此時(shí)整個(gè)網(wǎng)頁不一定要全部渲染完成,但需要展示當(dāng)前視窗需要的內(nèi)容
首屏加載可以說是用戶體驗(yàn)中最重要的環(huán)節(jié)
關(guān)于計(jì)算首屏?xí)r間
利用performance.timing
提供的數(shù)據(jù):
通過DOMContentLoad
或者performance
來計(jì)算出首屏?xí)r間
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime// performance.getEntriesByName("first-contentful-paint")[0]
// 會(huì)返回一個(gè) PerformancePaintTiming的實(shí)例,結(jié)構(gòu)如下:
{name: "first-contentful-paint",entryType: "paint",startTime: 507.80000002123415,duration: 0,
};
二、加載慢的原因
在頁面渲染的過程,導(dǎo)致加載速度慢的因素可能如下:
- 網(wǎng)絡(luò)延時(shí)問題
- 資源文件體積是否過大
- 資源是否重復(fù)發(fā)送請(qǐng)求去加載了
- 加載腳本的時(shí)候,渲染內(nèi)容堵塞了
三、解決方案
常見的幾種SPA首屏優(yōu)化方式
- 減小入口文件積
- 靜態(tài)資源本地緩存
- UI框架按需加載
- 圖片資源的壓縮
- 組件重復(fù)打包
- 開啟GZip壓縮
- 使用SSR
1. 減小入口文件體積
常用的手段是路由懶加載,把不同路由對(duì)應(yīng)的組件分割成不同的代碼塊,待路由被請(qǐng)求的時(shí)候會(huì)單獨(dú)打包路由,使得入口文件變小,加載速度大大增加
在vue-router
配置路由的時(shí)候,采用動(dòng)態(tài)加載路由的形式
routes:[ path: 'Blogs',name: 'ShowBlogs',component: () => import('./components/ShowBlogs.vue')
]
以函數(shù)的形式加載路由,這樣就可以把各自的路由文件分別打包,只有在解析給定的路由時(shí),才會(huì)加載路由組件
2. 靜態(tài)資源本地緩存
后端返回資源問題:
- 采用
HTTP
緩存,設(shè)置Cache-Control
,Last-Modified
,Etag
等響應(yīng)頭 - 采用
Service Worker
離線緩存
前端合理利用localStorage
3. UI框架按需加載
在日常使用UI
框架,例如element-UI
、或者antd
,我們經(jīng)常性直接引用整個(gè)UI
庫(kù)
import ElementUI from 'element-ui'
Vue.use(ElementUI)
但實(shí)際上我用到的組件只有按鈕,分頁,表格,輸入與警告 所以我們要按需引用
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
4. 組件重復(fù)打包
假設(shè)A.js
文件是一個(gè)常用的庫(kù),現(xiàn)在有多個(gè)路由使用了A.js
文件,這就造成了重復(fù)下載
解決方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3
minChunks
為3表示會(huì)把使用3次及以上的包抽離出來,放進(jìn)公共依賴文件,避免了重復(fù)加載組件
5. 圖片資源的壓縮
圖片資源雖然不在編碼過程中,但它卻是對(duì)頁面性能影響最大的因素
對(duì)于所有的圖片資源,我們可以進(jìn)行適當(dāng)?shù)膲嚎s
對(duì)頁面上使用到的icon
,可以使用在線字體圖標(biāo),或者雪碧圖,將眾多小圖標(biāo)合并到同一張圖上,用以減輕http
請(qǐng)求壓力。
6. 開啟GZip壓縮
拆完包之后,我們?cè)儆?code>gzip做一下壓縮 安裝compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {// 為生產(chǎn)環(huán)境修改配置...config.mode = 'production'return {plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, //匹配文件名threshold: 10240, //對(duì)超過10k的數(shù)據(jù)進(jìn)行壓縮deleteOriginalAssets: false //是否刪除原文件})]}}
在服務(wù)器我們也要做相應(yīng)的配置 如果發(fā)送請(qǐng)求的瀏覽器支持gzip
,就發(fā)送給它gzip
格式的文件 我的服務(wù)器是用express
框架搭建的 只要安裝一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中間件使用之前調(diào)用
7. 使用SSR
SSR(Server side ),也就是服務(wù)端渲染,組件或頁面通過服務(wù)器生成html字符串,再發(fā)送到瀏覽器
從頭搭建一個(gè)服務(wù)端渲染是很復(fù)雜的,vue
應(yīng)用建議使用Nuxt.js
實(shí)現(xiàn)服務(wù)端渲染
四、小結(jié)
減少首屏渲染時(shí)間的方法有很多,總的來講可以分成兩大部分 :資源加載優(yōu)化
和 頁面渲染優(yōu)化
下圖是更為全面的首屏優(yōu)化的方案
大家可以根據(jù)自己項(xiàng)目的情況選擇各種方式進(jìn)行首屏渲染的優(yōu)化