为了方便实现模板编译这里用<data> </data>来代替{{}}语法,没有虚拟dom和diff算法
着重于理解vue的非侵入式双向绑定原理
文件目录
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="module">
import MVVM from './MVVM.js'
const vm = new MVVM({
el: '#app',
data: {
name: 'wenhao',
age: 21,
intro: ''
},
methods: {
inputAge(e) {
this.age = e.target.value
},
inputName(e) {
this.name = e.target.value
},
},
watch: {
name(newValue, oldValue) {
console.log('im watch')
}
},
computed: {
intro() {
return this.name + this.age
}
}
})
</script>
</head>
<body>
<div id="app">
<p>
我叫1<data>name</data>
</p>
<p>
我的年龄2 <data>age</data>
</p>
<p>
我的年龄 3<data>age</data>
</p>
<p>
我的年龄 3<data>age</data>
</p>
<p>
intro:<data>intro</data>
</p>
<input type="text" :value="age" @input="inputAge">
<input type="text" :value="name" @input="inputName">
</div>
</body>
</html>
MVVM.js
import Watcher from "./Watcher.js"
import observe from "./observe.js"
export default class MVVM {
constructor({ el, data, methods, watch, computed }) {
this.el = document.querySelector(el)
this._data = data
this.methods = methods
this._watch = watch
this._computed = computed
this.$initData(this._data)
this.$compile(this.el)
this.$handleWatch()
this.$handleComputed()
this.__proto__ = this._data
Object.assign(this, this.methods)
}
$initData(data) {
observe(data) // 数据劫持
}
$compile(el) { // 模板编译
if (el) {
if (el.tagName == 'DATA') {
new Watcher(this._data, el.textContent.trim(), (newVal, oldVal) => {
el.textContent = newVal
}).update()
return
}
const attrs = el.getAttributeNames && el.getAttributeNames()
if (attrs && attrs.includes(':value')) {
new Watcher(this._data, el.getAttribute(':value'), (newVal, oldVal) => { // 在数据劫持的基础上,加入回调
el.value = newVal
}).update()
}
if (attrs && attrs.includes('@input')) {
console.log(el);
const prop = el.getAttribute('@input')
el.addEventListener('input', this.methods[prop].bind(this))
}
if (el.nodeType === 1 && el.tagName !== 'DATA') {
el.childNodes.forEach(element => {
this.$compile(element)
});
}
}
}
$handleWatch() {
if (this._watch)
Object.keys(this._watch).forEach(n => {
new Watcher(this._data, n, this._watch[n].bind(this._data))
})
}
$handleComputed() {
if (this._computed)
Object.keys(this._computed).forEach(n => {
new Watcher(this._data, n, this._computed[n].bind(this._data), {
computed: true
})
})
}
}
Dep.js (依赖管理,收集watcher)
export default class Dep { // 观察者模式
constructor() {
this.subs = []
}
depend() {
if (Dep.target && !this.subs.includes(Dep.target)) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(n => {
n.update()
})
}
}
observe.js (数据劫持)
import Dep from "./Dep.js";
function defReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
return val
},
set: function (newVal) {
if (val != newVal) {
val = newVal
dep.notify()
}
}
})
}
export default function observe(ob) {
// 只是浅层的监听,实际会递归整个对象
Object.keys(ob).forEach(n => {
defReactive(ob, n, ob[n])
})
}
watcher.js (依赖或中介)
import Dep from "./Dep.js";
export default class Watcher {
constructor(target, prop, cb, options) {
this.target = target;
this.prop = prop;
this.cb = cb;
this.op = options
if (!this.op || !this.op.computed)
this.value = this.target[this.prop] // 不是计算属性的时候,才需要value
this.invoke()
}
update() {
if (this.op && this.op.computed) {
this.target[this.prop] = this.cb() // 计算属性直接更新
}
else {
const newVal = this.target[this.prop] // 获取新值
this.cb(newVal, this.value)
this.value = newVal // 然后让旧值更新
}
}
invoke() {
Dep.target = this;
let temp = ''
if (this.op && this.op.computed) {
const res = this.cb()
temp = res
} else {
this.target[this.prop]
}
Dep.target = null
if (this.op && this.op.computed) { // 如果是计算属性就在他依赖收集后赋值一下,不能在上面直接赋值,因为 this.target[this.prop] 这个会被依赖收集
this.target[this.prop] = temp
}
}
}
总结
此次着重实现是右侧部分,左侧的模板编译分用data></data>简化了,并没有实现虚拟dom和diff算法