摘要:當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時(shí)候,我們希望能夠通知對(duì)方也更新,這就是所謂的數(shù)據(jù)雙向綁定。返回值返回傳入函數(shù)的對(duì)象,即第一個(gè)參數(shù)該方法重點(diǎn)是描述,對(duì)象里目前存在的屬性描述符有兩種主要形式數(shù)據(jù)描述符和存取描述符。
前言
談起當(dāng)前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對(duì)于大多數(shù)人來說,我們更多的是在使用框架,對(duì)于框架解決痛點(diǎn)背后使用的基本原理往往關(guān)注不多,近期在研讀 Vue.js 源碼,也在寫源碼解讀的系列文章。和多數(shù)源碼解讀的文章不同的是,我會(huì)嘗試從一個(gè)初級(jí)前端的角度入手,由淺入深去講解源碼實(shí)現(xiàn)思路和基本的語法知識(shí),通過一些基礎(chǔ)事例一步步去實(shí)現(xiàn)一些小功能。
本場(chǎng) Chat 是系列 Chat 的開篇,我會(huì)首先講解一下數(shù)據(jù)雙向綁定的基本原理,介紹對(duì)比一下三大框架的不同實(shí)現(xiàn)方式,同時(shí)會(huì)一步步完成一個(gè)簡單的mvvm示例。讀源碼不是目的,只是一種學(xué)習(xí)的方式,目的是在讀源碼的過程中提升自己,學(xué)習(xí)基本原理,拓展編碼的思維方式。
模板引擎實(shí)現(xiàn)原理對(duì)于頁面渲染,一般分為服務(wù)器端渲染和瀏覽器端渲染。一般來說服務(wù)器端吐html頁面的方式渲染速度更快、更利于SEO,但是瀏覽器端渲染更利于提高開發(fā)效率和減少維護(hù)成本,是一種相關(guān)舒服的前后端協(xié)作模式,后端提供接口,前端做視圖和交互邏輯。前端通過Ajax請(qǐng)求數(shù)據(jù)然后拼接html字符串或者使用js模板引擎、數(shù)據(jù)驅(qū)動(dòng)的框架如Vue進(jìn)行頁面渲染。
在ES6和Vue這類框架出現(xiàn)以前,前端綁定數(shù)據(jù)的方式是動(dòng)態(tài)拼接html字符串和js模板引擎。模板引擎起到數(shù)據(jù)和視圖分離的作用,模板對(duì)應(yīng)視圖,關(guān)注如何展示數(shù)據(jù),在模板外頭準(zhǔn)備的數(shù)據(jù), 關(guān)注那些數(shù)據(jù)可以被展示。模板引擎的工作原理可以簡單地分成兩個(gè)步驟:模板解析 / 編譯(Parse / Compile)和數(shù)據(jù)渲染(Render)兩部分組成,當(dāng)今主流的前端模板有三種方式:
String-based templating (基于字符串的parse和compile過程)
Dom-based templating (基于Dom的link或compile過程)
Living templating (基于字符串的parse 和 基于dom的compile過程)
String-based templating基于字符串的模板引擎,本質(zhì)上依然是字符串拼接的形式,只是一般的庫做了封裝和優(yōu)化,提供了更多方便的語法簡化了我們的工作。基本原理如下:
典型的庫:
art-template
mustache.js
doT
之前的一篇文章中我介紹了js模板引擎的實(shí)現(xiàn)思路,感興趣的朋友可以看看這里:JavaScript進(jìn)階學(xué)習(xí)(一)—— 基于正則表達(dá)式的簡單js模板引擎實(shí)現(xiàn)。這篇文章中我們利用正則表達(dá)式實(shí)現(xiàn)了一個(gè)簡單的js模板引擎,利用正則匹配查找出模板中{{}}之間的內(nèi)容,然后替換為模型中的數(shù)據(jù),從而實(shí)現(xiàn)視圖的渲染。
var template = function(tpl, data) { var re = /{{(.+?)}}/g, cursor = 0, reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g, code = "var r=[]; "; // 解析html function parsehtml(line) { // 單雙引號(hào)轉(zhuǎn)義,換行符替換為空格,去掉前后的空格 line = line.replace(/("|")/g, "$1").replace(/ /g, " ").replace(/(^s+)|(s+$)/g,""); code +="r.push("" + line + ""); "; } // 解析js代碼 function parsejs(line) { // 去掉前后的空格 line = line.replace(/(^s+)|(s+$)/g,""); code += line.match(reExp)? line + " " : "r.push(" + "this." + line + "); "; } // 編譯模板 while((match = re.exec(tpl))!== null) { // 開始標(biāo)簽 {{ 前的內(nèi)容和結(jié)束標(biāo)簽 }} 后的內(nèi)容 parsehtml(tpl.slice(cursor, match.index)); // 開始標(biāo)簽 {{ 和 結(jié)束標(biāo)簽 }} 之間的內(nèi)容 parsejs(match[1]); // 每一次匹配完成移動(dòng)指針 cursor = match.index + match[0].length; } // 最后一次匹配完的內(nèi)容 parsehtml(tpl.substr(cursor, tpl.length - cursor)); code += "return r.join("");"; return new Function(code.replace(/[ ]/g, "")).apply(data); }
源代碼:http://jsfiddle.net/zhaomengh...
現(xiàn)在ES6支持了模板字符串,我們可以用比較簡單的代碼就可以實(shí)現(xiàn)類似的功能:
const template = data => `name: ${data.name}
age: ${data.profile.age}
Dom-based templating 則是從DOM的角度去實(shí)現(xiàn)數(shù)據(jù)的渲染,我們通過遍歷DOM樹,提取屬性與DOM內(nèi)容,然后將數(shù)據(jù)寫入到DOM樹中,從而實(shí)現(xiàn)頁面渲染。一個(gè)簡單的例子如下:
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" } });
源代碼:http://jsfiddle.net/zhaomengh...
頁面渲染的函數(shù) renderDom 是直接遍歷DOM樹,而不是遍歷html字符串。遍歷DOM樹節(jié)點(diǎn)屬性(attributes)和子節(jié)點(diǎn)(childNodes),然后調(diào)用渲染函數(shù)render。當(dāng)DOM樹子節(jié)點(diǎn)的類型是元素時(shí),遞歸調(diào)用遍歷DOM樹的方法。根據(jù)DOM樹節(jié)點(diǎn)類型一直遍歷子節(jié)點(diǎn),直到文本節(jié)點(diǎn)。
render的函數(shù)作用是提取{{}}中的關(guān)鍵詞,然后使用數(shù)據(jù)模型中的數(shù)據(jù)進(jìn)行替換。我們通過textContent獲取Node節(jié)點(diǎn)的nodeValue,然后使用字符串的split方法對(duì)nodeValue進(jìn)行分割,提取{{}}中的關(guān)鍵詞然后替換為數(shù)據(jù)模型中的值。
DOM 的相關(guān)基礎(chǔ)
注:元素類型對(duì)應(yīng)NodeType
元素類型 | NodeType |
---|---|
元素 | 1 |
屬性 | 2 |
文本 | 3 |
注釋 | 8 |
文檔 | 9 |
childNodes 屬性返回包含被選節(jié)點(diǎn)的子節(jié)點(diǎn)的 NodeList。childNodes包含的不僅僅只有html節(jié)點(diǎn),所有屬性,文本、注釋等節(jié)點(diǎn)都包含在childNodes里面。children只返回元素如input, span, script, div等,不會(huì)返回TextNode,注釋。
數(shù)據(jù)雙向綁定實(shí)現(xiàn)原理js模板引擎可以認(rèn)為是一個(gè)基于MVC的結(jié)構(gòu),我們通過建立模板作為視圖,然后通過引擎函數(shù)作為控制器實(shí)現(xiàn)數(shù)據(jù)和視圖的綁定,從而實(shí)現(xiàn)實(shí)現(xiàn)數(shù)據(jù)在頁面渲染,但是當(dāng)數(shù)據(jù)模型發(fā)生變化時(shí),視圖不能自動(dòng)更新;當(dāng)視圖數(shù)據(jù)發(fā)生變化時(shí),模型數(shù)據(jù)不能實(shí)現(xiàn)更新,這個(gè)時(shí)候雙向數(shù)據(jù)綁定應(yīng)運(yùn)而生。檢測(cè)視圖數(shù)據(jù)更新實(shí)現(xiàn)數(shù)據(jù)綁定的方法有很多種,目前主要分為三個(gè)流派,Angular使用的是臟檢查,只在特定的事件下才會(huì)觸發(fā)視圖刷新,Vue使用的是Getter/Setter機(jī)制,而React則是通過 Virtual DOM 算法檢查DOM的變動(dòng)的刷新機(jī)制。
本文限于篇幅和內(nèi)容在此只探討一下 Vue.js 數(shù)據(jù)綁定的實(shí)現(xiàn),對(duì)于 angular 和 react 后續(xù)再做說明,讀者也可以自行閱讀源碼。Vue 監(jiān)聽數(shù)據(jù)變化的機(jī)制是把一個(gè)普通 JavaScript 對(duì)象傳給 Vue 實(shí)例的 data 選項(xiàng),Vue 將遍歷此對(duì)象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。Vue 2.x 對(duì) Virtual DOM 進(jìn)行了支持,這部分內(nèi)容后續(xù)我們?cè)僮鎏接憽?/p> 引子
為了更好的理解Vue中視圖和數(shù)據(jù)更新的機(jī)制,我們先看一個(gè)簡單的例子:
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"
這里我們可以看出對(duì)象o的b屬性的值依賴于a屬性的值,同時(shí)b屬性值的變化又可以改變a屬性的值,這個(gè)過程相關(guān)的屬性值的變化都會(huì)影響其他相關(guān)的值進(jìn)行更新。反過來我們看看如果不使用Object.defineProperty()方法,上述的問題通過直接給對(duì)象屬性賦值的方法實(shí)現(xiàn),代碼如下:
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"
很顯然使用Object.defineProperty()方法可以更方便的監(jiān)聽一個(gè)對(duì)象的變化。當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時(shí)候,我們希望能夠通知對(duì)方也更新,這就是所謂的數(shù)據(jù)雙向綁定。既然明白這個(gè)道理我們就可以看看Vue源碼中相關(guān)的處理細(xì)節(jié)。
Object.defineProperty()Object.defineProperty()方法可以直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)已經(jīng)存在的屬性, 并返回這個(gè)對(duì)象。
語法:Object.defineProperty(obj, prop, descriptor)
參數(shù):
obj:需要定義屬性的對(duì)象。
prop:需被定義或修改的屬性名。
descriptor:需被定義或修改的屬性的描述符。
返回值:返回傳入函數(shù)的對(duì)象,即第一個(gè)參數(shù)obj
該方法重點(diǎn)是描述,對(duì)象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和存取描述符。數(shù)據(jù)描述符是一個(gè)擁有可寫或不可寫值的屬性。存取描述符是由一對(duì) getter-setter 函數(shù)功能來描述的屬性。描述符必須是兩種形式之一;不能同時(shí)是兩者。
數(shù)據(jù)描述符和存取描述符均具有以下可選鍵值:
configurable:當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時(shí),該屬性才能夠被改變,也能夠被刪除。默認(rèn)為 false。
enumerable:當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時(shí),該屬性才能夠出現(xiàn)在對(duì)象的枚舉屬性中。默認(rèn)為 false。
數(shù)據(jù)描述符同時(shí)具有以下可選鍵值:
value:該屬性對(duì)應(yīng)的值??梢允侨魏斡行У?JavaScript 值(數(shù)值,對(duì)象,函數(shù)等)。默認(rèn)為 undefined。
writable:當(dāng)且僅當(dāng)僅當(dāng)該屬性的writable為 true 時(shí),該屬性才能被賦值運(yùn)算符改變。默認(rèn)為 false。
存取描述符同時(shí)具有以下可選鍵值:
get:一個(gè)給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認(rèn)為undefined。
set:一個(gè)給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一參數(shù),并將該參數(shù)的新值分配給該屬性。默認(rèn)為undefined。
我們可以通過Object.defineProperty()方法精確添加或修改對(duì)象的屬性。比如,直接賦值創(chuàng)建的屬性默認(rèn)情況是可以枚舉的,但是我們可以通過Object.defineProperty()方法設(shè)置enumerable屬性為false為不可枚舉。
var obj = { a: 0, b: 1 } for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 結(jié)果: "obj.a = 0" "obj.b = 1"
我們通過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]}`); } 結(jié)果: "obj.a = 0"
這里需要說明的是我們使用Object.defineProperty()默認(rèn)情況下是enumerable屬性為false,例如:
var obj = { a: 0 } Object.defineProperty(obj, "b", { value: 1 }) for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 結(jié)果: "obj.a = 0"
其他描述屬性使用方法類似,不做贅述。Vue源碼core/util/lang.jsS中定義了這樣一個(gè)方法:
/** * 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 }) }Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
語法:Object.getOwnPropertyDescriptor(obj, prop)
參數(shù):
obj:在該對(duì)象上查看屬性
prop:一個(gè)屬性名稱,該屬性的屬性描述符將被返回
返回值:如果指定的屬性存在于對(duì)象上,則返回其屬性描述符(property descriptor),否則返回 undefined??梢栽L問“屬性描述符”內(nèi)容,例如前面的例子:
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);Vue源碼分析
本次我們主要分析一下Vue 數(shù)據(jù)綁定的源碼,這里我直接將 Vue.js 1.0.28 版本的代碼稍作刪減拿過來進(jìn)行,2.x 的代碼基于 flow 靜態(tài)類型檢查器書寫的,代碼除了編碼風(fēng)格在整體結(jié)構(gòu)上基本沒有太大改動(dòng),所以依然基于 1.x 進(jìn)行分析,對(duì)于存在差異的部分加以說明。
監(jiān)聽數(shù)組變動(dòng)上面我們通過Object.defineProperty把對(duì)象的屬性全部轉(zhuǎn)為 getter/setter 從而實(shí)現(xiàn)監(jiān)聽對(duì)象的變動(dòng),但是對(duì)于數(shù)組對(duì)象無法通過Object.defineProperty實(shí)現(xiàn)監(jiān)聽。Vue 包含一組觀察數(shù)組的變異方法,所以它們也將會(huì)觸發(fā)視圖更新。
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 }) } // 數(shù)組的變異方法 ;[ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ] .forEach(function (method) { // 緩存數(shù)組原始方法 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("數(shù)組變動(dòng)") return original.apply(this, args) }) })
Vue.js 1.x 在Array.prototype原型對(duì)象上添加了$set 和 $remove方法,在2.X后移除了,使用全局 API Vue.set 和 Vue.delete代替了,后續(xù)我們?cè)俜治觥?/p>
定義一個(gè)數(shù)組作為數(shù)據(jù)模型,并對(duì)這個(gè)數(shù)組調(diào)用變異的七個(gè)方法實(shí)現(xiàn)監(jiān)聽。
let skills = ["JavaScript", "Node.js", "html5"] // 原型指針指向具有變異方法的數(shù)組對(duì)象 skills.__proto__ = arrayMethods skills.push("java") // 數(shù)組變動(dòng) skills.pop() // 數(shù)組變動(dòng)
效果如下:
我們將需要監(jiān)聽的數(shù)組的原型指針指向我們定義的數(shù)組對(duì)象,這樣我們的數(shù)組在調(diào)用上面七個(gè)數(shù)組的變異方法時(shí),能夠監(jiān)聽到變動(dòng)從而實(shí)現(xiàn)對(duì)數(shù)組進(jìn)行跟蹤。
對(duì)于__proto__屬性,在ES2015中正式被加入到規(guī)范中,標(biāo)準(zhǔn)明確規(guī)定,只有瀏覽器必須部署這個(gè)屬性,其他運(yùn)行環(huán)境不一定需要部署,所以 Vue 是先進(jìn)行了判斷,當(dāng)__proto__屬性存在時(shí)將原型指針__proto__指向具有變異方法的數(shù)組對(duì)象,不存在時(shí)直接將具有變異方法掛在需要追蹤的對(duì)象上。
我們可以在上面Observer觀察者構(gòu)造函數(shù)中添加對(duì)數(shù)組的監(jiān)聽,源碼如下:
const hasProto = "__proto__" in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods) // 觀察者構(gòu)造函數(shù) 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) } } // 觀察數(shù)組的每一項(xiàng) Observer.prototype.observeArray = function (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]) } } // 將目標(biāo)對(duì)象/數(shù)組的原型指針__proto__指向src function protoAugment (target, src) { target.__proto__ = src } // 將具有變異方法掛在需要追蹤的對(duì)象上 function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] def(target, key, src[key]) } }
原型鏈
對(duì)于不了解原型鏈的朋友可以看一下我這里畫的一個(gè)基本關(guān)系圖:
原型對(duì)象是構(gòu)造函數(shù)的prototype屬性,是所有實(shí)例化對(duì)象共享屬性和方法的原型對(duì)象;
實(shí)例化對(duì)象通過new構(gòu)造函數(shù)得到,都繼承了原型對(duì)象的屬性和方法;
原型對(duì)象中有個(gè)隱式的constructor,指向了構(gòu)造函數(shù)本身。
Object.create
Object.create 使用指定的原型對(duì)象和其屬性創(chuàng)建了一個(gè)新的對(duì)象。
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto)
這一步是通過 Object.create 創(chuàng)建了一個(gè)原型對(duì)象為Array.prototype的空對(duì)象。然后通過Object.defineProperty方法對(duì)這個(gè)對(duì)象定義幾個(gè)變異的數(shù)組方法。有些新手可能會(huì)直接修改 Array.prototype 上的方法,這是很危險(xiǎn)的行為,這樣在引入的時(shí)候會(huì)全局影響Array 對(duì)象的方法,而使用Object.create實(shí)質(zhì)上是完全了一份拷貝,新生成的arrayMethods對(duì)象的原型指針__proto__指向了Array.prototype,修改arrayMethods 對(duì)象不會(huì)影響Array.prototype。
基于這種原理,我們通常會(huì)使用Object.create 實(shí)現(xiàn)類式繼承。
// 實(shí)現(xiàn)繼承 var extend = function(Child, Parent) { // 拷貝Parent原型對(duì)象 Child.prototype = Object.create(Parent.prototype); // 將Child構(gòu)造函數(shù)賦值給Child的原型對(duì)象 Child.prototype.constructor = Child; } // 實(shí)例 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())發(fā)布-訂閱模式
在上面一部分我們通過Object.defineProperty把對(duì)象的屬性全部轉(zhuǎn)為 getter/setter 以及 數(shù)組變異方法實(shí)現(xiàn)了對(duì)數(shù)據(jù)模型變動(dòng)的監(jiān)聽,在數(shù)據(jù)變動(dòng)的時(shí)候,我們通過console.log打印出來提示了,但是對(duì)于框架而言,我們相關(guān)的邏輯如果直接寫在那些地方,自然是不夠優(yōu)雅和靈活的,這個(gè)時(shí)候就需要引入常用的設(shè)計(jì)模式去實(shí)現(xiàn),vue.js采用了發(fā)布-訂閱模式。發(fā)布-訂閱模式主要是為了達(dá)到一種“高內(nèi)聚、低耦合"的效果。
Vue的Watcher訂閱者作為Observer和Compile之間通信的橋梁,能夠訂閱并收到每個(gè)屬性變動(dòng)的通知,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖。
/** * 觀察者對(duì)象 */ 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 () { // 當(dāng)前訂閱者(Watcher)讀取被訂閱數(shù)據(jù)的最新更新后的值時(shí),通知訂閱者管理員收集當(dāng)前訂閱者 Dep.target = this // 觸發(fā)getter,將自身添加到dep中 const value = this.getter.call(this.vm, this.vm) // 依賴收集完成,置空,用于下一個(gè)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 } } /** * 依賴變動(dòng)更新 * * @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實(shí)例上 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 } }
Dep 是一個(gè)數(shù)據(jù)結(jié)構(gòu),其本質(zhì)是維護(hù)了一個(gè)watcher隊(duì)列,負(fù)責(zé)添加watcher,更新watcher,移除watcher,通知watcher更新。
let uid = 0 function Dep() { this.id = uid++ this.subs = [] } Dep.target = null /** * 添加一個(gè)訂閱者 * * @param {Directive} sub */ Dep.prototype.addSub = function (sub) { this.subs.push(sub) } /** * 移除一個(gè)訂閱者 * * @param {Directive} sub */ Dep.prototype.removeSub = function (sub) { let index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } /** * 將自身作為依賴添加到目標(biāo)watcher */ Dep.prototype.depend = function () { Dep.target.addDep(this) } /** * 通知數(shù)據(jù)變更 */ Dep.prototype.notify = function () { var subs = toArray(this.subs) // stablize the subscriber list first for (var i = 0, l = subs.length; i < l; i++) { // 執(zhí)行訂閱者的update更新函數(shù) subs[i].update() } }模板編譯
compile主要做的事情是解析模板指令,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面視圖,并將每個(gè)指令對(duì)應(yīng)的節(jié)點(diǎn)綁定更新函數(shù),添加監(jiān)聽數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動(dòng),收到通知,更新視圖。
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 = /{{((?:.| )+?)}}/ // 處理element節(jié)點(diǎn) if (self.isElementNode(node)) { self.compile(node) } else if (self.isTextNode(node) && reg.test(text)) { // 處理text節(jié)點(diǎn) self.compileText(node, RegExp.$1.trim()) } // 解析子節(jié)點(diǎn)包含的指令 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) { // 非最后一個(gè)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 } }
這種實(shí)現(xiàn)和我們講到的Dom-based templating類似,只是更加完備,具有自定義指令的功能。在遍歷節(jié)點(diǎn)屬性和文本節(jié)點(diǎn)的時(shí)候,可以編譯具備{{}}表達(dá)式或v-xxx的屬性值的節(jié)點(diǎn),并且通過添加 new Watcher()及綁定事件函數(shù),監(jiān)聽數(shù)據(jù)的變動(dòng)從而對(duì)視圖實(shí)現(xiàn)雙向綁定。
MVVM實(shí)例在數(shù)據(jù)綁定初始化的時(shí)候,我們需要通過new Observer()來監(jiān)聽數(shù)據(jù)模型變化,通過new Compile()來解析編譯模板指令,并利用Watcher搭起Observer和Compile之間的通信橋梁。
/** * @class 雙向綁定類 MVVM * @param {[type]} options [description] */ function MVVM(options) { this.$options = options || {} // 簡化了對(duì)data的處理 let data = this._data = this.$options.data // 監(jiān)聽數(shù)據(jù) observe(data) new Compile(options.el || document.body, this) } MVVM.prototype.$watch = function (expOrFn, cb) { new Watcher(this, expOrFn, cb) }
為了能夠直接通過實(shí)例化對(duì)象操作數(shù)據(jù)模型,我們需要為MVVM實(shí)例添加一個(gè)數(shù)據(jù)模型代理的方法:
MVVM.prototype._proxy = function (key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: (val) => { this._data[key] = val } }) }
至此我們可以通過一個(gè)小例子來說明本文的內(nèi)容:
{{user.name}}
{{modelValue}}
本文目的不是為了造一個(gè)輪子,而是在學(xué)習(xí)優(yōu)秀框架實(shí)現(xiàn)的過程中去提升自己,搞清楚框架發(fā)展的前因后果,由淺及深去學(xué)習(xí)基礎(chǔ),本文參考了網(wǎng)上很多優(yōu)秀博主的文章,由于時(shí)間關(guān)系,有些內(nèi)容沒有做深入探討,覺得還是有些遺憾,在后續(xù)的學(xué)習(xí)中會(huì)更多的獨(dú)立思考,提出更多自己的想法。
參考文檔前端模板技術(shù)面面觀
Object.defineProperty()
Vue.js 源碼學(xué)習(xí)筆記
vue早期源碼學(xué)習(xí)系列
解析最簡單的observer和watcher
剖析Vue實(shí)現(xiàn)原理 - 如何實(shí)現(xiàn)雙向綁定mvvm
說明本文的完整代碼及圖片可以在這里下載:learn-javascript/mvvm
原文首發(fā)于 GitChat :http://gitbook.cn/books/593fa...,歡迎關(guān)注我的新話題:JavaScript 進(jìn)階之 Vue.js + Node.js 入門實(shí)戰(zhàn)開發(fā)。
我在segmentfault上有兩期講座,歡迎來圍觀:
html5+ App開發(fā)工程化實(shí)踐之路
html5+ App開發(fā)之 Android 平臺(tái)離線集成 5+ SDK
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/84563.html
摘要:首次運(yùn)行代碼時(shí),會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并到當(dāng)前的執(zhí)行棧中。執(zhí)行上下文的創(chuàng)建執(zhí)行上下文分兩個(gè)階段創(chuàng)建創(chuàng)建階段執(zhí)行階段創(chuàng)建階段確定的值,也被稱為。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,,今天是第一天 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)...
摘要:進(jìn)階期理解中的執(zhí)行上下文和執(zhí)行棧進(jìn)階期深入之執(zhí)行上下文棧和變量對(duì)象但是今天補(bǔ)充一個(gè)知識(shí)點(diǎn)某些情況下,調(diào)用堆棧中函數(shù)調(diào)用的數(shù)量超出了調(diào)用堆棧的實(shí)際大小,瀏覽器會(huì)拋出一個(gè)錯(cuò)誤終止運(yùn)行。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,今天是第3天。 本計(jì)劃一共28期,每期重點(diǎn)攻...
摘要:使用上一篇文章的例子來說明下自由變量進(jìn)階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數(shù),也不是局部變量,所以是自由變量。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第7天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)...
摘要:引擎對(duì)堆內(nèi)存中的對(duì)象進(jìn)行分代管理新生代存活周期較短的對(duì)象,如臨時(shí)變量字符串等。內(nèi)存泄漏對(duì)于持續(xù)運(yùn)行的服務(wù)進(jìn)程,必須及時(shí)釋放不再用到的內(nèi)存。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,今天是第4天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)劃...
摘要:本計(jì)劃一共期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)劃,點(diǎn)擊查看前端進(jìn)階的破冰之旅本期推薦文章深入之執(zhí)行上下文棧和深入之變量對(duì)象,由于微信不能訪問外鏈,點(diǎn)擊閱讀原文就可以啦。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,今天是第二天。 本計(jì)劃一共28期,每期...
閱讀 1007·2023-04-26 01:47
閱讀 1685·2021-11-18 13:19
閱讀 2056·2019-08-30 15:44
閱讀 670·2019-08-30 15:44
閱讀 2310·2019-08-30 15:44
閱讀 1246·2019-08-30 14:06
閱讀 1433·2019-08-30 12:59
閱讀 1909·2019-08-29 12:49