自己实现一个简单的MVVM
框架,可以帮助我们更好的理解Vue
框架,接下来我们将一步一步去实现自己的框架。
我们不妨叫它Wue
,之前有一篇文章介绍过响应式原理,这里就不作赘述。
思路
首先我们要明白,我们的Wue
构造函数需要做什么。
- 接收初始化参数
- 将data中的属性注册到
Wue
实例上 - 监听
data
属性值的变化 - 解析指令和插值表达式
明确直到要做的事情之后,我们一步一步实现
接受初始化参数
这个很简单,只需要保存传入的对象,并做校验
class Wue {
constructor (options) {
//保存配置项
this.$options = options || {}
//保存dom
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
//保存data
this.$data = options.data
}
}
注册data
的属性
为了能通过Wue
实例来操作data
对象的属性,我们需要将其注册到Wue
实例上。
怎么注册呢?
其实也是通过Object.defineProperty
实现的,我们只需要在操作Wue
属性的时候,映射到data
属性上即可。
//将data属性的值注册到options上
_proxyData () {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return this.$data[key]
},
set (newVal) {
if (this.$data[key] !== newVal) {
this.$data[key] = newVal
}
}
})
})
}
测试一下,我们会发现,可以直接通过Wue
实例来操作data
属性了。
创建Obserer
通过Observer来监听data
属性。
这里需要注意,监听的是data
属性,而非Wue
的属性。
//监听data属性值的变化
class Observer {
constructor (data) {
this.walk(data)
}
//遍历data,进行监听
walk (data) {
if (!data || typeof data != 'object') {
return
}
Object.keys(data).forEach(key => {
let currentVal = data[key]
this.defineReactive(data, key, currentVal)
})
}
defineReactive (data, key, currentVal) {
const context = this
//假如属性的值是对象,需要添加响应式
this.walk(currentVal)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
return currentVal
},
set (newVal) {
if (newVal !== currentVal) {
currentVal = newVal
//假如新赋值给属性的值式对象,需要添加响应式
context.walk(newVal)
}
}
})
}
}
这里需要额外注意两种情况
- 初始化的时候,属性值是对象的情况
- 重新给属性赋值为对象的情况
这两种情况的处理方法,都在上面的注释中给出,就是单独将这个对象属性做响应式处理。
解析指令和插值表达式
通过字符串匹配,将dom元素里的指令和插值表达式用相应的响应式数据代替
//解析器
const NodeTypeElement = 1
const NodeTypeText = 3
class Complier {
//需要传入 Wue实例
constructor (wm) {
this.$wm = wm
this.$el = wm.$el
this.complie(wm.$el)
}
complie (el) {
Array.from(el.childNodes).forEach(node => {
if (node.childNodes && node.childNodes.lenth != 0) {
//有子节点的情况下遍历子节点
this.complie(node)
}
//处理插值表达式
if (this.isTextNode(node)) {
this.complieText(node)
}
//处理指令
if (this.isElementNode(node)) {
this.complieElement(node)
}
})
}
//判断是否为文本节点
isTextNode (node) {
return node.nodeType === NodeTypeText
}
//判断是否为元素节点
isElementNode (node) {
return node.nodeType === NodeTypeElement
}
//判断是否属性是指令
isDirective (attr) {
return attr.startsWith('v-')
}
//解析插值表达式,将{{msg}} 替换为 wm.msg
complieText (node) {
let reg = /\{\{(.+)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.$wm[key])
}
}
//解析元素节点
complieElement (node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
if (this.isDirective(attr.name)) {
let type = attr.name.slice(2) + 'Updater'
this[type].call(this, attr)
}
})
}
// v-text处理 将属性值替换为data属性值
textUpdater (attr) {
attr.value = this.$wm[attr.value]
}
// v-model处理
modelUpdater (attr) {
attr.value = this.$wm[attr.value]
}
}
到此我们完成了模板解析,接下来是需要把数据监听和模板解析联系起来,这样才能保证,当我们数据模型改变的时候,视图会自动更新。
我们要观察者模式,响应式对象的每个属性都会创建自己的发布者,负责收集依赖。而观察者负责适时的去更新dom。
发布者
发布者有两个核心功能,依赖收集和触发观察者
//发布者,依赖收集,通知观察者
class Dep {
constructor () {
this.sub = []
}
addSub (watcher) {
this.sub.push(watcher)
}
notify () {
this.sub.forEach(item => {
item.update && item.update()
})
}
}
观察者
某个属性更新之后,发布者会通知观察者,触发dom更新
//观察者
class Watcher {
constructor (wm, key, cb) {
//传入wm实例和key,是为了创建watcher之后马上调用get()来然dep收集依赖
this.$wm = wm
this.$key = key
//dep当前的watcher
Dep.target = this
//会调用get方法,让dep收集当前watcher
let oldVal = wm[key]
//收集完之后销毁,防止重复收集
Dep.target = undefined
this.cb = cb
}
update () {
this.cb(this.$wm[this.$key])
}
}
联系数据模型和视图
接下来我们需要用观察者模式将数据模型和视图联系起来,真正实现数据驱动视图。
创建dep
每个对象都要对应一个dep,所以在做响应化处理的时候来创建dep。
Observer.js
defineReactive (data, key, currentVal) {
const context = this
//假如属性的值是对象,需要添加响应式
this.walk(currentVal)
//数据响应式处理的时候创建一个新的发布者
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
//依赖收集
Dep.target && dep.addSub(Dep.target)
return currentVal
},
set (newVal) {
if (newVal !== currentVal) {
currentVal = newVal
//假如新赋值给属性的值式对象,需要添加响应式
context.walk(newVal)
//通知观察者
dep.notify()
}
}
})
}
创建watcher
watcher负责视图的更新,自然要在解析模板的时候创建。
我们拿v-text
指令举例
compiler.js
// v-text处理 将属性值替换为data属性值
textUpdater (node,attr) {
node.textContent = this.$wm[attr.value]
//创建watcher,进行视图更新
new Watcher(this.$wm,attr.value,function(newValue){
node.textContent = newValue
})
}
dep和watcher对应
现在有了dep
和watcher
,怎么把他们对应起来呢?某个属性的dep
应该被跟它相关的watcher
观察到。
使用Dep.target
静态属性来保存watcher
实例。创建watcher
实例的时候,把当前watcher
实例赋值给Dep.target
,然后马上访问data.key
,
在get
里,Dep
实例从Dep.target
里取到watcher
实例收集起来,然后马上将Dep.targer
清空。
这里不可谓不精妙,我当时想了很久。
代码上面已经给出过了,这里不再重复贴了。
双向数据绑定
我们已经实现了数据驱动视图,接下来是先双向数据绑定,视图更新修改数据模型。
道理很简单就是在相应的dom上添加事件,我们以input
为例,只需要在模板解析时加上相应代码。
// v-model处理
modelUpdater (node,attr) {
node.value = this.$wm[attr.value]
new Watcher(this.$wm,attr.value,function(newValue){
node.value = newValue
})
//双向数据绑定
window.addEventListener('input',(event)=>{
this.$wm[attr.value] = node.value
})
}