這是十篇 Vue 系列文章的第三篇,這篇文章咱們講講 Vue 最核心的功能之一 —— 響應式原理。前端
能夠這樣理解:當一個狀態改變以後,與這個狀態相關的事務也當即隨之改變,從前端來看就是數據狀態改變後相關 DOM 也隨之改變。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。react
咱們先看看咱們在 Vue 中常見的寫法:ios
<div id="app" @click="changeNum">
{{ num }}
</div>
var app = new Vue({
el: '#app',
data: {
num: 1
},
methods: {
changeNum() {
this.num = 2
}
}
})
複製代碼
這種寫法很常見,不過你考慮過當爲何執行
this.num = 2
後視圖爲何會更新呢?經過這篇文章我力爭把這個點講清楚。npm
個人第一想法是像下面這樣實現:數組
let data = {
num: 1
};
Object.defineProperty(data, 'num',{
set: function( newVal ){
document.getElementById('app').value = newVal;
}
});
input.addEventListener('input', function(){
data.num = 2;
});
複製代碼
這樣能夠粗略的實現點擊元素,自動更新視圖。bash
這裏咱們須要經過 Object.defineProperty 來操做對象的訪問器屬性。監聽到數據變化的時候,操做相關 DOM。app
而這裏用到了一個常見模式 —— 發佈/訂閱模式。異步
我畫了一個大概的流程圖,用來講明觀察者模式和發佈/訂閱模式。以下:函數
仔細的同窗會發現,我這個粗略的過程和使用 Vue 的不一樣的地方就是須要我本身操做 DOM 從新渲染。post
若是咱們使用 Vue 的話,這一步就是 Vue 內部的代碼來處理的。這也是咱們爲何在使用 Vue 的時候無需手動操做 DOM 的緣由。
關於 Object.defineProperty
我在上一篇文章已經說起,這裏就再也不復述。
咱們知道對象能夠經過 Object.defineProperty
操做其訪問器屬性,即對象擁有了 getter
和 setter
方法。這就是實現響應式的基石。
先看一張很直觀的流程圖:
在 Vue 的初始化的時候,其 _init()
方法會調用執行 initState(vm)
方法。initState
方法主要是對 props
、methods
、data
、computed
和 wathcer
等屬性作了初始化操做。
這裏咱們就對 data
初始化的過程作一個比較詳細的分析。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
......
}
// proxy data on instance
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 (props && hasOwn(props, key)) {
......
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
複製代碼
initData
初始化 data 的主要過程也是作兩件事:
proxy
把每個值 vm._data.[key]
都代理到 vm.[key]
上;observe
方法觀測整個 data 的變化,把 data 也變成響應式(可觀察),能夠經過 vm._data.[key]
訪問到定義 data 返回函數中對應的屬性。Observe
經過這個方法將 data 下面的全部屬性變成響應式(可觀察)。
// 給對象的屬性添加 getter 和 setter,用於依賴收集和發佈更新
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
// 實例化 Dep 對象
this.dep = new Dep()
this.vmCount = 0
// 把自身實例添加到數據對象 value 的 __ob__ 屬性上
def(value, '__ob__', this)
// value 是否爲數組的不一樣調用
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 取出全部屬性遍歷
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製代碼
def
函數內封裝了 Object.defineProperty
,因此你 console.log(data) ,會發現多了一個 __ob__
的屬性。
// 定義一個響應式對象的具體實現
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
..... // 省略部分兼容代碼,但不影響理解
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
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
..... // 省略部分兼容代碼,但不影響理解
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 對新的值進行監聽
childOb = !shallow && observe(newVal)
// 通知全部訂閱者,內部調用 watcher 的 update 方法
dep.notify()
}
})
}
複製代碼
defineReactive
方法最開始初始化 Dep 對象的實例,而後經過對子對象遞歸調用observe
方法,使全部子屬性也能變成響應式的對象。而且在 Object.defineProperty
的 getter
和 setter
方法中調用 dep
的相關方法。
即:
getter
方法完成的工做就是依賴收集 —— dep.depend()
setter
方法完成的工做就是發佈更新 —— dep.notify()
咱們發現這裏都和 Dep 對象有着不可忽略的關係。接下來咱們就看看 Dep 對象。這個 Dep
前文中咱們提到發佈/訂閱模式,在發佈者和訂閱者以前有一個調度中心。這裏的 Dep 扮演的角色就是調度中心,主要的做用就是:
詳細代碼以下:
// Dep 構造函數
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 向 dep 的觀察者列表 subs 添加 Watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 從 dep 的觀察者列表 subs 移除 Watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 進行依賴收集
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知全部訂閱者,內部調用 watcher 的 update 方法
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// Dep.target 是全局惟一的觀察者,由於在任什麼時候候只有一個觀察者被處理。
Dep.target = null
// 待處理的觀察者隊列
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
複製代碼
Dep 能夠理解成是對 Watcher
的一種管理,Dep 和 Watcher
是緊密相關的。因此咱們必須看一看 Watcher
的實現。
Watcher
中定義了許多原型方法,這裏我只粗略的講 update
和 get
這三個方法。
// 爲了方便理解,部分兼容代碼已被我省去
get () {
// 設置須要處理的觀察者
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
// deep 是否爲 true 的處理邏輯
if (this.deep) {
traverse(value)
}
// 將 Dep.target 指向棧頂的觀察者,並將他從待處理的觀察者隊列中移除
popTarget()
// 執行依賴清空動做
this.cleanupDeps()
return value
}
update () {
if (this.computed) {
...
} else if (this.sync) {
// 標記爲同步
this.run()
} else {
// 通常都是走這裏,即異步批量更新:nextTick
queueWatcher(this)
}
}
複製代碼
Vue 的響應式過程大概就是這樣了。感興趣的能夠看看源碼。
最後咱們在經過這個流程圖來複習一遍:
最近總有朋友問我 Vue 相關的問題,所以接下來我會輸出 10 篇 Vue 相關的文章,但願對你們有必定的幫助。我會保持在 7 到 10 天更新一篇。
建議你關注個人公衆號,第一時間就能夠接收最新的文章。
若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣: