上一篇:Vue原理解析(八):一塊兒搞明白使人頭疼的diff算法html
以前的章節,咱們按照流程介紹了vue
的初始化、虛擬Dom
生成、虛擬Dom
轉爲真實Dom
、深刻理解響應式以及diff
算法等這些核心概念,對它內部的實現作了分析,這些都是偏底層的原理。接下來咱們將介紹平常開發中常用的API
的原理,進一步豐富對vue
的認識,它們主要包括如下:vue
響應式相關
API
:this.$watch
、this.$set
、this.$delete
面試
事件相關
API
:this.$on
、this.$off
、this.$once
、this.$emit
算法
生命週期相關
API
:this.$mount
、this.$forceUpdate
、this.$destroy
api
全局
API
:Vue.extend
、Vue.nextTick
、Vue.set
、Vue.delete
、Vue.component
、Vue.use
、Vue.mixin
、Vue.compile
、Vue.version
、Vue.directive
、Vue.filter
數組
這一章節主要分析computed
和watch
屬性,對於接觸vue
不久的朋友可能會對computed
和watch
有疑惑,何時使用哪一個屬性留有存疑,接下來咱們將從內部實現的角度出發,完全搞懂它們分別適用的場景。緩存
這個API
是咱們以前介紹響應式時的Watcher
類的一種封裝,也就是三種watcher
中的user-watcher
,監聽屬性常常會被這樣使用到:bash
export default {
watch: {
name(newName) {...}
}
}
複製代碼
其實它只是this.$watch
這個API
的一種封裝:閉包
export default {
created() {
this.$watch('name', newName => {...})
}
}
複製代碼
監聽屬性初始化
爲何這麼說,咱們首先來看下初始化時watch
屬性都作了什麼:異步
function initState(vm) { // 初始化全部狀態時
vm._watchers = [] // 當前實例watcher集合
const opts = vm.$options // 合併後的屬性
... // 其餘狀態初始化
if(opts.watch) { // 若是有定義watch屬性
initWatch(vm, opts.watch) // 執行初始化方法
}
}
---------------------------------------------------------
function initWatch (vm, watch) { // 初始化方法
for (const key in watch) { // 遍歷watch內多個監聽屬性
const handler = watch[key] // 每個監聽屬性的值
if (Array.isArray(handler)) { // 若是該項的值爲數組
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]) // 將每一項使用watcher包裝
}
} else {
createWatcher(vm, key, handler) // 不是數組直接使用watcher
}
}
}
---------------------------------------------------------
function createWatcher (vm, expOrFn, handler, options) {
if (isPlainObject(handler)) { // 若是是對象,參數移位
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // 若是是字符串,表示爲方法名
handler = vm[handler] // 獲取methods內的方法
}
return vm.$watch(expOrFn, handler, options) // 封裝
}
複製代碼
以上對監聽屬性的多種不一樣的使用方式,都作了處理。使用示例在官網上都可找到:watch示例,這裏就不作過多的介紹了。能夠看到最後是調用了vm.$watch
方法。
監聽屬性實現原理
因此咱們來看下$watch
的內部實現:
Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
const vm = this
if (isPlainObject(cb)) { // 若是cb是對象,當手動建立監聽屬性時
return createWatcher(vm, expOrFn, cb, options)
}
options.user = true // user-watcher的標誌位,傳入Watcher類中
const watcher = new Watcher(vm, expOrFn, cb, options) // 實例化user-watcher
if (options.immediate) { // 當即執行
cb.call(vm, watcher.value) // 以當前值當即執行一次回調函數
} // watcher.value爲實例化後返回的值
return function unwatchFn () { // 返回一個函數,執行取消監聽
watcher.teardown()
}
}
---------------------------------------------------------------
export default {
data() {
return {
name: 'cc'
}
},
created() {
this.unwatch = this.$watch('name', newName => {...})
this.unwatch() // 取消監聽
}
}
複製代碼
雖然watch
內部是使用this.$watch
,可是咱們也是能夠手動調用this.$watch
來建立監聽屬性的,因此第二個參數cb
會出現是對象的狀況。接下來設置一個標記位options.user
爲true
,代表這是一個user-watcher
。再給watch
設置了immediate
屬性後,會將實例化後獲得的值傳入回調,並當即執行一次回調函數,這也是immediate的實現原理。最後的返回值是一個方法,執行後能夠取消對該監聽屬性的監聽。接下來咱們看看user-watcher
是如何定義的:
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this) // 添加到當前實例的watchers內
if(options) {
this.deep = !!options.deep // 是否深度監聽
this.user = !!options.user // 是不是user-wathcer
this.sync = !!options.sync // 是否同步更新
}
this.active = true // // 派發更新的標誌位
this.cb = cb // 回調函數
if (typeof expOrFn === 'function') { // 若是expOrFn是函數
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 若是是字符串對象路徑形式,返回閉包函數
}
...
}
}
複製代碼
當是user-watcher
時,Watcher
內部是以上方式實例化的,一般狀況下咱們是使用字符串的形式建立監聽屬性,因此首先來看下parsePath
方法是幹什麼的:
const bailRE = /[^\w.$]/ // 得是對象路徑形式,如info.name
function parsePath (path) {
if (bailRE.test(path)) return // 不匹配對象路徑形式,再見
const segments = path.split('.') // 按照點分割爲數組
return function (obj) { // 閉包返回一個函數
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] // 依次讀取到實例下對象末端的值
}
return obj
}
}
複製代碼
parsePath
方法最終返回一個閉包方法,此時Watcher
類中的this.getter
就是一個函數了,再執行this.get()
方法時會將this.vm
傳入到閉包內,補全Watcher
其餘的邏輯:
class Watcher {
constructor(vm, expOrFn, cb, options) {
...
this.getter = parsePath(expOrFn) // 返回的方法
this.value = this.get() // 執行get
}
get() {
pushTarget(this) // 將當前user-watcher實例賦值給Dep.target,讀取時收集它
let value = this.getter.call(this.vm, this.vm) // 將vm實例傳給閉包,進行讀取操做
if (this.deep) { // 若是有定義deep屬性
traverse(value) // 進行深度監聽
}
popTarget()
return value // 返回閉包讀取到的值,參數immediate使用的就是這裏的值
}
...
}
複製代碼
由於以前初始化已經將狀態已經所有都代理到了this
下,因此讀取this
下的屬性便可,好比:
export default {
data() { // data的初始化先與watch
return {
info: {
name: 'cc'
}
}
},
created() {
this.$watch('info.name', newName => {...}) // 況且手動建立
}
}
複製代碼
首先讀取this
下的info
屬性,而後讀取info
下的name
屬性。你們注意,這裏咱們使用了讀取這個動詞,因此會執行以前包裝data
響應式數據的get
方法進行依賴收集,將依賴收集到讀取到的屬性的dep
裏,不過收集的是user-watcher
,get
方法最後返回閉包讀取到的值。
以後就是當info.name
屬性被從新賦值時,走派發更新的流程,咱們這裏把和render-watcher
不一樣之處作單獨的說明,派發更新會執行Watcher
內的update
方法內:
class Watcher {
constructor(vm, expOrFn, cb, options) {
...
}
update() { // 執行派發更新
if(this.sync) { // 若是有設置sync爲true
this.run() // 不走nextTick隊列,直接執行
} else {
queueWatcher(this) // 不然加入隊列,異步執行run()
}
}
run() {
if (this.active) {
this.getAndInvoke(this.cb) // 傳入回調函數
}
}
getAndInvoke(cb) {
const value = this.get() // 從新求值
if(value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value // 緩存以前的值
this.value = value // 新值
if(this.user) { // 若是是user-watcher
cb.call(this.vm, value, oldValue) // 在回調內傳入新值和舊值
}
}
}
}
複製代碼
其實這裏的sync
屬性已經沒在官網作說明了,不過咱們看到源碼中仍是保留了相關代碼。接下來咱們看到爲何watch
的回調內能夠獲得新值和舊值的原理,由於cb.call(this.vm, value, oldValue)
這句代碼的緣由,內部將新值和舊值傳給了回調函數。
watch監聽屬性示例:
<template>
<div>{{name}}</div>
</template>
export default { // App組件
data() {
return {
name: 'cc'
}
},
watch: {
name(newName, oldName) {...} // 派發新值和舊值給回調
},
mounted() {
setTimeout(() => {
this.name = 'ww' // 觸發name的set
}, 1000)
}
}
複製代碼
監聽屬性的
deep
深度監聽原理
以前的get
方法內有說明,若是有deep
屬性,則執行traverse
方法:
const seenObjects = new Set() // 不重複添加
function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val) // val是不是數組
if ((!isA && !isObject(val)) // 若是不是array和object
|| Object.isFrozen(val) // 或者是已經凍結對象
|| val instanceof VNode) { // 或者是VNode實例
return // 再見
}
if (val.__ob__) { // 只有object和array纔有__ob__屬性
const depId = val.__ob__.dep.id // 手動依賴收集器的id
if (seen.has(depId)) { // 已經有收集過
return // 再見
}
seen.add(depId) // 沒有被收集,添加
}
if (isA) { // 是array
i = val.length
while (i--) {
_traverse(val[i], seen) // 遞歸觸發每一項的get進行依賴收集
}
}
else { // 是object
keys = Object.keys(val)
i = keys.length
while (i--) {
_traverse(val[keys[i]], seen) // 遞歸觸發子屬性的get進行依賴收集
}
}
}
複製代碼
看着還挺複雜,簡單來講deep
的實現原理就是遞歸的觸發數組或對象的get
進行依賴收集,由於只有數組和對象纔有__ob__
屬性,也就是咱們第七章說明的手動依賴管理器,將它們的依賴收集到Observer
類裏的dep
內,完成deep
深度監聽。
watch
總結:這裏說明了爲何watch
和this.$watch
的實現是一致的,以及簡單解釋它的原理就是爲須要觀察的數據建立並收集user-watcher
,當數據改變時通知到user-watcher
將新值和舊值傳遞給用戶本身定義的回調函數。最後分析了定義watch
時會被使用到的三個參數:sync
、immediate
、deep
它們的實現原理。簡單說明它們的實現原理就是:sync
是不將watcher
加入到nextTick
隊列而同步的更新、immediate
是當即以獲得的值執行一次回調函數、deep
是遞歸的對它的子值進行依賴收集。
這個API
已經在第七章的最後作了具體分析,你們能夠前往this.$set實現原理查閱。
這個API
也已經在第七章的最後作了具體分析,你們能夠前往this.$delete實現原理查閱。
計算屬性不是API
,但它是Watcher
類的最後也是最複雜的一種實例化的使用,仍是頗有必要分析的。(vue
版本2.6.10)其實主要就是分析計算屬性爲什麼能夠作到當它的依賴項發生改變時纔會進行從新的計算,不然當前數據是被緩存的。計算屬性的值能夠是對象,這個對象須要傳入get
和set
方法,這種並不經常使用,因此這裏的分析仍是介紹經常使用的函數形式,它們之間是大同小異的,不過能夠減小認知負擔,聚焦核心原理實現。
export default {
computed: {
newName: { // 不分析這種了~
get() {...}, // 內部會採用get屬性爲計算屬性的值
set() {...}
}
}
}
複製代碼
計算屬性初始化
function initState(vm) { // 初始化全部狀態時
vm._watchers = [] // 當前實例watcher集合
const opts = vm.$options // 合併後的屬性
... // 其餘狀態初始化
if(opts.computed) { // 若是有定義計算屬性
initComputed(vm, opts.computed) // 進行初始化
}
...
}
---------------------------------------------------------------------------
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 建立一個純淨對象
for(const key in computed) {
const getter = computed[key] // computed每項對應的回調函數
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 實例化computed-watcher
...
}
}
複製代碼
計算屬性實現原理
這裏仍是按照慣例,將定義的computed
屬性的每一項使用Watcher
類進行實例化,不過這裏是按照computed-watcher
的形式,來看下如何實例化的:
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty爲標記位,表示是否對computed計算
this.getter = expOrFn // computed的回調函數
this.value = undefined
}
}
複製代碼
這裏就點到爲止,實例化已經結束了。並無和以前render-watcher
以及user-watcher
那般,執行get
方法,這是爲何?咱們接着分析爲什麼如此,補全以前初始化computed
的方法:
function initComputed(vm, computed) {
...
for(const key in computed) {
const getter = computed[key] // // computed每項對應的回調函數
...
if (!(key in vm)) {
defineComputed(vm, key, getter)
}
... key不能和data裏的屬性重名
... key不能和props裏的屬性重名
}
}
複製代碼
這裏的App
組件在執行extend
建立子組件的構造函數時,已經將key
掛載到vm
的原型中了,不過以前也是執行的defineComputed
方法,因此不妨礙咱們看它作了什麼:
function defineComputed(target, key) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: noop
})
}
複製代碼
這個方法的做用就是讓computed
成爲一個響應式數據,並定義它的get
屬性,也就是說當頁面執行渲染訪問到computed
時,纔會觸發get
而後執行createComputedGetter
方法,因此以前的點到爲止再這裏會續上,看下get
方法是怎麼定義的:
function createComputedGetter (key) { // 高階函數
return function () { // 返回函數
const watcher = this._computedWatchers && this._computedWatchers[key]
// 原來this還能夠這樣用,獲得key對應的computed-watcher
if (watcher) {
if (watcher.dirty) { // 在實例化watcher時爲true,表示須要計算
watcher.evaluate() // 進行計算屬性的求值
}
if (Dep.target) { // 當前的watcher,這裏是頁面渲染觸發的這個方法,因此爲render-watcher
watcher.depend() // 收集當前watcher
}
return watcher.value // 返回求到的值或以前緩存的值
}
}
}
------------------------------------------------------------------------------------
class Watcher {
...
evaluate () {
this.value = this.get() // 計算屬性求值
this.dirty = false // 表示計算屬性已經計算,不須要再計算
}
depend () {
let i = this.deps.length // deps內是計算屬性內能訪問到的響應式數據的dep的數組集合
while (i--) {
this.deps[i].depend() // 讓每一個dep收集當前的render-watcher
}
}
}
複製代碼
這裏的變量watcher
就是以前computed
對應的computed-watcher
實例,接下來會執行Watcher
類專門爲計算屬性定義的兩個方法,在執行evaluate
方法進行求值的過程當中又會觸發computed
內能夠訪問到的響應式數據的get
,它們會將當前的computed-watcher
做爲依賴收集到本身的dep
裏,計算完畢以後將dirty
置爲false
,表示已經計算過了。
而後執行depend
讓計算屬性內的響應式數據訂閱當前的render-watcher
,因此computed
內的響應式數據會收集computed-watcher
和render-watcher
兩個watcher
,當computed
內的狀態發生變動觸發set
後,首先通知computed
須要進行從新計算,而後通知到視圖執行渲染,再渲染中會訪問到computed
計算後的值,最後渲染到頁面。
Ps: 計算屬性內的值須是響應式數據才能觸發從新計算。
當computed
內的響應式數據變動後觸發的通知:
class Watcher {
...
update() { // 當computed內的響應式數據觸發set後
if(this.lazy) {
this.diray = true // 通知computed須要從新計算了
}
...
}
}
複製代碼
最後仍是以一個示例結合流程圖來幫你們理清楚這裏的邏輯:
export default {
data() {
return {
manName: "cc",
womanName: "ww"
};
},
computed: {
newName() {
return this.manName + ":" + this.womanName;
}
},
methods: {
changeName() {
this.manName = "ss";
}
}
};
複製代碼
watch
總結:爲何計算屬性有緩存功能?由於當計算屬性通過計算後,內部的標誌位會代表已經計算過了,再次訪問時會直接讀取計算後的值;爲何計算屬性內的響應式數據發生變動後,計算屬性會從新計算?由於內部的響應式數據會收集computed-watcher
,變動後通知計算屬性要進行計算,也會通知頁面從新渲染,渲染時會讀取到從新計算後的值。
最後按照慣例咱們仍是以一道vue
可能會被問到的面試題做爲本章的結束~
面試官微笑而又不失禮貌的問道:
computed
屬性和watch
屬性分別什麼場景使用?懟回去:
順手點個贊或關注唄,找起來也方便~