响应式数据绑定是vue.js的核心之一,该功能的实现主要是利用Object.defineProperty
方法。
Object.defineProperty()
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。 存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。这里主要是通过操作存取描述符来实现某些功能。
下面简单的来实现一个hello存取访问器
var obj = {};
Object.defineProperty(obj, 'hello', {
get: function(){
console.log('调用了get取值属性');
},
set: function(v){
console.log('调用了set存值属性,值为:' + v);
}
})
obj.hello; //调用了get取值属性
obj.hello = 'How are you?'; // 调用了set存值属性,值为:How are you?
复制代码
这里的存取描述符也就是常说的访问属性,也就是对象内部的get和set方法,注意的是,访问器属性会覆盖同名的普通属性,因为访问器属性的优先级高于普通属性。
下面来实现一个极简版数据双向绑定效果
<input type="text" id="a" />
<span id="b"></span>
复制代码
var obj = {};
Object.defineProperty(obj, 'hello', {
set: function(newVal){
document.getElementById('a').value = newVal;
document.getElementById('b').innerHTML = newVal;
}
})
document.addEventListener('keyup',function(e){
obj.hello = e.target.value;
})
复制代码
- 上述是解析原理的简单案例,接下来我们一点一点,由外到内,来解析一个简单的vue案例,代码如下
<div id="app">
<input type="text" id="a1" v-model="text" />
{{text}}
</div>
复制代码
var vm = new Vue({
el: "app",
data: {
text: 'hello world'
}
})
复制代码
首先,这里有两个模块大的解析,一个是vue代码的解析,一个是DOM元素包括vue指令和插值语法的解析,我们先来看Vue的构造函数。 Vue构造函数主要包含两个功能,观察vue实例的data数据和编译DOM元素,最后把编译的内容插入到vue挂载的实例中。
function Vue(options){
this.data = options.data;
var data = this.data;
observe(data, this); // 解析vue实例的data
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this); //解析dom和vue指令
document.getElementById(id).appendChild(dom);
}
复制代码
这里的DocumentFragment(文档片段)可以看作节点的容器,当我们把它追加到某个节点上时,只有子节点会追加进去。
现在来看observe函数,这个主要是用来遍历vue实例的data数据
function observe(obj, vm){
Object.keys(obj).forEach(function(key){
defineReactive(vm, key, obj[key]);
})
}
复制代码
函数里面调用defineReactive函数,用户响应式绑定相关数据
function defineReactive(obj, key, val){
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function(){
//如果有新new的Watcher对象就往dep栈里面添加
if(Dep.target) dep.addSub(Dep.target);
return val;
},
set: function(newVal){
if( newVal === val) return
val = newVal;
dep.notify(); // 变更数据,发送notify
console.log(val);
}
})
}
复制代码
接下来看第二个模块,解析DOM建议vue指令,定义nodeToFragment函数
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){ // 循环第一个子元素
compile(child, vm); // 解析vue指令和插值语法
flag.appendChild(child); // 执行appendChild后会移除该子元素
}
return flag;
}
复制代码
compile函数负责编译vue指令和插值语法并监听input输入的变动。
function compile(node, vm){
var reg = /\{\{(.)\}\}/; // 匹配双括号插值
if(node.nodeType === 1){
var attr = node.attributes;
for(var i = 0; i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue;
node.addEventListener('input', function(e){
vm[name] = e.target.value;
})
node.value = vm[name];
node.removeAttribute('v-model');
}
}
new Watcher(vm, node, name, 'input'); // 节点编译会为每一个节点添加一个订阅
}
if(node.nodeType === 3){ //文本节点
if(reg.test(node.nodeValue)){
var name = RegExp.$1;
name = name.trim();
new Watcher(vm, node, name, 'text'); //节点编译会为每一个节点添加一个订阅
}
}
}
复制代码
最后,我们来看一下vue的订阅发布模式,既然是发布订阅,肯定会有发布方和订阅方,发布一般都是在特定的条件下触发的,所以这里先看订阅方
//参数说明:vue对象 添加订阅的节点 对应vue对象date的key 节点类型
function Watcher(vm, node, name, nodeType){
Dep.target = this; // 把当前new的Watcher对象赋予给全局对象Dep.target
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update(); // 执行update方法 下方prototype定义过
Dep.target = null;
}
Watcher.prototype = {
update: function(){ // 先获取vm的值 然后更新到节点上
this.get();
if(this.nodeType == 'text'){
this.node.nodeValue = this.value;
}
if(this.nodeType == 'input'){
this.node.vlaue = this.value;
}
},
get: function(){
this.value = this.vm[this.name];
}
}
复制代码
我们需要一个全局的主题对象,方便随时发布和订阅
function Dep(){
this.subs = []; // Watcher监听栈
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
};
复制代码
联系上面区块的代码,在上面vue data数据观察阶段,调用访问属性每取一个属性的值就会向主题栈中添加一个Watcher监听对象 而这个Watcher监听对象在编译DOM阶段通过new操作生成,调用访问属性每设置一个属性的值,表明数据已经变更,发送消息,告知主题对象 原数据已经变更,调用订阅对象Watcher执行更新操作。
至此,一个简单的vue小案例代码解析完成,双向绑定的效果也实现完成。