MVVM
model和view層通過中間的vm連接和驅(qū)動。model層數(shù)據(jù)變化會改變視圖美浦,view改變通過事件來修改數(shù)據(jù)项栏。vue參考了MVVM實(shí)現(xiàn)了雙向綁定沼沈,react是MVC,但是vue仍然可以通過parent等操作dom所以不全是mvvm
vue模板解析
1摊滔、先將代碼轉(zhuǎn)換為AST樹
根據(jù)正則匹配店乐,從第一個字符開始眨八,篩選過的就刪掉繼續(xù)index++向后匹配踪古。
如果匹配開始標(biāo)簽就放入一個stack中券腔,此時如果匹配到結(jié)束標(biāo)簽則出棧對比是否一致纷纫,不一致報(bào)錯
2辱魁、優(yōu)化AST樹
找出靜態(tài)節(jié)點(diǎn)并標(biāo)記诗鸭,之后就不需要diff了
遞歸遍歷ast樹中的節(jié)點(diǎn),如果沒有表達(dá)式强岸、v-if锻弓、v-for等青灼,就標(biāo)記static為true
3杂拨、生成render函數(shù)弹沽、在使用new Function(with() {})包裹
轉(zhuǎn)換成render函數(shù)策橘。編譯結(jié)束亏狰。一定要包裹new Function和with來更改上下文環(huán)境
<div id="app"><p>hello {{name}}</p> hello</div> ==>
new Function(with(this) {_c("div",{id:app},_c("p",undefined,_v('hello' + _s(name) )),_v('hello'))})
v-if解析出來就是三元表達(dá)式暇唾,v-for解析出來_l((3),..)
4策州、render函數(shù)執(zhí)行后得到的是虛擬dom
ast是需要吧代碼使用正則匹配生成的够挂,然后轉(zhuǎn)換成render孽糖,而虛擬dom則是通過render函數(shù)直接生成一個對象
初始化data中的proxy
將所有的數(shù)據(jù)全部代理到this上
for (let key in data) {
proxy(vm, '_data', key);
}
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key]
},
set(newValue){
vm[source][key] = newValue;
}
})
}
vue的雙向數(shù)據(jù)綁定办悟、響應(yīng)式原理
監(jiān)聽器 Observer 病蛉,用來劫持并監(jiān)聽所有屬性(轉(zhuǎn)變成setter/getter形式)铺然,如果屬性發(fā)生變化,就通知訂閱者
訂閱器 Dep赋铝,用來收集訂閱者柬甥,對監(jiān)聽器 Observer和訂閱者 Watcher進(jìn)行統(tǒng)一管理苛蒲,每一個屬性數(shù)據(jù)都有一個dep記錄保存訂閱他的watcher臂外。
訂閱者 Watcher漏健,可以收到屬性的變化通知并執(zhí)行相應(yīng)的方法蔫浆,從而更新視圖姐叁,每個watcher上都會保存對應(yīng)的dep
解析器 Compile外潜,可以解析每個節(jié)點(diǎn)的相關(guān)指令处窥,對模板數(shù)據(jù)和訂閱器進(jìn)行初始化
數(shù)據(jù)劫持
利用observe方法遞歸的去劫持谒麦,對外也可以使用這個api。使用defineReactive來劫持?jǐn)?shù)據(jù)
class Observe{
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 判斷是否為數(shù)組颅悉,如果是數(shù)組則修改__proto__原型剩瓶。會再原來原型和實(shí)例中間增加一層延曙。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
//遍歷數(shù)組枝缔,繼續(xù)調(diào)用observe方法愿卸。因?yàn)閿?shù)組中有可能有二維數(shù)組或者對象
this.observeArray(value)
} else {
// 如果是對象則直接綁定響應(yīng)式
this.walk(value)
}
}
}
對象的劫持:不斷的遞歸趴荸,劫持到每一個屬性发钝。在defineReactive中會繼續(xù)遞歸執(zhí)行l(wèi)et childOb = !shallow && observe(val)方法遞歸綁定酝豪,因?yàn)閷ο笾杏锌赡苓€有對象
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//直接遍歷對象去遞歸攔截get孵淘、set
defineReactive(obj, keys[i])
}
}
數(shù)組的劫持:不劫持下標(biāo),value.proto = arrayMethods,增加一層原型鏈重寫數(shù)組的push余黎、splice等方法來劫持新增的數(shù)據(jù)惧财。在數(shù)組方法中進(jìn)行派發(fā)更新ob.dep.notify()
// 繼續(xù)遍歷數(shù)組,再次執(zhí)行observe來遞歸綁定值乖坠。
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
響應(yīng)式原理數(shù)據(jù)劫持熊泵,首先執(zhí)行observe方法new 一個Observe類顽分,其中會判斷是數(shù)組還是對象卒蘸。
1缸沃、如果數(shù)據(jù)是[1,[2,3],{a:1}]修械,不會去劫持下標(biāo)祠肥。會修改數(shù)組的proto修改原型的方法仇箱。但是其中的[2,3]剂桥,{a:1}并沒有被監(jiān)控权逗,所以繼續(xù)調(diào)用observeArray遞歸調(diào)用斟薇,其中又遞歸調(diào)用了let childOb = !shallow && observe(val)繼續(xù)監(jiān)控
2堪滨、如果數(shù)據(jù)是{a:{b:2},c:3}, 會執(zhí)行walk去遍歷對象執(zhí)行defineReactive攔截key的get、set义矛。其中會去遞歸調(diào)用observe方法繼續(xù)遞歸劫持
依賴收集
渲染watcher的收集:
首次渲染:執(zhí)行完劫持之后凉翻,會走掛載流程會new一個渲染watcher制轰,watcher中會立即執(zhí)行回調(diào)render方法艇挨,方法中會去創(chuàng)建Vnode需要去數(shù)據(jù)中取值缩滨,就會進(jìn)入到屬性的get方法脉漏。會去收集依賴
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 如果有current watcher侧巨,會去收集依賴司忱。Dep.target全局只有一個坦仍。一個時刻只能更新一個watcher繁扎。每次在執(zhí)行watcher時會先pushStack梳玫,等執(zhí)行完后會去popstack
if (Dep.target) {
// 收集屬性的依賴提澎,每個屬性獲取后都會有個dep盼忌。這個dep掛在每個屬性上碴犬。
// 例如直接this.a = xxx修改屬性就可以找到這個屬性上的dep更新watcher
dep.depend()
// 如果兒子也是對象或者數(shù)組會去遞歸讓兒子也收集
if (childOb) {
// 這個dep掛在對象或者數(shù)組上。為了給$set或者數(shù)組派發(fā)更新使用偿荷。在 {b:1} 跳纳、[1,2,3]上掛dep
// 例如新增屬性寺庄,this.$set(obj, b, xxx)或this.arr.push(2)斗塘。
// a:{b:[1,2,3]}馍盟、a:{b:{c:1}},先收集a的依賴掛在屬性dep上贞岭,因?yàn)閏hildOb又為object瞄桨,需要繼續(xù)收集依賴掛在該對象上
// 此時如果更新a讲婚,則直接找到屬性上的dep更新筹麸。但是a上如果想新增一個c屬性物赶,則需要使用$set酵紫〗钡兀或者數(shù)組上push一個参歹。
// 此時是找不到屬性上的dep的犬庇,因?yàn)樵搶傩允切略龅某敉欤瑪?shù)組增加一項(xiàng)需要更新watcher葬荷。所以需要在對象或者數(shù)組的Ob類上掛一個dep方便更新
childOb.dep.depend()
// 如果仍然是數(shù)組需要持續(xù)遞歸收集
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
dep中收集watcher
// dep.js
depend () {
// Dep.target 是此刻唯一準(zhǔn)備被收集的watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.js
addDep (dep: Dep) {
const id = dep.id
// 去重闯狱,如果添加過就不需要添加了
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// dep中保存此watcher
dep.addSub(this)
}
}
}
派發(fā)更新
在set中派發(fā)更新哄孤,數(shù)組是在劫持的方法中派發(fā)更新瘦陈。會執(zhí)行當(dāng)前所有dep中的watcher的notify方法更新視圖或者數(shù)據(jù)晨逝。
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// this.a如果直接改了引用,需要重新遞歸劫持屬性趁窃,例如a:{b:1} this.a = {c:2}
childOb = !shallow && observe(newVal)
// 執(zhí)行派發(fā)更新操作
dep.notify()
}
})
dep中派發(fā)更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 遍歷執(zhí)行所有watcher的update方法
subs[i].update()
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
// 如果是computed, 會去執(zhí)行重新取值操作
this.dirty = true
} else if (this.sync) {
// 如果是同步watcher直接run()會去執(zhí)行watcher的get()
this.run()
} else {
// 默認(rèn)watcher都是放入隊(duì)列中異步執(zhí)行的
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
// .......調(diào)用全局的nextTick方法來異步執(zhí)行隊(duì)列
nextTick(flushSchedulerQueue)
}
watcher都是會異步更新,調(diào)用nexttick去更新刨摩,為了整合多次操作為一次澡刹。提高效率
watch
watch內(nèi)部會調(diào)用$watch創(chuàng)建一個user watcher(設(shè)置user為true)罢浇,等依賴變化執(zhí)行update方法
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// $watcher最終也會去new 一個watcher,傳入vm實(shí)例和檢測的key(expOrFn)和watcher的回調(diào)(cb)和watcher的設(shè)置(deep等設(shè)置)
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 如果設(shè)置是immediate凌受,則需要立即執(zhí)行一次cb
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
// watcher.js中會去判斷expOrFn 如果是function說明是渲染watcher傳入的回調(diào)胜蛉,
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 如果傳入的是字符串誊册,則將取值函數(shù)賦給getter(調(diào)用一次就是取值一次),例如watcher中監(jiān)控的是'a.b.c'則需要從this中一直取到c
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
依賴收集:會去執(zhí)行watcher的getter方法嘲碱,其實(shí)就是去取值麦锯,此時獲取的值保存起來扶欣。執(zhí)行取值函數(shù)會走屬性的get進(jìn)行依賴收集。
watch初始化時會去取值术陶,為了保存下一次變化時的oldvalue
this.value = this.lazy
? undefined
: this.get()
get () {
pushTarget(this)
let value
const vm = this.vm
try {
//如果是user watcher的話梧宫,執(zhí)行的就是取值函數(shù)其實(shí)就是依賴收集過程
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 如果設(shè)置了deep則需要遍歷獲取子屬性進(jìn)行全部的依賴收集(把子屬性都取值一遍)
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
派發(fā)更新:更新時會執(zhí)行watcher的update方法脓豪,其中如果設(shè)置同步則直接run扫夜,如果沒有默認(rèn)放入隊(duì)列異步更新
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// run就是重新調(diào)用get執(zhí)行g(shù)etter笤闯,去重新取值颗味,取出來的就是新值
const value = this.get()
// 如果是新老值不相同才需要調(diào)用user watcher的回調(diào)
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 取老的值并設(shè)置新值
const oldValue = this.value
this.value = value
// 調(diào)用user watcher的回調(diào)
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
computed
computed會設(shè)置lazy為true。并且會執(zhí)行臟檢查晶默,只有當(dāng)這些依賴變化時才會去重新計(jì)算computed的值磺陡,獲取完之后再設(shè)置dirty為false
// 設(shè)置lazy為true表示是computed
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// 如果用戶傳入對象表示自己定義了get函數(shù)則使用用戶的仅政,沒有則直接設(shè)置getter
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 創(chuàng)建一個computed watcher圆丹, 初始化時其中不會執(zhí)行g(shù)et函數(shù)獲取值辫封。
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 定義computed倦微,需要去劫持計(jì)算屬性的值進(jìn)行依賴收集。
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
// 定義computed
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 其實(shí)就是重新劫持computed的值拓劝, sharedPropertyDefinition中有定義的get函數(shù)
Object.defineProperty(target, key, sharedPropertyDefinition)
}
收集依賴: 創(chuàng)建computed時雏逾,需要對computed的變量也進(jìn)行劫持,如果頁面中使用到了這個計(jì)算屬性郑临,則會走下面的createComputedGetter 創(chuàng)建的get方法栖博。之后會去收集依賴。
// 創(chuàng)建劫持computed的get函數(shù)
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果dirty為true才取值厢洞,創(chuàng)建時默認(rèn)第一次是true,會去執(zhí)行g(shù)et方法
if (watcher.dirty) {
watcher.evaluate()
}
// 如果有target則去收集依賴躺翻。firstName和lastName收集渲染依賴, 計(jì)算屬性上不需要收集渲染watcher丧叽,因?yàn)槿绻撁嬷惺褂玫搅诉@個計(jì)算屬性,計(jì)算屬性是根據(jù)函數(shù)中依賴變化計(jì)算的公你,所以其中任何一個依賴都需要收集一下渲染watcher踊淳,因?yàn)槿魏我粋€變化都有可能導(dǎo)致重新渲染
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// 取值
evaluate () {
// 執(zhí)行g(shù)et方法會去取值,例如:return this.firstName + this.lastName,此時也是對依賴firstName和lastName的取值收集依賴的過程省店,那么他們也會將當(dāng)前的computed watcher添加到dep的sub隊(duì)列中嚣崭。取值完置換成false
this.value = this.get()
this.dirty = false
}
所以如果計(jì)算屬性中寫了data中其他的值也會使他進(jìn)行收集依賴笨触,浪費(fèi)性能
let vm = new Vue({
el:'#app',
data: {
firstName: 'super',
lastName: 'kimi',
kimi: 888
},
computed: {
fullName() {
// 最后返回沒有kimi但是打印進(jìn)行取值了懦傍,他就會收集computed和渲染watcher
console.log(this.kimi)
return `${this.firstName}-${this.lastName}`
}
}
})
// 如果更新了kimi也會讓視圖重新渲染
vm.kimi = 999
派發(fā)更新:如果此時改變了firstName的值,因?yàn)閒irstName之前收集依賴中有依賴他的computed watcher和渲染watcher芦劣,會去執(zhí)行兩個watcher上的update方法
update () {
// 如果是計(jì)算屬性則設(shè)置dirty為true即可粗俱,之后再去執(zhí)行渲染watcher的update會重新渲染,那就會重新取計(jì)算屬性的值虚吟,到時候就可以取到最新的值了
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
provide寸认、inject
provide是定義在當(dāng)前實(shí)例上,inject會去遍歷$parent找到誰定義了串慰,然后再轉(zhuǎn)成響應(yīng)式掛在當(dāng)前實(shí)例偏塞,只是單向
nextTick
優(yōu)雅降級,先使用promise邦鲫,如果不支持會使用MutationObserver灸叼,不兼容再使用setImmediate,最后降級成setTimeout
slot
普通插槽和作用域插槽的實(shí)現(xiàn)庆捺。它們有一個很大的差別是數(shù)據(jù)作用域古今,普通插槽是在父組件編譯和渲染階段生成 vnodes,所以數(shù)據(jù)的作用域是父組件實(shí)例滔以,子組件渲染的時候直接拿到這些渲染好的 vnodes捉腥。而對于作用域插槽,父組件在編譯和渲染階段并不會直接生成 vnodes你画,而是在父節(jié)點(diǎn) vnode 的 data 中保留一個 scopedSlots 對象抵碟,存儲著不同名稱的插槽以及它們對應(yīng)的渲染函數(shù)桃漾,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes,由于是在子組件環(huán)境執(zhí)行的立磁,所以對應(yīng)的數(shù)據(jù)作用域是子組件實(shí)例呈队。
Vue.extend
傳入一個vue組件配置,然后創(chuàng)建一個構(gòu)造函數(shù)唱歧,然后進(jìn)行合并配置宪摧,修改指針等操作。生成一個vue的構(gòu)造函數(shù)颅崩,之后進(jìn)行new操作就可以生成一個vue組件實(shí)例几于,然后進(jìn)行vm.$mount可以動態(tài)掛載
Vue.$set
1、對象會重新遞歸添加響應(yīng)式沿后,數(shù)組則會調(diào)用splice方法沿彭,方法已經(jīng)被劫持
2、執(zhí)行ob.dep.notify()尖滚,讓視圖更新
Vue組件化
全局組件:Vue.component內(nèi)部會調(diào)用Vue.extend方法喉刘,將定義掛載到Vue.options.components上。這也說明所有的全局組件最終都會掛載到這個變量上
局部組件:在調(diào)用render時漆弄,也會去調(diào)用Vue.extend方法睦裳,在真正patch時會去new
data.hook = {
init(vnode){
let child = vnode.componentInstance = new Ctor({});
child.$mount(); // 組件的掛載
}
}
虛擬DOM
用js對象來表示dom節(jié)點(diǎn)。配合diff算法可以提高渲染的效率撼唾。
和ast的區(qū)別:ast是轉(zhuǎn)換語法(js廉邑、html語法轉(zhuǎn)換為ast)兩者很相像
生命周期
組件的渲染生命周期都是先子后父。beforeCreate中拿不到this倒谷。create中可以拿到data蛛蒙,但是沒掛載拿不到$el.
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
diff算法
1、首先比對標(biāo)簽 <div>...</div> --> <ul></ul>
在diff過程中會先比較標(biāo)簽是否一致渤愁,如果標(biāo)簽不一致用新的標(biāo)簽替換掉老的標(biāo)簽
// 如果標(biāo)簽不一致說明是兩個不同元素
if(oldVnode.tag !== vnode.tag){
oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
}
如果標(biāo)簽一致牵祟,有可能都是文本節(jié)點(diǎn),那就比較文本的內(nèi)容即可
// 如果標(biāo)簽一致但是不存在則是文本節(jié)點(diǎn)
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
oldVnode.el.textContent = vnode.text;
}
}
2抖格、對比屬性<div>...</div> --> <div className=‘a(chǎn)aa’>...</div>
當(dāng)標(biāo)簽相同時诺苹,我們可以復(fù)用老的標(biāo)簽元素,并且進(jìn)行屬性的比對他挎。只需要把新的屬性賦值到老的標(biāo)簽上即可
3筝尾、對比子元素<div><p>a</p></div> -> <div><p>b</p></div>
[1]新老都有孩子需要updateChildren比對
[2]新有老沒有則需要遍歷插入
[3]新沒有老有則需要刪除即可
// 比較孩子節(jié)點(diǎn)
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比對兒子
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el, oldChildren, newChildren)
// 老的有兒子新的沒有清空即可
}else if(oldChildren.length > 0 ){
el.innerHTML = '';
// 新的有兒子
}else if(newChildren.length > 0){
for(let i = 0 ; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
4、updateChildren 核心
設(shè)置四個index:oldS办桨、oldE筹淫、newS、newE
<1>先比對oldS和newS,通過判斷sameNode()方法比對key和tag等损姜。如果匹配相等則oldS和newS都++饰剥,節(jié)點(diǎn)復(fù)用即可 例如:ABCD -> ABCE
// 優(yōu)化向后追加邏輯
if(isSameVnode(oldStartVnode,newStartVnode)){
patch(oldStartVnode,newStartVnode); // 遞歸比較兒子
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
<2>oldS和newS如果不相等再比對oldE和newE,通過判斷sameNode()方法比對key和tag等摧阅。如果匹配相等則oldE和newE都--汰蓉,節(jié)點(diǎn)復(fù)用即可 例如:ABCD -> EBCD
// 優(yōu)化向前追加邏輯
else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 遞歸比較孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
<3>oldE和newE如果不相等再比對oldS和newE,通過判斷sameNode()方法比對key和tag等棒卷。如果匹配相等則oldS++和newE--顾孽,將old節(jié)點(diǎn)插入到最后 例如:ABCD -> BCDA
// 頭移動到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode); // 遞歸處理兒子
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
}
<4>oldS和newE如果不相等再比對oldE和newS,通過判斷sameNode()方法比對key和tag等比规。如果匹配相等則oldE--和newS++若厚,將old節(jié)點(diǎn)插入到最前 例如:ABCD -> DABC
// 尾部移動到頭部
else if(isSameVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode,newStartVnode);
parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
}
<5>如果使用index都判斷節(jié)點(diǎn)不相同,則需要建立vnode的key-index map表,然后匹配map表蜒什,如果能匹配上挪到當(dāng)前oldS前面测秸,如果匹配不上則創(chuàng)建新節(jié)點(diǎn)往當(dāng)前oldS前面插入,newS++ 例如:ABCD -> CDME
// 建立key-index的map表
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
// 在map表中尋找有沒有key匹配的vnode
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中沒有將新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的話做移動操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
圖中第一步比對index都不同灾常,則開始比對key發(fā)現(xiàn)有C相同則把C挪到最前面霎冯,newS++;下來發(fā)現(xiàn)D有相同的把D挪到oldS前面钞瀑,newS++沈撞;接著M找不到則插入oldS前面,newS++仔戈;最后E找不到則插入前面关串,newS++拧廊;
<6>全部比對完后需要對當(dāng)前index進(jìn)行檢查监徘,因?yàn)橛锌赡苡卸嗷蛘呱俟?jié)點(diǎn)的情況
if (oldStartIdx > oldEndIdx) {
// oldNode先掃完說明new有多余,需要添加進(jìn)去
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// newNode先掃完說明old有多余吧碾,需要刪除掉
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
diff對開頭凰盔、結(jié)尾插入刪除節(jié)點(diǎn)&頭節(jié)點(diǎn)移到尾部&尾節(jié)點(diǎn)移到頭部有很大的優(yōu)化
key:為了高效復(fù)用
A B C D E
A B F C D E
如果沒有key:首先比對頭和頭,A倦春、B都復(fù)用户敬,
比到C和F時
,tag一樣key相同(都為undefined)則會復(fù)用睁本,會成下圖情況如果有key:比對到C和F時
,C和F的key不相同所以跳過尿庐,此時就該比oldE和newE,EDC都相同呢堰,多下來的F直接插入
如果key使用index抄瑟,遇到表單元素比如帶checkbox的列表,如果狀態(tài)勾選后枉疼,會復(fù)用勾選狀態(tài)產(chǎn)生bug
keep-alive組件
會將組件緩存到this.cache中皮假,放入內(nèi)存中緩存起來鞋拟。
vue-router
1、install方法注冊全局組件惹资,掛載router
// 遞歸給每個子組件實(shí)例上都掛載一個_routerRoot 贺纲、_router屬性,以便于每個組件實(shí)例上都可以取到路由實(shí)例
export default function install(Vue) {
_Vue = Vue;
Vue.mixin({ // 給所有組件的生命周期都增加beforeCreate方法
beforeCreate() {
if (this.$options.router) { // 如果有router屬性說明是根實(shí)例
this._routerRoot = this; // 將根實(shí)例掛載在_routerRoot屬性上
this._router = this.$options.router; // 將當(dāng)前router實(shí)例掛載在_router上
this._router.init(this); // 初始化路由,這里的this指向的是根實(shí)例
} else { // 父組件渲染后會渲染子組件
this._routerRoot = this.$parent && this.$parent._routerRoot;
// 保證所有子組件都擁有_routerRoot 屬性褪测,指向根實(shí)例
// 保證所有組件都可以通過 this._routerRoot._router 拿到用戶傳遞進(jìn)來的路由實(shí)例對象
}
}
})
}
// 做一層代理猴誊,方便用戶$route和$router取值
Object.defineProperty(Vue.prototype,'$route',{ // 每個實(shí)例都可以獲取到$route屬性
get(){
return this._routerRoot._route;
}
});
Object.defineProperty(Vue.prototype,'$router',{ // 每個實(shí)例都可以獲取router實(shí)例
get(){
return this._routerRoot._router;
}
})
2、路由先生成map表
addRouter方法其實(shí)就是給路由表中插入對應(yīng)的值即可侮措。
export default function createMatcher(routes) {
// 收集所有的路由路徑, 收集路徑的對應(yīng)渲染關(guān)系
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = {'/':'/的記錄','/about':'/about記錄'...}
let {pathList,pathMap} = createRouteMap(routes);
// 這個方法就是動態(tài)加載路由的方法
function addRoutes(routes){
// 將新增的路由追加到pathList和pathMap中
createRouteMap(routes,pathList,pathMap);
}
function match(){} // 稍后根據(jù)路徑找到對應(yīng)的記錄
return {
addRoutes,
match
}
}
3稠肘、三種模式,如果是hash監(jiān)聽onHashChange事件萝毛,hash變化會賦值給this.current项阴,并且利用defineReactive方法定義響應(yīng)式對象_route。
window.addEventListener('hashchange', ()=> {
// 根據(jù)當(dāng)前hash值 過度到對應(yīng)路徑
this.transitionTo(getHash());
})
// 核心邏輯
transitionTo(location, onComplete) {
// 去匹配路徑
let route = this.router.match(location);
// 相同路徑不必過渡
if(
location === route.path &&
route.matched.length === this.current.matched.length){
return
}
this.updateRoute(route); // 更新路由即可
onComplete && onComplete();
}
updateRoute(route){ // 跟新current屬性
this.current =route;
}
//使用vue的方法defineReactive將_route變?yōu)轫憫?yīng)式并設(shè)置值為this.current
Vue.util.defineReactive(this,'_route',this._router.history.current);
4笆包、router-view拿到$route去使用render函數(shù)渲染其中的組件环揽。(如果/about/a會先渲染about再渲染a)
export default {
functional:true,
render(h,{parent,data}){
// 拿到$route其實(shí)就是拿到了_route,其實(shí)也是設(shè)置的this.current庵佣,此時取值也就相當(dāng)于收集依賴歉胶。收集到渲染watcher
let route = parent.$route;
let depth = 0;
data.routerView = true;
while(parent){ // 根據(jù)matched 渲染對應(yīng)的router-view
if (parent.$vnode && parent.$vnode.data.routerView){
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
if(!record){
return h();
}
// 讀取路由表中配置的component(此時已經(jīng)轉(zhuǎn)換成render函數(shù)了),執(zhí)行render
return h(record.component, data);
}
}
渲染過程:頁面開始渲染后會去取
$route
巴粪,會去找內(nèi)部_route通今,之前此屬性已經(jīng)變?yōu)轫憫?yīng)式,所以會進(jìn)行收集依賴操作肛根,添加渲染watcher辫塌。
當(dāng)hash改變時,會修改_route屬性派哲,此時進(jìn)行派發(fā)更新臼氨,執(zhí)行渲染watcher update重新渲染,router-view組件會去重新獲取$route屬性渲染芭届。
路由鉤子
①導(dǎo)航被觸發(fā)储矩。
②在失活的組件里調(diào)用 beforeRouteLeave 守衛(wèi)。
③調(diào)用全局的 beforeEach 守衛(wèi)褂乍。
④在重用的組件里調(diào)用 beforeRouteUpdate 守衛(wèi) (2.2+)持隧。
⑤在路由配置里調(diào)用 beforeEnter。
⑥解析異步路由組件逃片。
⑦在被激活的組件里調(diào)用 beforeRouteEnter屡拨。
⑧調(diào)用全局的 beforeResolve 守衛(wèi) (2.5+)。
⑨導(dǎo)航被確認(rèn)。
⑩調(diào)用全局的 afterEach 鉤子洁仗。
?觸發(fā) DOM 更新层皱。
?調(diào)用 beforeRouteEnter 守衛(wèi)中傳給 next 的回調(diào)函數(shù),創(chuàng)建好的組件實(shí)例會作為回調(diào)函數(shù)的參數(shù)傳入
Vuex
1赠潦、創(chuàng)建一個Store類叫胖,再導(dǎo)出一個install方法,同樣是利用mixin在beforeCreate鉤子中遞歸注入$store對象
export const install = (_Vue) =>{
_Vue.mixin({
beforeCreate() {
const options = this.$options;
if (options.store) {
// 給根實(shí)例增加$store屬性
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 給組件增加$store屬性
this.$store = options.parent.$store;
}
}
})
}
2她奥、實(shí)現(xiàn)state和getter瓮增。都是利用vue中的data和computed來實(shí)現(xiàn)。這樣可以為每一個store中的數(shù)據(jù)綁定響應(yīng)式哩俭。并做一層代理绷跑,如果用戶調(diào)用this.store.getter會去返回創(chuàng)建的vue實(shí)例上的屬性
// state
export class Store {
constructor(options){
let state = options.state;
this._vm = new Vue({
data:{
$$state:state,
}
});
}
get state(){
return this._vm._data.$$state
}
}
// getter
this.getters = {};
const computed = {}
forEachValue(options.getters, (fn, key) => {
computed[key] = () => {
return fn(this.state);
}
Object.defineProperty(this.getters,key,{
get:()=> this._vm[key]
})
});
this._vm = new Vue({
data: {
$$state: state,
},
computed // 利用計(jì)算屬性實(shí)現(xiàn)緩存
});
3、添加mutation和action凡资。其實(shí)就是存儲一個對象砸捏,利用發(fā)布訂閱來保存回調(diào)函數(shù)數(shù)組。
export class Store {
constructor(options) {
this.mutations = {};
forEachValue(options.mutations, (fn, key) => {
this.mutations[key] = (payload) => fn.call(this, this.state, payload)
});
}
commit = (type, payload) => {
this.mutations[type](payload);
}
}
export class Store {
constructor(options) {
this.actions = {};
forEachValue(options.actions, (fn, key) => {
this.actions[key] = (payload) => fn.call(this, this,payload);
});
}
dispatch = (type, payload) => {
this.actions[type](payload);
}
}
整體流程:vuex ->install方法中會去遍歷綁定$store隙赁。所以組件都可以取到-> 格式化用戶配置成一個樹形結(jié)構(gòu)垦藏。->安裝模塊,遞歸把模塊mutation伞访、action掂骏、getter、state都掛在store上厚掷,mutation弟灼、action都是數(shù)組(子模塊和父模塊重名會push都執(zhí)行),getter是對象(子模塊和父模塊重名會覆蓋)冒黑。state也是對象->會new 一個vue將state放到data上田绑、將getter放到computed上利用vue的原理來實(shí)現(xiàn)響應(yīng)式和計(jì)算緩存。
4薛闪、namespace模塊, 其實(shí)就是給安裝的模塊增加了path
1辛馆、如果不寫namespace是沒有作用域的俺陋,調(diào)用根豁延、子模塊的同名mutations都會執(zhí)行修改。
2腊状、狀態(tài)不能和模塊重名诱咏,默認(rèn)會使用模塊, a模塊namespace:true state中也有a
3缴挖、默認(rèn)會找當(dāng)前模塊的namespace袋狞,再向上找父親的。比如父親b有namespace兒子c沒有,會給兒子也加 使用方式:b/c/苟鸯,子c有父b沒有同蜻。則調(diào)用時不需要加父親b。調(diào)用:c/xxx
假設(shè)根模塊下中有a模塊并且都有命名空間
mutation早处、action如果在子模塊和父模塊中都有湾蔓,會都掛到store中的——mutation、action對象中砌梆,其中增加命名空間默责。例如:store.action = {'setA':[()=>{}] ,'a/b/setA':[()=>{}]}
,如果namespace沒寫的話就都在一個數(shù)組中,不會覆蓋
使用就$store.mutation('a/b/setA')
getter如果在子模塊和父模塊中都有的話咸包,會都掛載到store的_getter對象中桃序,增加命名空間,但是不是數(shù)組烂瘫,重名會覆蓋媒熊,例如:{'getA':()=>{},'a/b/getA':()=>{}}
,如果namespace沒寫的話就都在一個對象中會覆蓋
使用就$store.getter('a/b/getA')
state會遞歸放入store中坟比,變成一個對象泛释,例如 {name:'kimi', a:{name:'bob'}}
代表根節(jié)點(diǎn)中的state name是kimi,模塊a中是bob温算。所以使用的時候$store.state.a.name
vuex 插件
插件會使用發(fā)布訂閱怜校。在每次數(shù)據(jù)mutation更新的時候去發(fā)布。然后提供replaceState方法來替換state注竿,可以寫一個持久化插件茄茁,存到localStorge中,刷新后再從localStorge中取使用replaceState方法替換
function persists(store) { // 每次去服務(wù)器上拉去最新的 session巩割、local
let local = localStorage.getItem('VUEX:state');
if (local) {
store.replaceState(JSON.parse(local)); // 會用local替換掉所有的狀態(tài)
}
store.subscribe((mutation, state) => {
// 這里需要做一個節(jié)流 throttle lodash
localStorage.setItem('VUEX:state', JSON.stringify(state));
});
}
plugins: [
persists
]
內(nèi)部原理實(shí)現(xiàn):
// 執(zhí)行插件
options.plugins.forEach(plugin => plugin(this));
subscribe(fn){
this._subscribers.push(fn);
}
replaceState(state){
this._vm._data.$$state = state;
}
registered
也提供動態(tài)注冊模塊功能裙顽,就是重新走 -> 格式化樹形數(shù)據(jù) -> 安裝模塊到store上 -> 重新new vue實(shí)例,此時會銷毀之前的vue實(shí)例
strict模式
如果開啟strict模式宣谈,mutation中只能放同步代碼愈犹,不能放異步。并且不能直接修改state只能通過commit修改state闻丑。
更改屬性時包裹一層切片漩怎,先置換狀態(tài)_commite修改完再改回
也就是說只要是正常操作(不是通過state修改的)都會將_committing改為true
this._committing = false;
_withCommitting(fn) {
let committing = this._committing;
this._committing = true; // 在函數(shù)調(diào)用前 表示_committing為true
fn();
this._committing = committing;
}
此時修改mutation的值是需要包裹一層_withCommitting
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload); // 這里更改狀態(tài)
})
嚴(yán)格模式會去利用vue的$watch方法去監(jiān)控state,并且設(shè)置deep嗦嗡,sync為true勋锤,sync代表同步觸發(fā),如果data變了會立即執(zhí)行回調(diào)不會放入queue中nextTick執(zhí)行侥祭。這樣就可以監(jiān)控state變化叁执,如果其中之前的_commite為false說明沒有經(jīng)過commit或者異步更新(fn是異步執(zhí)行茄厘,則此時的_committing已經(jīng)重置回false了)。就可以拋錯
if (store.strict) {
// 只要狀態(tài)一變化會立即執(zhí)行,在狀態(tài)變化后同步執(zhí)行
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了狀態(tài)')
}, { deep: true, sync: true });
}
內(nèi)部正常的操作(不是通過state直接修改的)都需要包裝一層_withCommitting
replaceState(newState) { // 用最新的狀態(tài)替換掉
this._withCommitting(() => {
this._vm._data.$$state = newState;
})
}
store._withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state);
})