最近一段時間在閱讀Vue源碼缨伊,從它的核心原理入手蜂大,開始了源碼的學習婴程,而其核心原理就是其數(shù)據(jù)的響應式鲤氢,講到Vue的響應式原理飘痛,我們可以從它的兼容性說起晚胡,Vue不支持IE8以下版本的瀏覽器灵奖,因為Vue是基于 [Object.defineProperty](https://funteas.com/go/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FObject%2FdefineProperty) 來實現(xiàn)數(shù)據(jù)響應的,而 Object.defineProperty 是 ES5 中一個無法 shim 的特性估盘,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器的原因瓷患;Vue通過Object.defineProperty的 **getter/setter** 對收集的依賴項進行監(jiān)聽,在屬性被訪問和修改時通知變化,進而更新視圖數(shù)據(jù);
受現(xiàn)代JavaScript 的限制 (以及廢棄 Object.observe)遣妥,Vue不能檢測到對象屬性的添加或刪除擅编。由于 Vue 會在初始化實例時對屬性執(zhí)行 getter/setter 轉(zhuǎn)化過程,所以屬性必須在 data 對象上存在才能讓Vue轉(zhuǎn)換它,這樣才能讓它是響應的爱态。 <a id=“more”></a>
我們這里是根據(jù)Vue2.3源碼進行分析,Vue數(shù)據(jù)響應式變化主要涉及 Observer, Watcher , Dep 這三個主要的類谭贪;因此要弄清Vue響應式變化需要明白這個三個類之間是如何運作聯(lián)系的;以及它們的原理锦担,負責的邏輯操作俭识。那么我們從一個簡單的Vue實例的代碼來分析Vue的響應式原理
var vue = new Vue({
el: "#app",
data: {
name: 'Junga'
},
created () {
this.helloWorld()
},
methods: {
helloWorld: function() {
console.log('my name is' + this.name)
}
}
...
})
# [](https://funteas.com/go/?target=%23Vue%25E5%2588%259D%25E5%25A7%258B%25E5%258C%2596%25E5%25AE%259E%25E4%25BE%258B "Vue初始化實例")Vue初始化實例
根據(jù)Vue的[生命周期](https://funteas.com/go/?target=https%3A%2F%2Fcn.vuejs.org%2Fv2%2Fguide%2Finstance.html%23%E5%AE%9E%E4%BE%8B%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90)我們知道,Vue首先會進行init初始化操作洞渔;源碼在[src/core/instance/init.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Finstance%2Finit.js)中
/初始化生命周期/
initLifecycle(vm)
/初始化事件/
initEvents(vm)Object.defineProperty
/初始化render/
initRender(vm)
/調(diào)用beforeCreate鉤子函數(shù)并且觸發(fā)beforeCreate鉤子事件/
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/初始化props套媚、methods、data痘煤、computed與watch/
initState(vm)
initProvide(vm) // resolve provide after data/props
/調(diào)用created鉤子函數(shù)并且觸發(fā)created鉤子事件/
callHook(vm, 'created')
以上代碼可以看到 **initState(vm)** 是用來初始化props,methods,data,computed和watch;
[src/core/instance/state.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Finstance%2Fstate.js)
/初始化props、methods猿规、data衷快、computed與watch/
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}defi
...
//遍歷data中的數(shù)據(jù)
while (i--) {
/保證data中的key不與props中的key重復,props優(yōu)先姨俩,如果有沖突會產(chǎn)生warning/
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
The data property "${keys[i]}" is already declared as a prop.
+
Use prop default value instead.
,
vm
)
} else if (!isReserved(keys[i])) {
/判斷是否是保留字段/
/這里是我們前面講過的代理蘸拔,將data上面的屬性代理到了vm實例上/
proxy(vm, _data
, keys[i])
}
}
// observe data
/這里通過observe實例化Observe對象,開始對數(shù)據(jù)進行綁定环葵,asRootData用來根數(shù)據(jù)调窍,用來計算實例化根數(shù)據(jù)的個數(shù),下面會進行遞歸observe進行對深層對象的綁定张遭。則asRootData為非true/
observe(data, true /* asRootData */)
}
## [](https://funteas.com/go/?target=%231%25E3%2580%2581initData "1邓萨、initData")1、initData
現(xiàn)在我們重點分析下**initData**菊卷,這里主要做了兩件事缔恳,一是將_data上面的數(shù)據(jù)代理到vm上,二是通過執(zhí)行 observe(data, true / *asRootData* /)將所有data變成可觀察的洁闰,即對data定義的每個屬性進行g(shù)etter/setter操作歉甚,這里就是Vue實現(xiàn)響應式的基礎;**observe**的實現(xiàn)如下 [src/core/observer/index.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Fobserver%2Findex.js)
/嘗試創(chuàng)建一個Observer實例(ob)扑眉,如果成功創(chuàng)建Observer實例則返回新的Observer實例纸泄,如果已有Observer實例則返回現(xiàn)有的Observer實例。/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
/這里用ob這個屬性來判斷是否已經(jīng)有Observer實例腰素,如果沒有Observer實例則會新建一個Observer實例并賦值給ob這個屬性聘裁,如果已有Observer實例則直接返回該Observer實例,這里可以看Observer實例化的代碼def(value, 'ob', this)/
if (hasOwn(value, 'ob') && value.ob instanceof Observer) {
ob = value.ob
} else if (
/這里的判斷是為了確保value是單純的對象弓千,而不是函數(shù)或者是Regexp等情況咧虎。而且該對象在shouldConvert的時候才會進行Observer。這是一個標識位计呈,避免重復對value進行Observer
/
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
/如果是根數(shù)據(jù)則計數(shù)砰诵,后面Observer中的observe的asRootData非true/
ob.vmCount++
}
return ob
}
這里 **new Observer(value)** 就是實現(xiàn)響應式的核心方法之一了征唬,通過它將data轉(zhuǎn)變可以成觀察的,而這里正是我們開頭說的茁彭,用了 **Object.defineProperty** 實現(xiàn)了data的 **getter/setter** 操作总寒,通過 **Watcher** 來觀察數(shù)據(jù)的變化,進而更新到視圖中理肺。
## [](https://funteas.com/go/?target=%232%25E3%2580%2581Observer "2摄闸、Observer")2、Observer
Observer類是將每個目標對象(即data)的鍵值轉(zhuǎn)換成getter/setter形式妹萨,用于進行依賴收集以及調(diào)度更新年枕。
[src/core/observer/index.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Fobserver%2Findex.js)
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
/* 將Observer實例綁定到data的ob屬性上面去,之前說過observe的時候會先檢測是否已經(jīng)有ob對象存放Observer實例了乎完,def方法定義可以參考/src/core/util/lang.js/
def(value, 'ob', this)
if (Array.isArray(value)) {
/如果是數(shù)組熏兄,將修改后可以截獲響應的數(shù)組方法替換掉該數(shù)組的原型中的原生方法,達到監(jiān)聽數(shù)組數(shù)據(jù)變化響應的效果树姨。這里如果當前瀏覽器支持proto屬性摩桶,則直接覆蓋當前數(shù)組對象原型上的原生數(shù)組方法,如果不支持該屬性帽揪,則直接覆蓋數(shù)組對象的原型硝清。/
const augment = hasProto
? protoAugment /直接覆蓋原型的方法來修改目標對象/
: copyAugment /定義(覆蓋)目標對象或數(shù)組的某一個方法/
augment(value, arrayMethods, arrayKeys)
/如果是數(shù)組則需要遍歷數(shù)組的每一個成員進行observe/
this.observeArray(value)
} else {
/如果是對象則直接walk進行綁定/
this.walk(value)
},
walk (obj: Object) {
const keys = Object.keys(obj)
/walk方法會遍歷對象的每一個屬性進行defineReactive綁定*/
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
1. 首先將Observer實例綁定到data的**ob**屬性上面去,防止重復綁定转晰;
2. 若data為數(shù)組芦拿,先實現(xiàn)對應的[變異方法](https://funteas.com/go/?target=https%3A%2F%2Fcn.vuejs.org%2Fv2%2Fguide%2Flist.html%23%E5%8F%98%E5%BC%82%E6%96%B9%E6%B3%95)(這里變異方法是指Vue重寫了數(shù)組的7種原生方法,這里不做贅述查邢,后續(xù)再說明)防嗡,再將數(shù)組的每個成員進行observe,使之成響應式數(shù)據(jù)侠坎;
3. 否則執(zhí)行walk()方法蚁趁,遍歷data所有的數(shù)據(jù),進行g(shù)etter/setter綁定实胸,這里的核心方法就是 **defineReative(obj, keys[i], obj[keys[i]])**
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/在閉包中定義一個dep對象/
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/如果之前該對象已經(jīng)預設了getter以及setter函數(shù)則將其取出來他嫡,新定義的getter/setter中會將其執(zhí)行,保證不會覆蓋之前已經(jīng)定義的getter/setter庐完。/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/對象的子對象遞歸進行observe并返回子節(jié)點的Observer對象/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/如果原本對象擁有g(shù)etter方法則執(zhí)行/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/進行依賴收集/
dep.depend()
if (childOb) {
/子對象進行依賴收集钢属,其實就是將同一個watcher觀察者實例放進了兩個depend中,一個是正在本身閉包中的depend门躯,另一個是子元素的depend/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/是數(shù)組則需要對每一個成員都進行依賴收集淆党,如果數(shù)組的成員還是數(shù)組,則遞歸。/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/通過getter方法獲取當前值染乌,與新值進行比較山孔,一致則不需要執(zhí)行下面的操作/
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方法則執(zhí)行setter/
setter.call(obj, newVal)
} else {
val = newVal
}
/新的值需要重新進行observe,保證數(shù)據(jù)響應式/
childOb = observe(newVal)
/dep對象通知所有的觀察者*/
dep.notify()
}
})
}
其中g(shù)etter方法:
1. 先為每個data聲明一個 **Dep** 實例對象荷憋,被用于getter時執(zhí)行dep.depend()進行收集相關的依賴;
2. 根據(jù)Dep.target來判斷是否收集依賴台颠,還是普通取值。Dep.target是在什么時候勒庄,如何收集的后面再說明串前,先簡單了解它的作用,
那么問題來了实蔽,我們?yōu)樯兑占嚓P依賴呢荡碾?
new Vue({
template:
<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>
,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
我們可以從以上代碼看出,data中text3并沒有被模板實際用到局装,為了提高代碼執(zhí)行效率坛吁,我們沒有必要對其進行響應式處理,因此贼邓,依賴收集簡單點理解就是收集只在實際頁面中用到的data數(shù)據(jù)阶冈,然后打上標記闷尿,這里就是標記為Dep.target塑径。
在setter方法中:
1. 獲取新的值并且進行observe,保證數(shù)據(jù)響應式填具;
2. 通過dep對象通知所有觀察者去更新數(shù)據(jù)统舀,從而達到響應式效果。
在Observer類中劳景,我們可以看到在getter時誉简,dep會收集相關依賴,即收集依賴的watcher盟广,然后在setter操作時候通過dep去通知watcher,此時watcher就執(zhí)行變化闷串,我們用一張圖描述這三者之間的關系: 
從圖我們可以簡單理解:Dep可以看做是書店,Watcher就是書店訂閱者筋量,而Observer就是書店的書烹吵,訂閱者在書店訂閱書籍,就可以添加訂閱者信息桨武,一旦有新書就會通過書店給訂閱者發(fā)送消息肋拔。
## [](https://funteas.com/go/?target=%233%25E3%2580%2581Watcher "3、Watcher")3呀酸、Watcher
Watcher是一個觀察者對象凉蜂。依賴收集以后Watcher對象會被保存在Dep的subs中,數(shù)據(jù)變動的時候Dep會通知Watcher實例,然后由Watcher實例回調(diào)cb進行視圖的更新窿吩。
[src/core/observer/watcher.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Fobserver%2Fwatcher.js)
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
/_watchers存放訂閱者實例/
vm._watchers.push(this)
// options
if (options) {
this.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()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
/把表達式expOrFn解析成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()
}
/**
- Evaluate the getter, and re-collect dependencies.
/
/獲得getter的值并且重新進行依賴收集/
get () {
/將自身watcher觀察者實例設置給Dep.target茎杂,用以依賴收集。/
pushTarget(this)
let value
const vm = this.vm
/執(zhí)行了getter操作爆存,看似執(zhí)行了渲染操作蛉顽,其實是執(zhí)行了依賴收集。
在將Dep.target設置為自生觀察者實例以后先较,執(zhí)行g(shù)etter操作携冤。
譬如說現(xiàn)在的的data中可能有a、b闲勺、c三個數(shù)據(jù)曾棕,getter渲染需要依賴a跟c,
那么在執(zhí)行g(shù)etter的時候就會觸發(fā)a跟c兩個數(shù)據(jù)的getter函數(shù)菜循,
在getter函數(shù)中即可判斷Dep.target是否存在然后完成依賴收集翘地,
將該觀察者對象放入閉包中的Dep的subs中去。/
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm,getter for watcher "${this.expression}"
)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
/如果存在deep癌幕,則觸發(fā)每個深層對象的依賴衙耕,追蹤其變化/
if (this.deep) {
/遞歸每一個對象或者數(shù)組,觸發(fā)它們的getter勺远,使得對象或數(shù)組的每一個成員都被依賴收集橙喘,形成一個“深(deep)”依賴關系/
traverse(value)
}
/將觀察者實例從target棧中取出并設置給Dep.target/
popTarget()
this.cleanupDeps()
return value
}
/* - Add a dependency to this directive.
/
/添加一個依賴關系到Deps集合中/
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)
}
}
}
/* - Clean up for dependency collection.
/
/清理依賴收集/
cleanupDeps () {
/移除所有觀察者對象/
...
}
/* - Subscriber interface.
- Will be called when a dependency changes.
/
/
調(diào)度者接口,當依賴發(fā)生改變的時候進行回調(diào)胶逢。
/
update () {
/ istanbul ignore else /
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/同步則執(zhí)行run直接渲染視圖/
this.run()
} else {
/異步推送到觀察者隊列中厅瞎,下一個tick時調(diào)用。/
queueWatcher(this)
}
}
/* - Scheduler job interface.
- Will be called by the scheduler.
/
/
調(diào)度者工作接口初坠,將被調(diào)度者回調(diào)和簸。
/
run () {
if (this.active) {
/ get操作在獲取value本身也會執(zhí)行g(shù)etter從而調(diào)用update更新視圖 /
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
/
即便值相同,擁有Deep屬性的觀察者以及在對象/數(shù)組上的觀察者應該被觸發(fā)更新碟刺,因為它們的值可能發(fā)生改變锁保。
/
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
/設置新的值/
this.value = value
/觸發(fā)回調(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)
}
}
}
}
/* - Evaluate the value of the watcher.
- This only gets called for lazy watchers.
/
/獲取觀察者的值/
evaluate () {
this.value = this.get()
this.dirty = false
}
/* - Depend on all deps collected by this watcher.
/
/收集該watcher的所有deps依賴/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/* - Remove self from all dependencies' subscriber list.
/
/將自身從所有依賴收集訂閱列表刪除*/
teardown () {
...
}
}
## [](https://funteas.com/go/?target=%234%25E3%2580%2581Dep "4、Dep")4半沽、Dep
被Observer的data在觸發(fā) **getter** 時爽柒,**Dep** 就會收集依賴的 **Watcher** ,其實 **Dep** 就像剛才說的是一個書店抄囚,可以接受多個訂閱者的訂閱霉赡,當有新書時即在data變動時,就會通過 **Dep** 給 **Watcher** 發(fā)通知進行更新幔托。
[src/core/observer/dep.js](https://funteas.com/go/?target=https%3A%2F%2Fgithub.com%2Fhuangzhuangjia%2FVue-learn%2Fblob%2Fmaster%2Fcore%2Fobserver%2Fdep.js)
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)
}
/依賴收集穴亏,當存在Dep.target的時候添加觀察者對象/
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()
}
}
}
# [](https://funteas.com/go/?target=%23%25E6%2580%25BB%25E7%25BB%2593 "總結(jié)")總結(jié)
其實在 **Vue** 中初始化渲染時蜂挪,視圖上綁定的數(shù)據(jù)就會實例化一個 **Watcher**,依賴收集就是是通過屬性的 **getter** 函數(shù)完成的嗓化,文章一開始講到的 **Observer** 棠涮、**Watcher** 、**Dep** 都與依賴收集相關刺覆。其中 **Observer** 與 **Dep** 是一對一的關系严肪, **Dep** 與 **Watcher** 是多對多的關系,**Dep** 則是 **Observer** 和 **Watcher** 之間的紐帶谦屑。依賴收集完成后驳糯,當屬性變化會執(zhí)行被 **Observer** 對象的 **dep.notify()** 方法,這個方法會遍歷訂閱者(Watcher)列表向其發(fā)送消息氢橙, **Watcher** 會執(zhí)行 **run** 方法去更新視圖酝枢,我們再來看一張圖總結(jié)一下: 
1. 在 **Vue** 中模板編譯過程中的指令或者數(shù)據(jù)綁定都會實例化一個 **Watcher** 實例,實例化過程中會觸發(fā) **get()**將自身指向 **Dep.target**;
2. data在 **Observer** 時執(zhí)行 **getter** 會觸發(fā) **dep.depend()** 進行依賴收集;依賴收集的結(jié)果:1悍手、data在 **Observer** 時閉包的dep實例的subs添加觀察它的 **Watcher** 實例帘睦;2. **Watcher** 的deps中添加觀察對象 **Observer** 時的閉包dep;
3. 當data中被 **Observer** 的某個對象值變化后坦康,觸發(fā)subs中觀察它的watcher執(zhí)行 **update()** 方法竣付,最后實際上是調(diào)用watcher的回調(diào)函數(shù)cb,進而更新視圖滞欠。
# [](https://funteas.com/go/?target=%23%25E5%258F%2582%25E8%2580%2583 "參考")參考
</article>
[Vue](https://funteas.com/tag/Vue)
[收藏](https://funteas.com/topic/5a809f5847dc830a0e4690c2#myModal)