Created
May 25, 2017 01:32
-
-
Save yiyizym/593d535dee849ebe61b63834751fee8c to your computer and use it in GitHub Desktop.
一个能用的简单 mvvm 实现
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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