好久好久好久都没有更新过文章了。无论怎样,要开始加油了!切入正题ing~~~
使用过vue的人都知道,vue的双向绑定是靠Object.defineProperty来实现的。抛开vue,如果让我们通过Object.defineProperty来实现一个双向绑定,应该如何实现呢?
先看一段vue使用实例:
"app"> <form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加button>
form>
<h3 v-bind="number">h3>
div>
var app = new Vue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
}
}
})
通过这段代码,我们可以看到其涉及到的功能点有
v-model
v-bind
v-click
那么怎么来实现呢?我们根据代码拆分一下功能点吧
Vue是一个构造函数,因此,首先声明一个带有配置参数的构造函数
监听data中数据变化
监听到data变化后,去触发view的渲染
view中数据更新触发data变更(v-model)和事件(onclick)绑定
根据上面的功能点,我们一个个来实现它
一、声明一个构造函数
function Vue(options) { this.$el = options.el this.$data = options.data this.$methods = options.methods this._observe(this.$data) // 监听数据变化}
二、监听data中数据变化
我们都知道,监听数据变化有两种方法Object.defineProperty和Proxy(下篇文章来介绍),vue2.0使用了前者。我们先来简单熟悉一下Object.defineProperty的用法。
let value = 'dandan'Object.defineProperty(window, 'name', { configurable:true, enumerable: true, get: function() { return value }, set: function(val) { console.log('变更啦---') if (val!==value) { value = val } }})console.log(name) // dandanvalue = 'xiaoqi' // 变更啦---console.log(name) // xiaoqi
通过这段代码,相信大家已经大概知道Object.defineProperty如何监听数据变化了吧
接下来,我们监听下data中数据变化,监听data数据变化,无非就是递归循环data中所有属性,并通过Object.defineProperty来定义它。从而实现数据监听。我们就不递归每一层了,只循环遍历下data第一层数据。
Vue.prototype._observe = function(obj) { for(let key in obj) { let value = obj[key] if(obj.hasOwnProperty(key)) { Object.defineProperty(this.$data, key, { enumerable: true, configurable: true, get: function (param) { return value }, set: function(newV) { if (value !== newVal) { value = newVal; // todo 数据变化后,触发dom渲染,从而实现mv绑定 } } }) } } }
三、监听到data变化后,去触发view的渲染
第二步我们已经监听到数据的变化了,那么怎么去触发dom更新呢?我们首先需要拿到dom和data&methods之间的关系
那么问题来了,怎么拿?仔细看下我们的代码,data和dom之间的映射关系其实是存在我们的视图view中,因此,我们通过遍历视图代码,即可拿到对应关系(v-model=**,v-bind=**,v-click=**)
代码如下:
function Vue(options) { this.$el = options.el this.$data = options.data this.$methods = options.methods this.$binding = {} this._observe(this.$data) // 监听数据变化}Vue.prototype._complie = function (root) { // root 为 id为app的Element元素,也就是我们的根元素 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事件,即number++ 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['vaule'] = _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' )) } } function Watcher(name, el, vm, exp, attr) { this.name = name; //指令名称,例如文本节点,该值设为"text" this.el = el; //指令对应的DOM元素 this.vm = vm; //指令所属myVue实例 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内容进行了更新。 }
拿到指令与data或methods的关系后,我们就可以在set中,更新dom了,从而实现双向绑定,更新的代码如下:
this.binding[key]._directives.forEach(function (item) { // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新 item.update(); })
好了,一个简单的双向绑定实现了。
拖了一周的文章,终于完成了
再去看会儿react去!