前言
引用文章: Vue.js 是如何实现 MVVM 的?
上文中对mvvm的讲解很有一套,收获颇多. 文章最后实现了vue.js. 自己敲了五六遍,最后做了三点优化:
- compile.js 文件中 createHandleElement函数
- 删除xvue.js 中的proxydata代理函数.
- 修复多级侦听
ps1: 强烈建议自己可以纯手打实现这个代码. 也建议深读一下,原作者写的注释很好.
ps2: 作者在代码中有一句console.log(dep.deps)注释掉了,各位可以想一下会输出什么.
代码用到的知识点
这个代码用到的知识点,我总结一下:
- 闭包
- Object.defienPrototype
- bind的使用
- 观察者模式
- fragement优化渲染
在实现的过程中,也可以顺便把这些知识点学习一下。
我的代码
1. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" >
<title>123</title>
</head>
<body>
<div id="app">
{
{test}}
<div v-text='test'></div>
<p>
<input type="text" v-model='test' />
</p>
<p v-html='html'></p>
<p>
<button @click='onClick'>按钮</button>
</p>
</div>
<script src="./compile.js"></script>
<script src="./XVue.js"></script>
<script>
var o = new XVue({
el:'#app',
data:{
test:'123',
foo:{
bar:'bar'
},
html:'<button>test html</button>'
},
methods:{
onClick(){
alert('点击按钮!')
}
}
})
console.log(o.$data.test)
o.$data.test = 'hello vue'
console.log(o.$data.test)
</script>
</body>
</html>
2. XVue.js
class XVue {
constructor(option) {
this.$data = option.data;
this.$options = option;
this.observe(this.$data)
new Compile(option.el, this)
}
observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key])
})
}
defineReactive(obj, key, value) {
this.observe(value);
var dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return value
},
set(newVal) {
value = newVal
dep.notify()
}
})
}
}
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => {
dep.update()
})
}
}
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this;
if (this.key.indexOf('.') > 0) {
var value = this.key.split('.').reduce((initial, value) => { return initial[value] }, vm)
console.log(value)
} else {
this.vm[this.key];
}
Dep.target = null
}
update() {
if (this.key.indexOf('.') > 0) {
var value = this.key.split('.').reduce((initial, value) => { return initial[value] }, this.vm)
this.cb.call(this.vm, value)
} else {
this.cb.call(this.vm, this.vm[this.key])
}
}
}
3. compile.js
var arr1 = [];
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.$fragement = this.node2Fragement(this.$el)
this.compile(this.$fragement)
this.$el.appendChild(this.$fragement)
}
node2Fragement(el) {
var fragement = document.createDocumentFragment();
var child
while (child = el.firstChild) {
fragement.appendChild(child)
}
return fragement;
}
compile(el) {
var childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isNodeType(node)) {
this.compileNode(node)
} else if (this.isElementType(node) &&
/\{\{(.*)\}\}/.test(node.textContent)) {
this.compileElement(node, RegExp.$1)
}
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
isNodeType(node) {
return node.nodeType == 1;
}
isElementType(node) {
return node.nodeType == 3;
}
compileNode(node) {
var attrs = node.attributes;
Array.from(attrs).forEach(attr => {
var name = attr.name;
var exp = attr.value;
if (this.isDirective(name)) {
const dir = name.substr(2)
this[dir] && this[dir](node, this.$vm, exp)
} else if (this.isDirectiveEvent(name)) {
const dir = name.substr(1)
this.createHandleElement(node, this.$vm, exp, dir)
}
})
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text');
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
node.addEventListener('input', function (el) {
vm.$data[exp] = el.target.value;
})
}
update(node, vm, exp, dir) {
var fn = this[dir + 'Updater'];
if (exp.indexOf('.') > 0) {
var value = exp.split('.').reduce((initial, value) => { return initial[value] }, vm.$data)
fn && fn(node, value);
} else {
fn && fn(node, vm.$data[exp]);
}
new Watcher(vm.$data, exp, function (value) {
fn && fn(node, value)
})
}
textUpdater(node, value) {
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
modelUpdater(node, value) {
node.value = value
}
isDirective(str) {
return str.indexOf('v-') === 0;
}
isDirectiveEvent(str) {
return str.indexOf('@') === 0;
}
compileElement(node, value) {
this.text(node, this.$vm, value)
}
createHandleElement(node, vm, exp, dir) {
var fn = vm.$options && vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm), false)
}
}
}