深刻解析vue 1實現原理,並實現vue雙向數據綁定模型
vueImitate
,此模型(vueImitate)只適用於學習和了解vue實現原理;沒法做爲項目中使用,沒有進行任何異常錯誤處理及各類使用場景的兼容;但經過此項目,可讓你:
總體效果以下:html
下面咱們重頭開始框架的實現,咱們知道,vue的使用方式以下:vue
var vm = new Vue({ el: 'root', data() { return { message: 'this is test', number: 5, number1: 1, number2: 2, showNode: false } }, methods: { add() { this.number1 += 1; this.number += 1; }, show() { this.showNode = !this.showNode; } } })
因而可知,vue爲一個構造函數,而且調用時傳入一個對象參數,因此主函數vueImitate
能夠以下,源碼可見這裏;並對參數進行對應的初始化處理:node
// init.js export default function vueImitate(options) { this.options = options || {}; this.selector = options.el ? ('#' + options.el) : 'body'; // 根節點selector this.data = typeof options.data === 'function' ? options.data() : options.data; // 保存傳入的data this.el = document.querySelectorAll(this.selector)[0]; // 保存根節點 this._directives = []; }
此時可使用new vueImitate(options)
的方式進行調用,首先,咱們須要界面上展現正確的數據,也就是將下面頁面進行處理,使其能夠正常訪問;git
咱們能夠參考vue的實現方式,vue將{{ }}
這種綁定數據的方式轉化爲指令(directive),即v-text
相似;而v-text
又是如何進行數據綁定的呢?經過下面代碼可知,是經過對文本節點從新賦值方式實現,源碼見這裏:github
export default { bind () { this.attr = this.el.nodeType === 3 ? 'data' : 'textContent' }, update (value) { this.el[this.attr] = value } }
那麼,問題來了,若是須要按照上面的方式實現數據的綁定,咱們須要將如今的字符串{{number}}
轉化爲一個文本節點,並對它進行指令化處理;這些其實也就是vue compile(編譯)、link過程完成的,下面咱們就先實現上面功能需求;express
整個編譯過程確定從根元素開始,逐步向子節點延伸處理;數組
export default function Compile(vueImitate) { vueImitate.prototype.compile = function() { let nodeLink = compileNode(this.el), nodeListLink = compileNodeList(this.el.childNodes, this), _dirLength = this._directives.length; nodeLink && nodeLink(this); nodeListLink && nodeListLink(this); let newDirectives = this._directives.slice(_dirLength); for(let i = 0, _i = newDirectives.length; i < _i; i++) { newDirectives[i]._bind(); } } } function compileNode(el) { let textLink, elementLink; // 編譯文本節點 if(el.nodeType === 3 && el.data.trim()) { textLink = compileTextNode(el); } else if(el.nodeType === 1) { elementLink = compileElementNode(el); } return function(vm) { textLink && textLink(vm); elementLink && elementLink(vm); } } function compileNodeList(nodeList, vm) { let nodeLinks = [], nodeListLinks = []; if(!nodeList || !nodeList.length) { return; } for(let i = 0, _i = nodeList.length; i < _i; i++) { let node = nodeList[i]; nodeLinks.push(compileNode(node)), nodeListLinks.push(compileNodeList(node.childNodes, vm)); } return function(vm) { if(nodeLinks && nodeLinks.length) { for(let i = 0, _i = nodeLinks.length; i < _i; i++) { nodeLinks[i] && nodeLinks[i](vm); } } if(nodeListLinks && nodeListLinks.length) { for(let i = 0, _i = nodeListLinks.length; i < _i; i++) { nodeListLinks[i] && nodeListLinks[i](vm); } } } }
如上代碼,首先,咱們經過定義一個Compile
函數,將編譯方法放到構造函數vueImitate.prototype
,而方法中,首先主要使用compileNode
編譯根元素,而後使用compileNodeList(this.el.childNodes, this)
編譯根元素下面的子節點;而在compileNodeList
中,經過對子節點進行循環,繼續編譯對應節點及其子節點,以下代碼:性能優化
// function compileNodeList for(let i = 0, _i = nodeList.length; i < _i; i++) { let node = nodeList[i]; nodeLinks.push(compileNode(node)), nodeListLinks.push(compileNodeList(node.childNodes, vm)); }
而後進行遞歸調用,直到最下層節點:而在對節點進行處理時,主要分爲文本節點和元素節點;文本節點主要處理上面說的{{number}}
的編譯,元素節點主要處理節點屬性如v-model
、v-text
、v-show
、v-bind:click
等處理;app
function compileTextNode(el) { let tokens = parseText(el.wholeText); var frag = document.createDocumentFragment(); for(let i = 0, _i = tokens.length; i < _i; i++) { let token = tokens[i], el = document.createTextNode(token.value) frag.appendChild(el); } return function(vm) { var fragClone = frag.cloneNode(true); var childNodes = Array.prototype.slice.call(fragClone.childNodes), token; for(let j = 0, _j = tokens.length; j < _j; j++) { if((token = tokens[j]) && token.tag) { let _el = childNodes[j], description = { el: _el, token: tokens[j], def: publicDirectives['text'] } vm._directives.push(new Directive(vm, _el, description)) } } // 經過這兒將`THIS IS TEST {{ number }} test` 這種轉化爲三個textNode if(tokens.length) { replace(el, fragClone); } } } function compileElementNode(el) { let attrs = getAttrs(el); return function(vm) { if(attrs && attrs.length) { attrs.forEach((attr) => { let name = attr.name, description, matched; if(bindRE.test(attr.name)) { description = { el: el, def: publicDirectives['bind'], name: name.replace(bindRE, ''), value: attr.value } } else if((matched = name.match(dirAttrRE))) { description = { el: el, def: publicDirectives[matched[1]], name: matched[1], value: attr.value } } if(description) { vm._directives.push(new Directive(vm, el, description)); } }) } } }
這裏,先主要說明對文本節點的處理,咱們上面說過,咱們須要對{{number}}
之類進行處理,咱們首先必須將其字符串轉化爲文本節點,如this is number1: {{number1}}
這種,咱們必須轉換爲兩個文本節點,一個是this is number1:
,它不須要進行任何處理;另外一個是{{number1}}
,它須要進行數據綁定,並實現雙向綁定;由於只有轉化爲文本節點,才能使用v-text
相似功能實現數據的綁定;而如何進行將字符串文本分割爲不一樣的文本節點呢,那麼,就只能使用正則方式let reg = /\{\{(.+?)\}\}/ig;
將{{ number }}
這種形式數據與普通正常文本分割以後,再分別建立textNode
,以下:框架
function parseText(str) { let reg = /\{\{(.+?)\}\}/ig; let matchs = str.match(reg), match, tokens = [], index, lastIndex = 0; while (match = reg.exec(str)) { index = match.index if (index > lastIndex) { tokens.push({ value: str.slice(lastIndex, index) }) } tokens.push({ value: match[1], html: match[0], tag: true }) lastIndex = index + match[0].length } return tokens; }
經過上面parseText
方法,能夠將this is number: {{number}}
轉化爲以下結果:
轉化爲上圖結果後,就對返回數組進行循環,分別經過建立文本節點;這兒爲了性能優化,先建立文檔碎片,將節點放入文檔碎片中;
// function compileTextNode // el.wholeText => 'this is number: {{number}}' let tokens = parseText(el.wholeText); var frag = document.createDocumentFragment(); for(let i = 0, _i = tokens.length; i < _i; i++) { let token = tokens[i], el = document.createTextNode(token.value) frag.appendChild(el); }
而在最後編譯完成,執行linker
時,主要作兩件事,第一是對須要雙向綁定的節點建立directive
,第二是將整個文本節點進行替換;怎麼替換呢?如最開始是一個文本節點this is number: {{number}}
,通過上面處理以後,在frag
中實際上是兩個文本節點this is number:
和{{number}}
;此時就使用replaceChild
方法使用新的節點替換原始的節點;
// compile.js function compileTextNode(el) { let tokens = parseText(el.wholeText); var frag = document.createDocumentFragment(); for(let i = 0, _i = tokens.length; i < _i; i++) { let token = tokens[i], el = document.createTextNode(token.value) frag.appendChild(el); } return function(vm) { var fragClone = frag.cloneNode(true); var childNodes = Array.prototype.slice.call(fragClone.childNodes), token; // 建立directive ...... // 經過這兒將`THIS IS TEST {{ number }} test` 這種轉化爲三個textNode if(tokens.length) { replace(el, fragClone); } } } // util.js export function replace (target, el) { var parent = target.parentNode if (parent) { parent.replaceChild(el, target) } }
替換後結果以下圖:
通過與最開始圖比較能夠發現,已經將this is number: {{number}} middle {{number2}}
轉化爲this is number: number middle number2
;只是此時,仍然展現的是變量名稱,如number
,number2
;那麼,咱們下面應該作的確定就是須要根據咱們初始化時傳入的變量的值,將其進行正確的展現;最終結果確定應該爲this is number: 5 middle 2
;即將number
替換爲5
、將number2
替換爲2
;那麼,如何實現上述功能呢,咱們上面提過,使用指令(directive)的方式;下面,就開始進行指令的處理;
對於每個指令,確定是隔離開的,互相不受影響且有本身的一套處理方式;因此,咱們就使用對象的方式;一個指令就是一個實例化的對象,彼此之間互不影響;以下代碼:
export default function Directive(vm, el, description) { this.vm = vm; this.el = el; this.description = description; this.expression = description ? description.value : ''; }
在建立一個指令時,須要傳入三個參數,一個是最開始初始化var vm = new vueImitate(options)
時實例化的對象;而el是須要初始化指令的當前元素,如<p v-show="showNode">this is test</p>
,須要建立v-show
的指令,此時的el
就是當前的p
標籤;而description
主要包含指令的描述信息;主要包含以下:
// 源碼見 './directives/text.js' var text = { bind () { this.attr = this.el.nodeType === 3 ? 'data' : 'textContent' }, update (value) { this.el[this.attr] = value } } // 如,'{{number}}' description = { el: el, // 須要建立指令的元素 def: text, // 對指令的操做方法,包括數據綁定(bind)、數據更新(update),見上面 text name: 'text', // 指令名稱 value: 'number' // 指令對應數據的key }
經過new Directive(vm, el, description)
就建立了一個指令,並初始化一些數據;下面就先經過指令對界面進行數據渲染;全部邏輯就放到了_bind
方法中,以下:
// directive.js Directive.prototype._bind = function() { extend(this, this.description.def); if(this.bind) { this.bind(); } var self = this, watcher = new Watcher(this.vm, this.expression, function() { self.update(watcher.value); }) if(this.update) { this.update(watcher.value); } } // util.js export function extend(to, from) { Object.keys(from).forEach((key) => { to[key] = from[key]; }) return to; }
方法首先將傳入的指令操做方法合併到this
上,方便調用,主要包括上面說的bind
、update
等方法;其主要根據指令不一樣,功能不一樣而不一樣定義;全部對應均在./directives/*
文件夾下面,包括文本渲染text.js、事件添加bind.js、v-model對應model.js、v-show對應show.js等;經過合併之後,就執行this.bind()
方法進行數據初始化綁定;可是,目前爲止,當去看界面時,仍然沒有將number
轉化爲5
;爲何呢?經過查看代碼:
export default { bind () { this.attr = this.el.nodeType === 3 ? 'data' : 'textContent' }, update (value) { this.el[this.attr] = value } }
bind
並無改變節點展現值,而是經過update
; 因此,若是調用this.update(123)
,可發現有以下結果:
其實咱們並非直接固定數值,而是根據初始化時傳入的值動態渲染;可是目前爲止,至少已經完成了界面數據的渲染,只是數據不對而已;
而後,咱們回頭看下編譯過程,咱們須要在編譯過程去實例化指令(directive),並調用其_bind
方法,對指令進行初始化處理;
// 見compile.js 'function compileTextNode' let _el = childNodes[j], description = { el: _el, name: 'text', value: tokens[j].value, def: publicDirectives['text'] } vm._directives.push(new Directive(vm, _el, description)); // 見compile.js 'function compile' let newDirectives = this._directives.slice(_dirLength); for(let i = 0, _i = newDirectives.length; i < _i; i++) { newDirectives[i]._bind(); }
上面說了,目前尚未根據傳入的數據進行綁定,下面,就來對數據進行處理;
數據處理包括如下幾個方面:
須要實現雙向綁定,就是在數據變化後可以自動的將對應界面進行更新;那麼,如何監控數據的變化呢?目前有幾種方式,一種是angular的髒檢查方式,就是對用戶因此操做、會致使數據變化的行爲進行攔截,如ng-click
、$http
、$timeout
等;當用戶進行請求數據、點擊等時,會對全部的數據進行檢查,若是數據變化了,就會觸發對應的處理;而另外一種是vue的實現方式,使用Object.definProperty()
方法,對數據添加setter
和getter
;當對數據進行賦值時,會自動觸發setter
;就能夠監控數據的變化;主要處理以下, 源碼見這裏:
export function Observer(data) { this.data = data; Object.keys(data).forEach((key) => { defineProperty(data, key, data[key]); }) } export function observer(data, vm) { if(!data || typeof data !== 'object') { return; } let o = new Observer(data); return o; } function defineProperty(data, key, val) { let _value = data[key]; let childObj = observer(_value); let dep = new Dep(); //生成一個調度中心,管理此字段的全部訂閱者 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { if (Dep.target) { dep.depend(); } return val; }, set: function(value) { val = value; childObj = observer(value); dep.notify(); } }) }
Observer
是一個構造函數,主要對傳入的數據進行Object.defineProperty
綁定;能夠監控到數據的變化;而在每個Observer中,會初始化一個Dep
的稱爲‘調度管理器’的對象,它主要負責保存界面更新的操做和操做的觸發;
在經過上面Observer
實現數據監控以後,如何通知界面更新呢?這裏使用了‘發佈/訂閱模式’;若是須要對此模式進行更深刻理解,可查看此連接;而每一個數據key都會維護了一個獨立的調度中心Dep
;經過在上面defineProperty
時建立;而Dep
主要保存數據更新後的處理任務及對任務的處理,代碼也很是簡單,就是使用subs
保存全部任務,使用addSub
添加任務,使用notify
處理任務,depend
做用會在下面watcher
中進行說明:
// Dep.js let uid = 0; // 調度中心 export default function Dep() { this.id = uid++; this.subs = []; //訂閱者數組 this.target = null; // 有何用處? } // 添加任務 Dep.prototype.addSub = function(sub) { this.subs.push(sub); } // 處理任務 Dep.prototype.notify = function() { this.subs.forEach((sub) => { if(sub && sub.update && typeof sub.update === 'function') { sub.update(); } }) } Dep.prototype.depend = function() { Dep.target.addDep(this); }
那麼,處理任務來源哪兒呢?vue中又維護了一個watcher
的對象,主要是對任務的初始化和收集處理;也就是一個watcher
就是一個任務;而整個watcher
代碼以下, 線上源碼見這裏:
export default function Watcher(vm, expression, cb) { this.cb = cb; this.vm = vm; this.expression = expression; this.depIds = {}; if (typeof expression === 'function') { this.getter = expOrFn; } else { this.getter = this.parseGetter(expression); } this.value = this.get(); } let _prototype = Watcher.prototype; _prototype.update = function() { this.run(); } _prototype.run = function() { let newValue = this.get(), oldValue = this.value; if(newValue != oldValue) { this.value = newValue; this.cb.call(this.vm, newValue); } } _prototype.addDep = function(dep) { // console.log(dep) if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this); this.depIds[dep.id] = dep; } } _prototype.get = function() { Dep.target = this; var value = this.getter && this.getter.call(this.vm, this.vm); Dep.target = null; return value; } _prototype.parseGetter = function(exp) { if (/[^\w.$]/.test(exp)) return; var exps = exp.split('.'); return function(obj) { let value = ''; for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return; value = obj[exps[i]]; } return value; } }
在初始化watcher
時,須要傳入vm(整個項目初始化時實例化的vueImitate對象,由於須要用到裏面的對應數據)、expression(任務對應的數據的key,如上面的‘number’)、cb(一個當數據變化後,界面如何更新的函數,也就是上面directive裏面的update方法);咱們須要實現功能有,第一是每一個任務有個update
方法,主要用於在數據變化時,進行調用,即:
// 處理任務 Dep.prototype.notify = function() { this.subs.forEach((sub) => { if(sub && sub.update && typeof sub.update === 'function') { sub.update(); } }) }
第二個是在初始化watcher
時,須要將實例化的watcher(任務)放入調度中心dep
的subs
中;如何實現呢?這裏,使用了一些黑科技,流程以下,這兒咱們以expression
爲'number'爲例:
一、在初始化watcher時,會去初始化一個獲取數據的方法this.getter
就是,可以經過傳入的expression
取出對應的值;如經過number
取出對應的初始化時的值5
;
二、調用this.value = this.get();
方法,方法中會去數據源中取值,並將此時的watcher放入Dep.target
中備用,並返回取到的值;
// watcher.js _prototype.get = function() { Dep.target = this; var value = this.getter && this.getter.call(this.vm, this.vm); Dep.target = null; return value; }
三、由於咱們在上面Observer
已經對數據進行了Object.defineProperty
綁定,因此,當上面2步取值時,會觸發對應的getter
,以下, 觸發get函數以後,由於上面2已經初始化Dep.target = this;
了,因此會執行dep.depend();
,就是上面說的depend
函數了:
// Observer.js let dep = new Dep(); //生成一個調度中心,管理此字段的全部訂閱者 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { if (Dep.target) { dep.depend(); } return val; }, set: function(value) { val = value; childObj = observer(value); dep.notify(); } })
三、觸發dep.depend();
以後,以下代碼,會執行Dep.target.addDep(this);
, 此時的this
就是上面實例化的dep
, Dep.target
則對應的是剛剛1步中實例化的watcher
,即執行watcher.addDep(dep)
;
// Dep.js Dep.prototype.depend = function() { Dep.target.addDep(this); }
四、觸發watcher.addDep(dep)
,以下代碼,若是目前還沒此dep;就執行dep.addSub(this);
,此時的this
就是指代當前watcher
,也就是1步時實例化的watcher;此時dep是步驟3中實例化的dep
; 便是,dep.addSub(watcher);
// watcher.js _prototype.addDep = function(dep) { // console.log(dep) if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this); this.depIds[dep.id] = dep; } }
五、最後執行dep.addSub(watcher);
,以下代碼,到這兒,就將初始化的watcher
添加到了調度中心的數組中;
// Dep.js Dep.prototype.addSub = function(sub) { this.subs.push(sub); }
那麼,在哪兒去初始化watcher
呢?就是在對指令進行_bind()
時,以下代碼,在執行_bind
時,會實例化Watcher
; 在第三個參數的回調函數裏執行self.update(watcher.value);
,也就是當監控到數據變化,會執行對應的update
方法進行更新;
// directive.js Directive.prototype._bind = function() { extend(this, this.description.def); if(this.bind) { this.bind(); } var self = this, watcher = new Watcher(this.vm, this.expression, function() { self.update(watcher.value); }) if(this.update) { this.update(watcher.value); } }
而前面說了,開始時沒有數據,使用this.update(123)
會將界面對應number
更新爲123,當時沒有對應number
真實數據;而此時,在watcher中,獲取到了對應數據並保存到value
中,所以,就執行this.update(watcher.value);
,此時就能夠將真實數據與界面進行綁定,而且當數據變化時,界面也會自動進行更新;最終結果以下圖:
爲何全部數據都是undefined
呢?咱們能夠經過下面代碼知道, 在實例化watcher
時,調用this.value = this.get();
時,實際上是經過傳入的key在this.vm
中直接取值;可是咱們初始化時,全部值都是經過this.options = options || {};
放到this.options
裏面,因此根本沒法取到:
// watcher.js _prototype.get = function() { Dep.target = this; var value = this.getter && this.getter.call(this.vm, this.vm); Dep.target = null; return value; } _prototype.parseGetter = function(exp) { if (/[^\w.$]/.test(exp)) return; var exps = exp.split('.'); return function(obj) { let value = ''; for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return; value = obj[exps[i]]; } return value; } }
那麼,咱們如何能直接能夠經過諸如this.number
取到值呢?只能以下,經過下面extend(this, data);
方式,就將數據綁定到了實例化的vueImitate
上面;
import { extend } from './util.js'; import { observer } from './Observer.js'; import Compile from './compile.js'; export default function vueImitate(options) { this.options = options || {}; this.selector = options.el ? ('#' + options.el) : 'body'; this.data = typeof options.data === 'function' ? options.data() : options.data; this.el = document.querySelectorAll(this.selector)[0]; this._directives = []; this.initData(); this.compile(); } Compile(vueImitate); vueImitate.prototype.initData = function() { let data = this.data, self = this; extend(this, data); observer(this.data); }
處理後結果以下:
數據也綁定上了,可是當咱們嘗試使用下面方式對數據進行改變時,發現並無自動更新到界面,界面數據並無變化;
methods: { add() { this.number1 += 1; this.number += 1; } }
爲何呢?經過上面代碼可知,咱們其實observer
的是vueImitate
實例化對象的data
對象;而咱們更改值是經過this.number += 1;
實現的;其實並無改vueImitate.data.number
的值,而是改vueImitate.number
的值,因此也就不會觸發observer
裏面的setter
;也不會去觸發對應的watcher
裏面的update
;那如何處理呢?咱們能夠經過以下方式實現, 完整源碼見這裏:
// init.js vueImitate.prototype.initData = function() { let data = this.data, self = this; extend(this, data); Object.keys(data).forEach((key) => { Object.defineProperty(self, key, { set: function(newVal) { self.data[key] = newVal; }, get: function() { return self.data[key]; } }) }) observer(this.data); }
這裏經過對vueImitate
裏對應的data
的屬性進行Object.defineProperty
處理,當對其進行賦值時,會再將其值賦值到vueImitate.data
對應的屬性上面,那樣,就會去觸發observer(this.data);
裏面的setter
,從而去更新界面數據;
至此,整個數據處理就已經完成,總結一下:
一、首先,在初始化vueImitate
時,咱們會將初始化數據經過options.data
傳入,後會進行處理,保存至this.data
中;
二、經過initData
方法將數據綁定到vueImitate
實例化對象上面,並對其進行數據監控,而後使用observer
對this.data
進行監控,在實例化Observer
時,會去實例化一個對應的調度中心Dep
;
三、在編譯過程當中,會建立指令,經過指令實現每一個須要處理節點的數據處理和雙向綁定;
四、在指令_bind()
時,會去實例化對應的watcher
,建立一個任務,主要實現數據獲取、數據變化時,對應界面更新(也就是更新函數的調用)、並將生成的watcher存儲到對應的步驟2中實例化的調度中心中;
五、當數據更新時,會觸發對應的setter
,而後調用dep.notify();
觸發調度中心中全部任務的更新,即執行全部的watcher.update
,從而實現對應界面的更新;
到目前爲止,整個框架的實現基本已經完成。其中包括compile、linker、oberver、directive(v-model、v-show、v-bind、v-text)、watcher;若是須要更深刻的研究,可見項目代碼; 能夠本身clone
下來,運行起來;文中有些可能思考不夠充分,忘見諒,也歡迎你們指正;