Vue双向数据绑定这一块看了好久,也找了很多博客掘金啊之类的看,总想整理出来,一直拖到现在,写写吧。
Vue双向数据绑定介绍
vue 双向数据绑定也就是说数据和视图同步。即数据发生变化,视图跟着变化;视图变化,数据也随之发生改变。即Vue中V-model v-html之类的。
Vue双向数据绑定原理
通过 数据劫持结合发布订阅模式的方式来实现的,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
第1步:需要observe的数据对象进行遍历,包括子属性对象的属性,都加上setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化;
第2步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;
第3步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
(1)在自身实例化时往属性订阅器(dep)里面添加自己
(2)自身必须有一个update()方法
(3)待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第4步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
原理核心:Object.defineProperty()函数
从这里说起:假定一个对象:
var dreamapple = {
firstName: 'dream',
lastName: 'apple'
};
为了给dreamapple一个fullName属性,并且当firstName和lastName发生变化,fullName属性也要变化。在Vue.js使用计算属性(computed)实现,即设置fullName属性,通过set/get方法实现该功能。即:
// ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
Vue.js是一个js框架,当然可以用原生js实现上述需求。通过给dreamapple这个对象设置了属性fullName的getter和setter方法实现。即
var dreamapple = {
firstName: 'dream',
lastName: 'apple',
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName(fullName) {
var names = fullName.trim().split(' ');
if(2 === names.length) {
this.firstName = names[0];
this.lastName = names[1];
}
}
};
dreamapple.firstName = 'Dream';
dreamapple.lastName = 'Apple';
console.log(dreamapple.fullName); // Dream Apple
dreamapple.fullName = 'Jams King';
console.log(dreamapple.firstName); // Jams
console.log(dreamapple.lastName); // King
这里呢,我们可以考虑一种更好的方法实现,即本节的主题:Object.defineProperty()。这个方法的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象;
Object.defineProperty(obj, prop, descriptor)
其中参数obj表示的是需要定义属性的那个对象,参数prop表示需要被定义或者修改的属性名,参数descriptor就是我们定义的那个属性prop的描述。
(1)value 该属性对应的值,可以是任何有效的JavaScript值(数值,对象,函数等),默认为undefined.
一个小例子说明参数descriptor:
(2) writable 当且仅当仅当该属性的writable为true时,该属性才能被赋值运算符改变;它的默认值为false.
(3) enumerable这个特性决定定义的属性是否是可枚举,默认是false;把它设置为true时这属性才可使用for(prop in obj)和Object.keys()中枚举出.
(4) configurable 这个特性决定对象的属性是否可被删除,以及除writable特性外的其它特性是否可被修改;并且writable特性值只可以是false
get 一个给属性提供getter的方法,如果没有getter则为undefined;该方法返回值被用作属性值,默认为undefined.
set一个给属性提供setter的方法,如果没有setter则为undefined;该方法将接受唯一参数,并将该参数的新值分配给该属性,默认为undefined.
下面解决该问题
Object.defineProperty(dreamapple, 'fullName', {
enumerable: true,
get: function () {
return this.firstName + ' ' + this.lastName;
},
set: function (fullName) {
var names = fullName.trim().split(' ');
if (2 === names.length) {
this.firstName = names[0];
this.lastName = names[1];
}
}
});
手写最简单的双向数据绑定实现代码
(1)首先通过compile()函数渲染数据(登录页面显示input数据),然后给input添加一个事件监听器,将改变后的数据赋值给$data.data;
(2)然后定义函数defineReactive()获取数据的变化返还给input;实际进行这一步骤策略是defineProperty()。
<body>
<div id="app">
<input type="text" v-model="messege">
</div>
</body>
<script src="./vue.js"></script>
<script>
// 目标:一打开页面就能渲染input数据;修改input,messege也自动改变;修改messege,input也自动改变。
const app = new Vue({
el: '#app',
data: {
messege: 'shawn'
}
})
</script>
vue.js文件
class Vue {
constructor(options) {
//挂载数据
this.$el = options.el;
this.$data = options.data;
this.observe();
this.compile();
}
observe() {
Object.keys(this.$data).forEach((key) => {
this.defineReactive(this.$data, key, this.$data[key]);
})
}
//负责input=>data.messege
compile() {
//获取inputDom
const divDom = document.querySelector(this.$el);
const inputDom = divDom.children[0];
//赋予inputDom第一次渲染的值
inputDom.value = this.$data.messege;
//事件监听器:当input变化时,赋值给messege
inputDom.addEventListener('input', e => {
this.$data.messege = e.target.value;
})
}
//负责数据劫持传递给订阅者
defineReactive(data, key, value){
Object.defineProperty(data, 'messege',{
get() {
return value;
},
set:(newValue) => {
value = newValue;
//获取inputDom
const divDom = document.querySelector(this.$el);
const inputDom = divDom.children[0];
//赋予inputDom第一次渲染的值
inputDom.value = value;
//事件监听器:当input变化时,赋值给messege
}
})
}
}
刚才说了,这是最简单的方式,不可避免有一些问题:没有实现指令v-model等指令,元素并列和嵌套的双向数据绑定也没实现。但是也可以算是手写了一个双向数据绑定。
手写双向数据绑定
有了上面的基础,理解这个就不难了,下面说一下我写这个代码的思路。
首先根据Vue真实双向数据绑定效果构建vue的构造函数,给它一个init属性,然后挂载原型的一些初始化操作。然后实现_obverse函数,对data进行处理,重写data的set和get函数。写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新。接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model)等,并在这个过程中对view与model进行绑定。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
<form>
<input type="text" v-model="count">
<button type="button" v-click="incre">增加</button>
</form>
<h3 v-bind="count"></h3>
</div>
</body>
<script>
function myVue(options) {
this._init(options);
}
myVue.prototype._init = function (options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this._binding = {};
this._obverse(this.$data);
this._complie(this.$el);
}
myVue.prototype._obverse = function (obj) {
var _this = this;
Object.keys(obj).forEach(function (key) {
if (obj.hasOwnProperty(key)) {
_this._binding[key] = {
_directives: []
};
console.log(_this._binding[key])
var value = obj[key];
if (typeof value === 'object') {
_this._obverse(value);
}
var binding = _this._binding[key];
Object.defineProperty(_this.$data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`${key}获取${value}`);
return value;
},
set: function (newVal) {
console.log(`${key}更新${newVal}`);
if (value !== newVal) {
value = newVal;
binding._directives.forEach(function (item) {
item.update();
})
}
}
})
}
})
}
myVue.prototype._complie = function (root) {
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')) {
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click');
return _this.$methods[attrVal].bind(_this.$data);
})();
}
if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function (key) {
var attrVal = node.getAttribute('v-model');
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function () {
_this.$data[attrVal] = nodes[key].value;
}
})(i));
}
if (node.hasAttribute('v-bind')) {
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];
}
window.onload = function () {
var app = new myVue({
el: '#app',
data: {
number: 0,
count: 0,
},
methods: {
increment: function () {
this.number++;
},
incre: function () {
this.count++;
}
}
})
}
</script>
</html>
到此结束啦,有用请点赞哦 git: github.com/Shawn199402…