最近面試經(jīng)常會(huì)問這個(gè)問題,你知道vue的雙向綁定是通過什么實(shí)現(xiàn)的嗎?了解的同學(xué)應(yīng)該知道vue是使用Object.defineProperty屬性绩郎,重寫data的get和set方法來實(shí)現(xiàn)的逛薇。先引用網(wǎng)上的一張圖,那么接下來我們就按照這張圖的步驟去用代碼實(shí)現(xiàn)這個(gè)功能浦夷。
image
先看DOM結(jié)構(gòu)部分,結(jié)構(gòu)部分很簡(jiǎn)單
<div id="app">
<input type="text" v-model="number">
<input type="button" value="增加" v-click="increment"/>
<input type="button" value="減少" v-click="subtract">
<h3 v-bind="number"></h3>
</div>
然后就是js部分了辜王,按照上圖的步驟我們定義一個(gè)構(gòu)造函數(shù)劈狐,并且init這個(gè)構(gòu)造函數(shù)
//初始化構(gòu)造函數(shù)
function Vm(options) {
this._init(options);
}
Vm.prototype._init = function (options) {
this.$options = options; //options為上面使用時(shí)傳入的結(jié)構(gòu)體,包括el呐馆、data肥缔、methods
this.$el = document.querySelector(options.el); //el是#app,this.$el是id為app的Element元素
this.$data = options.data; //this.$data = {number: 0 ...}
this.$methods = options.methods; //this.$methods = {increment: function(){} ...}
this._binding = {}; //_binding保存著model與view的映射關(guān)系汹来,也就是我們前面定義的Watcher的實(shí)例续膳。當(dāng)model改變時(shí),會(huì)觸發(fā)其中的指令類更新收班,保證view也能實(shí)時(shí)更新
}
這個(gè)時(shí)候我們還需要給實(shí)例添加一個(gè)observer坟岔、compile方法,observer方法實(shí)現(xiàn)對(duì)數(shù)據(jù)的劫持摔桦,compile方法負(fù)責(zé)編譯解析指令并且初始化視圖
//實(shí)現(xiàn)_observer函數(shù)社付,對(duì)data進(jìn)行處理,重寫data的set和get函數(shù)
Vm.prototype._observer = function (data) {
//如果data數(shù)據(jù)為空
if (!data) {
return;
}
if (typeof data !== "object") {
throw new Error("data必須是一個(gè)對(duì)象");
}
var self = this;
/*
* 遍歷data中所有屬性
* Object.keys(obj)函數(shù)返回一個(gè)由一個(gè)給定對(duì)象的自身可枚舉屬性組成的數(shù)組
* */
Object.keys(data).forEach(function (key) {
//當(dāng)前屬性的值
var oldValue = data[key];
/*
* 按照前面的數(shù)據(jù)
* _binding = {
* number:{
* _directives: [...watch實(shí)例]
* }
* }
* 這里是先聲明一個(gè)空數(shù)組邻耕,以后所有和data中某值有關(guān)系的都會(huì)追加到相應(yīng)的_directives中鸥咖。
* 為何要在這里聲明?
* 因?yàn)檫@里要根據(jù)_binding的key必須為data中的屬性(鍵)
* 那這個(gè)數(shù)組何時(shí)才有東西呢兄世?
* 解析指令(比如v-bind)的時(shí)候push進(jìn)去的啼辣,因?yàn)榻馕鲋噶畹臅r(shí)候,會(huì)解析每一個(gè)dom,然后解析出你的是點(diǎn)擊事件還是修改文本內(nèi)容御滩。
* 然后在生成watcher追加進(jìn)去這個(gè)數(shù)組-以后所有和data中某值發(fā)生改變只要循環(huán)執(zhí)行這個(gè)數(shù)組中watcher的update更新方法就可以更新所有和這個(gè)data中的某值相關(guān)聯(lián)的dom
* */
self._binding[key] = {
_directives: []
}
//獲取本data某屬性對(duì)應(yīng)的_directives
var binding = self._binding[key];
/**
* 語法:Object.defineProperty(obj, key, descriptor)
* @param: obj:需要定義屬性的對(duì)象鸥拧;
* key:需要定義或修改的屬性党远;
* descriptor:將被定義或修改屬性的描述符
*/
Object.defineProperty(data, key, { //實(shí)現(xiàn)雙向綁定的關(guān)鍵代碼
enumerable: true, // 可枚舉--可被for-in和Object.keys()枚舉。
configurable: true, //當(dāng)且僅當(dāng)值為true時(shí)住涉,該屬性描述符才能夠被改變麸锉,也能被刪除
//value: undefined, //該屬性對(duì)應(yīng)的值∮呱可以是任何有效的 JavaScript 值(數(shù)值花沉,對(duì)象,函數(shù)等)媳握。默認(rèn)為 undefined
//writable: false, //當(dāng)且僅當(dāng)該屬性的writable為true時(shí)碱屁,value才能被賦值運(yùn)算符改變。默認(rèn)為 false
get: function () { //一個(gè)給屬性提供getter的方法蛾找,當(dāng)訪問該屬性時(shí)方法會(huì)被執(zhí)行娩脾,執(zhí)行時(shí)沒有參數(shù)傳入,但會(huì)傳入this對(duì)象
//發(fā)現(xiàn)這個(gè)oldValue如果替換成data[key]會(huì)造成堆棧溢出打毛。
return oldValue;
},
set: function (newValue) { //一個(gè)給屬性提供setter的方法柿赊,當(dāng)屬性值修改時(shí)觸發(fā)該方法,該方法將接受唯一參數(shù)幻枉,即該屬性新的參數(shù)值碰声。
if (oldValue == newValue) return;
console.log("監(jiān)聽到值變化了");
//發(fā)現(xiàn)這個(gè)oldValue如果替換成data[key]會(huì)造成堆棧溢出。
oldValue = newValue;
// 當(dāng)data中某屬性改變時(shí)熬甫,觸發(fā)_binding['某值']._directives 中的綁定的Watcher類的更新--這樣實(shí)現(xiàn)一個(gè)data中的值發(fā)生改變胰挑,和它相關(guān)聯(lián)的dom更新。
binding._directives.forEach(function (item) {
item.update();
})
}
});
})
}
//定義complie函數(shù)椿肩,用來解析指令(v-bind,v-model,v-click)等瞻颂,并在這個(gè)過程中對(duì)view與model進(jìn)行綁定。
Vm.prototype._complie = function (root) { //root為id為app的ELement元素郑象,也就是vue的根元素
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) { //對(duì)所有的元素進(jìn)行遍歷贡这,并處理
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute("v-click")) { //如果有v-click屬性,我們監(jiān)聽onclick事件厂榛,觸發(fā)increment盖矫、subtract方法
node.onclick = (function () {
var attrVal = nodes[i].getAttribute("v-click");
return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域與method函數(shù)的作用域保持一致
})();
}
if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model屬性,并且元素為input或者textarea噪沙,我們監(jiān)聽它的input事件
node.addEventListener("input", (function (key) {
var attrVal = node.getAttribute("v-model");
/**
* _this._binding["number"]._directives = [一個(gè)Watcher實(shí)例]
* 其中Watcher.prototype.update = function() {
* node["value"] = _this.$data["number"]; 這就將node的值保持與number一致
* }
*/
_this._binding[attrVal]._directives.push(new Watcher(
"input",
node,
_this,
attrVal,
"value",
))
return function () {
_this.$data[attrVal] = nodes[key].value; //使number的值與node的value保持一致炼彪,實(shí)現(xiàn)雙向綁定
}
})(i));
}
if (node.hasAttribute("v-bind")) { //如果有v-bind屬性吐根,只要使node的值及時(shí)更新為data中number的值即可
var attrVal = node.getAttribute("v-bind");
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
"innerHTML"
))
}
}
}
方法定義完了當(dāng)然得調(diào)用正歼,回到init方法
Vm.prototype._init = function (options) {
this.$options = options; //options為上面使用時(shí)傳入的結(jié)構(gòu)體,包括el拷橘、data局义、methods
this.$el = document.querySelector(options.el); //el是#app喜爷,this.$el是id為app的Element元素
this.$data = options.data; //this.$data = {number: 0}
this.$methods = options.methods; //this.$methods = {increment: function(){}}
this._binding = {}; //_binding保存著model與view的映射關(guān)系,也就是我們前面定義的Watcher的實(shí)例萄唇。當(dāng)model改變時(shí)檩帐,會(huì)觸發(fā)其中的指令類更新,保證view也能實(shí)時(shí)更新
this._observer(this.$data);
this._complie(this.$el);
}
接下來實(shí)現(xiàn)一個(gè)Watcher另萤,用來綁定update方法湃密,實(shí)現(xiàn)對(duì)DOM的更新
//實(shí)現(xiàn)一個(gè)指令類Watcher,用來綁定更新函數(shù)四敞,實(shí)現(xiàn)對(duì)DOM元素的更新
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名稱泛源,例如文本節(jié)點(diǎn),該值設(shè)置為"text"
this.el = el; //指令對(duì)應(yīng)的DOM元素
this.vm = vm; //指令所屬的實(shí)例
this.exp = exp; //指令對(duì)應(yīng)的值忿危,本例為:"number"
this.attr = attr; //指令綁定的屬性值达箍,本例為:"innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp]; //比如H3.innerHtml = this.data.number;當(dāng)number改變時(shí)會(huì)觸發(fā)update函數(shù),保證對(duì)應(yīng)的DOM內(nèi)容進(jìn)行更新
}
調(diào)用也很簡(jiǎn)單铺厨,也是vue最熟悉的調(diào)用方式
window.onload = function () {
var vm = new Vm({
el: "#app",
data: {
number: 0,
age: 18
},
methods: {
increment: function () {
this.number++;
},
subtract: function () {
this.number--;
}
}
})
}
完整代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-model="number">
<input type="button" value="增加" v-click="increment"/>
<input type="button" value="減少" v-click="subtract">
<h3 v-bind="number"></h3>
</div>
</body>
</html>
<script>
//初始化構(gòu)造函數(shù)
function Vm(options) {
this._init(options);
}
Vm.prototype._init = function (options) {
this.$options = options; //options為上面使用時(shí)傳入的結(jié)構(gòu)體缎玫,包括el、data解滓、methods
this.$el = document.querySelector(options.el); //el是#app赃磨,this.$el是id為app的Element元素
this.$data = options.data; //this.$data = {number: 0}
this.$methods = options.methods; //this.$methods = {increment: function(){}}
this._binding = {}; //_binding保存著model與view的映射關(guān)系,也就是我們前面定義的Watcher的實(shí)例伐蒂。當(dāng)model改變時(shí)煞躬,會(huì)觸發(fā)其中的指令類更新,保證view也能實(shí)時(shí)更新
this._observer(this.$data);
this._complie(this.$el);
}
//實(shí)現(xiàn)_observer函數(shù)逸邦,對(duì)data進(jìn)行處理恩沛,重寫data的set和get函數(shù)
Vm.prototype._observer = function (data) {
//如果data數(shù)據(jù)為空
if (!data) {
return;
}
if (typeof data !== "object") {
throw new Error("data必須是一個(gè)對(duì)象");
}
var self = this;
/*
* 遍歷data中所有屬性
* Object.keys(obj)函數(shù)返回一個(gè)由一個(gè)給定對(duì)象的自身可枚舉屬性組成的數(shù)組
* */
Object.keys(data).forEach(function (key) {
//當(dāng)前屬性的值
var oldValue = data[key];
/*
* 按照前面的數(shù)據(jù)
* _binding = {
* number:{
* _directives: [...watch實(shí)例]
* }
* }
* 這里是先聲明一個(gè)空數(shù)組,以后所有和data中某值有關(guān)系的都會(huì)追加到相應(yīng)的_directives中缕减。
* 為何要在這里聲明雷客?
* 因?yàn)檫@里要根據(jù)_binding的key必須為data中的屬性(鍵)
* 那這個(gè)數(shù)組何時(shí)才有東西呢?
* 解析指令(比如v-bind)的時(shí)候push進(jìn)去的桥狡,因?yàn)榻馕鲋噶畹臅r(shí)候搅裙,會(huì)解析每一個(gè)dom,然后解析出你的是點(diǎn)擊事件還是修改文本內(nèi)容。
* 然后在生成watcher追加進(jìn)去這個(gè)數(shù)組-以后所有和data中某值發(fā)生改變只要循環(huán)執(zhí)行這個(gè)數(shù)組中watcher的update更新方法就可以更新所有和這個(gè)data中的某值相關(guān)聯(lián)的dom
* */
self._binding[key] = {
_directives: []
}
//獲取本data某屬性對(duì)應(yīng)的_directives
var binding = self._binding[key];
/**
* 語法:Object.defineProperty(obj, key, descriptor)
* @param: obj:需要定義屬性的對(duì)象裹芝;
* key:需要定義或修改的屬性部逮;
* descriptor:將被定義或修改屬性的描述符
*/
Object.defineProperty(data, key, { //實(shí)現(xiàn)雙向綁定的關(guān)鍵代碼
enumerable: true, // 可枚舉--可被for-in和Object.keys()枚舉。
configurable: true, //當(dāng)且僅當(dāng)值為true時(shí)嫂易,該屬性描述符才能夠被改變兄朋,也能被刪除
//value: undefined, //該屬性對(duì)應(yīng)的值×担可以是任何有效的 JavaScript 值(數(shù)值颅和,對(duì)象傅事,函數(shù)等)。默認(rèn)為 undefined
//writable: false, //當(dāng)且僅當(dāng)該屬性的writable為true時(shí)峡扩,value才能被賦值運(yùn)算符改變蹭越。默認(rèn)為 false
get: function () { //一個(gè)給屬性提供getter的方法,當(dāng)訪問該屬性時(shí)方法會(huì)被執(zhí)行教届,執(zhí)行時(shí)沒有參數(shù)傳入响鹃,但會(huì)傳入this對(duì)象
//發(fā)現(xiàn)這個(gè)oldValue如果替換成data[key]會(huì)造成堆棧溢出。
return oldValue;
},
set: function (newValue) { //一個(gè)給屬性提供setter的方法案训,當(dāng)屬性值修改時(shí)觸發(fā)該方法茴迁,該方法將接受唯一參數(shù),即該屬性新的參數(shù)值萤衰。
if (oldValue == newValue) return;
console.log("監(jiān)聽到值變化了");
//發(fā)現(xiàn)這個(gè)oldValue如果替換成data[key]會(huì)造成堆棧溢出堕义。
oldValue = newValue;
// 當(dāng)data中某屬性改變時(shí),觸發(fā)_binding['某值']._directives 中的綁定的Watcher類的更新--這樣實(shí)現(xiàn)一個(gè)data中的值發(fā)生改變脆栋,和它相關(guān)聯(lián)的dom更新倦卖。
binding._directives.forEach(function (item) {
item.update();
})
}
});
})
}
//定義complie函數(shù),用來解析指令(v-bind,v-model,v-click)等椿争,并在這個(gè)過程中對(duì)view與model進(jìn)行綁定怕膛。
Vm.prototype._complie = function (root) { //root為id為app的ELement元素,也就是vue的根元素
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) { //對(duì)所有的元素進(jìn)行遍歷秦踪,并處理
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute("v-click")) { //如果有v-click屬性褐捻,我們監(jiān)聽onclick事件,觸發(fā)increment椅邓、subtract方法
node.onclick = (function () {
var attrVal = nodes[i].getAttribute("v-click");
return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域與method函數(shù)的作用域保持一致
})();
}
if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model屬性柠逞,并且元素為input或者textarea,我們監(jiān)聽它的input事件
node.addEventListener("input", (function (key) {
var attrVal = node.getAttribute("v-model");
/**
* _this._binding["number"]._directives = [一個(gè)Watcher實(shí)例]
* 其中Watcher.prototype.update = function() {
* node["value"] = _this.$data["number"]; 這就將node的值保持與number一致
* }
*/
_this._binding[attrVal]._directives.push(new Watcher(
"input",
node,
_this,
attrVal,
"value",
))
return function () {
_this.$data[attrVal] = nodes[key].value; //使number的值與node的value保持一致景馁,實(shí)現(xiàn)雙向綁定
}
})(i));
}
if (node.hasAttribute("v-bind")) { //如果有v-bind屬性板壮,只要使node的值及時(shí)更新為data中number的值即可
var attrVal = node.getAttribute("v-bind");
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
"innerHTML"
))
}
}
}
//實(shí)現(xiàn)一個(gè)指令類Watcher,用來綁定更新函數(shù)合住,實(shí)現(xiàn)對(duì)DOM元素的更新
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名稱绰精,例如文本節(jié)點(diǎn),該值設(shè)置為"text"
this.el = el; //指令對(duì)應(yīng)的DOM元素
this.vm = vm; //指令所屬的實(shí)例
this.exp = exp; //指令對(duì)應(yīng)的值透葛,本例為:"number"
this.attr = attr; //指令綁定的屬性值笨使,本例為:"innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp]; //比如H3.innerHtml = this.data.number;當(dāng)number改變時(shí)會(huì)觸發(fā)update函數(shù),保證對(duì)應(yīng)的DOM內(nèi)容進(jìn)行更新
}
window.onload = function () {
var vm = new Vm({
el: "#app",
data: {
number: 0,
age: 18
},
methods: {
increment: function () {
this.number++;
},
subtract: function () {
this.number--;
}
}
})
}
</script>