1、通过Object.defineProperty(obj, prop, descriptor)劫持对象的属性读写,其中obj是要在上面定义属性的对象,prop是要定义或修改的属性名称,descriptor是属性的描述符。描述符中可选get和set键值。get是属性的getter方法,返回属性值;set为setter方法,接受唯一参数,并将该参数的值赋值给属性,get和set的默认值均为undefined。
2、双向绑定的简单实现。
<input type="input" id="input">
<span id="show"></span>
<script>
var obj = {};
Object.defineProperty(obj, 'txt', {
get: function () {
return obj;
},
set: function (newValue) {
document.getElementById('input').value = newValue;
document.getElementById('show').innerHTML = newValue;
}
});
document.getElementById('input').addEventListener('keyup', function (e) {
obj.txt = e.target.value;
});
</script>
当通过input进行输入时,obj.txt的值会相应更新;当通过控制台改变obj.txt的值时,setter会改变view,从而实现了view=>model,model=>view的双向绑定。不过这种简单绑定不会真正执行obj.txt = e.target.value,obj永远为{},如果在set方法中赋值obj.txt = e.target.value,则会造成无限循环。为解决这个问题,将Object.defineProperty()封装为一个函数,即可在其中保存状态obj.txt,修改如下:
<input type="text" id="input">
<div id="show"></div>
<script>
function defineProperty(obj, attr){
var val;
Object.defineProperty(obj, attr, {
get: function () {
return val;
},
set: function (newValue) {
if (newValue === val){
return;
}
val = newValue;
document.getElementById("input").value = newValue;
document.getElementById("show").innerHTML = newValue;
}
});
}
var obj = {};
defineProperty(obj, "txt");
document.getElementById("input").addEventListener("keyup", function(e){
obj.txt = e.target.value;
})
</script>
<div id='app'>
<input type="text" v-model="input">
{{text}}
</div>
<script>
function compile(node, vm){
if(node.nodeType === 1){
var attr = node.attributes;
for(let i = 0; i<attr.length; i++){
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}
}
if(node.nodeType === 3){
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
}
}
}
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child, vm);
flag.appendChild(child);
}
return flag;
}
function Vue(options){
var id = options.el;
this.data = options.data;
var dom = nodeToFragment(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
input: 'hello',
text: 'world'
}
});
</script>
function compile(node, vm){
if(node.nodeType === 1){
var attr = node.attributes;
for(let i = 0; i<attr.length; i++){
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = vm[name];
node.removeAttribute('v-model');
}
}
if (child = node.firstChild) {
compile(child, vm);
}
}
if(node.nodeType === 3){
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
}
}
}
function observe(data, vm){
Object.keys(data).forEach(function(key){
Object.defineProperty(vm, key, {
get: function (){
return vm[key];
},
set: function (newValue){
document.getElementById("show").innerHTML = newValue;
document.getElementById("input").value = newValue;
}
});
});
}
function compile(node, vm){
if(node.nodeType === 1){
var attr = node.attributes;
for(let i = 0; i<attr.length; i++){
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.addEventListener('keyup', function(e){
vm[name] = e.target.value;
});
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}
if (child = node.firstChild) {
compile(child, vm);
}
}
...
}
function Vue(options){
var id = options.el;
this.data = options.data;
observe(this.data, this);//添加view=>model的绑定
...
}
<div id='app'>
<input type="text" v-model="input" id="input">
<span id="show"></span>
</div>
function defineProperty(vm, key, val){
Object.defineProperty(vm, key, {
get: function (){
return val;
},
set: function (newValue){
document.getElementById("show").innerHTML = newValue;
document.getElementById("input").value = newValue;
if(newValue === val){
return;
}
val = newValue;
}
});
}
function observe(data, vm){
Object.keys(data).forEach(function(key){
defineProperty(vm, key, data[key]);
});
}
修改后vm.input的值就能正确改变和返回了。但此时view的变化是通过setter手动添加的,而且只能是元素形式的节点,如果节点是{{text}}模板字符串则无法动态改变。解决办法是采用订阅/发布模式进行修改,对每个节点绑定一个观察者Watcher,对每个数据绑定一个分发者Dep,数据改变时,调用分发者的通知方法,通知每个观察者进行相应的改变。
5、订阅/发布模式(subscribe&publish)
采用订阅/发布模式对代码进行修改。首先定义观察者Watcher,并在编译函数compile()中对每个节点添加观察着Watcher,当接收到分发者指令时,调用update方法更新视图。接下来定义消息分发者Dep,Dep维护观察者数组,当值发生变化时,通知各观察者调用update方法。完整代码如下:
//第三部分
function Watcher(vm, node, name, nodeType){
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function(){
this.get();
if (this.nodeType === 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType === 'input') {
this.node.value = this.value;
}
},
get: function(){
this.value = this.vm[this.name];
}
}
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
});
}
}
//第二部分
function defineProperty(vm, key, val){
var dep = new Dep();
Object.defineProperty(vm, key, {
get: function (){
if(Dep.target){
dep.addSub(Dep.target);
}
return val;
},
set: function (newValue){
if(newValue === val){
return;
}
val = newValue;
dep.notify();
}
});
}
function observe(data, vm){
//Object.keys(data)返回data的key数组
Object.keys(data).forEach(function(key){
defineProperty(vm, key, data[key]);
});
}
//第一部分
function compile(node, vm){
if(node.nodeType === 1){
var attr = node.attributes;
for(let i = 0; i<attr.length; i++){
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.addEventListener('keyup', function(e){
vm[name] = e.target.value;
});
node.value = vm[name];
node.removeAttribute('v-model');
new Watcher(vm, node, name, "input");
}
}
if (child = node.firstChild) {
compile(child, vm);
}
}
if(node.nodeType === 3){
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
// node.nodeValue = vm.data[name];
new Watcher(vm, node, name, "text");
}
}
}
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child, vm);
flag.appendChild(child);
}
return flag;
}
function Vue(options){
var id = options.el;
var data = options.data;
observe(data, this);
var dom = nodeToFragment(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
input: 'hello'
}
});
原文连接:https://blog.csdn.net/orangecsy/article/details/79759663
以上适用于类vue的mvvm框架,angular2使用的是自己实现的双向绑定