今天讲下vue的响应式数据,也就是mvvm双向绑定模式,主要的目的是要让大家了解该模式在vue中是如何实现的,所以将以极简的代码进行示例。
我们先假设这样的一个使用情景:
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>复制代码
let vm=new Vue({
el:'app',
data:{
text:'hello world'
}
});复制代码
这里就涉及到了vue的双向绑定。
接下来我就用一些非常简单代码实现以上功能。
首先,我们得解析vue中的v-model
指令,也就是html中的自定义属性,以及插入组件中的变量{{text}}
。这两者都与响应式数据text
有关。
vue2.x后就使用了虚拟dom和diff算法,但今天的目的是理解双休绑定的原理,为了方便大家的理解,这里使用vue1.x用过document Fragment
来代替。document Fragment
是一个节点片段,对它的进行dom操作不会导致页面的重排和重绘。它就是一个优化dom操作的对象。具体的大家可以参考下MDN教程。
/**
* 节点转换成节点片段,优化动态改变节点的性能
* @param root - vue的根节点
* @param vm - vue实例
*/
function nodeToFramge(root,vm) {
let df=document.createDocumentFragment();
let node;
//不断的把vue要管理的dom元素放到document Fragment中。
while(node=root.firstChild){
//优化dom操作性能问题。
df.appendChild(node);
//进行dom的解析,也可以理解成编译吧
compile(node,vm);
}
return df;
}复制代码
如何解析呢,一般就是遍历dom节点了,然后判断其中的节点类型,是元素节点就获取其中的属性节点,然后再进行遍历,最后获取到v-model
属性,简单示例:
/**
*编译模板
* @param node - vue管理的节点
* @param vm - vue实例
*/复制代码
function compile(node,vm) {
//节点类型为元素
if(node.nodeType===1){
let attr=node.attributes;
//遍历解析html的属性
for(let i=0;i<attr.length;i++){
//v-m的数据响应
if(attr[i].nodeName==='v-model'){
//获取html属性的值,也就是响应式数据的键名
let name=attr[i].nodeValue;
//初始化输入控件的数据
node.value=vm[name];
//监听数据的变化,实现v-m的数据响应
node.addEventListener('input',function (e) {
vm[name]=e.target.value;
});
//删除v-model自定义属性
node.removeAttribute('v-model');
}
}
}
}复制代码
而当遍历到文本节点时:
//节点为文本类型
else if(node.nodeType===3){
//识别响应式数据的正则表达式
let reg=/\{\{(.*)\}\}/;
//找出响应式数据
if(reg.test(node.nodeValue)){
//从正则表达式的子表达式中获取响应式数据的键名
let name=RegExp.$1.trim();
//创建观察者
new Watcher(vm,node,name);
}
}复制代码
然后我们开始为每个插入dom的中数据实现一个观察者:
/**
* 观察者
* @param vm
* @param node
* @param name
* @constructor
*/
function Watcher(vm,node,name) {
//标志变量。判断是否要进行观察者的注册。
Moniter.target=this;
//要改变的节点
this.node=node;
//响应式数据的键名
this.name=name;
//vue实例
this.vm=vm;
//初始化数据和注册观察者
this.update();
//注册完成,取消标志变量
Moniter.target=null;
}
//更新数据
Watcher.prototype.update=function () {
//第一次调用时就是触发数据的get方法去初始化数据和注册观察者。之后时更新数据
this.node.nodeValue=this.vm[this.name];
};复制代码
然后数据劫持,注册观察者。数据劫持主要用到了Object.defineProperty方法,具体的同学可以看MDN的教程。
/**
* 数据劫持
* @param vm
*/
function defineReactive(vm) {
Object.keys(vm.data).forEach(function (name) {
//保存未被访问器属性覆时,数据属性的值。
let value=vm.data[name];
//注册监听者
let mo=new Moniter();
//数据劫持
Object.defineProperty(vm,name,{
set:function (newValue) {
if(value===newValue) return;
//触发观察者实现数据更新
value=newValue;
mo.dispatch();
},
get:function () {
//判断是否时初始化数据,然后注册观察者
if(Moniter.target) mo.addWatcher(Moniter.target);
return value;
}
})
})
}复制代码
并为每个响应式的属性实现一个监听者:
/**
* 监听者
* @constructor
*/
function Moniter() {
//保存观察者的数组
this.watchers=[];
}
//触发观察者
Moniter.prototype.dispatch=function () {
this.watchers.forEach(function (watcher) {
watcher.update();
})
};
//注册观察者
Moniter.prototype.addWatcher=function (target) {
this.watchers.push(target);
}; 复制代码
vue的观察者并不是一个函数,而是一个对像,如watcher对象。每个属性都有一个监听者,就是保存观察者的数组。观察者和监听者之间又个全局标准,判断是否要实现数据监听。view到model方向的数据变化是js的事件监听实现的,也算是内置的观察者模式吧,在编译模板的时候就已经实现观察者的注册,mode到view方向的数据变化是自定义的观察者模式。在编译模板中创建观察者,在为数据创建访问器属性时创建坚监听者,在get方法中注册观察者,在set方法中触发监听器。
整体来说就是元素提取,模板编译,事件监听。
/**
* vue类
* @param options - 配置的数据
* @constructor
*/
function Vue(options) {
//将响应式数据与vue实例关联
this.data=options.data;
//获取vue的根节点
let root=document.getElementById(options.el);
//数据劫持
defineReactive(this);
//编译模板
root.appendChild(nodeToFramge(root,this));
}复制代码
这篇文章感觉涉及的东西有点多,而且有点绕,一直想不好该怎么写才能让大家更好的理解,因此,我只好把几乎每句代码都写上了注释,希望大家能够理解并且有所收获吧。
参考:Vue.js双向绑定的实现原理(这篇文章写得很好)