摘要:深入解析實(shí)現(xiàn)原理,并實(shí)現(xiàn)雙向數(shù)據(jù)綁定模型,此模型只適用于學(xué)習(xí)和了解實(shí)現(xiàn)原理無法作為項(xiàng)目中使用,沒有進(jìn)行任何異常錯誤處理及各種使用場景的兼容但通過此項(xiàng)目,可以讓你深入了解實(shí)現(xiàn)原理親手一步一步自己實(shí)現(xiàn)相應(yīng)功能,包括雙向綁定指令如等整體效果
深入解析vue 1實(shí)現(xiàn)原理,并實(shí)現(xiàn)vue雙向數(shù)據(jù)綁定模型vueImitate,此模型(vueImitate)只適用于學(xué)習(xí)和了解vue實(shí)現(xiàn)原理;無法作為項(xiàng)目中使用,沒有進(jìn)行任何異常錯誤處理及各種使用場景的兼容;但通過此項(xiàng)目,可以讓你:
深入了解vue實(shí)現(xiàn)原理
親手一步一步自己實(shí)現(xiàn)vue相應(yīng)功能,包括雙向綁定、指令如v-model、v-show、v-bind等
整體效果如下:
下面我們重頭開始框架的實(shí)現(xiàn),我們知道,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為一個構(gòu)造函數(shù),并且調(diào)用時傳入一個對象參數(shù),所以主函數(shù)vueImitate可以如下,源碼可見這里;并對參數(shù)進(jìn)行對應(yīng)的初始化處理:
// init.js export default function vueImitate(options) { this.options = options || {}; this.selector = options.el ? ("#" + options.el) : "body"; // 根節(jié)點(diǎn)selector this.data = typeof options.data === "function" ? options.data() : options.data; // 保存?zhèn)魅氲膁ata this.el = document.querySelectorAll(this.selector)[0]; // 保存根節(jié)點(diǎn) this._directives = []; }
此時可以使用new vueImitate(options)的方式進(jìn)行調(diào)用,首先,我們需要界面上展示正確的數(shù)據(jù),也就是將下面頁面進(jìn)行處理,使其可以正常訪問;
我們可以參考vue的實(shí)現(xiàn)方式,vue將{{ }}這種綁定數(shù)據(jù)的方式轉(zhuǎn)化為指令(directive),即v-text類似;而v-text又是如何進(jìn)行數(shù)據(jù)綁定的呢?通過下面代碼可知,是通過對文本節(jié)點(diǎn)重新賦值方式實(shí)現(xiàn),源碼見這里:
export default { bind () { this.attr = this.el.nodeType === 3 ? "data" : "textContent" }, update (value) { this.el[this.attr] = value } }
那么,問題來了,如果需要按照上面的方式實(shí)現(xiàn)數(shù)據(jù)的綁定,我們需要將現(xiàn)在的字符串{{number}}轉(zhuǎn)化為一個文本節(jié)點(diǎn),并對它進(jìn)行指令化處理;這些其實(shí)也就是vue compile(編譯)、link過程完成的,下面我們就先實(shí)現(xiàn)上面功能需求;
compile整個編譯過程肯定從根元素開始,逐步向子節(jié)點(diǎn)延伸處理;
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; // 編譯文本節(jié)點(diǎn) 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函數(shù),將編譯方法放到構(gòu)造函數(shù)vueImitate.prototype,而方法中,首先主要使用compileNode編譯根元素,然后使用compileNodeList(this.el.childNodes, this)編譯根元素下面的子節(jié)點(diǎn);而在compileNodeList中,通過對子節(jié)點(diǎn)進(jìn)行循環(huán),繼續(xù)編譯對應(yīng)節(jié)點(diǎn)及其子節(jié)點(diǎn),如下代碼:
// 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)); }
然后進(jìn)行遞歸調(diào)用,直到最下層節(jié)點(diǎn):而在對節(jié)點(diǎn)進(jìn)行處理時,主要分為文本節(jié)點(diǎn)和元素節(jié)點(diǎn);文本節(jié)點(diǎn)主要處理上面說的{{number}}的編譯,元素節(jié)點(diǎn)主要處理節(jié)點(diǎn)屬性如v-model、v-text、v-show、v-bind:click等處理;
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` 這種轉(zhuǎn)化為三個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)); } }) } } }
這里,先主要說明對文本節(jié)點(diǎn)的處理,我們上面說過,我們需要對{{number}}之類進(jìn)行處理,我們首先必須將其字符串轉(zhuǎn)化為文本節(jié)點(diǎn),如this is number1: {{number1}}這種,我們必須轉(zhuǎn)換為兩個文本節(jié)點(diǎn),一個是this is number1: ,它不需要進(jìn)行任何處理;另一個是{{number1}},它需要進(jìn)行數(shù)據(jù)綁定,并實(shí)現(xiàn)雙向綁定;因?yàn)橹挥修D(zhuǎn)化為文本節(jié)點(diǎn),才能使用v-text類似功能實(shí)現(xiàn)數(shù)據(jù)的綁定;而如何進(jìn)行將字符串文本分割為不同的文本節(jié)點(diǎn)呢,那么,就只能使用正則方式let reg = /{{(.+?)}}/ig;將{{ number }}這種形式數(shù)據(jù)與普通正常文本分割之后,再分別創(chuàng)建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}}轉(zhuǎn)化為如下結(jié)果:
轉(zhuǎn)化為上圖結(jié)果后,就對返回?cái)?shù)組進(jìn)行循環(huán),分別通過創(chuàng)建文本節(jié)點(diǎn);這兒為了性能優(yōu)化,先創(chuàng)建文檔碎片,將節(jié)點(diǎn)放入文檔碎片中;
// 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); }
而在最后編譯完成,執(zhí)行linker時,主要做兩件事,第一是對需要雙向綁定的節(jié)點(diǎn)創(chuàng)建directive,第二是將整個文本節(jié)點(diǎn)進(jìn)行替換;怎么替換呢?如最開始是一個文本節(jié)點(diǎn)this is number: {{number}},經(jīng)過上面處理之后,在frag中其實(shí)是兩個文本節(jié)點(diǎn)this is number: 和{{number}};此時就使用replaceChild方法使用新的節(jié)點(diǎn)替換原始的節(jié)點(diǎn);
// 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; // 創(chuàng)建directive ...... // 通過這兒將`THIS IS TEST {{ number }} test` 這種轉(zhuǎn)化為三個textNode if(tokens.length) { replace(el, fragClone); } } } // util.js export function replace (target, el) { var parent = target.parentNode if (parent) { parent.replaceChild(el, target) } }
替換后結(jié)果如下圖:
經(jīng)過與最開始圖比較可以發(fā)現(xiàn),已經(jīng)將this is number: {{number}} middle {{number2}}轉(zhuǎn)化為this is number: number middle number2;只是此時,仍然展示的是變量名稱,如number,number2;那么,我們下面應(yīng)該做的肯定就是需要根據(jù)我們初始化時傳入的變量的值,將其進(jìn)行正確的展示;最終結(jié)果肯定應(yīng)該為this is number: 5 middle 2;即將number替換為5、將number2替換為2;那么,如何實(shí)現(xiàn)上述功能呢,我們上面提過,使用指令(directive)的方式;下面,就開始進(jìn)行指令的處理;
Directive(指令)對于每一個指令,肯定是隔離開的,互相不受影響且有自己的一套處理方式;所以,我們就使用對象的方式;一個指令就是一個實(shí)例化的對象,彼此之間互不影響;如下代碼:
export default function Directive(vm, el, description) { this.vm = vm; this.el = el; this.description = description; this.expression = description ? description.value : ""; }
在創(chuàng)建一個指令時,需要傳入三個參數(shù),一個是最開始初始化var vm = new vueImitate(options)時實(shí)例化的對象;而el是需要初始化指令的當(dāng)前元素,如
this is test
,需要創(chuàng)建v-show的指令,此時的el就是當(dāng)前的p標(biāo)簽;而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, // 需要創(chuàng)建指令的元素 def: text, // 對指令的操作方法,包括數(shù)據(jù)綁定(bind)、數(shù)據(jù)更新(update),見上面 text name: "text", // 指令名稱 value: "number" // 指令對應(yīng)數(shù)據(jù)的key }
通過new Directive(vm, el, description)就創(chuàng)建了一個指令,并初始化一些數(shù)據(jù);下面就先通過指令對界面進(jìn)行數(shù)據(jù)渲染;所有邏輯就放到了_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上,方便調(diào)用,主要包括上面說的bind、update等方法;其主要根據(jù)指令不同,功能不同而不同定義;所有對應(yīng)均在./directives/*文件夾下面,包括文本渲染text.js、事件添加bind.js、v-model對應(yīng)model.js、v-show對應(yīng)show.js等;通過合并以后,就執(zhí)行this.bind()方法進(jìn)行數(shù)據(jù)初始化綁定;但是,目前為止,當(dāng)去看界面時,仍然沒有將number轉(zhuǎn)化為5;為什么呢?通過查看代碼:
export default { bind () { this.attr = this.el.nodeType === 3 ? "data" : "textContent" }, update (value) { this.el[this.attr] = value } }
bind并沒有改變節(jié)點(diǎn)展示值,而是通過update; 所以,如果調(diào)用this.update(123),可發(fā)現(xiàn)有如下結(jié)果:
其實(shí)我們并不是直接固定數(shù)值,而是根據(jù)初始化時傳入的值動態(tài)渲染;但是目前為止,至少已經(jīng)完成了界面數(shù)據(jù)的渲染,只是數(shù)據(jù)不對而已;
然后,我們回頭看下編譯過程,我們需要在編譯過程去實(shí)例化指令(directive),并調(diào)用其_bind方法,對指令進(jìn)行初始化處理;
// 見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(); }
上面說了,目前還沒有根據(jù)傳入的數(shù)據(jù)進(jìn)行綁定,下面,就來對數(shù)據(jù)進(jìn)行處理;
數(shù)據(jù)處理數(shù)據(jù)處理包括以下幾個方面:
數(shù)據(jù)雙向綁定
數(shù)據(jù)變化后,需要通知到ui界面,并自動變化
對于輸入框,使用v-model時,需要將輸入內(nèi)容反應(yīng)到對應(yīng)數(shù)據(jù)
數(shù)據(jù)雙向綁定需要實(shí)現(xiàn)雙向綁定,就是在數(shù)據(jù)變化后能夠自動的將對應(yīng)界面進(jìn)行更新;那么,如何監(jiān)控?cái)?shù)據(jù)的變化呢?目前有幾種方式,一種是angular的臟檢查方式,就是對用戶所以操作、會導(dǎo)致數(shù)據(jù)變化的行為進(jìn)行攔截,如ng-click、$http、$timeout等;當(dāng)用戶進(jìn)行請求數(shù)據(jù)、點(diǎn)擊等時,會對所有的數(shù)據(jù)進(jìn)行檢查,如果數(shù)據(jù)變化了,就會觸發(fā)對應(yīng)的處理;而另一種是vue的實(shí)現(xiàn)方式,使用Object.definProperty()方法,對數(shù)據(jù)添加setter和getter;當(dāng)對數(shù)據(jù)進(jìn)行賦值時,會自動觸發(fā)setter;就可以監(jiān)控?cái)?shù)據(jù)的變化;主要處理如下, 源碼見這里:
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(); //生成一個調(diào)度中心,管理此字段的所有訂閱者 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是一個構(gòu)造函數(shù),主要對傳入的數(shù)據(jù)進(jìn)行Object.defineProperty綁定;可以監(jiān)控到數(shù)據(jù)的變化;而在每一個Observer中,會初始化一個Dep的稱為‘調(diào)度管理器’的對象,它主要負(fù)責(zé)保存界面更新的操作和操作的觸發(fā);
界面更新在通過上面Observer實(shí)現(xiàn)數(shù)據(jù)監(jiān)控之后,如何通知界面更新呢?這里使用了‘發(fā)布/訂閱模式’;如果需要對此模式進(jìn)行更深入理解,可查看此鏈接;而每個數(shù)據(jù)key都會維護(hù)了一個獨(dú)立的調(diào)度中心Dep;通過在上面defineProperty時創(chuàng)建;而Dep主要保存數(shù)據(jù)更新后的處理任務(wù)及對任務(wù)的處理,代碼也非常簡單,就是使用subs保存所有任務(wù),使用addSub添加任務(wù),使用notify處理任務(wù),depend作用會在下面watcher中進(jìn)行說明:
// Dep.js let uid = 0; // 調(diào)度中心 export default function Dep() { this.id = uid++; this.subs = []; //訂閱者數(shù)組 this.target = null; // 有何用處? } // 添加任務(wù) Dep.prototype.addSub = function(sub) { this.subs.push(sub); } // 處理任務(wù) 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); }
那么,處理任務(wù)來源哪兒呢?vue中又維護(hù)了一個watcher的對象,主要是對任務(wù)的初始化和收集處理;也就是一個watcher就是一個任務(wù);而整個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(整個項(xiàng)目初始化時實(shí)例化的vueImitate對象,因?yàn)樾枰玫嚼锩娴膶?yīng)數(shù)據(jù))、expression(任務(wù)對應(yīng)的數(shù)據(jù)的key,如上面的‘number’)、cb(一個當(dāng)數(shù)據(jù)變化后,界面如何更新的函數(shù),也就是上面directive里面的update方法);我們需要實(shí)現(xiàn)功能有,第一是每個任務(wù)有個update方法,主要用于在數(shù)據(jù)變化時,進(jìn)行調(diào)用,即:
// 處理任務(wù) Dep.prototype.notify = function() { this.subs.forEach((sub) => { if(sub && sub.update && typeof sub.update === "function") { sub.update(); } }) }
第二個是在初始化watcher時,需要將實(shí)例化的watcher(任務(wù))放入調(diào)度中心dep的subs中;如何實(shí)現(xiàn)呢?這里,使用了一些黑科技,流程如下,這兒我們以expression為"number"為例:
1、在初始化watcher時,會去初始化一個獲取數(shù)據(jù)的方法this.getter就是,能夠通過傳入的expression取出對應(yīng)的值;如通過number取出對應(yīng)的初始化時的值5;
2、調(diào)用this.value = this.get();方法,方法中會去數(shù)據(jù)源中取值,并將此時的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; }
3、因?yàn)槲覀冊谏厦?b>Observer已經(jīng)對數(shù)據(jù)進(jìn)行了Object.defineProperty綁定,所以,當(dāng)上面2步取值時,會觸發(fā)對應(yīng)的getter,如下, 觸發(fā)get函數(shù)之后,因?yàn)樯厦?已經(jīng)初始化Dep.target = this;了,所以會執(zhí)行dep.depend();,就是上面說的depend函數(shù)了:
// Observer.js let dep = new Dep(); //生成一個調(diào)度中心,管理此字段的所有訂閱者 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(); } })
3、觸發(fā)dep.depend();之后,如下代碼,會執(zhí)行Dep.target.addDep(this);, 此時的this就是上面實(shí)例化的dep, Dep.target則對應(yīng)的是剛剛1步中實(shí)例化的watcher,即執(zhí)行watcher.addDep(dep);
// Dep.js Dep.prototype.depend = function() { Dep.target.addDep(this); }
4、觸發(fā)watcher.addDep(dep),如下代碼,如果目前還沒此dep;就執(zhí)行dep.addSub(this);,此時的this就是指代當(dāng)前watcher,也就是1步時實(shí)例化的watcher;此時dep是步驟3中實(shí)例化的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; } }
5、最后執(zhí)行dep.addSub(watcher);,如下代碼,到這兒,就將初始化的watcher添加到了調(diào)度中心的數(shù)組中;
// Dep.js Dep.prototype.addSub = function(sub) { this.subs.push(sub); }
那么,在哪兒去初始化watcher呢?就是在對指令進(jìn)行_bind()時,如下代碼,在執(zhí)行_bind時,會實(shí)例化Watcher; 在第三個參數(shù)的回調(diào)函數(shù)里執(zhí)行self.update(watcher.value);,也就是當(dāng)監(jiān)控到數(shù)據(jù)變化,會執(zhí)行對應(yīng)的update方法進(jìn)行更新;
// 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); } }
而前面說了,開始時沒有數(shù)據(jù),使用this.update(123)會將界面對應(yīng)number更新為123,當(dāng)時沒有對應(yīng)number真實(shí)數(shù)據(jù);而此時,在watcher中,獲取到了對應(yīng)數(shù)據(jù)并保存到value中,因此,就執(zhí)行this.update(watcher.value);,此時就可以將真實(shí)數(shù)據(jù)與界面進(jìn)行綁定,并且當(dāng)數(shù)據(jù)變化時,界面也會自動進(jìn)行更新;最終結(jié)果如下圖:
為什么所有數(shù)據(jù)都是undefined呢?我們可以通過下面代碼知道, 在實(shí)例化watcher時,調(diào)用this.value = this.get();時,其實(shí)是通過傳入的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; } }
那么,我們?nèi)绾文苤苯涌梢酝ㄟ^諸如this.number取到值呢?只能如下,通過下面extend(this, data);方式,就將數(shù)據(jù)綁定到了實(shí)例化的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); }
處理后結(jié)果如下:
數(shù)據(jù)也綁定上了,但是當(dāng)我們嘗試使用下面方式對數(shù)據(jù)進(jìn)行改變時,發(fā)現(xiàn)并沒有自動更新到界面,界面數(shù)據(jù)并沒有變化;
methods: { add() { this.number1 += 1; this.number += 1; } }
為什么呢?通過上面代碼可知,我們其實(shí)observer的是vueImitate實(shí)例化對象的data對象;而我們更改值是通過this.number += 1;實(shí)現(xiàn)的;其實(shí)并沒有改vueImitate.data.number的值,而是改vueImitate.number的值,所以也就不會觸發(fā)observer里面的setter;也不會去觸發(fā)對應(yīng)的watcher里面的update;那如何處理呢?我們可以通過如下方式實(shí)現(xiàn), 完整源碼見這里:
// 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里對應(yīng)的data的屬性進(jìn)行Object.defineProperty處理,當(dāng)對其進(jìn)行賦值時,會再將其值賦值到vueImitate.data對應(yīng)的屬性上面,那樣,就會去觸發(fā)observer(this.data);里面的setter,從而去更新界面數(shù)據(jù);
至此,整個數(shù)據(jù)處理就已經(jīng)完成,總結(jié)一下:
1、首先,在初始化vueImitate時,我們會將初始化數(shù)據(jù)通過options.data傳入,后會進(jìn)行處理,保存至this.data中;
2、通過initData方法將數(shù)據(jù)綁定到vueImitate實(shí)例化對象上面,并對其進(jìn)行數(shù)據(jù)監(jiān)控,然后使用observer對this.data進(jìn)行監(jiān)控,在實(shí)例化Observer時,會去實(shí)例化一個對應(yīng)的調(diào)度中心Dep;
3、在編譯過程中,會創(chuàng)建指令,通過指令實(shí)現(xiàn)每個需要處理節(jié)點(diǎn)的數(shù)據(jù)處理和雙向綁定;
4、在指令_bind()時,會去實(shí)例化對應(yīng)的watcher,創(chuàng)建一個任務(wù),主要實(shí)現(xiàn)數(shù)據(jù)獲取、數(shù)據(jù)變化時,對應(yīng)界面更新(也就是更新函數(shù)的調(diào)用)、并將生成的watcher存儲到對應(yīng)的步驟2中實(shí)例化的調(diào)度中心中;
5、當(dāng)數(shù)據(jù)更新時,會觸發(fā)對應(yīng)的setter,然后調(diào)用dep.notify();觸發(fā)調(diào)度中心中所有任務(wù)的更新,即執(zhí)行所有的watcher.update,從而實(shí)現(xiàn)對應(yīng)界面的更新;
到目前為止,整個框架的實(shí)現(xiàn)基本已經(jīng)完成。其中包括compile、linker、oberver、directive(v-model、v-show、v-bind、v-text)、watcher;如果需要更深入的研究,可見項(xiàng)目代碼; 可以自己clone下來,運(yùn)行起來;文中有些可能思考不夠充分,忘見諒,也歡迎大家指正;
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/93653.html
摘要:哪吒別人的看法都是狗屁,你是誰只有你自己說了才算,這是爹教我的道理。哪吒去他個鳥命我命由我,不由天是魔是仙,我自己決定哪吒白白搭上一條人命,你傻不傻敖丙不傻誰和你做朋友太乙真人人是否能夠改變命運(yùn),我不曉得。我只曉得,不認(rèn)命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出處 查看github最新的Vue...
摘要:儲存訂閱器因?yàn)閷傩员槐O(jiān)聽,這一步會執(zhí)行監(jiān)聽器里的方法這一步我們把也給弄了出來,到這一步我們已經(jīng)實(shí)現(xiàn)了一個簡單的雙向綁定了,我們可以嘗試把兩者結(jié)合起來看下效果。總結(jié)本文主要是對雙向綁定原理的學(xué)習(xí)與實(shí)現(xiàn)。 當(dāng)今前端天下以 Angular、React、vue 三足鼎立的局面,你不選擇一個陣營基本上無法立足于前端,甚至是兩個或者三個陣營都要選擇,大勢所趨。 所以我們要時刻保持好奇心,擁抱變化,...
摘要:當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時候,我們希望能夠通知對方也更新,這就是所謂的數(shù)據(jù)雙向綁定。返回值返回傳入函數(shù)的對象,即第一個參數(shù)該方法重點(diǎn)是描述,對象里目前存在的屬性描述符有兩種主要形式數(shù)據(jù)描述符和存取描述符。 前言 談起當(dāng)前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對于大多數(shù)人來說,我們更多的是在使用框架,對于框架解決痛點(diǎn)背后使用的基本原理往往關(guān)注...
摘要:接下來要看看這個訂閱者的具體實(shí)現(xiàn)了實(shí)現(xiàn)訂閱者作為和之間通信的橋梁,主要做的事情是在自身實(shí)例化時往屬性訂閱器里面添加自己自身必須有一個方法待屬性變動通知時,能調(diào)用自身的方法,并觸發(fā)中綁定的回調(diào),則功成身退。 本文能幫你做什么?1、了解vue的雙向數(shù)據(jù)綁定原理以及核心代碼模塊2、緩解好奇心的同時了解如何實(shí)現(xiàn)雙向綁定為了便于說明原理與實(shí)現(xiàn),本文相關(guān)代碼主要摘自vue源碼, 并進(jìn)行了簡化改造,...
閱讀 2797·2023-04-25 23:08
閱讀 1583·2021-11-23 09:51
閱讀 1564·2021-10-27 14:18
閱讀 3115·2019-08-29 13:25
閱讀 2831·2019-08-29 13:14
閱讀 2895·2019-08-26 18:36
閱讀 2193·2019-08-26 12:11
閱讀 811·2019-08-26 11:29