JavaScript 進階之深入理解數據雙向綁定
前言
談起當前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對于大多數人來說,我們更多的是在使用框架,對于框架解決痛點背后使用的基本原理往往關注不多,近期在研讀 Vue.js 源碼,也在寫源碼解讀的系列文章。和多數源碼解讀的文章不同的是,我會嘗試從一個初級前端的角度入手,由淺入深去講解源碼實現思路和基本的語法知識,通過一些基礎事例一步步去實現一些小功能。
本場 Chat 是系列 Chat 的開篇,我會首先講解一下數據雙向綁定的基本原理,介紹對比一下三大框架的不同實現方式,同時會一步步完成一個簡單的mvvm示例。讀源碼不是目的,只是一種學習的方式,目的是在讀源碼的過程中提升自己,學習基本原理,拓展編碼的思維方式。
模板引擎實現原理
對于頁面渲染,一般分為服務器端渲染和瀏覽器端渲染。一般來說服務器端吐html頁面的方式渲染速度更快、更利于SEO,但是瀏覽器端渲染更利于提高開發效率和減少維護成本,是一種相關舒服的前后端協作模式,后端提供接口,前端做視圖和交互邏輯。前端通過Ajax請求數據然后拼接html字符串或者使用js模板引擎、數據驅動的框架如Vue進行頁面渲染。
在ES6和Vue這類框架出現以前,前端綁定數據的方式是動態拼接html字符串和js模板引擎。模板引擎起到數據和視圖分離的作用,模板對應視圖,關注如何展示數據,在模板外頭準備的數據, 關注那些數據可以被展示。模板引擎的工作原理可以簡單地分成兩個步驟:模板解析 / 編譯(Parse / Compile)和數據渲染(Render)兩部分組成,當今主流的前端模板有三種方式:
-
String-based templating (基于字符串的parse和compile過程)
-
Dom-based templating (基于Dom的link或compile過程)
-
Living templating (基于字符串的parse 和 基于dom的compile過程)
String-based templating
基于字符串的模板引擎,本質上依然是字符串拼接的形式,只是一般的庫做了封裝和優化,提供了更多方便的語法簡化了我們的工作。基本原理如下:
典型的庫:
之前的一篇文章中我介紹了js模板引擎的實現思路,感興趣的朋友可以看看這里: JavaScript進階學習(一)—— 基于正則表達式的簡單js模板引擎實現 。這篇文章中我們利用正則表達式實現了一個簡單的js模板引擎,利用正則匹配查找出模板中 {{}} 之間的內容,然后替換為模型中的數據,從而實現視圖的渲染。
var template = function(tpl, data) {
var re = /{{(.+?)}}/g,
cursor = 0,
reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,
code = 'var r=[];\n';
// 解析html
function parsehtml(line) {
// 單雙引號轉義,換行符替換為空格,去掉前后的空格
line = line.replace(/('|")/g, '\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");
code +='r.push("' + line + '");\n';
}
// 解析js代碼
function parsejs(line) {
// 去掉前后的空格
line = line.replace(/(^\s+)|(\s+$)/g,"");
code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n';
}
// 編譯模板
while((match = re.exec(tpl))!== null) {
// 開始標簽 {{ 前的內容和結束標簽 }} 后的內容
parsehtml(tpl.slice(cursor, match.index));
// 開始標簽 {{ 和 結束標簽 }} 之間的內容
parsejs(match[1]);
// 每一次匹配完成移動指針
cursor = match.index + match[0].length;
}
// 最后一次匹配完的內容
parsehtml(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
}</code></pre>
源代碼: http://jsrun.net/yaYKp/embedded/all/light/
現在ES6支持了模板字符串,我們可以用比較簡單的代碼就可以實現類似的功能:
const template = data => <p>name: ${data.name}</p>
<p>age: ${data.profile.age}</p>
<ul>
${data.skills.map(skill =>
<li>${skill}</li>
).join('')}
</ul>
const data = {
name: 'zhaomenghuan',
profile: { age: 24 },
skills: ['html5', 'javascript', 'android']
}
document.body.innerHTML = template(data)</code></pre>
Dom-based templating

Dom-based templating 則是從DOM的角度去實現數據的渲染,我們通過遍歷DOM樹,提取屬性與DOM內容,然后將數據寫入到DOM樹中,從而實現頁面渲染。一個簡單的例子如下:
function MVVM(opt) {
this.dom = document.querySelector(opt.el);
this.data = opt.data || {};
this.renderDom(this.dom);
}
MVVM.prototype = {
init: {
sTag: '{{',
eTag: '}}'
},
render: function (node) {
var self = this;
var sTag = self.init.sTag;
var eTag = self.init.eTag;
var matchs = node.textContent.split(sTag);
if (matchs.length){
var ret = '';
for (var i = 0; i < matchs.length; i++) {
var match = matchs[i].split(eTag);
if (match.length == 1) {
ret += matchs[i];
} else {
ret = self.data[match[0]];
}
node.textContent = ret;
}
}
},
renderDom: function(dom) {
var self = this;
var attrs = dom.attributes;
var nodes = dom.childNodes;
Array.prototype.forEach.call(attrs, function(item) {
self.render(item);
});
Array.prototype.forEach.call(nodes, function(item) {
if (item.nodeType === 1) {
return self.renderDom(item);
}
self.render(item);
});
}
}
var app = new MVVM({
el: '#app',
data: {
name: 'zhaomenghuan',
age: '24',
color: 'red'
}
});</code></pre>
源代碼: http://jsrun.net/faYKp/embedded/all/light/
頁面渲染的函數 renderDom 是直接遍歷DOM樹,而不是遍歷html字符串。遍歷DOM樹節點屬性(attributes)和子節點(childNodes),然后調用渲染函數render。當DOM樹子節點的類型是元素時,遞歸調用遍歷DOM樹的方法。根據DOM樹節點類型一直遍歷子節點,直到文本節點。
render的函數作用是提取 {{}} 中的關鍵詞,然后使用數據模型中的數據進行替換。我們通過textContent獲取Node節點的nodeValue,然后使用字符串的split方法對nodeValue進行分割,提取 {{}} 中的關鍵詞然后替換為數據模型中的值。
DOM 的相關基礎
注:元素類型對應NodeType
元素類型
NodeType
元素
1
屬性
2
文本
3
注釋
8
文檔
9
childNodes 屬性返回包含被選節點的子節點的 NodeList。childNodes包含的不僅僅只有html節點,所有屬性,文本、注釋等節點都包含在childNodes里面。children只返回元素如input, span, script, div等,不會返回TextNode,注釋。
數據雙向綁定實現原理
js模板引擎可以認為是一個基于MVC的結構,我們通過建立模板作為視圖,然后通過引擎函數作為控制器實現數據和視圖的綁定,從而實現實現數據在頁面渲染,但是當數據模型發生變化時,視圖不能自動更新;當視圖數據發生變化時,模型數據不能實現更新,這個時候雙向數據綁定應運而生。檢測視圖數據更新實現數據綁定的方法有很多種,目前主要分為三個流派,Angular使用的是臟檢查,只在特定的事件下才會觸發視圖刷新,Vue使用的是Getter/Setter機制,而React則是通過 Virtual DOM 算法檢查DOM的變動的刷新機制。
本文限于篇幅和內容在此只探討一下 Vue.js 數據綁定的實現,對于 angular 和 react 后續再做說明,讀者也可以自行閱讀源碼。Vue 監聽數據變化的機制是把一個普通 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Vue 2.x 對 Virtual DOM 進行了支持,這部分內容后續我們再做探討。
引子
為了更好的理解Vue中視圖和數據更新的機制,我們先看一個簡單的例子:
var o = {
a: 0
}
Object.defineProperty(o, "b", {
get: function () {
return this.a + 1;
},
set: function (value) {
this.a = value / 2;
}
});
console.log(o.a); // "0"
console.log(o.b); // "1"
// 更新o.a
o.a = 5;
console.log(o.a); // "5"
console.log(o.b); // "6"
// 更新o.b
o.b = 10;
console.log(o.a); // "5"
console.log(o.b); // "6"</code></pre>
這里我們可以看出對象o的b屬性的值依賴于a屬性的值,同時b屬性值的變化又可以改變a屬性的值,這個過程相關的屬性值的變化都會影響其他相關的值進行更新。反過來我們看看如果不使用Object.defineProperty()方法,上述的問題通過直接給對象屬性賦值的方法實現,代碼如下:
var o = {
a: 0
}
o.b = o.a + 1;
console.log(o.a); // "0"
console.log(o.b); // "1"
// 更新o.a
o.a = 5;
o.b = o.a + 1;
console.log(o.a); // "5"
console.log(o.b); // "6"
// 更新o.b
o.b = 10;
o.a = o.b / 2;
o.b = o.a + 1;
console.log(o.a); // "5"
console.log(o.b); // "6"</code></pre>
很顯然使用 Object.defineProperty() 方法可以更方便的監聽一個對象的變化。當我們的視圖和數據任何一方發生變化的時候,我們希望能夠通知對方也更新,這就是所謂的數據雙向綁定。既然明白這個道理我們就可以看看Vue源碼中相關的處理細節。
Object.defineProperty()方法可以直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 并返回這個對象。
語法:Object.defineProperty(obj, prop, descriptor)
參數:
-
obj:需要定義屬性的對象。
-
prop:需被定義或修改的屬性名。
-
descriptor:需被定義或修改的屬性的描述符。
返回值:返回傳入函數的對象,即第一個參數obj.
該方法重點是描述,對象里目前存在的屬性描述符有兩種主要形式: 數據描述符 和 存取描述符 。 數據描述符 是一個擁有可寫或不可寫值的屬性。 存取描述符 是由一對 getter-setter 函數功能來描述的屬性。描述符必須是兩種形式之一;不能同時是兩者。
數據描述符和 存取描述符 均具有以下可選鍵值:
-
configurable:當且僅當該屬性的 configurable 為 true 時,該屬性才能夠被改變,也能夠被刪除。默認為 false。
-
enumerable:當且僅當該屬性的 enumerable 為 true 時,該屬性才能夠出現在對象的枚舉屬性中。默認為 false。
數據描述符同時具有以下可選鍵值:
-
value:該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認為 undefined。
-
writable:當且僅當僅當該屬性的writable為 true 時,該屬性才能被賦值運算符改變。默認為 false。
存取描述符同時具有以下可選鍵值:
-
get:一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認為undefined。
-
set:一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一參數,并將該參數的新值分配給該屬性。默認為undefined。
我們可以通過Object.defineProperty()方法精確添加或修改對象的屬性。比如,直接賦值創建的屬性默認情況是可以枚舉的,但是我們可以通過Object.defineProperty()方法設置enumerable屬性為false為不可枚舉。
var obj = {
a: 0,
b: 1
}
for (var prop in obj) {
console.log(obj.${prop} = ${obj[prop]}
);
}
結果:
"obj.a = 0"
"obj.b = 1"</code></pre>
我們通過Object.defineProperty()修改如下:
var obj = {
a: 0,
b: 1
}
Object.defineProperty(obj, 'b', {
enumerable: false
})
for (var prop in obj) {
console.log(obj.${prop} = ${obj[prop]}
);
}
結果:
"obj.a = 0"</code></pre>
這里需要說明的是我們使用Object.defineProperty()默認情況下是enumerable屬性為false,例如:
var obj = {
a: 0
}
Object.defineProperty(obj, 'b', {
value: 1
})
for (var prop in obj) {
console.log(obj.${prop} = ${obj[prop]}
);
}
結果:
"obj.a = 0"</code></pre>
其他描述屬性使用方法類似,不做贅述。Vue源碼 core/util/lang.js S中定義了這樣一個方法:
/**
- 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
})
}</code></pre>
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性) 語法:Object.getOwnPropertyDescriptor(obj, prop)
參數:
-
obj:在該對象上查看屬性。
-
prop:一個屬性名稱,該屬性的屬性描述符將被返回。
返回值:如果指定的屬性存在于對象上,則返回其屬性描述符(property descriptor),否則返回 undefined。可以訪問“屬性描述符”內容,例如前面的例子:
var o = {
a: 0
}
Object.defineProperty(o, "b", {
get: function () {
return this.a + 1;
},
set: function (value) {
this.a = value / 2;
}
});
var des = Object.getOwnPropertyDescriptor(o,'b');
console.log(des);
console.log(des.get);</code></pre>
Vue源碼分析
本次我們主要分析一下Vue 數據綁定的源碼,這里我直接將 Vue.js 1.0.28 版本的代碼稍作刪減拿過來進行,2.x 的代碼基于 flow 靜態類型檢查器書寫的,代碼除了編碼風格在整體結構上基本沒有太大改動,所以依然基于 1.x 進行分析,對于存在差異的部分加以說明。

監聽對象變動
// 觀察者構造函數
function Observer (value) {
this.value = value
this.walk(value)
}
// 遞歸調用,為對象綁定getter/setter
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
this.convert(keys[i], obj[keys[i]])
}
}
// 將屬性轉換為getter/setter
Observer.prototype.convert = function (key, val) {
defineReactive(this.value, key, val)
}
// 創建數據觀察者實例
function observe (value) {
// 當值不存在或者不是對象類型時,不需要繼續深入監聽
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
// 定義對象屬性的getter/setter
function defineReactive (obj, key, val) {
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 保存對象屬性預先定義的getter/setter
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
console.log("訪問:"+key)
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 對新值進行監聽
childOb = observe(newVal)
console.log('更新:' + key + ' = ' + newVal)
}
})
}</code></pre>
定義一個對象作為數據模型,并監聽這個對象。
let data = {
user: {
name: 'zhaomenghuan',
age: '24'
},
address: {
city: 'beijing'
}
}
observe(data)
console.log(data.user.name)
// 訪問:user
// 訪問:name
data.user.name = 'ZHAO MENGHUAN'
// 訪問:user
// 更新:name = ZHAO MENGHUAN</code></pre>
效果如下:

監聽數組變動
上面我們通過Object.defineProperty把對象的屬性全部轉為 getter/setter 從而實現監聽對象的變動,但是對于數組對象無法通過Object.defineProperty實現監聽。Vue 包含一組觀察數組的變異方法,所以它們也將會觸發視圖更新。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
// 數組的變異方法
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 緩存數組原始方法
var original = arrayProto[method]
def(arrayMethods, method, function mutator () {
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
console.log('數組變動')
return original.apply(this, args)
})
})</code></pre>
Vue.js 1.x 在Array.prototype原型對象上添加了 $set 和 $remove 方法,在2.X后移除了,使用全局 API Vue.set 和 Vue.delete 代替了,后續我們再分析。
定義一個數組作為數據模型,并對這個數組調用變異的七個方法實現監聽。
let skills = ['JavaScript', 'Node.js', 'html5']
// 原型指針指向具有變異方法的數組對象
skills.proto = arrayMethods
skills.push('java')
// 數組變動
skills.pop()
// 數組變動</code></pre>
效果如下:

我們將需要監聽的數組的原型指針指向我們定義的數組對象,這樣我們的數組在調用上面七個數組的變異方法時,能夠監聽到變動從而實現對數組進行跟蹤。
對于 __proto__ 屬性,在ES2015中正式被加入到規范中,標準明確規定,只有瀏覽器必須部署這個屬性,其他運行環境不一定需要部署,所以 Vue 是先進行了判斷,當 __proto__ 屬性存在時將原型指針 __proto__ 指向具有變異方法的數組對象,不存在時直接將具有變異方法掛在需要追蹤的對象上。
我們可以在上面Observer觀察者構造函數中添加對數組的監聽,源碼如下:
const hasProto = 'proto' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// 觀察者構造函數
function Observer (value) {
this.value = value
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 觀察數組的每一項
Observer.prototype.observeArray = function (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
// 將目標對象/數組的原型指針proto指向src
function protoAugment (target, src) {
target.proto = src
}
// 將具有變異方法掛在需要追蹤的對象上
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i]
def(target, key, src[key])
}
}</code></pre>
原型鏈
對于不了解原型鏈的朋友可以看一下我這里畫的一個基本關系圖:

-
原型對象是構造函數的prototype屬性,是所有實例化對象共享屬性和方法的原型對象。
-
實例化對象通過new構造函數得到,都繼承了原型對象的屬性和方法。
-
原型對象中有個隱式的constructor,指向了構造函數本身。
Object.create
Object.create 使用指定的原型對象和其屬性創建了一個新的對象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
這一步是通過 Object.create 創建了一個原型對象為Array.prototype的空對象。然后通過Object.defineProperty方法對這個對象定義幾個變異的數組方法。有些新手可能會直接修改 Array.prototype 上的方法,這是很危險的行為,這樣在引入的時候會全局影響Array 對象的方法,而使用Object.create實質上是完全了一份拷貝,新生成的arrayMethods對象的原型指針 __proto__ 指向了Array.prototype,修改arrayMethods 對象不會影響Array.prototype。
基于這種原理,我們通常會使用Object.create 實現類式繼承。
// 實現繼承
var extend = function(Child, Parent) {
// 拷貝Parent原型對象
Child.prototype = Object.create(Parent.prototype);
// 將Child構造函數賦值給Child的原型對象
Child.prototype.constructor = Child;
}
// 實例
var Parent = function () {
this.name = 'Parent';
}
Parent.prototype.getName = function () {
return this.name;
}
var Child = function () {
this.name = 'Child';
}
extend(Child, Parent);
var child = new Child();
console.log(child.getName())</code></pre>
發布-訂閱模式
在上面一部分我們通過Object.defineProperty把對象的屬性全部轉為 getter/setter 以及 數組變異方法實現了對數據模型變動的監聽,在數據變動的時候,我們通過console.log打印出來提示了,但是對于框架而言,我們相關的邏輯如果直接寫在那些地方,自然是不夠優雅和靈活的,這個時候就需要引入常用的設計模式去實現,vue.js采用了發布-訂閱模式。發布-訂閱模式主要是為了達到一種“高內聚、低耦合"的效果。
Vue的Watcher訂閱者作為Observer和Compile之間通信的橋梁,能夠訂閱并收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖。
/**
- 觀察者對象
*/
function Watcher(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.depIds = {}
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = this.parseExpression(expOrFn)
}
this.value = this.get()
}
/**
- 收集依賴
*/
Watcher.prototype.get = function () {
// 當前訂閱者(Watcher)讀取被訂閱數據的最新更新后的值時,通知訂閱者管理員收集當前訂閱者
Dep.target = this
// 觸發getter,將自身添加到dep中
const value = this.getter.call(this.vm, this.vm)
// 依賴收集完成,置空,用于下一個Watcher使用
Dep.target = null
return value
}
Watcher.prototype.addDep = function (dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depIds[dep.id] = dep
}
}
/**
- 依賴變動更新
*
- @param {Boolean} shallow
*/
Watcher.prototype.update = function () {
this.run()
}
Watcher.prototype.run = function () {
var value = this.get()
if (value !== this.value) {
var oldValue = this.value
this.value = value
// 將newVal, oldVal掛載到MVVM實例上
this.cb.call(this.vm, value, oldValue)
}
}
Watcher.prototype.parseExpression = function (exp) {
if (/[^\w.$]/.test(exp)) {
return
}
var exps = exp.split('.')
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return
obj = obj[exps[i]]
}
return obj
}
}</code></pre>
Dep 是一個數據結構,其本質是維護了一個watcher隊列,負責添加watcher,更新watcher,移除watcher,通知watcher更新。
let uid = 0
function Dep() {
this.id = uid++
this.subs = []
}
Dep.target = null
/**
- 添加一個訂閱者
*
- @param {Directive} sub
*/
Dep.prototype.addSub = function (sub) {
this.subs.push(sub)
}
/**
- 移除一個訂閱者
*
- @param {Directive} sub
*/
Dep.prototype.removeSub = function (sub) {
let index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
}
/**
- 將自身作為依賴添加到目標watcher
*/
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
/**
- 通知數據變更
*/
Dep.prototype.notify = function () {
var subs = toArray(this.subs)
// stablize the subscriber list first
for (var i = 0, l = subs.length; i < l; i++) {
// 執行訂閱者的update更新函數
subs[i].update()
}
}</code></pre>
模板編譯
compile主要做的事情是解析模板指令,將模板中的變量替換成數據,然后初始化渲染頁面視圖,并將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖。
function Compile(el, value) {
this.$vm = value
this.$el = this.isElementNode(el) ? el : document.querySelector(el)
if (this.$el) {
this.compileElement(this.$el)
}
}
Compile.prototype.compileElement = function (el) {
let self = this
let childNodes = el.childNodes
;[].slice.call(childNodes).forEach(node => {
let text = node.textContent
let reg = /\{\{((?:.|\n)+?)\}\}/
// 處理element節點
if (self.isElementNode(node)) {
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) { // 處理text節點
self.compileText(node, RegExp.$1.trim())
}
// 解析子節點包含的指令
if (node.childNodes && node.childNodes.length) {
self.compileElement(node)
}
})
}
Compile.prototype.compile = function (node) {
let nodeAttrs = node.attributes
let self = this
;[].slice.call(nodeAttrs).forEach(attr => {
var attrName = attr.name
if (self.isDirective(attrName)) {
let exp = attr.value
let dir = attrName.substring(2)
if (self.isEventDirective(dir)) {
compileUtil.eventHandler(node, self.$vm, exp, dir)
} else {
compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
}
node.removeAttribute(attrName)
}
});
}
Compile.prototype.compileText = function (node, exp) {
compileUtil.text(node, this.$vm, exp);
}
Compile.prototype.isDirective = function (attr) {
return attr.indexOf('v-') === 0
}
Compile.prototype.isEventDirective = function (dir) {
return dir.indexOf('on') === 0;
}
Compile.prototype.isElementNode = function (node) {
return node.nodeType === 1
}
Compile.prototype.isTextNode = function (node) {
return node.nodeType === 3
}
// 指令處理集合
var compileUtil = {
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text')
},
html: function (node, vm, exp) {
this.bind(node, vm, exp, 'html')
},
model: function (node, vm, exp) {
this.bind(node, vm, exp, 'model')
let self = this, val = this._getVMVal(vm, exp)
node.addEventListener('input', function (e) {
var newValue = e.target.value
if (val === newValue) {
return
}
self._setVMVal(vm, exp, newValue)
val = newValue
});
},
bind: function (node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater']
updaterFn && updaterFn(node, this._getVMVal(vm, exp))
new Watcher(vm, exp, function (value, oldValue) {
updaterFn && updaterFn(node, value, oldValue)
})
},
eventHandler: function (node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal: function (vm, exp) {
var val = vm
exp = exp.split('.')
exp.forEach(function (k) {
val = val[k]
})
return val
},
_setVMVal: function (vm, exp, value) {
var val = vm;
exp = exp.split('.')
exp.forEach(function (k, i) {
// 非最后一個key,更新val的值
if (i < exp.length - 1) {
val = val[k]
} else {
val[k] = value
}
})
}
}
var updater = {
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value
},
htmlUpdater: function (node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value
},
modelUpdater: function (node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value
}
}</code></pre>
這種實現和我們講到的Dom-based templating類似,只是更加完備,具有自定義指令的功能。在遍歷節點屬性和文本節點的時候,可以編譯具備 {{}} 表達式或 v-xxx 的屬性值的節點,并且通過添加 new Watcher() 及綁定事件函數,監聽數據的變動從而對視圖實現雙向綁定。
MVVM實例
在數據綁定初始化的時候,我們需要通過 new Observer() 來監聽數據模型變化,通過 new Compile() 來解析編譯模板指令,并利用Watcher搭起Observer和Compile之間的通信橋梁。
/**
- @class 雙向綁定類 MVVM
- @param {[type]} options [description]
*/
function MVVM(options) {
this.$options = options || {}
// 簡化了對data的處理
let data = this._data = this.$options.data
// 監聽數據
observe(data)
new Compile(options.el || document.body, this)
}
MVVM.prototype.$watch = function (expOrFn, cb) {
new Watcher(this, expOrFn, cb)
}</code></pre>
為了能夠直接通過實例化對象操作數據模型,我們需要為MVVM實例添加一個數據模型代理的方法:
MVVM.prototype._proxy = function (key) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: (val) => {
this._data[key] = val
}
})
}
至此我們可以通過一個小例子來說明本文的內容。

<div id="app">
<h3>{{user.name}}</h3>
<input type="text" v-model="modelValue">
<p>{{modelValue}}</p>
</div>
<script>
let vm = new MVVM({
el: '#app',
data: {
modelValue: '',
user: {
name: 'zhaomenghuan',
age: '24'
},
address: {
city: 'beijing'
},
skills: ['JavaScript', 'Node.js', 'html5']
}
})
vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`))
</script></code></pre>
本文目的不是為了造一個輪子,而是在學習優秀框架實現的過程中去提升自己,搞清楚框架發展的前因后果,由淺及深去學習基礎,本文參考了網上很多優秀博主的文章,由于時間關系,有些內容沒有做深入探討,覺得還是有些遺憾,在后續的學習中會更多的獨立思考,提出更多自己的想法。
參考文檔
-
-
-
-
-
-
來自:http://gitbook.cn/books/593faaf7d3845323661a4cec/index.html