Skip to content

Instantly share code, notes, and snippets.

@yiyizym
Created May 25, 2017 01:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yiyizym/593d535dee849ebe61b63834751fee8c to your computer and use it in GitHub Desktop.
Save yiyizym/593d535dee849ebe61b63834751fee8c to your computer and use it in GitHub Desktop.
一个能用的简单 mvvm 实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src='test_1.js'></script>
</head>
<body>
<div id="mvvm-app">
<input type="text" v-model="word">
<p>{{word}}</p>
<button v-on:click="sayHi">change model</button>
</div>
<script>
function MVVM(options){
this.$options = options || {};
var data = this._data = this.$options.data;
var me = this;
Object.keys(data).forEach(function(key){
me._proxyData(key);
});
this._initComputed();
observe(data, this);
this.$compile = new Compile(options.el || document.body, this);
}
MVVM.prototype = {
$watch: function(key, cb, options){
new Watcher(this, key, cb);
},
_proxyData: function(key, setter, getter){
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter(){
return me._data[key];
},
set: function proxySetter(newVal){
me._data[key] = newVal;
}
});
},
_initComputed: function(){
var me = this;
var computed = this.$options.computed;
if(typeof computed === 'object'){
Object.keys(computed).forEach(function(key){
Object.defineProperty(me, key, {
get: typeof computed[key] === 'function'
? computed[key]
: computed[key].get,
set: function() {}
})
})
}
}
}
function observe(value, vm){
if(!value || typeof value !== 'object'){
return;
}
return new Observer(value);
}
function Observer(data){
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk: function(data){
var me = this;
Object.keys(data).forEach(function(key){
me.convert(key, data[key]);
});
},
convert: function(key, val){
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val){
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function(){
if(Dep.target){
dep.depend();
}
return val;
},
set: function(newVal){
if(val === newVal){ return; }
val = newVal;
childObj = observe(newVal);
dep.notify();
}
})
}
}
var uid = 0;
function Dep(){
this.id = uid ++;
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
depend: function(){
Dep.target.addDep(this);
},
removeSub: function(sub){
var index = this.subs.indexOf(sub);
if(index !== -1){
this.subs.splice(index, 1);
}
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
});
}
}
Dep.target = null;
function Watcher(vm, expOrFn, cb){
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if(typeof expOrFn === 'function'){
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn);
}
this.value = this.get();
}
Watcher.prototype = {
update: function(){
this.run();
},
run: function(){
var value = this.get();
var oldVal = this.value;
if(value !== oldVal){
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
addDep: function(dep){
if(!this.depIds.hasOwnProperty(dep.id)){
dep.addSub(this);
this.depIds[dep.id] = dep;
}
},
get: function(){
Dep.target = this;
var value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
},
parseGetter: function(exp){
if(/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj){
for(var i = 0, len = exps.length; i < len; i++){
if(!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
function Compile(el, vm){
this.$vm = vm;
this.$el = this.isElementNode(el) ? el: document.querySelector(el);
if(this.$el){
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
node2Fragment: function(el){
var fragment = document.createDocumentFragment(),
child;
while(child = el.firstChild){
fragment.appendChild(child);
}
return fragment;
},
init: function(){
this.compileElement(this.$fragment);
},
compileElement: function(el){
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node){
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
if(me.isElementNode(node)){
me.compile(node);
} else if(me.isTextNode(node) && reg.test(text)){
me.compileText(node, RegExp.$1);
}
if(node.childNodes && node.childNodes.length){
me.compileElement(node, RegExp.$1);
}
});
},
compile: function(node){
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr){
var attrName = attr.name;
if(me.isDirective(attrName)){
var exp = attr.value;
var dir = attrName.substring(2);
if(me.isEventDirective(dir)){
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
})
},
compileText: function(node, exp){
compileUtil.text(node, this.$vm, exp);
},
isDirective: function(attr){
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir){
return dir.indexOf('on') === 0;
},
isElementNode: function(node){
return node.nodeType == 1;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
}
var compileUtil = {
text: function(node, vm, exp){
this.bind(node, vm, exp, 'text');
},
html: function(node, vm, exp){
this.bind(node, vm, exp, 'html');
},
model: function(node, vm, exp){
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e){
var newValue = e.target.value;
if(val === newValue){ return; }
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
class: function(node, vm, exp){
this.bind(node, vm, exp, 'class');
},
bind: function(node, vm, exp, dir){
var updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue){
updaterFn && updaterFn(node, value, oldValue);
});
},
eventHandler: function(node, vm, exp, dir){
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if(eventType && fn){
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal: function(vm, exp){
var val = vm;
exp = exp.split('.');
exp.forEach(function(k){
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value){
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i){
if(i < exp.length - 1){
val = val[k];
} else {
val[k] = value;
}
});
}
};
var updater = {
textUpdater: function(node, value){
node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater: function(node, value){
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater: function(node, value, oldValue){
var className = node.className;
className = className
.replace(oldValue, '')
.replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
modelUpdater: function(node, value, oldValue){
node.value = typeof value == 'undefined' ? '' : value;
}
};
</script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Hello World!'
},
methods: {
sayHi: function() {
this.word = 'Hi, everybody!';
}
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment