一、版本:2.5.9
二、建議
vue最重要的應該就是響應式更新了,剛開始接觸vue或多或少都能從官方文件或者其他地方知道vue響應式更新依賴於Object.defineProperty()
方法,這個方法在MDN上有詳細講解,不過,如果是初學者的話,直接去看響應式更新原始碼還有點難度的,最好是先用專案練一遍,對vue有個相對熟悉的瞭解,然後可以去各大熱門講解的部落格上看看人家的講解,這樣彙總一番有點底子了再去看原始碼實現相對輕鬆點。
最低階別的監聽可以看我這個庫:https://github.com/lizhongzhen11/obj
參考:https://segmentfault.com/a/1190000009054946
https://segmentfault.com/a/1190000004384515
三、閱讀
從github上把vueclone下來,或者直接在github上看也行。
別的先不管,直接去src/core/observer資料夾,這個明顯就是vue響應式更新原始碼精華所在,內部共有array.js
,dep.js
,index.js
,scheduler.js
,traverse.js
,watcher.js
6個檔案,先看哪一個呢?第一次看沒有頭緒的話就先看index.js
。
index.js
開頭import
了不少檔案,先不用管,往下看需要用到時再去查詢不遲。而第一步就用到了arrayMethods
,該物件來自array.js
,下面同時列出array.js
中的相關程式碼:
// index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
複製程式碼
如上所示,arrayMethods
其實是一個Array.prototype
的例項,只不過中間經過arrayProto
過渡,一開始我還在糾結下方的程式碼(對陣列push
等方法遍歷新增到剛剛建立的例項arrayMethods
中,這裡沒有列出來),因為沒看到下方程式碼有export
,感覺很奇怪,而且他程式碼是下面這樣的,[]
前有個;
,感覺很奇怪,vue作者是不寫;
的,這裡出現一個;
感覺很突兀。PS:後來問了前輩,前輩解釋說:在js檔案合併的時候,防止前一個js檔案沒有;
結尾導致的錯誤
;['push','pop','shift','unshift','splice','sort','reverse']
複製程式碼
接下來,go on!定義了一個“觀察狀態”變數,內部有一個是否可以覆蓋的布林屬性。註釋裡面說不想強制覆蓋凍結資料結構下的巢狀值,以避免優化失敗。
export const observerState = {
shouldConvert: true
}
複製程式碼
繼續往下看,來到了重頭戲:Observer
類,註釋中也說的明白:該類屬於每個被觀察的物件,observer
在目標物件的屬性的getter/setters
覆蓋鍵同時蒐集依賴以及分發更新。
import Dep from './dep'
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
建構函式裡面第二步this.dep = new Dep()
,這個Dep
來自dep.js
,這時候,得需要去看看dep.js
裡面相關的程式碼了:
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
* dep是可觀察的,可以有多個指令訂閱它
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 更新 Watcher 陣列中的資料
}
}
}
複製程式碼
Dep
內部用到了Watcher
,而Watcher
又來自watcher.js
。先說Dep
,內部主要對Watcher
型別的陣列進行增加刪除以及更新維護,自己內部沒有什麼太多複雜的邏輯,主要還是在watcher.js
中。接下來列出watcher.js
相關程式碼:
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
// 先看建構函式,內部變數不列出來了,太多了
constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this // 直接在vue 頁面裡列印 this 可以找到_watcher屬性
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep // 這裡可能是怕萬一 options 物件裡沒有 deep 等屬性,所以用了 !! 來強轉成布林型
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} 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 = []
this.depIds = new Set() // es6語法,類似java Set集合,不會新增重複資料
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
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()
}
複製程式碼
上面建構函式第一個引數vm
是什麼?如果一直用vue-cli
構建工具開發的話,可能沒怎麼注意過,**其實vm
就是vue
的一個例項!!!**第二個引數expOrFn
暫時還不清楚,如果是函式的話直接賦給this.getter
,否則this.getter
直接指向一個空函式,同時還發出警報,需要傳遞一個函式。最後,判斷this.lazy
,為true
的話呼叫this.get()
方法:
import Dep, { pushTarget, popTarget } from './dep'
/**
* Evaluate the getter, and re-collect dependencies.
* 對 getter 求值,並重新收集依賴
*/
get () {
pushTarget(this) // 相當於 Dep.target = this,
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps() // 清理deps,為了依賴收集
}
return value
}
// dep.js
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
複製程式碼
get()
中最終會判斷cthis.deep
是否為true
,如果是呼叫traverse(value)
,而traverse()
來自traverse.js
,其目的是把dep.id
加進去;popTarget()
是為了將之前pushTarget(this)
的target
移除。
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear() // newDepIds 是Set型別,可以通過clear()清空
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
複製程式碼
cleanupDeps()
方法將舊的依賴編號與新的依賴集合編號進行對比,如果舊依賴陣列中存在的編號,而新依賴集合編號中不存在,就需要刪除對應編號的依賴;接下來交換新舊依賴集合編號,然後清空this.newDepIds
(其實此時該集合內儲存的是舊有的依賴集合編號);隨後交換新舊依賴陣列,然後來了一步騷操作:this.newDeps.length = 0
,將this.newDeps
清空,比較騷。
也就是說,利用
get()
方法求值後會清理依賴收集。 到了get()
可以先暫停回顧一下。這裡是在Watcher
建構函式中呼叫的,也就是說,當new Watcher()
時就會走遍上述程式碼,包括呼叫get()
來取值。
這時候如果繼續強行看完Watcher
下面的原始碼,會發現沒什麼頭緒,所以依然回到index.js
中。繼續研究Observer
類的建構函式。
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
複製程式碼
建構函式中緊跟著呼叫了def(value, '__ob__', this)
,這個方法是幹嘛的?在哪裡?
通過查詢發現def
方法位於util/lang.js
內,下面貼出原始碼:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
複製程式碼
def
內部呼叫了Object.defineProperty()
,結合Observer
建構函式的傳參,可知這裡給每個物件定義了一個__ob__
屬性,在日常開發中,當我們列印輸出時經常能看到__ob__
。 接下來進一步判斷value
是不是陣列,如果不是的話呼叫walk()
,當然要確保引數是Object
,然後遍歷物件的key
並且每個呼叫defineReactive(obj, keys[i], obj[keys[i]])
。
看看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 // 快取物件屬性內的get方法
const setter = property && property.set // 快取物件屬性內的set方法
let childOb = !shallow && observe(val) // observe(val)嘗試返回一個 observer例項,如果 !shallow === true 那麼 childOb === ob
// 其實也可以理解為, childOb === val.__ob__
Object.defineProperty(obj, key, { // 這裡開始是真正的核心所在,其實就是重新物件的get、set方法,方便監聽
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // getter 存在的話就呼叫原生的 get 方法取值,否則用傳進來的值
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // childOb === newVal.__ob__
dep.notify() // 內部呼叫了 watcher.js 裡面的 uodate(),內部又呼叫了 run(),run()裡面設定值,其中還用到了watcher佇列
}
})
}
複製程式碼
響應式更新的重中之重就是首先得監聽到物件屬性值的改變,
vue
通過defineReactive()
內部重寫傳入的物件屬性中的set
以及get
方法,其中,js
原生的call()
也有很大的功勞。
總結
再一次看vue
原始碼明顯比第一次看好多了,但是不斷地呼叫其它方法,理解上還是有一定的難度,這一次閱讀原始碼更多的就是做個筆記,寫得並不好,但是留個印象,方便下次再看。