前言
看了Vue
的一些思想之后,开始有想法去模仿Vue
写一个小的MVVM
,奈何当自己真正开始写的时候才知道有多难,不过也让自己明白,自身的编码水平和设计代码的思维还有很大的提升空间,哈哈哈。
开始
先来一个基本的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
类了。我感觉写这个的话一种由上而下的思路会比较好,就是先把最顶层的思路想好,然后再慢慢往下写细节。
MVVM
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
节点确实存在的话,就可以开始我们的初始化编译模板操作。
Compiler
function Compiler(el, vm) {}
看上面我们知道,Compiler
的参数有两个,一个是$el字符串
,还有一个就是我们的MVVM
实例,上面我传了this
。
遍历子节点
首先我们先来思考,编译模板的时候希望的是将类似{{key}}
的部分用我们的data
对象中的对应的value
来取代。所以我们应该先遍历所有的dom
节点,找到形如{{key}}
所在的位置,再进行下一步操作。先来两个函数
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
做了啥。
Watcher
function Watcher(child, vm, initKey) { this.initKey = initKey this.update = function (key) { if (key === initKey) { compilerTextNode(child, vm, initKey) } } }
首先把所对应的子节点child
传入,然后vm
实例也要传入,因为下面有一个函数需要用到vm实例,然后这个initKey
是我自己的一些骚操作(流下了没有技术的泪水),它的作用主要是记录一开始的那个key
值,为啥要记录呢,请看下面的方法。
compilerTextNode
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
数组里,并且返回。
Observer
function Observer(data, watchers) {}
然后就到了我们熟悉的响应式数据啦,这个函数接受两个参数,一个就是我们一开始定义的data
对象,还有一个就是刚才我们拿到的watchers
数组。
observe
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
。
defineReactive
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
(捂脸),编码能力真心还有待提高,继续加油吧!