當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。Object.defineProperty 是 ES5 中一個沒法 shim 的特性,這也就是爲何 Vue 不支持 IE8 以及更低版本瀏覽器。
上面那段話是Vue官方文檔中截取的,能夠看到是使用Object.defineProperty
實現對數據改變的監聽。Vue主要使用了觀察者模式來實現數據與視圖的雙向綁定。
javascript
function initData(vm) { //將data上數據複製到_data並遍歷全部屬性添加代理
vm._data = vm.$options.data;
const keys = Object.keys(vm._data);
let i = keys.length;
while(i--) {
const key = keys[i];
proxy(vm, `_data`, key);
}
observe(data, true /* asRootData */) //對data進行監聽
}
複製代碼
在第一篇數據初始化中,執行new Vue()操做後會執行initData()去初始化用戶傳入的data,最後一步操做就是爲data添加響應式。
html
在Vue內部存在三個對象:Observer、Dep、Watcher,這也是實現響應式的核心。
vue
Observer對象將data中全部的屬性轉爲getter/setter形式,如下是簡化版代碼,詳細代碼請看這裏。
java
export function observe (value) {
//遞歸子屬性時的判斷
if (!isObject(value) || value instanceof VNode) {
return
}
...
ob = new Observer(value)
}
export class Observer {
constructor (value) {
... //此處省略對數組的處理
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) //爲每一個屬性建立setter/getter
}
}
...
}
//設置set/get
export function defineReactive ( obj: Object, key: string, val: any ) {
//利用閉包存儲每一個屬性關聯的watcher隊列,當setter觸發時依然能訪問到
const dep = new Dep()
...
//若是屬性爲對象也建立相應observer
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
if (Dep.target) {
dep.depend() //將當前dep傳到對應watcher中再執行watcher.addDep將watcher添加到當前dep.subs中
if (childOb) { //若是屬性是對象則繼續收集依賴
childOb.dep.depend()
...
}
}
return value
},
set: function reactiveSetter (newVal) {
...
childOb = observe(newVal) //若是設置的新值是對象,則爲其建立observe
dep.notify() //通知隊列中的watcher進行更新
}
})
}
複製代碼
建立Observer
對象時,爲data的每一個屬性都執行了一遍defineReactive
方法,若是當前屬性爲對象,則經過遞歸進行深度遍歷。該方法中建立了一個Dep
實例,每個屬性都有一個與之對應的dep
,存儲全部的依賴。而後爲屬性設置setter/getter,在getter時收集依賴,setter時派發更新。這裏收集依賴不直接使用addSub是爲了能讓Watcher建立時自動將本身添加到dep.subs
中,這樣只有當數據被訪問時纔會進行依賴收集,能夠避免一些沒必要要的依賴收集。
react
Dep
就是一個發佈者,負責收集依賴,當數據更新是去通知訂閱者(watcher)。源碼地址
git
export default class Dep {
static target: ?Watcher; //指向當前watcher
constructor () {
this.subs = []
}
//添加watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
//移除watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//經過watcher將自身添加到dep中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//派發更新信息
notify () {
...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製代碼
源碼地址
github
//解析表達式(a.b),返回一個函數
export function parsePath (path: string): any {
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
}
}
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
//對建立的watcher進行收集,destroy時對這些watcher進行銷燬
vm._watchers.push(this)
// options
if (options) {
...
this.before = options.before
}
...
//上一輪收集的依賴集合Dep以及對應的id
this.deps = []
this.depIds = new Set()
//新收集的依賴集合Dep以及對應的id
this.newDeps = []
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)
...
}
...
this.value = this.get()
}
/** * Evaluate the getter, and re-collect dependencies. */
get () {
pushTarget(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() //清空上一輪的依賴
}
return value
}
/** * Add a dependency to this directive. */
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
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()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
},
update () {
...
//通過一些優化處理後,最終執行this.get
this.get();
}
// ...
}
複製代碼
依賴收集的觸發是在執行render以前,會建立一個渲染Watcher:
express
updateComponent = () => {
vm._update(vm._render(), hydrating) //執行render生成VNode並更新dom
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製代碼
在渲染Watcher
建立時會將Dep.target
指向自身並觸發updateComponent
也就是執行_render
生成VNode
並執行_update
將VNode
渲染成真實DOM,在render過程當中會對模板進行編譯,此時就會對data進行訪問從而觸發getter,因爲此時Dep.target
已經指向了渲染Watcher
,接着渲染Watcher
會執行自身的addDep
,作一些去重判斷而後執行dep.addSub(this)
將自身push到屬性對應的dep.subs
中,同一個屬性只會被添加一次,表示數據在當前Watcher
中被引用。數組
當_render結束後,會執行popTarget()
,將當前Dep.target
回退到上一輪的指,最終又回到了null,也就是全部收集已完畢。以後執行cleanupDeps()
將上一輪不須要的依賴清除。當數據變化是,觸發setter,執行對應Watcher
的update屬性,去執行get方法又從新將Dep.target
指向當前執行的Watcher觸發該Watcher的更新。瀏覽器
這裏能夠看到有deps,newDeps
兩個依賴表,也就是上一輪的依賴和最新的依賴,這兩個依賴表主要是用來作依賴清除的。但在addDep
中能夠看到if (!this.newDepIds.has(id))
已經對收集的依賴進行了惟一性判斷,不收集重複的數據依賴。爲什麼又要在cleanupDeps
中再做一次判斷呢?
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()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
複製代碼
在cleanupDeps
中主要清除上一輪中的依賴在新一輪中沒有從新收集的,也就是數據刷新後某些數據再也不被渲染出來了,例如:
<body> <div id="app"> <div v-if='flag'> </div> <div v-else> </div> <button @click="msg1 += '1'">change</button> <button @click="flag = !flag">toggle</button> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { flag: true, msg1: 'msg1', msg2: 'msg2' } }) </script> </body> 複製代碼
每次點擊change
,msg1都會拼接一個1,此時就會觸發從新渲染。當咱們點擊toggle時,因爲flag改變,msg1再也不被渲染,但當咱們點擊change時,msg1發生了變化,但卻沒有觸發從新渲染,這就是cleanupDeps
起的做用。若是去除掉cleanupDeps
這個步驟,只是能防止添加相同的依賴,可是數據每次更新都會觸發從新渲染,又去從新收集依賴。這個例子中,toggle後,從新收集的依賴中並無msg1,由於它不須要被顯示,可是因爲設置了setter,此時去改變msg1依然會觸發setter,若是沒有執行cleanupDeps
,那麼msg1的依賴依然存在依賴表裏,又會去觸發從新渲染,這是不合理的,因此須要每次依賴收集完畢後清除掉一些不須要的依賴。
依賴收集其實就是收集每一個數據被哪些Watcher
(渲染Watcher、computedWatcher等)所引用,當這些數據更新時,就去通知依賴它的Watcher
去更新。