vue用了有段时间了,参考了github的文章,写了个简化版
github文章链接:github.com/DMQ/mvvm
如果看不懂就看我的啊,嘿嘿嘿
前言
使用Vue也有一段时间了,vue作为一个MVVM框架,最有名的就是双向绑定
一般来说当数据变化时,视图层跟着变化,这是单向的
当视图层变化时,数据也跟着变,这就是双向的
比如在vue中通过v-model绑定的输入框,当输入框内值变化时数据也跟着变化
今天就来实现一个类似于vue的双向绑定,预期效果:
// 视图层
<div id="app">
this is {{name}}, {{age}} years old!
<h1>
{{name}}
</h1>
</div>
// 数据层
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})
Object.defineProperty的使用
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 MDN:dwz.cn/umKlHoaq
定义对象descriptor时注意事项:
- 属性描述(configurable,enumerable): 通过这个定义的属性默认不可被删除(delete obj.xxx),默认不可遍历(for in)
- 值描述(value, writable): value和writable,value默认为undefined,writable默认为false值不能被改变
- 存取描述:get,set函数
- 值描述符和存取描述符不能同时存在
观察者模式
Subject是一个主题(网站)。Observer相当于一个个的观察者(网民),他们可以订阅Subject,当Subject更新时通知Observer,触发Observer之前定义的回调。
//es6实现
class Subject {
constructor(){
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state){
this.state = state
this.notifyAllObservers()
}
notifyAllObservers(){
this.observers.forEach(observer=>{
observer.update()
})
}
attach(observer) {
this.observers.push(observer)
}
}
class Observer {
constructor(name, subject){
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update,state:${this.subject.getState()}`)
}
}
let s = new Subject()
let o = new Observer('o', s)
let o2 = new Observer('o2', s)
s.setState(1)
实现原理
通过使用Object.defineProperty(数据拦截)和观察者模式实现双向绑定
- 主题是什么? data中的一个个key,比如name
- 观察者是什么? 视图里面的{{xxxx}},需要被替换成数据的地方
- 观察者什么时候订阅?
一开始执行MVVM初始化时候根据el遍历dom节点,发现{{xxxx}}时候时订阅对应主题xxxx - 主题什么时候通知更新?
当xxxx改变时,通知观察者更新内容。可以在一开始就监控data通过Object.defineProperty()实现
实现单向数据流(数据=>视图)
// 总入口
class MVVM {
constructor(opts){
// 初始化,绑定数据到vm对象上
this.init(opts)
// 遍历所有data值,设置get和set方法
observe(this.$data)
// 遍历dom树,查找{{}}的特殊标记
this.compile()
}
init(opts){
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
compile(){
this.traverse(this.$el)
}
traverse(node){
if(node.nodeType === 1){
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
} else if(node.nodeType === 3){
this.renderText(node)
}
}
renderText(node){
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()
// 初始化时绑定data里的数据到视图对应节点
node.nodeValue = node.nodeValue.replace(raw, this.$data[key])
// 对当前的key设置监听
new Observer(this, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
}
let currentObserver = null
// 遍历data,添加监听的observe方法
function observe(data) {
if(!data || typeof data !== 'object') return
for(var key in data){
//如果下面的get方法直接返回data[key]会引起循环调用导致栈溢出
let val = data[key]
//每一个key都有一个自己的subject
let subject = new Subject()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('run get')
// currentObserver是一个全局的对象
if(currentObserver){
// 订阅某一个subject(data中的某一个键值对的key)
currentObserver.subscribeTo(subject)
}
return val
},
set: function (newVal) {
val = newVal
console.log('run set')
// 更改了值,通知所有订阅者
subject.notify()
}
})
if(typeof val === 'object'){
observe(val)
}
}
}
// 主题Subject,data中的每一个key都是一个subject
class Subject {
constructor(){
// 一个主题会有多个订阅者,比如说在视图中会有多个地方有{{name}}
this.observers = []
}
addObserver(observer){
this.observers.push(observer)
}
notify(){
this.observers.forEach(observer=>{
observer.update()
})
}
}
// 订阅者Observer
class Observer {
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb
// currentObserver是一个全局的变量,设置他有值
// 并执行下一行触发get方法,会运行observe方法中的订阅操作
currentObserver = this
this.value = this.getValue()
currentObserver = null
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal){
this.value = value
this.cb.bind(this.vm)(value, oldVal)
}
}
subscribeTo(subject){
subject.addObserver(this)
}
getValue(){
// 这里会触发data中元素的get函数
let value = this.vm.$data[this.key]
return value
}
}
// 调用
<div id="app">
this is {{name}}, {{age}} years old!
</div>
<script type="text/javascript">
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})
</script>
实现双向绑定
在vue中,如果要对一个input框进行双向绑定,需要设置v-model指令
这里我们进行相同的设置,对于设置了v-model的input实现双向绑定
在前面已经实现单向数据流的基础上很容易实现双向绑定
在解析dom时候检测v-model指令,对应的元素绑定监听事件,当值改变时触发设置data即可
// 修改上面代码的MVVM
class MVVM {
// ......
traverse(node){
if(node.nodeType === 1){
this.compileNode(node)
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
} else if(node.nodeType === 3){
this.renderText(node)
}
}
// ......
compileNode(node){
let attrs = [...node.attributes]
attrs.forEach(attr=>{
if(this.isDirective(attr.name)){
let key = attr.value
node.value = this.$data[key]
new Observer(this, key, function (newVal) {
node.value = newVal
})
node.oninput = (e)=>{
this.$data[key] = e.target.value
}
}
})
}
isDirective(attrName){
return attrName === 'v-model'
}
}
调用
// 调用
<div id="app">
this is {{name}}, {{age}} years old!
<h1>
{{name}}
</h1>
<input type="text" v-model="name">
</div>
<script type="text/javascript">
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})
</script>