21.v-model中的實現原理及如何自定義v-model
- v-model 可以看成是 value+input方法 的語法糖 input v-model checkbox v-model select v-model
組件的v-model 就是value+input的語法糖
理解:
- 組件的 v-model 是 value+input方法 的語法糖
<el-checkbox :value="" @input=""></el-checkbox>
<el-checkbox v-model="check"></el-checkbox>
- 可以自己重新定義 v-model 的含義
Vue.component('el-checkbox',{
template:`<input type="checkbox" :checked="check"
@change="$emit('change',$event.target.checked)">`,
model:{
prop:'check', // 更改默認的value的名字
event:'change' // 更改默認的方法名
},
props: {
check: Boolean
},
})
原理:
- 會將組件的 v-model 默認轉化成value+input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></elcheckbox>');
// with(this) {
// return _c('el-checkbox', {
// model: {
// value: (check),
// callback: function ($$v) {
// check = $$v
// },
// expression: "check"
// }
// })
// }
function 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 ,會根據標籤的不同生成不同的事件和屬性
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
}
}
})
}
*/
編譯時:不同的標籤解析出的內容不一樣
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
}
運行時:會對元素處理一些關於輸入法的問題
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
}}}}
22.Vue中v-html會導致哪些問題?
理解:
- 可能會導致 xss 攻擊
- v-html 會替換掉標籤內部的子元素
原理:
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])
}
}
23. Vue父子組件生命週期調用順序
加載渲染過程
- 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
子組件更新過程
- 父beforeUpdate->子beforeUpdate->子updated->父updated
父組件更新過程
- 父beforeUpdate->父updated
銷燬過程
- 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
理解:
- 組件的調用順序都是先父後子,渲染完成的順序肯定是先子後父
- 組件的銷燬操作是先父後子,銷燬完成的順序是先子後父
原理:
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = [] // 定義收集所有組件的insert hook方法的數組
// somthing ...
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// somthing...
// 最終會依次調用收集的insert hook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// createChildren會遞歸創建兒子組件
createChildren(vnode, children, insertedVnodeQueue)
// something...
}
// 將組件的vnode插入到數組中
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
// insert方法中會依次調用mounted方法
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
}
function invokeInsertHook(vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]); // 調用insert方法
}
}
}
Vue.prototype.$destroy = function () {
callHook(vm, 'beforeDestroy') //
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null) // 先銷燬兒子
// fire destroyed hook
callHook(vm, 'destroyed')
}
24.Vue組件如何通信? 單向數據流
- 父子間通信 父->子通過 props 、子-> 父 emit (發佈訂閱)
- 獲取父子組件實例的方式 children
- 在父組件中提供數據子組件進行消費 Provide、inject 插件
- Ref 獲取實例的方式調用組件的屬性或者方法
- Event Bus 實現跨組件通信 Vue.prototype.$bus = new Vue
- Vuex 狀態管理實現通信 $attrs $listeners
25.Vue中相同邏輯如何抽離?
- Vue.mixin 用法 給組件每個生命週期,函數等都混入一些公共邏輯
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin); // 將當前定義的屬性合併到每個
組件中
return this
}
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
if (!child._base) {
if (child.extends) { // 遞歸合併extends
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) { // 遞歸合併mixin
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {} // 屬性及生命週期的合併
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key) {
const strat = strats[key] || defaultStrat
// 調用不同屬性合併策略進行合併
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
26.爲什麼要使用異步組件?
理解:
- 如果組件功能多打包出的結果會變大,我可以採用異步的方式來加載組件。主要依賴 import() 這
個語法,可以實現文件的分割加載。
components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") //
require([])
}
原理:
export function (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默認調用此函數時返回
undefiend
// 第二次渲染時Ctor不爲undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染佔位符 空虛擬節點
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent(
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
if (isDef(factory.resolved)) { // 3.在次渲染時可以拿到獲取的最新組件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 強制更新視圖重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
const res = factory(resolve, reject)// 1.將resolve方法和reject方法傳入,用戶調用
resolve方法後
sync = false
return factory.resolved
}
27.什麼是作用域插槽?
理解:
1.插槽:
<app><div slot="a">xxxx</div><div slot="b">xxxx</div></app>
slot name="a"
slot name="b"
- 創建組件虛擬節點時,會將組件的兒子的虛擬節點保存起來。當初始化組件時,通過插槽屬性將兒
子進行分類 {a:[vnode],b[vnode]} - 渲染組件時會拿對應的slot屬性的節點進行替換操作。(插槽的作用域爲父組件)
2.作用域插槽:
- 作用域插槽在解析的時候,不會作爲組件的孩子節點。會解析成函數,當子組件渲染時,會調用此
函數進行渲染。(插槽的作用域爲子組件)
原理:
1.插槽:
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<my-component>
<div slot="header">node</div>
<div>react</div>
<div slot="footer">vue</div>
</my-component>
`)
/**
with(this) {
return _c('my-component',
[_c('div', {
attrs: {
"slot": "header"
},
slot: "header"
}, [_v("node")] // _文本及誒點
), _v(" "), _c('div', [_v("react")]), _v(" "), _c('div', {
attrs: {
"slot": "footer"
},
slot: "footer"
}, [_v("vue")])])
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
<slot></slot>
</div>
`);
/**
with(this) {
return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "),
_t("default")], 2)
}
**/
// _t定義在 core/instance/render-helpers/index.js
作用域插槽:
let ele = VueTemplateCompiler.compile(`
<app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app>
`);
/**
with(this) {
return _c('app', {
scopedSlots: _u([{ // 作用域插槽的內容會被渲染成一個函數
key: "footer",
fn: function (msg) {
return _c('div', {}, [_v(_s(msg.a))])
}
}])
})
}
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(`
<div>
<slot name="footer" a="1" b="2"></slot>
</div>
`);
/**
with(this) {
return _c('div', [_t("footer", null, {
"a": "1",
"b": "2"
})], 2)
}
**/
28.談談你對 keep-alive 的瞭解?
理解:
- keep-alive 可以實現組件的緩存,當組件切換時不會對當前組件進行卸載,常用的2個屬性
include / exclude ,2個生命週期 activated , deactivated LRU算法
原理:
export default {
name: 'keep-alive',
abstract: true, // 抽象組件
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 創建緩存列表
this.keys = [] // 創建緩存組件的key列表
},
destroyed() { // keep-alive銷燬時 會清空所有的緩存和key
for (const key in this.cache) { // 循環銷燬
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() { // 會監控include 和 include屬性 進行組件的緩存處理
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
const slot = this.$slots.default // 會默認拿插槽
const vnode: VNode = getFirstComponentChild(slot) // 只緩存第一個組件
const componentOptions: ?VNodeComponentOptions = vnode &&
vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions) // 取出組件的名字
const { include, exclude } = this
if ( // 判斷是否緩存
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ?
`::${componentOptions.tag}` : '')
: vnode.key // 如果組件沒key 就自己通過 組件的標籤和key和cid 拼接一個key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance // 直接拿到組件實
例
// make current key freshest
remove(keys, key) // 刪除當前的 [b,c,d,e,a] // LRU 最近最久未使用法
keys.push(key) // 並將key放到後面[b,a]
} else {
cache[key] = vnode // 緩存vnode
keys.push(key) // 將key 存入
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) { // 緩存的太多超過了max
就需要刪除掉
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 要刪除第0個 但是現
在渲染的就是第0個
}
}
vnode.data.keepAlive = true // 並且標準keep-alive下的組件是一個緩存組件
}
return vnode || (slot && slot[0]) // 返回當前的虛擬節點
}
}
29.Vue中常見性能優化
1.編碼優化:
-
- 不要將所有的數據都放在data中,data中的數據都會增加getter和setter,會收集對應的
watcher
- 不要將所有的數據都放在data中,data中的數據都會增加getter和setter,會收集對應的
-
- vue 在 v-for 時給每項元素綁定事件需要用事件代理
-
- SPA 頁面採用keep-alive緩存組件
-
- 拆分組件( 提高複用性、增加代碼的可維護性,減少不必要的渲染 )
-
- v-if 當值爲false時內部指令不會執行,具有阻斷功能,很多情況下使用v-if替代v-show
-
- key 保證唯一性 ( 默認 vue 會採用就地複用策略 )
-
- Object.freeze 凍結數據
-
- 合理使用路由懶加載、異步組件
-
- 儘量採用runtime運行時版本
-
- 數據持久化的問題 (防抖、節流)
2. Vue 加載性能優化:
- 第三方模塊按需導入 ( babel-plugin-component )
- 滾動到可視區域動態加載 ( https://tangbc.github.io/vue-virtual-scroll-list )
- 圖片懶加載 (https://github.com/hilongjw/vue-lazyload.git)
3.用戶體驗:
- app-skeleton 骨架屏
- app-shell app殼
- pwa serviceworker
4. SEO 優化:
- 預渲染插件 prerender-spa-plugin
- 服務端渲染 ssr
5.打包優化:
- 使用 cdn 的方式加載第三方模塊
- 多線程打包 happypack
- splitChunks 抽離公共文件
- sourceMap 生成
6.緩存,壓縮
- 客戶端緩存、服務端緩存
- 服務端 gzip 壓縮
30.Vue3.0你知道有哪些改進?
- Vue3 採用了TS來編寫
- 支持 Composition API
- Vue3 中響應式數據原理改成 proxy
- vdom 的對比算法更新,只更新 vdom 的綁定了動態數據的部分
31.實現hash路由和history路由
- onhashchange #
- history.pushState h5 api
32.Vue-Router中導航守衛有哪些?
完整的導航解析流程 (runQueue)
-
- 導航被觸發。
-
- 在失活的組件裏調用離開守衛。
-
- 調用全局的 beforeEach 守衛。
-
- 在重用的組件裏調用 beforeRouteUpdate 守衛 (2.2+)。
-
- 在路由配置裏調用 beforeEnter 。
-
- 解析異步路由組件。
-
- 在被激活的組件裏調用 beforeRouteEnter 。
-
- 調用全局的 beforeResolve 守衛 (2.5+)。
-
- 導航被確認。
-
- 調用全局的 afterEach 鉤子。
-
- 觸發 DOM 更新。
-
- 用創建好的實例調用 beforeRouteEnter 守衛中傳給 next 的回調函數。
33.action 和 mutation區別
- mutation 是同步更新數據(內部會進行是否爲異步方式更新數據的檢測) $watch 嚴格模式下會報
錯 - action 異步操作,可以獲取數據後調傭 mutation 提交最終數據