看了Vue
的一些思想以后,开始有想法去模仿Vue
写一个小的MVVM
,奈何当本身真正开始写的时候才知道有多难,不过也让本身明白,自身的编码水平和设计代码的思惟还有很大的提高空间,哈哈哈。html
先来一个基本的index.html
文件,而后咱们模仿Vue
的写法,实例化一个MVVM
类和定义data
对象(Vue
里为了拥有本身的命名空间data
应该为函数)数组
<!DOCTYPE html><html lang="en">
<head> ```</head>
<body>
<div id="app">
<div>
<div>
<span>{{hello}}</span>
</div>
<div>{{msg}}</div>
</div>
</div>
<script src="./src/index.js"></script>
<script>
const app = new MVVM({
$el: '#app',
data: {
msg: 'mvvm',
hello: 'david'
},
})
</script>
</body>
</html>复制代码
咱们设想是这样来操做滴,而后就能够编写咱们的MVVM
类了。我感受写这个的话一种由上而下的思路会比较好,就是先把最顶层的思路想好,而后再慢慢往下写细节。bash
class MVVM {
constructor(options) {
this.$el = options.$el
this.data = options.data
if (this.$el) {
const wathcers = new Compiler(this.$el, this)
new Observer(this.data, wathcers)
}
}
}复制代码
这里咱们定义了一个MVVM
类,在options
里面能够拿到$el
和data
参数,由于咱们上面的模板里面就是这么传的。若是传入的$el
节点确实存在的话,就能够开始咱们的初始化编译模板操做。app
function Compiler(el, vm) {}复制代码
看上面咱们知道,Compiler
的参数有两个,一个是$el字符串
,还有一个就是咱们的MVVM
实例,上面我传了this
。dom
首先咱们先来思考,编译模板的时候但愿的是将相似{{key}}
的部分用咱们的data
对象中的对应的value
来取代。因此咱们应该先遍历全部的dom
节点,找到形如{{key}}
所在的位置,再进行下一步操做。先来两个函数mvvm
this.forDom = function (root) {
const childrens = root.children
this.forChildren(childrens)
}复制代码
这是一个获取dom
节点的子节点的函数,而后将子节点传入下一个函数函数
this.forChildren = function (children) {
for (let i = 0; i < children.length; i++) {
//每一个子节点
let child = children[i];
//判断child下面有没有子节点,若是还有子节点,那么就继续的遍历
if (child.children.length !== 0) {
this.forDom(child);
} else {
//将vm与child传入一个新的Watcher中
let key = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "")
let watcher = new Watcher(child, vm, key)
//初始转换模板
compilerTextNode(child, vm)
watchers.push(watcher)
}
}
}复制代码
若是子节点还有子节点,就继续调用forDOM函数。不然就将标签中{{key}}
里面的key
拿出来(这里我只考虑了形如<div>{{key}}</div>
的状况,大佬轻喷),拿到key
以后就实例化一个watcher
,让咱们来看看watcher
作了啥。ui
function Watcher(child, vm, initKey) {
this.initKey = initKey
this.update = function (key) {
if (key === initKey) {
compilerTextNode(child, vm, initKey)
}
}
}复制代码
首先把所对应的子节点child
传入,而后vm
实例也要传入,由于下面有一个函数须要用到vm实例,而后这个initKey
是我本身的一些骚操做(流下了没有技术的泪水),它的做用主要是记录一开始的那个key
值,为啥要记录呢,请看下面的方法。this
compilerTextNode = function (child, vm, initKey) {
if (!initKey) {
//第一次初始化
const keyPrev = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "") //获取key的内容
if (vm.data[keyPrev]) {
child.innerText = vm.data[keyPrev]
} else {
throw new Error(
`${key} is not defined`
)
}
} else {
child.innerText = vm.data[initKey]
}复制代码
首先这个函数会有两个逻辑,一个是初始化的时候,还有一个是数据更新的时候。能够看到初始化的时候咱们是这样作的compilerTextNode(child, vm)
,也就是会进入这个if
逻辑。这里就是拿到了模板中的key
值,而后节点的值替换成咱们data
对象里面的值。为啥要记录这个initKey
呢,就是在这里若是模板的innerText
直接被整个替换掉了,例如说本来模板中是{{msg}}
,它通过这个函数处理以后,会变成mvvm
,那咱们的data
中是没有mvvm
这个key
的,这里记录是为了更新的时候用。最后,全部的watcher
都会被push
进watchers
数组里,而且返回。编码
function Observer(data, watchers) {}复制代码
而后就到了咱们熟悉的响应式数据啦,这个函数接受两个参数,一个就是咱们一开始定义的data
对象,还有一个就是刚才咱们拿到的watchers
数组。
this.observe = function (data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
this.observe(data[key]) //递归深度劫持
})
}复制代码
首先咱们先来对data
作一下判断,而后调用defineReactive
方法对data
作响应式处理,最后来个递归深度劫持data
。
this.defineReactive = function (obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newValue) {
if (newValue !== value) {
that.observe(newValue)
value = newValue
//从新赋值以后 应该通知编译器
watchers.forEach(watcher => {
watcher.update(key)
})
}
}
})
}复制代码
get
方法调用时直接返回value
,set
方法调用时若是value
有从新赋值,那么应该从新监听value
的新值,而后用watcher
通知编译器从新渲染模板。
而后调用observe方法,this.observe(data)
这里咱们再看回watcher.update
方法,在defineReactive
方法中调用时传入的key
是咱们data
中定义的,而这个initKey
也就是咱们以前在初始化模板的时候保存的,当这两个相等的时候才从新渲染对应的模板块
this.update = function (key) {
if (key === initKey) {
compilerTextNode(child, vm, initKey)
}
}复制代码
最后让咱们来看一眼效果,加上一小段改变数据的代码。
setTimeout(() => {
app.data.msg = 'change'
}, 2000)复制代码
咱们来思考一下Observer
、Watcher
、Compiler
三者之间的关系。Observer
最重要的职责是把数据变成响应式的,换句话说就是咱们能够在数据被取值或者赋值的时候加入一些本身的操做。Compiler
就是把HTML
模板中的{{key}}
变成咱们data
中的值。Watcher
就是它们两者之间的桥梁了,在一开始的时候观察全部存在插值的节点,当data
中的数据更新时,能够通知模板,让其从新渲染同步data
中的数据。
最后,其实我也不知道写的这个算不算MVVM
(捂脸),编码能力真心还有待提升,继续加油吧!