本文會帶你們手動實現一個雙向綁定過程(僅僅涵蓋一些簡單的指令解析,如:v-text
,v-model
,插值),固然借鑑的是Vue1的源碼,相信你們在閱讀完本文後對Vue1會有一個更好的理解,源代碼放到了github,因爲本人水平有限,理解不到位的地方還請你們指出。css
MVVM
使開發能夠更加關注於數據,減小了很大的工做量,也使代碼可讀性,可維護性更高,MVVM
核心的思想就是視圖是狀態的函數:View = ViewModel(Model),因此當Model發生改變時,ViewModel會來操做View來怎麼作,而非是本身寫代碼來作。不管是雙向綁定仍是單向綁定,都是符合MVVM
思想的。Vue提倡的是雙向綁定,也就是容許View到Model的變化,其實這個場景出如今的也就是表單操做上,看個例子,例子中分別利用了Vue和React實現了一下表單value
變化,影響頁面與其相關的dom
節點發生變化,能夠發現的是雙向綁定的Vue是input
的value
發生變化則h1
的innerText
就發生了變化,變化是由View->Model,而提倡單向數據流的React
須要手動監聽事件,事件觸發後,更改Model的值,從而使input
的value
發生了變化。看了Vue的源碼後不難發現Vue的雙向綁定的實現也就是在表單元素上添加了input
事件,能夠說雙向綁定是單向綁定的一個語法糖。html
上圖是一個大致的流程,下面按照流程來實現下:node
利用observer
對data
進行了監聽,而且提供訂閱某個數據項的變化的能力git
這點的實現,須要藉助的是Object.defineProperty()
來爲對象的屬性綁定get/set
特性(因爲利用了Object.defineProperty()
,因此Vue不支持ie8),observer
須要將data
的全部屬性都綁定get/set
,很容易想到的就是利用遞歸來實現,具體代碼就不貼出,請參見這裏。github
利用Compile
對模板進行解析app
這點實現的是將咱們的模板轉化爲html
,過程當中會將數據與View中的節點相關聯起來,最終會將編譯好的html
頁面替換到頁面上。首先來看解析,首先從根節點開始,根據不一樣的節點類型採用不一樣的解析方式:dom
function compileNode(node, vm) { const type = node.nodeType; if (type === 1 && !isScript(node)) { compileElement(node, vm); } else if (type === 3 && node.data.trim()) { compileTextNode(node, vm); } else { return null; } }
對於文本節點來講,可能存在狀況只有兩種:異步
與數據不相關不用操做函數
含有插值,須要與數據進行關聯優化
{{}}
文本插值
{{{}}}
純html
插值
利用下面正就能夠將插值找出:
/\{\{\{(.*?)\}\}\}|\{\{(.*?)\}\}/g
採用下面函數來對文本節點的內容解析:
function parseText(node) { var text = node.wholeText; if (!tagRE.test(text)) { return void 0; } const tokens = []; var lastIndex = tagRE.lastIndex = 0, match, index, html, value; while (match = tagRE.exec(text)) { index = match.index; if (index > lastIndex) { tokens.push({ value: text.slice(lastIndex, index) }) } html = htmlRE.test(match[0]); value = html ? match[1] : match[2]; tokens.push({ value: value, tag: true, html: html }); lastIndex = index + match[0].length; } if (lastIndex < text.length) { tokens.push({ value: text.slice(lastIndex) }) } return tokens; }
返回了tokens
,裏面存儲了每個塊內容,一個插值or一個普通文本,tag
來標記是否爲插值,html
來標記是否爲純html
插值。遍歷返回的tokens
,根據不一樣的類型,來採用不一樣的方式將其添加到其父節點上:
function compileTextNode(node, vm) { const tokens = parseText(node); if (tokens == null) return void 0; var frag = document.createDocumentFragment(); tokens.forEach(token => { var el; if (token.tag) { if (token.html) { el = document.createDocumentFragment(); el.$parent = node.parentNode; el.$oneTime = true; dirCollection["html"](el, vm, token.value); } else { el = document.createTextNode(" "); dirCollection["text"](el, vm, token.value); } } else { el = document.createTextNode(token.value); } el && frag.appendChild(el); }); return replace(node, frag); }
dirCollection
是一個指令集合,也就是決定了如何初始化以及如何更新該節點。對於nodeType
爲1
的節點來講,指令所有存儲在其屬性中,遍歷屬性,倘若指令中含有v-html,v-model,v-text
,則中止遍歷其子樹,直接將調用相應指令便可,不然,則須要遍歷其子節點,對其子節點應用compileNode
進行解析:
function compileNodeList(nodes, vm) { for (let val of nodes) { compileNode(val, vm); } } function compileElement(node, vm) { var flag = false; const attrs = Array.prototype.slice.call(node.attributes); attrs.forEach((val) => { const name = val.name, value = val.value; if (dirRE.test(name)) { var dir; // 事件指令 if ( (dir = name.match(eventRE)) && (dir = dir[1]) ) { dirCollection["eventDir"](node, dir, vm, value); } else { dir = name.match(dirRE)[1]; dirCollection[dir](node, vm, value); } // 指令中爲v-html or v-text or v-model終止遞歸 flag = flag || name === vhtml || name === vtext; node.removeAttribute(name); } }); const childs = node.childNodes; if (!flag && childs && childs.length) { compileNodeList(childs, vm); } }
在dirCollections
中還會作的就是將數據與View的dom
節點相關聯,利用的就是Dep
與Watcher
,頁面上每個與數據相關聯的節點都含有一個Watcher
,當數據發生變化是Watcher
用於計算,是否須要更新該節點;數據的每個屬性都有一個Dep
,當該屬性發生變化時,Dep
會通知與該數據相關聯的Watcher
來進行計算是否須要更新對應頁面。Dep代碼,Watcher代碼。
異步更新隊列
異步更新隊列,是一個優化,將更新dom
的操做變爲異步的,放到下一個事件循環來作,這樣作能夠減小沒必要要的dom
更新,看下面狀況:
vm.value++; vm.value++; vm.value++;
三次數據改變,倘若同步更新的話,則每次數據改變會當即更新dom
,而異步更新的話,能夠先將更新推入一個隊列中,因爲是異步,也能夠保證每個Watcher
只被推入到一次,這樣就避免了沒必要要的更新,異步更新主要利用的是nextTick
,這個函數會優先使用Promise
,不兼容則利用MutationObserver
,再不兼容的話會利用setTimeout
。
看過了Vue的源碼不得不感嘆Vue的優美,而Vue2又增長了虛擬dom,這樣就能夠作到服務端渲染,給了咱們更多的可能!
這篇博客最好配合着源碼來看,關於源碼歡迎star