關(guān)于Vue.js
Vue.js是一款MVVM框架顶瞳,通過響應(yīng)式在修改數(shù)據(jù)的時候更新視圖昂利。Vue.js的響應(yīng)式原理依賴于Object.defineProperty
诽里,尤大大在Vue.js文檔
中就已經(jīng)提到過纫溃,這也是Vue.js不支持IE8 以及更低版本瀏覽器的原因父能。Vue通過設(shè)定對象屬性的 setter/getter 方法來監(jiān)聽數(shù)據(jù)的變化臣镣,通過getter進(jìn)行依賴收集盼理,而每個setter方法就是一個觀察者谈山,在數(shù)據(jù)變更的時候通知訂閱者更新視圖 ,下面來一探究竟宏怔。
4.1奏路、Object.defineProperty
Object.defineProperty方法會直接在一個對象上定義一個新的屬性畴椰,或者修改對象的現(xiàn)有屬性并返回這個對象,它的語法如下:
Object.defineProperty(obj, prop, descriptor)
- obj是當(dāng)前要操作的對象
- props是要定義或修改的屬性名稱
- descriptor是要被定義或修改的屬性的描述符
?較核?的是 descriptor 鸽粉,它有很多可選鍵值斜脂,具體的可以去參閱它的?檔。這?我們最關(guān)?的是
get 和 set 潜叛, get 是?個給屬性提供的 getter ?法秽褒,當(dāng)我們訪問了該屬性的時候會觸發(fā) getter ?
法; set 是?個給屬性提供的 setter ?法威兜,當(dāng)我們對該屬性做修改的時候會觸發(fā) setter ?法销斟。
?旦對象擁有了 getter 和 setter,我們可以簡單地把這個對象稱為響應(yīng)式對象椒舵。那么 Vue.js 把哪些對象
變成了響應(yīng)式對象了呢蚂踊,接下來我們從源碼層?分析。
4.2笔宿、initState初始化數(shù)據(jù)
我們上面講過犁钟,在 Vue 的初始化階段, _init ?法執(zhí)?的時候泼橘,會執(zhí)? initState(vm) ?法涝动,這個?法主要是對 props 、 methods 炬灭、 data 醋粟、 computed 和 wathcer 等屬性做了初始化操作。這?我們重點分析 props 和 data 重归。
4.2.1米愿、 initProps
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
//初始化vm_props對象,最終可以通過vn._props訪問props數(shù)據(jù)
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
//遍歷props中所有數(shù)據(jù)
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
//調(diào)用defineReactive,把每個prop對應(yīng)的值變成響應(yīng)式
defineReactive(props, key, value)
}
if (!(key in vm)) {
//通過proxy將vm._props.xxx代理到vm.xxx上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
總結(jié):props 的初始化主要過程鼻吮,就是遍歷定義的 props 配置育苟。遍歷的過程主要做兩件事情:?個是調(diào)? defineReactive ?法把每個 prop 對應(yīng)的值變成響應(yīng)式,可以通過 vm._props.xxx 訪問到定
義 props 中對應(yīng)的屬性椎木。對于 defineReactive ?法违柏,我們稍后會介紹;另?個是通過 proxy
把 vm._props.xxx 的訪問代理到 vm.xxx 上香椎。
4.2.2漱竖、initData
function initData (vm: Component) {
//獲取實例上的數(shù)據(jù)
let data = vm.$options.data
//先判斷data是否是一個函數(shù)
data = vm._data = typeof data === '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 instance
//將data的數(shù)據(jù)和props,methods上的數(shù)據(jù)進(jìn)行比較,不能出現(xiàn)重復(fù)的定義
//因為他們最終都會掛載到vm上
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
//如果methods上存在這個鍵值
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
//如果props中存在這個鍵值
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
)
} //如果沒有重復(fù)定義
else if (!isReserved(key)) {
//給數(shù)據(jù)進(jìn)行代理
proxy(vm, `_data`, key)
}
}
// observe data
//對數(shù)據(jù)進(jìn)行響應(yīng)式的處理
observe(data, true /* asRootData */)
}
總結(jié):data初始化做了三件事
- 遍歷data士鸥,檢查data中的數(shù)據(jù)是否和methods,props中定義的數(shù)據(jù)重復(fù)
- 遍歷data,將每一個值vm._data.xxx都代理到vm.xxx上
- 調(diào)用observer方法觀測整個data的變化谆级,使data變成響應(yīng)式數(shù)據(jù)
我們看到烤礁,無論是props還是data初始化讼积,都是把他們變成響應(yīng)式對象,在這個過程中我們使用了幾個重要的函數(shù)函數(shù)脚仔,下面就是介紹這些函數(shù)勤众。
4.2.3、proxy
首先介紹下代理鲤脏,代理的作用就是吧props和data上的屬性代理到vm實例上们颜,這也是為什么我們定義了props可以直接通過this.xxx調(diào)用。
const sharedPropertyDefinition = {
enumerable: true,//是否是可枚舉的
configurable: true,//是否是可配置的
get: noop,//空函數(shù)
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
//初始化變量的get方法,把target[sourceKey][key]的讀取變成了對target[key]的讀取
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
//初始化變量的set方法猎醇,把target[sourceKey][key]的設(shè)置變成了對target[key]的設(shè)置
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
4.1.4窥突、observer
observe ?法的作?就是給? VNode 的對象類型數(shù)據(jù)添加?個 Observer ,如果已經(jīng)添加過則直接返回硫嘶,否則在滿??定條件下去實例化?個 Observer 對象實例阻问。接下來我們來看?下 Observer
類。
export class Observer {
value: any;
//observer和watcher的紐帶沦疾,當(dāng)數(shù)據(jù)發(fā)生變化時會被observer觀察到称近,然后由dep通知watcher去更新視圖
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value//被觀察到的數(shù)據(jù)對象
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)//增加一個標(biāo)志,表示已經(jīng)被observer觀察
// 如果value是數(shù)組哮塞,就辨遍歷數(shù)組刨秆,對數(shù)組中每一項進(jìn)行observer觀察
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)//遍歷數(shù)組的函數(shù)
} else {
//如果是對象,就遍歷對象的每一個key忆畅,對每個key調(diào)用defineReactive獲取對key的set和get的控制權(quán)
this.walk(value)
}
}
Observer 是?個類衡未,它的作?是給對象的屬性添加 getter 和 setter,?于依賴收集和派發(fā)更新邻眷。在 Observer 的構(gòu)造函數(shù)中眠屎,會對 value 做判斷,對于數(shù)組會調(diào)? observeArray ?法肆饶,否則對純對象調(diào)? walk ?法改衩。可以看到 observeArray 是遍歷數(shù)組再次調(diào)? observe ?法驯镊,?walk ?法是遍歷對象的 key 調(diào)? defineReactive ?法葫督。
-
observeArray
:遍歷數(shù)組,對數(shù)組的每個元素調(diào)用observer -
walk
:遍歷對象的每個Key板惑,對對象上的每個key調(diào)用defineReactive -
defineReactive
:通過Object.defineProperty 設(shè)置對象的key屬性橄镜,使得我們能夠獲得該屬性的該屬性的get/set使用權(quán),一般是由Watcher的實例進(jìn)行g(shù)et操作冯乘,此時Watcher實例對象會被添加到Dep數(shù)組中洽胶,在外部操作觸發(fā)set時,通過Dep通知Watcher進(jìn)行更新裆馒。
4.1.5姊氓、defineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val,
//Dep.target全局變量指向當(dāng)前正在解析指令的Compile生成的Watcher
if (Dep.target) {
dep.depend()//被讀取了丐怯,將這個以來收集起來
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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
}
childOb = !shallow && observe(newVal)
dep.notify()//被更新了,通知所有watcher去更新
}
})
}
defineReactive 函數(shù)最開始初始化 Dep 對象的實例翔横,接著拿到 obj 的屬性描述符读跷,然后對?對
象遞歸調(diào)? observe ?法,這樣就保證了?論 obj 的結(jié)構(gòu)多復(fù)雜禾唁,它的所有?屬性也能變成響應(yīng)
式的對象效览,這樣我們訪問或修改 obj 中?個嵌套較深的屬性,也能觸發(fā) getter 和 setter荡短。最后利?
Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter
4.3丐枉、依賴收集
經(jīng)過上面的分析我們知道,Vue會把普通對象變成響應(yīng)式對象肢预,響應(yīng)式對象的getter就是用來收集依賴的矛洞,再看看getter的實現(xiàn)。
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val,
//Dep.target全局變量指向當(dāng)前正在解析指令的Compile生成的Watcher
if (Dep.target) {
dep.depend()//被讀取了,將這個以來收集起來
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
getter函數(shù)最重要的一步就是通過調(diào)用depend函數(shù)進(jìn)行依賴的收集,depend函數(shù)是dep定義的一個函數(shù)漠畜,稍后會詳細(xì)介紹。
4.3.1抽兆、Dep
Dep是observer和watcher的紐帶,當(dāng)數(shù)據(jù)發(fā)生變化時會被observer觀察到族淮,然后由dep通知watcher辫红。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++//每個dep都有唯一的id
this.subs = []//用于存放依賴
}
//向subs數(shù)組添加依賴
addSub (sub: Watcher) {
this.subs.push(sub)
}
//移除依賴
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//設(shè)置Watcher的依賴,這里添加Deo.target目的是判斷是不是Watcher的構(gòu)造函數(shù)的調(diào)用
//也就是說判斷他是Watcher的this.get調(diào)用的的而不是普通調(diào)用
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//通知所有綁定的Watcher調(diào)用update()進(jìn)行更新
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++) {
subs[i].update()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
//這是全局唯一的祝辣,在任何時候只有一個watcher正在評估
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
// 將當(dāng)前的watcher推入堆棧中贴妻,關(guān)于為什么要推入堆棧,主要是要處理模板或render函數(shù)中嵌套了多層組件蝙斜,需要遞歸處理
targetStack.push(target)
// 設(shè)置當(dāng)前watcher到全局的Dep.target名惩,通過在此處設(shè)置,key使得在進(jìn)行g(shù)et的時候?qū)Ξ?dāng)前的訂閱者進(jìn)行依賴收集
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
-
add
:接受參數(shù)為Watcher實例孕荠,并把Watcher實例記錄依賴的數(shù)組中 -
depend
:Dep.target存放的是當(dāng)前需要操作的Watcher實例娩鹉,調(diào)用depend會調(diào)用該實例的addDep方法 -
notify
:通知數(shù)組中所有Watcher進(jìn)行更新操作
4.3.2、Watcher
Watcher作為觀察者稚伍,用來訂閱數(shù)據(jù)變化并執(zhí)行相應(yīng)的操作弯予,比如視圖的更新。
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean//是否為渲染watcher
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
//當(dāng)前Watcher添加到vue實例上
vm._watchers.push(this)
// options
//參數(shù)配置个曙,默認(rèn)是false
if (options) {
....
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
//內(nèi)容不可重復(fù)
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
//將watcher對象的getter設(shè)置成uptateComponent
this.getter = expOrFn
} else {
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
//在get函數(shù)中锈嫩,主要是收集一些依賴,然后在初始化或者有更新時,調(diào)用this.getter(對應(yīng)著updateComponent函數(shù))
get () {
//將Dep的target添加到targetStack呼寸,同時Dep的target賦值為當(dāng)前watcher
pushTarget(this)
let value
const vm = this.vm
try {
//調(diào)用updateComponent方法那槽,之后在updateComponent中接著會調(diào)用_update方法更新dom
//這時掛載到vue原型上的方法,而_render方法重新渲染了VNode
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
//update執(zhí)行完成之后等舔,又將dep.target從targetStack彈出
popTarget()
this.cleanupDeps()
}
return value
}
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.addSub(this)
}
}
}
cleanupDeps () {
let i = this.deps.length
......
}
//通知watcher更新
update () {
.......
}
run () {
.......
}
.............
}
-
addDep
:接收參數(shù)dep,讓當(dāng)前Watcher訂閱dep -
cleanupDeps
:將新的newDepIds(這里保存的是dep的id)和舊的deps去對比糟趾,找存在于出舊的deps但不存在于新的newDeplds中的dep慌植,就從這些dep中移除當(dāng)前的依賴,這樣可以有效地避免沒必要的updata义郑,稍后在new Watcher()渲染過程分析中我會可以舉個簡單的例子說明蝶柿。 -
updata
:立即執(zhí)行watcher或者將watcher加入隊列等待統(tǒng)一flush -
run
:運(yùn)行watcher,調(diào)用this.get()求值非驮,然后觸發(fā)回調(diào)
4.3.3交汤、new Watcher發(fā)生了什么
之前我們講過,在$mount函數(shù)中我們主要是通過調(diào)用mountComponent()函數(shù)去實現(xiàn)我們的數(shù)據(jù)掛載劫笙,而在mountComponent函數(shù)中就使用了new Watcher(),在這個過程中我們調(diào)用了每個數(shù)據(jù)的getter函數(shù)芙扎,這樣就實現(xiàn)了每個數(shù)據(jù)首次依賴的收集。
當(dāng)我們?nèi)嵗?個渲染 watcher 的時候填大,?先進(jìn)? watcher 的構(gòu)造函數(shù)邏輯戒洼,然后會執(zhí)?它的
this.get() ?法,進(jìn)? get 函數(shù)允华,?先會執(zhí)?
pushTarget(this)
//函數(shù)的實現(xiàn)
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
執(zhí)行這個函數(shù)就是把 Dep.target 賦值為當(dāng)前的渲染 watcher 并壓棧(為了恢復(fù)?)圈浇。接著?執(zhí)?了
value = this.getter.call(vm, vm)
//this.getter 對應(yīng)就是 updateComponent 函數(shù),這實際上就是在執(zhí)?:
vm._update(vm._render(), hydrating)
它會先執(zhí)? vm._render() ?法靴寂,因為之前分析過這個?法會?成 渲染 VNode磷蜀,并且在這個過程中
會對 vm 上的數(shù)據(jù)訪問,這個時候就觸發(fā)了數(shù)據(jù)對象的 getter百炬。那么每個對象值的 getter 都持有?個 dep 褐隆,在觸發(fā) getter 的時候會調(diào)? dep.depend() ?法,也就會執(zhí)?
Dep.target.addDep(this)
剛才我們提到這個時候 Dep.target 已經(jīng)被賦值為當(dāng)前在操作的 watcher 收壕,那么就執(zhí)?到 addDep ?法:
addDep (dep: Dep) {
const id = dep.id//獲取dep的id
//如果新的DepIds數(shù)組中沒有當(dāng)前dep妓灌,將入組
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
//如果dep沒有當(dāng)前正在操作的Watcher
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
這時候會做?些邏輯判斷(保證同?數(shù)據(jù)不會被添加多次)后執(zhí)? dep.addSub(this) ,那么就會執(zhí)
? this.subs.push(sub) 蜜宪,也就是說把當(dāng)前的 watcher 訂閱到這個數(shù)據(jù)持有的 dep 的 subs中虫埂。
所以在 vm._render() 過程中,會觸發(fā)所有數(shù)據(jù)的 getter圃验,這樣實際上已經(jīng)完成了?個依賴收集的過程掉伏。那么到這?就結(jié)束了么,其實并沒有,再完成依賴收集后斧散,還有?個邏輯要執(zhí)?供常,?先是:
if (this.deep) {
traverse(value)
}
這個是要遞歸去訪問 value ,觸發(fā)它所有?項的 getter 鸡捐,這個之后會詳細(xì)講栈暇。接下來執(zhí)?:
popTarget()
//函數(shù)實現(xiàn)
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
實際上就是把 Dep.target 恢復(fù)成上?個狀態(tài),因為當(dāng)前 vm 的數(shù)據(jù)依賴收集已經(jīng)完成箍镜,那么對應(yīng)的渲染 Dep.target 也需要改變源祈。最后執(zhí)?this.cleanupDeps()
這里著重講下this.cleanupDeps()這個函數(shù)。
考慮到 Vue 是數(shù)據(jù)驅(qū)動的色迂,所以每次數(shù)據(jù)變化都會重新render香缺,那么 vm._render() ?法?會再次執(zhí)?,并再次觸發(fā)數(shù)據(jù)的 getters歇僧,我們知道每次執(zhí)行一次render都是一次的性能消耗图张,那有什么辦法能幫助我們?nèi)プ柚箾]必要render,所以 Wathcer 在構(gòu)造函數(shù)中會初始化 2 個 Dep 實例數(shù)組诈悍, newDeps 表?新添加的 Dep 實例數(shù)組祸轮,? deps 表?上?次添加的 Dep 實例數(shù)組。在執(zhí)? cleanupDeps 函數(shù)的時候侥钳,會?先遍歷 deps 倔撞,移除對 dep 的訂閱,然后把 newDepIds和 depIds 交換慕趴, newDeps 和 deps 交換痪蝇,并把 newDepIds 和 newDeps 清空。那么為什么需要做 deps 訂閱的移除呢冕房,在添加 deps 的訂閱過程躏啰,已經(jīng)能通過 id 去重避免重復(fù)訂閱了。
考慮到?種場景耙册,我們的模板會根據(jù) v-if 去渲染不同?模板 a 和 b给僵,當(dāng)我們滿?某種條件的時候渲染 a 的時候,會訪問到 a 中的數(shù)據(jù)详拙,這時候我們對 a 使?的數(shù)據(jù)添加了 getter帝际,做了依賴收集,那么當(dāng)我們?nèi)バ薷?a 的數(shù)據(jù)的時候饶辙,理應(yīng)通知到這些訂閱者蹲诀。那么如果我們?旦改變了條件渲染了 b 模板,?會對 b 使?的數(shù)據(jù)添加了 getter弃揽,如果我們沒有依賴移除的過程脯爪,那么這時候我去修改 a 模板的數(shù)據(jù)则北,會通知 a 數(shù)據(jù)的訂閱的回調(diào),這顯然是有浪費(fèi)的痕慢。因此 Vue 設(shè)計了在每次添加完新的訂閱尚揣,會移除掉舊的訂閱,這樣就保證了在我們剛才的場景中掖举,如果渲染 b 模板的時候去修改 a 模板的數(shù)據(jù)快骗,a 數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了,所以不會有任何浪費(fèi)塔次,真的是?常贊嘆 Vue 對?些細(xì)節(jié)上的處理滨巴。
4.4、異步更新DOM策略及nextTick
Vue實現(xiàn)響應(yīng)式并不是數(shù)據(jù)變化后DOM立即變化俺叭,而是按照一定策略進(jìn)行DOM更新,在Vue文檔中指出Vue是異步執(zhí)行DOM更新泰偿。那Vue為什么要使用異步更新又是怎么實現(xiàn)的熄守?
我們先看看Watcher隊列 ,當(dāng)觸發(fā)某個數(shù)據(jù)的setter時耗跛,Dep就會通過調(diào)用notify去通知所有的依賴watcher.
//通知watcher更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步則執(zhí)行run直接渲染視圖*/
this.run()
} else {
/*異步推送到觀察者隊列中裕照,下一個tick時調(diào)用。*/
queueWatcher(this)
}
}
從源碼中我們可以知道Vue.js默認(rèn)是使用異步更新调塌,當(dāng)執(zhí)行updata時晋南,會調(diào)用queueWatcher函數(shù)。
//將一個觀察者對象push進(jìn)觀察者隊列羔砾,在隊列中已經(jīng)存在相同的id則該觀察者對象將被跳過负间,除非是在隊列被刷新時推送。
export function queueWatcher (watcher: Watcher) {
//獲取watcher的id
const id = watcher.id
//檢測id是否存在姜凄,已經(jīng)存在直接跳過政溃,不存在直接標(biāo)志哈希表has
if (has[id] == null) {
has[id] = true
/*如果沒有flush掉,直接push到隊列中即可*/
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
這?引?了?個隊列的概念态秧,這也是 Vue 在做派發(fā)更新的時候的?個優(yōu)化的點董虱,它并不會每次數(shù)據(jù)改
變都觸發(fā) watcher 的回調(diào),?是把這些 watcher 先添加到?個隊列?申鱼,然后在 nextTick 后執(zhí)
? flushSchedulerQueue 愤诱。這?有?個細(xì)節(jié)要注意?下,?先? has 對象保證同?個 Watcher 只添加?次捐友;接著對flushing 的判斷淫半,else 部分的邏輯稍后我會講;最后通過 wating 保證對
nextTick(flushSchedulerQueue) 的調(diào)?邏輯只有?次匣砖。那什么是nextTick?
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
/*下一個tick時的回調(diào)*/
function flushCallbacks () {
pending = false
//將每次保存的cb函數(shù)取出并執(zhí)行
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
/*
一共有Promise撮慨、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法
優(yōu)先使用Promise竿痰,在Promise不存在的情況下使用MutationObserver,這兩個方法都會在microtask中執(zhí)行砌溺,會比setTimeout更早執(zhí)行影涉,所以優(yōu)先使用。如果上述兩種方法都不支持的環(huán)境則會使用setTimeout规伐,在task尾部推入這個函數(shù)蟹倾,等待調(diào)用執(zhí)行。
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
/*新建一個textNode的DOM對象猖闪,用MutationObserver綁定該DOM并指定回調(diào)函數(shù)鲜棠,在DOM變化的時候則會觸發(fā)回調(diào),該回調(diào)會進(jìn)入主線程(比任務(wù)隊列優(yōu)先執(zhí)行),即textNode.data = String(counter)時便會觸發(fā)回調(diào)*/
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
/*使用setTimeout將回調(diào)推入任務(wù)隊列尾部*/
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
//cb是一個函數(shù)遍歷隊列中的watcher培慌,執(zhí)行watcher.run();
let _resolve
/*cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
這里解釋一下豁陆,一共有Promise、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法吵护。 優(yōu)先使用Promise盒音,在Promise不存在的情況下使用MutationObserver,這兩個方法的回調(diào)函數(shù)都會在microtask中執(zhí)行馅而,它們會比setTimeout更早執(zhí)行祥诽,所以優(yōu)先使用。 如果上述兩種方法都不支持的環(huán)境則會使用setTimeout瓮恭,在task尾部推入這個函數(shù)雄坪,等待調(diào)用執(zhí)行。 為什么在miscrotask執(zhí)行會更早呢屯蹦,JS 的 event loop 執(zhí)行時會區(qū)分 task 和 microtask维哈,引擎在每個 task 執(zhí)行完畢,從隊列中取下一個 task 來執(zhí)行之前登澜,會先執(zhí)行完所有 microtask 隊列中的 microtask笨农。setTimeout 回調(diào)會被分配到一個新的 task 中執(zhí)行,而 Promise 的 resolver帖渠、MutationObserver 的回調(diào)都會被安排到一個新的 microtask 中執(zhí)行谒亦,會比 setTimeout 產(chǎn)生的 task 先執(zhí)行。
綜上空郊,nextTick的目的就是產(chǎn)生一個回調(diào)函數(shù)加入task或者microtask中份招,當(dāng)前棧執(zhí)行完以后(可能中間還有別的排在前面的函數(shù))調(diào)用該回調(diào)函數(shù),起到了異步觸發(fā)(即下一個tick時觸發(fā))的目的狞甚。
為什么要異步更新視圖呢锁摔?
來看一下下面這一段代碼
<template>
<div>
<div>{{test}}</div>
</div>
</template>
export default {
data () {
return {
test: 0
};
},
mounted () {
for(let i = 0; i < 1000; i++) {
this.test++;
}
}
}
現(xiàn)在有這樣的一種情況,mounted的時候test的值會被++循環(huán)執(zhí)行1000次哼审。 每次++時谐腰,都會根據(jù)響應(yīng)式觸發(fā)setter->Dep->Watcher->update->patch孕豹。 如果這時候沒有異步更新視圖,那么每次++都會直接操作DOM更新視圖十气,這是非常消耗性能的励背。 所以Vue.js實現(xiàn)了一個queue隊列,在下一個tick的時候會統(tǒng)一執(zhí)行queue中Watcher的run砸西。同時叶眉,擁有相同id的Watcher不會被重復(fù)加入到該queue中去,所以不會執(zhí)行1000次Watcher的run芹枷。最終更新視圖只會直接將test對應(yīng)的DOM的0變成1000衅疙。 保證更新視圖操作DOM的動作是在當(dāng)前棧執(zhí)行完以后下一個tick的時候調(diào)用,大大優(yōu)化了性能 鸳慈。
4.5饱溢、computed和watch
- computed
computed實質(zhì)上是一個computed watcher,當(dāng)它所依賴的數(shù)據(jù)發(fā)生變化時會進(jìn)行重新計算走芋,然后對比新舊值绩郎,如果發(fā)生了變化就會觸發(fā)渲染watcher重新渲染,所以對于計算屬性Vue想確保不僅僅是計算屬性依賴的值發(fā)生變化绿聘,而是想當(dāng)計算屬性最終計算的值發(fā)生變化才會觸發(fā)渲染watcher重新渲染,這本質(zhì)上是一種優(yōu)化次舌。
watch
?旦我們 watch 的數(shù)據(jù)發(fā)送變化熄攘,它最終會執(zhí)? watcher 的run ?法,執(zhí)?回調(diào)函數(shù) cb 彼念,并且如果我們設(shè)置了 immediate 為 true挪圾,則直接會執(zhí)?回調(diào)函數(shù)cb ,最后返回了?個 unwatchFn ?法,它會調(diào)? teardown ?法去移除這個 watcher 逐沙。
拓展:watcher 總共有 4 種類型哲思,這里介紹deep watcher
deep watcher
通常,如果我們想對?下對象做深度觀測的時候吩案,需要設(shè)置這個屬性為 true棚赔,考慮到這種情況:
var vm = new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console.log(newVal) } } } }) vm.a.b = 2
這個時候是不會 log 任何數(shù)據(jù)的,因為我們是 watch 了 a 對象徘郭,只觸發(fā)了 a 的 getter靠益,并沒有觸發(fā)a.b 的 getter,所以并沒有訂閱它的變化残揉,導(dǎo)致我們對 vm.a.b = 2 賦值的時候胧后,雖然觸發(fā)了setter,但沒有可通知的對象抱环,所以也并不會觸發(fā) watch 的回調(diào)函數(shù)了壳快。?我們只需要對代碼做稍稍修改纸巷,就可以觀測到這個變化了
watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } }
這樣就創(chuàng)建了?個 deep watcher 了。