如今的前端面試無論你用的什麼框架,總會問你這個框架的雙向綁定機制,有的甚至要求你現場實現一個雙向綁定出來,那對於沒有好好研究過這方面知識的同窗來講,固然是很難的,接下來本文用160行代碼帶你實現一個極簡的雙向綁定機制。若是喜歡的話能夠點波贊/關注,支持一下,但願你們看完本文能夠有所收穫。html
本文是在面試題:你能寫一個Vue的雙向數據綁定嗎?的基礎上仔細研究+改動,並添加了詳細註釋,而成的。前端
我的博客瞭解一下:obkoro1.comnode
codepen:仿Vue極簡雙向綁定git
Github:仿Vue極簡雙向綁定github
這個API是實現雙向綁定的核心,最主要的做用是重寫數據的get
、set
方法。web
let obj = {
singer: "周杰倫"
};
let value = "青花瓷";
Object.defineProperty(obj, "music", {
// value: '七里香', // 設置屬性的值 下面設置了get set函數 因此這裏不能設置
configurable: false, // 是否能夠刪除屬性 默認不能刪除
// writable: true, // 是否能夠修改對象 下面設置了get set函數 因此這裏不能設置
enumerable: true, // music是否能夠被枚舉 默認是不能被枚舉(遍歷)
// ☆ get,set設置時不能設置writable和value,要一對一對設置,交叉設置/同時存在 就會報錯
get() {
// 獲取obj.music的時候就會調用get方法
// let value = "強行設置get的返回值"; // 打開註釋 讀取屬性永遠都是‘強行設置get的返回值’
return value;
},
set(val) {
// 將修改的值從新賦給song
value = val;
}
});
console.log(obj.music); // 青花瓷
delete obj.music; // configurable設爲false 刪除無效
console.log(obj.music); // 青花瓷
obj.music = "聽媽媽的話";
console.log(obj.music); // 聽媽媽的話
for (let key in obj) {
// 默認狀況下經過defineProperty定義的屬性是不能被枚舉(遍歷)的
// 須要設置enumerable爲true才能夠 不然只能拿到singer 屬性
console.log(key); // singer, music
}
複製代碼
對,這裏有個demo。面試
defineProperty
設置的屬性,默認不能刪除,不能遍歷,固然你能夠經過設置更改他們。兼容性:IE 9,Firefox 4, Chorme 5,Opera 11.6,Safari 5.1segmentfault
更詳細的能夠看一下MDN併發
Object.defineProperty()
重寫數據的get、set,值更新就在set中通知訂閱者更新數據。這部分講的很清楚,如今有點懵逼也不要緊,看完代碼,本身copy下來玩一玩以後,回頭再看實現思路,相信會有收穫的。app
<div id="app">
<input type="text" v-model="name">
<h3 v-bind="name"></h3>
<input type="text" v-model="testData1">
<h3>{{ testData1 }}</h3>
<input type="text" v-model="testData2">
<h3>{{ testData2 }}</h3>
</div>
複製代碼
看到這個模板,相信用過Vue的同窗都不會陌生。
採用類Vue方式來使用雙向綁定:
window.onload = function () {
var app = new myVue({
el: '#app', // dom
data: { // 數據
testData1: '仿Vue',
testData2: '極簡雙向綁定',
name: 'OBKoro1'
}
})
}
複製代碼
實際上這裏是咱們實現思路中的第四步,用於整合數據監聽器this._observer()
、指令解析器this._compile()
以及鏈接Observer和Compile的_watcherTpl的watch池。
function myVue(options = {}) { // 防止沒傳,設一個默認值
this.$options = options; // 配置掛載
this.$el = document.querySelector(options.el); // 獲取dom
this._data = options.data; // 數據掛載
this._watcherTpl = {}; // watcher池
this._observer(this._data); // 傳入數據,執行函數,重寫數據的get set
this._compile(this.$el); // 傳入dom,執行函數,編譯模板 發佈訂閱
};
複製代碼
這是實現思路中的第三步,由於下方數據監聽器_observer()
須要用到Watcher函數,因此這裏就先講了。
像實現思路中所說的,這裏起到了鏈接Observer和Compile的做用:
在模板編譯_compile()階段發佈訂閱
在賦值操做的時候,更新視圖
// new Watcher() 爲this._compile()發佈訂閱+ 在this._observer()中set(賦值)的時候更新視圖
function Watcher(el, vm, val, attr) {
this.el = el; // 指令對應的DOM元素
this.vm = vm; // myVue實例
this.val = val; // 指令對應的值
this.attr = attr; // dom獲取值,如value獲取input的值 / innerHTML獲取dom的值
this.update(); // 更新視圖
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm._data[this.val]; // 獲取data的最新值 賦值給dom 更新視圖
}
複製代碼
沒有看錯,代碼量就這麼多,可能須要把整個代碼鏈接起來,多看幾遍纔可以理解。
實現思路中的第一步,用Object.defineProperty()
遍歷data重寫全部屬性的get set。
而後在給對象的某個屬性賦值的時候,就會觸發set。
在set中咱們能夠監聽到數據的變化,而後就能夠觸發watch更新視圖。
myVue.prototype._observer = function (obj) {
var _this = this;
Object.keys(obj).forEach(key => { // 遍歷數據
_this._watcherTpl[key] = { // 每一個數據的訂閱池()
_directives: []
};
var value = obj[key]; // 獲取屬性值
var watcherTpl = _this._watcherTpl[key]; // 數據的訂閱池
Object.defineProperty(_this._data, key, { // 雙向綁定最重要的部分 重寫數據的set get
configurable: true, // 能夠刪除
enumerable: true, // 能夠遍歷
get() {
console.log(`${key}獲取值:${value}`);
return value; // 獲取值的時候 直接返回
},
set(newVal) { // 改變值的時候 觸發set
console.log(`${key}更新:${newVal}`);
if (value !== newVal) {
value = newVal;
watcherTpl._directives.forEach((item) => { // 遍歷訂閱池
item.update();
// 遍歷全部訂閱的地方(v-model+v-bind+{{}}) 觸發this._compile()中發佈的訂閱Watcher 更新視圖
});
}
}
})
});
}
複製代碼
這裏是實現思路中的第三步,讓咱們來總結一下這裏作了哪些事情:
首先是深度遍歷dom樹,遍歷每一個節點以及子節點。
將模板中的變量替換成數據,初始化渲染頁面視圖。
把指令綁定的屬性添加到對應的訂閱池中
一旦數據有變更,收到通知,更新視圖。
myVue.prototype._compile = function (el) {
var _this = this, nodes = el.children; // 獲取app的dom
for (var i = 0, len = nodes.length; i < len; i++) { // 遍歷dom節點
var node = nodes[i];
if (node.children.length) {
_this._compile(node); // 遞歸深度遍歷 dom樹
}
// 若是有v-model屬性,而且元素是INPUT或者TEXTAREA,咱們監聽它的input事件
if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function (key) {
var attVal = node.getAttribute('v-model'); // 獲取v-model綁定的值
_this._watcherTpl[attVal]._directives.push(new Watcher( // 將dom替換成屬性的數據併發布訂閱 在set的時候更新數據
node,
_this,
attVal,
'value'
));
return function () {
_this._data[attVal] = nodes[key].value; // input值改變的時候 將新值賦給數據 觸發set=>set觸發watch 更新視圖
}
})(i));
}
if (node.hasAttribute('v-bind')) { // v-bind指令
var attrVal = node.getAttribute('v-bind'); // 綁定的data
_this._watcherTpl[attrVal]._directives.push(new Watcher( // 將dom替換成屬性的數據併發布訂閱 在set的時候更新數據
node,
_this,
attrVal,
'innerHTML'
))
}
var reg = /\{\{\s*([^}]+\S)\s*\}\}/g, txt = node.textContent; // 正則匹配{{}}
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// matched匹配的文本節點包括{{}}, placeholder 是{{}}中間的屬性名
var getName = _this._watcherTpl; // 全部綁定watch的數據
getName = getName[placeholder]; // 獲取對應watch 數據的值
if (!getName._directives) { // 沒有事件池 建立事件池
getName._directives = [];
}
getName._directives.push(new Watcher( // 將dom替換成屬性的數據併發布訂閱 在set的時候更新數據
node,
_this,
placeholder,
'innerHTML'
));
return placeholder.split('.').reduce((val, key) => {
return _this._data[key]; // 獲取數據的值 觸發get 返回當前值
}, _this.$el);
});
}
}
}
複製代碼
codepen:仿Vue極簡雙向綁定
Github:仿Vue極簡雙向綁定
若是以爲還不錯的話,就給個Star⭐️鼓勵一下我吧~
本文只是一個簡單的實現雙向綁定的方法,主要目的是幫助各位同窗理解mvvm框架的雙向綁定機制,也並無很完善,這裏仍是有不少缺陷,好比:沒有實現數據的深度對數據進行get
、set
等。但願看完本文,你們能有所收穫。
我的blog and 掘金我的主頁,如需轉載,請放上原文連接並署名。碼字不易,感謝支持!本人寫文章本着交流記錄的心態,寫的很差之處,不撕逼,可是歡迎指點。
若是喜歡本文的話,歡迎關注個人訂閱號,漫漫技術路,期待將來共同窗習成長。
以上2018.6.24