本文学习了https://www.cnblogs.com/wjyz/p/11419073.html,完整的代码在最下面会展示
有关于图形化的编程,我们往往会采用MVVM的模式来进行编程,将页面抽象成数据可以让编程变得更好把握,网页前端也是如此,频繁的dom操作势必造成逻辑上的混乱,当项目特别庞大的时候,比如开发前端Excel时,一个单元格合并操作会造成大量dom元素的变更,删除行和添加行也会造成dom的大量变更,造成逻辑上的混乱,维护和开发就会变得非常困难,因此,我们程序员只要修改和页面UI绑定的数据 (对象,数组,list一类的存在于内存中的数据结构),然后通过程序去映射在界面上,这样的开发才能更好的维护。
首先先看个案例。
var student = {"name":"hzy","age":20};
observe(student);
student.age = 24;
console.log('student.age is '+student.age);
我们定义了一个对象,调用了一个js函数,并且修改了age的值,但是在打印时并没有改变,这证明js是通过get,set方法来修改值的,js提供了一层包装,而提供编写get,set函数的方法我这里采用 Object.defineProperty(data, item , {});
用法如下;
var student = {"name":"hzy","age":20};
Object.defineProperty(student,"proxyName",{
get: function(){
console.log("before get value");
return student.name;
},
set: function(newVal){
console.log("before set value");
this["name"] = newVal;
}
})
console.log(student.proxyName);
student.proxyName = "proxyNameHzy";
console.log(student.name);
MDN解释
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
这样我们就知道在set的时候是可以调用我们自定义方法的,这就为 js 对象的值改变时去更改界面样式提供了支持。
当然,用户是不会用proxyName来操作name的,用户的写法肯定还是student.name = newValue;但如果把Object.defineProperty的第二个参数换成name,是会造成栈溢出的,代码如下。
var student = {"name":"hzy","age":20};
Object.defineProperty(student,"name",{
get: function(){
return this[name];
},
set: function(newVal){
this["name"] = newVal;
}
})
student.name = "newNameHzy";
原因是无限递归了。
我们的目标自然也不是给某一个特定的对象的特定的值加set,而是任意对象的全部Object属性添加set。(function属性是不需要添加set的。)
代码如下
function observe(data) {
if(typeof data !== 'object' || !data) return;
Object.keys(data).forEach(item =>{
let val = data[item];
Object.defineProperty(data, item , {
get: function(){
return val;
},
set: function(newVal){
console.log('before set');
val = newVal;
}
})
})
}
var student = {"name":"hzy","age":20};
var cat = {"color":"orange","type":"orange cat"};
observe(student);
observe(cat);
student.name = "newName";
cat.color = "black";
这样用户在用自己的对象时不会有任何的不一样,但是在修改时可以加入我们的逻辑,我们可以在set时做脏值检查来修改前端的UI界面。
接下来我们思考用户至少需要提供给我们什么数据,经过思考得知用户至少给一个dom元素和一个值,这就够了。如下所示
<div id="app">
<div>{{inputData}}</div>
<input type="text" id="input" v-model="inputData">
</div>
function MyMVVM(options) {
this.$options = options;
this.$el = options.el;
this.$data = options.data;
new Compile(this);
}
let appData = {val: 123};
new MyMVVM({
el: document.getElementById('app'),
data: appData
})
接下来编写脏值检查逻辑,脏值的存储应该是一个数组,数组中放着一个对象的全部属性的脏值,也就是一个键值对的形式来表示脏值。脏值就是dom元素的value,新值就是js对象的值。watch就是用于脏值检查的对象。
option就是用户传递给我们的数据对象,也就是上面的new MyMVV(),nodeValue就是绑定的值这里是val,node就是和nodeValue绑定的dom元素了就是上面的<input>
function Watcher(option, nodeValue ,node){
this.option = option;
this.node = node;
//利用这种方式可以和observe监听器的sub对象产生关联,应该可以采取更加巧妙的方法
Sub.target = this;
this.oldValue = this.option.$data[nodeValue];
Sub.target = null;
}
Watcher.prototype.dirtyValueCheck = function(newVal){
if(this.oldValue !== newVal){
this.value = newVal;
this.oldValue = newVal;
this.node.value = newVal;
this.node.textContent = newVal;
}
}
function Sub(){
this.subs = [];
}
Sub.prototype = {
add: function(watch){
this.subs.push(watch);
},
trigger: function(newVal) {
this.subs.forEach(watch => {
watch.dirtyValueCheck(newVal);
})
}
};
Sub.target = null;
Sub就是容器,循环遍历检查脏值,这个逻辑应该在observe中去调用,并且是在set中去使用。
function observe(data) {
if(typeof data !== 'object' || !data) return;
let sub = new Sub();
Object.keys(data).forEach(item =>{
let val = data[item];
Object.defineProperty(data, item , {
get: function(){
if(Sub.target){
sub.add(Sub.target);
}
return val;
},
set: function(newVal){
sub.trigger(newVal);
val = newVal;
}
})
})
}
总的来说就是当用户传递的数据发生了修改会调用set,set调用sub的trigger然后遍历里面的watch,如果出现脏值就要利用dom去修改UI界面,这就完成了模型绑定view的能力。
那么接下来就是如何添加watch了,watch就是提供node和value的一个容器,node的来源自然是html中的document,而value的来源就是用户提供的一个对象了。那就需要去遍历dom树了,然后把和用户提供的数据有绑定关系的node添加到sub中就可以了。对于插值表达式来说也是一样的。
//compile
function Compile(option){
this.option = option;
this.el = option.$el;
this.init();
}
Compile.prototype.init = function () {
observe(this.option.$data);
let fragment = document.createDocumentFragment();
let child = this.el.firstChild;
while(child) {
fragment.append(child);
child = this.el.firstChild;
}
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
let name = attr.nodeName;
if (name === 'v-model') {
//双向绑定
bingModelView(node,attr,this);
}
})
}
//实现插值表达式
interpolationRender(node,this);
})
//经过转化后的界面添加到节点下面就可以了
this.el.appendChild(fragment);
}
function bingModelView(node,attr,bindData){
let nodeValue = attr.nodeValue;
let bindValue = bindData.option.$data[nodeValue];
node.value = bindValue;
new Watcher(bindData.option, nodeValue, node);
node.addEventListener('input', e => {
let newVal = e.target.value;
if (bindValue !== newVal) {
bindData.option.$data[nodeValue] = newVal;
}
})
}
function interpolationRender(node,bindData){
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (reg.test(text)) {
let nodeValue = RegExp.$1;
let val = bindData.option.$data[nodeValue];
node.textContent = val;
new Watcher(bindData.option, nodeValue, node);
}
}
这样就完成了双向绑定最为基础的功能,还有很多功能没有实现,也有非常多的细节没有考虑到,以后会逐渐的完善。
下面附上完整的代码
MVVM.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src = "./winston_mvvm.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<div>{{inputData}}</div>
<input type="text" id="input" v-model="inputData">
</div>
<script>
function MyMVVM(options) {
this.$options = options;
this.$el = options.el;
this.$data = options.data;
new Compile(this);
}
let appData = {"inputData": 123};
new MyMVVM({
el: document.getElementById('app'),
data: appData
})
</script>
</body>
</html>
winston_mvvm.js
function Sub(){
this.subs = [];
}
Sub.prototype = {
add: function(watch){
this.subs.push(watch);
},
trigger: function(newVal) {
this.subs.forEach(watch => {
watch.dirtyValueCheck(newVal);
})
}
};
Sub.target = null;
function observe(data) {
if(typeof data !== 'object' || !data) return;
let sub = new Sub();
Object.keys(data).forEach(item =>{
let val = data[item];
Object.defineProperty(data, item , {
get: function(){
if(Sub.target){
sub.add(Sub.target);
}
return val;
},
set: function(newVal){
sub.trigger(newVal);
val = newVal;
}
})
})
}
//watcher
function Watcher(option, nodeValue ,node){
this.option = option;
this.node = node;
//利用这种方式可以和observe监听器的sub对象产生关联
Sub.target = this;
this.oldValue = this.option.$data[nodeValue];
Sub.target = null;
}
Watcher.prototype.dirtyValueCheck = function(newVal){
if(this.oldValue !== newVal){
this.value = newVal;
this.oldValue = newVal;
this.node.value = newVal;
this.node.textContent = newVal;
}
}
//compile
function Compile(option){
this.option = option;
this.el = option.$el;
this.init();
}
Compile.prototype.init = function () {
observe(this.option.$data);
let fragment = document.createDocumentFragment();
let child = this.el.firstChild;
while(child) {
fragment.append(child);
child = this.el.firstChild;
}
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
let name = attr.nodeName;
if (name === 'v-model') {
//双向绑定
bingModelView(node,attr,this);
}
})
}
//实现插值表达式
interpolationRender(node,this);
})
//经过转化后的界面添加到节点下面就可以了
this.el.appendChild(fragment);
}
function bingModelView(node,attr,bindData){
let nodeValue = attr.nodeValue;
let bindValue = bindData.option.$data[nodeValue];
node.value = bindValue;
new Watcher(bindData.option, nodeValue, node);
node.addEventListener('input', e => {
let newVal = e.target.value;
if (bindValue !== newVal) {
bindData.option.$data[nodeValue] = newVal;
}
})
}
function interpolationRender(node,bindData){
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (reg.test(text)) {
let nodeValue = RegExp.$1;
let val = bindData.option.$data[nodeValue];
node.textContent = val;
new Watcher(bindData.option, nodeValue, node);
}
}