--文末附視頻教程
本文主要學(xué)習(xí)掌握 Vue 雙向綁定的核心部分原理盐肃。
代碼為簡化版砸王,相對比較簡陋谦铃。也未考慮數(shù)組等其他處理。
歡迎一起學(xué)習(xí)交流瘪菌。
一、準(zhǔn)備工作
1. 什么是 MVVM 框架诵肛?
MVVM 是 Model-View-ViewModel 的簡寫怔檩,雙向數(shù)據(jù)綁定蓄诽,即視圖影響模型仑氛,模型影響數(shù)據(jù)调衰。它本質(zhì)上就是MVC 的改進(jìn)版。
- Model(模型)是數(shù)據(jù)訪問層,例如后臺接口傳遞的數(shù)據(jù)
- View(視圖)是用戶在屏幕上看到的頁面的結(jié)構(gòu)趋箩、布局叫确、外觀(UI)
- ViewModel(視圖模型)負(fù)責(zé)將 View 的變化同步到 Model芍锦,或 Model 的變化轉(zhuǎn)化為 View娄琉。
2. Vue 怎么實現(xiàn)雙向數(shù)據(jù)綁定
Vue2.x 是通過 Object.defineProperty 實現(xiàn)的雙向數(shù)據(jù)綁定,該方法不支持 ie8 及以下版本票腰。
相關(guān)語法直接查看mdn文檔杏慰。
其中,屬性描述符有很多個轰胁,下面簡單說明一下常用的幾個赃阀,具體詳細(xì)內(nèi)容直接查看文檔吟税。
下面是給定義 obj 對象定義一個名稱為 Vue 的屬性肠仪。
Object.defineProperty(obj, 'vue', {
configurable: true,
writable: true,
enmerbale: true,
value: 'Hello, Vue',
get() {
return 'Hello, Vue'
}
set(val) {
console.log(val)
}
})
configurable: 指定屬性是否可以配置异旧,如果不設(shè)置為true,則無法刪除該屬性荤崇,例如:delete obj.vue='react'無效
writable: 指定屬性是否能被賦值運算符改變术荤,如果不設(shè)置為true每篷,則給 vue 屬性賦值焦读,例如:obj.vue='react'無效
enmerbale: 指定屬性是否可以被枚舉到矗晃,如果不設(shè)置為true,使用 for...in... 或 Object.keys 是讀不到該屬性的
value: 指定屬性對應(yīng)的值仓技,與 get 屬性沖突浑彰,一起使用會報錯
get: 訪問該屬性時郭变,如果有設(shè)置 get 方法,會執(zhí)行這個方法并返回
set: 修改該屬性值時周伦,如果有設(shè)置 set 方法专挪,會執(zhí)行這個方法片排,并把新的值作為參數(shù)傳進(jìn)入 object.vue = 'hello, Vuex'
3. 流程圖
4. 流程分析
這里我們先看看代碼實現(xiàn)率寡,大概了解一下整個過程冶共,最后再對整個過程進(jìn)行分析捅僵。
結(jié)合代碼家卖、注釋上荡、過程分析可以更好的理解整個過程榛臼。
二窜司、開始實現(xiàn)
參考 Vue2.x 源碼實現(xiàn)塞祈,與實際 Vue 的實現(xiàn)有差別议薪,但原理上差不多媳友。建議看完可以繼續(xù)深入學(xué)習(xí) Vue 實際源碼
1. Vue 入口
// 模擬 Vue 的入口
function MVVM(options) {
var vm = this;
vm.$options = options || {};
vm._data = vm.$options.data;
/**
* initState 主要對數(shù)據(jù)對處理
* 實現(xiàn) observe醇锚,即對 data/computed 等做響應(yīng)式處理以及將數(shù)據(jù)代理到 vm 實例上
*/
initState(vm)
// 編譯模版
this.$compile = new Compile(options.el || document.body, this)
}
2. 模版編譯
這里的 Compile 只是簡單的模版編譯,與 Vue 實際Compile 有較大區(qū)別看靠,實際的 Compile 實現(xiàn)比較復(fù)雜挟炬,需要經(jīng)過 parse嗦哆、optimize老速、generate 三個階段處理烁峭。
- parse: 使用正則解析template中的vue的指令(v-xxx) 變量等等 形成抽象語法樹AST
- optimize: 標(biāo)記一些靜態(tài)節(jié)點,用作后面的性能優(yōu)化缩挑,在diff的時候直接略過
- generate: 把第一部生成的AST 轉(zhuǎn)化為渲染函數(shù) render function
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 將原生節(jié)點轉(zhuǎn)為文檔碎片節(jié)點供置,提高操作效率
this.$fragment = this.node2Fragment(this.$el);
// 編譯模版內(nèi)容芥丧,同時進(jìn)行依賴收集
this.compile(this.$fragment);
// 將處理后的 dom 樹掛載到真實 dom 節(jié)點中
this.$el.appendChild(this.$fragment);
}
}
// compile 相關(guān)方法實現(xiàn)
Compile.prototype = {
node2Fragment(el) {
const fragment = document.createDocumentFragment();
/**
* 將原生節(jié)點拷貝到 fragment续担,
* 每次循環(huán)都會把 el 中的第一個節(jié)點取出來追加到 fragment 后面活孩,直到 el 沒有字節(jié)點
*/
let child;
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
compile: function (el) {
const childNodes = el.childNodes
// childNodes 不是標(biāo)準(zhǔn)數(shù)組憾儒,通過 Array.from 把 childNodes 轉(zhuǎn)成數(shù)組并遍歷處理每一個節(jié)點起趾。
Array.from(childNodes).forEach(node => {
// 利用閉包機(jī)制诗舰,保存文本節(jié)點最初的文本,后面更新根據(jù)最初的文本進(jìn)行替換更新训裆。
const text = node.textContent;
// 元素節(jié)點眶根,對元素屬性綁定對指令進(jìn)行處理
if (this.isElementNode(node)) {
this.compileElement(node);
}
// 文本節(jié)點并且包含 {{xx}} 字符串對文本蜀铲,模版內(nèi)容替換
else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(text)) {
this.compileText(node, RegExp.$1.trim(), text);
}
// 遞歸編譯子節(jié)點的內(nèi)容
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
},
compileElement: function (node) {
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name;
// 判斷屬性是否是一個指令,例如: v-text 等
if (this.isDirective(attrName)) {
const exp = attr.value;
const dir = attrName.substring(2);
// 事件指令
if (this.isEventDirective(dir)) {
compileUtil.eventHandler(node, this.$vm, exp, dir);
}
// 普通指令
else {
compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
compileText: function (node, exp) {
// compileUtil.text(node, this.$vm, exp);
// 利用閉包機(jī)制汛闸,保存文本節(jié)點最初的文本蝙茶,后面更新根據(jù)最初的文本進(jìn)行替換更新。
const vm = this.$vm
let text = node.textContent
const updaterFn = updater.textUpdater
let value = text.replace(/\{\{(.*)\}\}/, compileUtil._getVMVal(vm, exp))
updaterFn && updaterFn(node, value);
new Watcher(vm, exp, function (value) {
updaterFn && updaterFn(node, text.replace(/\{\{(.*)\}\}/, value));
});
},
// ... 省略
};
指令集合處理
// 指令處理集合
const compileUtil = {
text: function (node, vm, exp) {
this.update(node, vm, exp, 'text');
},
// ... 省略
update: function (node, vm, exp, dir) {
// 針對不同的指令使用不同的函數(shù)渲染诸老、更新數(shù)據(jù)。
const updaterFn = updater[dir + 'Updater'];
// 這里取值别伏,然后進(jìn)行初次的內(nèi)容渲染
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function (value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// ... 省略
};
const updater = {
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
// ... 省略
};
3. 響應(yīng)式對象
initState
initState
方法主要是對 props
蹄衷、methods
、data
厘肮、computed
和 wathcer
等屬性做了初始化操作愧口。這里我們主要實現(xiàn)對 data
跟 computed
的操作。
function initState(vm) {
const opts = vm.$options
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
}
initData
主要實現(xiàn)以下兩個操作:
調(diào)用
observe
方法觀測整個data
的變化类茂,把data
也變成響應(yīng)式耍属,可以通過vm._data.xxx
訪問到定義data
返回函數(shù)中對應(yīng)的屬性。對定義
data
函數(shù)返回對象的遍歷巩检,通過proxy
把每一個值vm._data.xxx
都代理到vm.xxx
上厚骗。
function initData(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ?
data.call(vm, vm) :
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 (methods && hasOwn(methods, key)) {
console.log(`Method "${key}" has already been defined as a data property.`, vm)
}
if (props && hasOwn(props, key)) {
console.log(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm)
} else if (!isReserved(key)) {
// 數(shù)據(jù)代理,實現(xiàn) vm.xxx -> vm._data.xxx兢哭,相當(dāng)于 vm 上面多了 xxx 這個屬性
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true)
}
proxy
把每一個值 vm._data.xxx
都代理到 vm.xxx
上领舰。
這是一個公用的方法。這里我們只是對 data 定義對屬性做里代理迟螺。實際上 vue 還通過這個方法對 props 也做了代理冲秽,proxy(vm, '_props', key)
。
// 數(shù)據(jù)代理矩父,proxy(vm, '_data', key)锉桑。
function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function proxyGetter() {
// initData 里把 vm._data 處理成響應(yīng)式對象。
// 這里返回 this['_data'][key]窍株,實現(xiàn) vm[key] -> vm._data[key]
return this[sourceKey][key]
},
set: function proxySetter(val) {
// 這里修改 vm[key] 實際上是修改了 this['_data'][key]
this[sourceKey][key] = val
}
})
}
observe
observe
的功能就是用來監(jiān)測數(shù)據(jù)的變化刨仑。
function observe(value) {
if (!isObject(value)) {
return
}
return new Observer(value);
}
Observer
Observer
是一個類,它的作用是給對象的屬性添加 getter 和 setter夹姥,用于依賴收集和派發(fā)更新
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
this.walk(value)
}
walk (obj) {
// 遍歷 data 對象的 key 調(diào)用 defineReactive 方法創(chuàng)建響應(yīng)式對象
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[I])
}
}
}
defineReactive
defineReactive
的功能就是定義一個響應(yīng)式對象,給對象動態(tài)添加 getter 和 setter辙诞,getter 做的事情是依賴收集辙售,setter 做的事情是派發(fā)更新。
function defineReactive (obj, key) {
// 初始化 Dep飞涂,用于依賴收集
const dep = new Dep()
let val = obj[key]
// 對子對象遞歸調(diào)用 observe 方法旦部,這樣就保證了無論 obj 的結(jié)構(gòu)多復(fù)雜祈搜,
// 它的所有子屬性也能變成響應(yīng)式的對象,
// 這樣我們訪問或修改 obj 中一個嵌套較深的屬性士八,也能觸發(fā) getter 和 setter容燕。
// 使 foo.bar 等多層的對象也可以實現(xiàn)響應(yīng)式。
let childOb = observe(val)
// Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// Dep.target 指向 watcher
if (Dep.target) {
// 依賴收集婚度,每個使用到 data 里的值的地方蘸秘,都會調(diào)用一次 get,然后就會被收集到一個數(shù)組中蝗茁。
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return val
},
set: function reactiveSetter (newVal) {
// 當(dāng)值沒有變化時醋虏,直接返回
if (newVal === val) {
return
}
// 對 val 設(shè)置新的
val = newVal
// 如果新傳入的值時一個對象,需要重新進(jìn)行 observe哮翘,給對象的屬性做響應(yīng)式處理颈嚼。
childOb = observe(newVal)
dep.notify()
}
})
}
4.依賴收集、派發(fā)更新
Dep
Dep
是整個 getter 依賴收集的核心饭寺,這里需要特別注意的是它有一個靜態(tài)屬性 target
阻课,這是一個全局唯一 Watcher
,這是一個非常巧妙的設(shè)計艰匙,因為在同一時間只能有一個全局的 Watcher
被計算限煞,另外它的自身屬性 subs
是 Watcher
的數(shù)組。
Dep
實際上就是對 Watcher
的一種管理旬薯,Dep
脫離 Watcher
單獨存在是沒有意義的晰骑。
class Dep {
static target;
constructor () {
// 存放 watcher 的地方
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 派發(fā)更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
Watcher
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.expOrFn = expOrFn;
this.depIds = {};
// 判斷 expOrFn 是不是一個函數(shù),如果不是函數(shù)會通過 parsePath 把它變成一個函數(shù)绊序。
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// parsePath 把 expOrFn 變成一個函數(shù)
this.getter = parsePath(expOrFn) || function noop (a, b, c) {}
}
// 取值硕舆,觸發(fā)依賴收集。
this.value = this.get()
}
get() {
// 這里 Dep.target 指向 watcher 本身骤公,然后會取值抚官,取值觸發(fā)對應(yīng)屬性的 getter 方法。
// 此時 getter 方法里面使用的 Dep.target 就有值了阶捆。
// 通過一系列的代碼執(zhí)行 dep.depend() -> Dep.target.addDep(dep) -> dep.addSub(watcher)
// 最后把 watcher 存到 subs 數(shù)組里凌节,完成依賴收集。
// 最后把 Dep.target 刪除洒试,保證來 Dep.target 在同一時間內(nèi)只有唯一一個倍奢。
Dep.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
Dep.target = null;
return value
}
addDep(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
update() {
// this.value 是 watcher 緩存的值,用來與改變后的值進(jìn)行對比垒棋,如果前后值沒有變化卒煞,就不進(jìn)行更新。
const value = this.get()
const oldValue = this.value
if (value !== oldValue) {
// 緩存新的值叼架,下次操作用
this.value = value
// 以 vm 為 cb 的 this 值畔裕,調(diào)用 cb衣撬。
// cb 就是 在 new watcher 使傳入的更新函數(shù)。會把新的值傳入通過更新函數(shù)扮饶,更新到視圖上具练。
this.cb.call(this.vm, value, oldValue)
}
}
}
三、過程分析
-
new MVVM()
的時候甜无,首先扛点,會對data
、props
毫蚓、computed
進(jìn)行初始化占键,使它們變成響應(yīng)式的對象。 - 響應(yīng)式是通過使用
Object.defineProperty
給對象的屬性設(shè)置get
元潘、set
畔乙,為屬性提供 getter、setter 方法翩概,一旦對象擁有了 getter 和 setter牲距,我們可以簡單地把這個對象稱為響應(yīng)式對象。钥庇。 - 當(dāng)我們訪問了該屬性的時候會觸發(fā) getter 方法牍鞠,當(dāng)我們對該屬性做修改的時候會觸發(fā) setter 方法。
- 在 getter 方法里做依賴的收集评姨。因為在使用屬性的時候难述,就會觸發(fā) getter,這時就會把這個使用記錄起來吐句,后面屬性有改動的時候胁后,就會根據(jù)這個收集的記錄進(jìn)行更新。
- 在 setter 方法里做派發(fā)更新嗦枢。因為在對屬性做修改的時候會觸發(fā)這個setter攀芯,這時就可以根據(jù)之前在 getter 里面收集的記錄,去做對應(yīng)的更新文虏。
- getter 的實現(xiàn)中侣诺,是通過
Dep
實現(xiàn)依賴收集的。getter 方法中調(diào)用了Dep.depend()
進(jìn)行收集氧秘,Dep.depend()
中又調(diào)用了Dep.target.addDep(this)
年鸳。 - 這里
Dep.target
是個非常巧妙的設(shè)計,因為在同一時間Dep.target
只指向一個Watcher
丸相,使得同一時間內(nèi)只能有一個全局的Watcher
被計算阻星。 -
Dep.target.addDep(this)
等于調(diào)用Watcher.addDep(dep)
,里面又調(diào)用了dep.addSub(this)
把這個全局唯一的 watcher 添加到dep.subs
數(shù)組中,收集了起來妥箕,并且 watcher 本身也通過depIds
收集持有的Dep
實例。 - 上面只是定義了一個流程更舞,但是需要訪問數(shù)據(jù)對象才能觸發(fā) getter 使這個流程運轉(zhuǎn)起來畦幢。那什么時候觸發(fā)呢?
- Vue 會通過
compile
把模版編譯成render
函數(shù)缆蝉,并在render
函數(shù)中訪問數(shù)據(jù)對象觸發(fā) getter宇葱。這里我們是直接在compile
的時候訪問數(shù)據(jù)對象觸發(fā) getter。 -
compile
負(fù)責(zé)內(nèi)容的渲染與數(shù)據(jù)更新刊头。compile
編譯模版中的內(nèi)容黍瞧,把模版中的 {{xx}} 字符串替換成對應(yīng)的屬性值時會訪問數(shù)據(jù)對象觸發(fā) getter,不過此時還沒有watcher
原杂,沒有依賴收集印颤。 -
compile
接下來會實例化Watcher
,實例化過程會再去取一次值穿肄,此時觸發(fā)到 getter 才會進(jìn)行依賴收集年局。具體看Watcher
的 構(gòu)造函數(shù)與 get 方法實現(xiàn)。 - 到這里咸产,頁面渲染完成矢否,依賴收集也完成。
- 接下來會監(jiān)控數(shù)據(jù)的變化脑溢,數(shù)據(jù)如果發(fā)生變化僵朗,就會觸發(fā)屬性值的 setter 方法,setter 方法除了把值設(shè)置為新的值之外屑彻,還會進(jìn)行派發(fā)更新验庙。執(zhí)行
dep.notify()
,循環(huán)調(diào)用subs
里面保存的watcher
的update
方法進(jìn)行更新酱酬。