最近面试经常会问这个问题,你知道vue的双向绑定是通过什么实现的吗?了解的同学应该知道vue是使用Object.defineProperty属性,重写data的get和set方法来实现的。先引用网上的一张图,那么接下来我们就按照这张图的步骤去用代码实现这个功能。
image
先看DOM结构部分,结构部分很简单
<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部分了,按照上图的步骤我们定义一个构造函数,并且init这个构造函数
//初始化构造函数
function Vm(options) {
this._init(options);
}
Vm.prototype._init = function (options) {
this.$options = options; //options为上面使用时传入的结构体,包括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的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
}
这个时候我们还需要给实例添加一个observer、compile方法,observer方法实现对数据的劫持,compile方法负责编译解析指令并且初始化视图
//实现_observer函数,对data进行处理,重写data的set和get函数
Vm.prototype._observer = function (data) {
//如果data数据为空
if (!data) {
return;
}
if (typeof data !== "object") {
throw new Error("data必须是一个对象");
}
var self = this;
/*
* 遍历data中所有属性
* Object.keys(obj)函数返回一个由一个给定对象的自身可枚举属性组成的数组
* */
Object.keys(data).forEach(function (key) {
//当前属性的值
var oldValue = data[key];
/*
* 按照前面的数据
* _binding = {
* number:{
* _directives: [...watch实例]
* }
* }
* 这里是先声明一个空数组,以后所有和data中某值有关系的都会追加到相应的_directives中。
* 为何要在这里声明?
* 因为这里要根据_binding的key必须为data中的属性(键)
* 那这个数组何时才有东西呢?
* 解析指令(比如v-bind)的时候push进去的,因为解析指令的时候,会解析每一个dom,然后解析出你的是点击事件还是修改文本内容。
* 然后在生成watcher追加进去这个数组-以后所有和data中某值发生改变只要循环执行这个数组中watcher的update更新方法就可以更新所有和这个data中的某值相关联的dom
* */
self._binding[key] = {
_directives: []
}
//获取本data某属性对应的_directives
var binding = self._binding[key];
/**
* 语法:Object.defineProperty(obj, key, descriptor)
* @param: obj:需要定义属性的对象;
* key:需要定义或修改的属性;
* descriptor:将被定义或修改属性的描述符
*/
Object.defineProperty(data, key, { //实现双向绑定的关键代码
enumerable: true, // 可枚举--可被for-in和Object.keys()枚举。
configurable: true, //当且仅当值为true时,该属性描述符才能够被改变,也能被删除
//value: undefined, //该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
//writable: false, //当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
get: function () { //一个给属性提供getter的方法,当访问该属性时方法会被执行,执行时没有参数传入,但会传入this对象
//发现这个oldValue如果替换成data[key]会造成堆栈溢出。
return oldValue;
},
set: function (newValue) { //一个给属性提供setter的方法,当属性值修改时触发该方法,该方法将接受唯一参数,即该属性新的参数值。
if (oldValue == newValue) return;
console.log("监听到值变化了");
//发现这个oldValue如果替换成data[key]会造成堆栈溢出。
oldValue = newValue;
// 当data中某属性改变时,触发_binding['某值']._directives 中的绑定的Watcher类的更新--这样实现一个data中的值发生改变,和它相关联的dom更新。
binding._directives.forEach(function (item) {
item.update();
})
}
});
})
}
//定义complie函数,用来解析指令(v-bind,v-model,v-click)等,并在这个过程中对view与model进行绑定。
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++) { //对所有的元素进行遍历,并处理
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute("v-click")) { //如果有v-click属性,我们监听onclick事件,触发increment、subtract方法
node.onclick = (function () {
var attrVal = nodes[i].getAttribute("v-click");
return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致
})();
}
if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model属性,并且元素为input或者textarea,我们监听它的input事件
node.addEventListener("input", (function (key) {
var attrVal = node.getAttribute("v-model");
/**
* _this._binding["number"]._directives = [一个Watcher实例]
* 其中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保持一致,实现双向绑定
}
})(i));
}
if (node.hasAttribute("v-bind")) { //如果有v-bind属性,只要使node的值及时更新为data中number的值即可
var attrVal = node.getAttribute("v-bind");
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
"innerHTML"
))
}
}
}
方法定义完了当然得调用,回到init方法
Vm.prototype._init = function (options) {
this.$options = options; //options为上面使用时传入的结构体,包括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的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
this._observer(this.$data);
this._complie(this.$el);
}
接下来实现一个Watcher,用来绑定update方法,实现对DOM的更新
//实现一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名称,例如文本节点,该值设置为"text"
this.el = el; //指令对应的DOM元素
this.vm = vm; //指令所属的实例
this.exp = exp; //指令对应的值,本例为:"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;当number改变时会触发update函数,保证对应的DOM内容进行更新
}
调用也很简单,也是vue最熟悉的调用方式
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>
//初始化构造函数
function Vm(options) {
this._init(options);
}
Vm.prototype._init = function (options) {
this.$options = options; //options为上面使用时传入的结构体,包括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的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
this._observer(this.$data);
this._complie(this.$el);
}
//实现_observer函数,对data进行处理,重写data的set和get函数
Vm.prototype._observer = function (data) {
//如果data数据为空
if (!data) {
return;
}
if (typeof data !== "object") {
throw new Error("data必须是一个对象");
}
var self = this;
/*
* 遍历data中所有属性
* Object.keys(obj)函数返回一个由一个给定对象的自身可枚举属性组成的数组
* */
Object.keys(data).forEach(function (key) {
//当前属性的值
var oldValue = data[key];
/*
* 按照前面的数据
* _binding = {
* number:{
* _directives: [...watch实例]
* }
* }
* 这里是先声明一个空数组,以后所有和data中某值有关系的都会追加到相应的_directives中。
* 为何要在这里声明?
* 因为这里要根据_binding的key必须为data中的属性(键)
* 那这个数组何时才有东西呢?
* 解析指令(比如v-bind)的时候push进去的,因为解析指令的时候,会解析每一个dom,然后解析出你的是点击事件还是修改文本内容。
* 然后在生成watcher追加进去这个数组-以后所有和data中某值发生改变只要循环执行这个数组中watcher的update更新方法就可以更新所有和这个data中的某值相关联的dom
* */
self._binding[key] = {
_directives: []
}
//获取本data某属性对应的_directives
var binding = self._binding[key];
/**
* 语法:Object.defineProperty(obj, key, descriptor)
* @param: obj:需要定义属性的对象;
* key:需要定义或修改的属性;
* descriptor:将被定义或修改属性的描述符
*/
Object.defineProperty(data, key, { //实现双向绑定的关键代码
enumerable: true, // 可枚举--可被for-in和Object.keys()枚举。
configurable: true, //当且仅当值为true时,该属性描述符才能够被改变,也能被删除
//value: undefined, //该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
//writable: false, //当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
get: function () { //一个给属性提供getter的方法,当访问该属性时方法会被执行,执行时没有参数传入,但会传入this对象
//发现这个oldValue如果替换成data[key]会造成堆栈溢出。
return oldValue;
},
set: function (newValue) { //一个给属性提供setter的方法,当属性值修改时触发该方法,该方法将接受唯一参数,即该属性新的参数值。
if (oldValue == newValue) return;
console.log("监听到值变化了");
//发现这个oldValue如果替换成data[key]会造成堆栈溢出。
oldValue = newValue;
// 当data中某属性改变时,触发_binding['某值']._directives 中的绑定的Watcher类的更新--这样实现一个data中的值发生改变,和它相关联的dom更新。
binding._directives.forEach(function (item) {
item.update();
})
}
});
})
}
//定义complie函数,用来解析指令(v-bind,v-model,v-click)等,并在这个过程中对view与model进行绑定。
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++) { //对所有的元素进行遍历,并处理
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute("v-click")) { //如果有v-click属性,我们监听onclick事件,触发increment、subtract方法
node.onclick = (function () {
var attrVal = nodes[i].getAttribute("v-click");
return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致
})();
}
if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model属性,并且元素为input或者textarea,我们监听它的input事件
node.addEventListener("input", (function (key) {
var attrVal = node.getAttribute("v-model");
/**
* _this._binding["number"]._directives = [一个Watcher实例]
* 其中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保持一致,实现双向绑定
}
})(i));
}
if (node.hasAttribute("v-bind")) { //如果有v-bind属性,只要使node的值及时更新为data中number的值即可
var attrVal = node.getAttribute("v-bind");
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
"innerHTML"
))
}
}
}
//实现一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
function Watcher(name, el, vm, exp, attr) {
this.name = name; //指令名称,例如文本节点,该值设置为"text"
this.el = el; //指令对应的DOM元素
this.vm = vm; //指令所属的实例
this.exp = exp; //指令对应的值,本例为:"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;当number改变时会触发update函数,保证对应的DOM内容进行更新
}
window.onload = function () {
var vm = new Vm({
el: "#app",
data: {
number: 0,
age: 18
},
methods: {
increment: function () {
this.number++;
},
subtract: function () {
this.number--;
}
}
})
}
</script>
网友评论