vue用了有段时间了,参考了github的文章,写了个简化版
github文章链接:https://github.com/DMQ/mvvm 如果看不懂就看我的啊,嘿嘿嘿
前言 使用Vue也有一段时间了,vue作为一个MVVM框架,最有名的就是双向绑定 一般来说当数据变化时,视图层跟着变化,这是单向的 当视图层变化时,数据也跟着变,这就是双向的 比如在vue中通过v-model绑定的输入框,当输入框内值变化时数据也跟着变化
今天就来实现一个类似于vue的双向绑定,预期效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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:https://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之前定义的回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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(数据拦截)和观察者模式实现双向绑定 原理图:
主题是什么? 一个个key,比如name
观察者是什么? 视图里面的,需要被替换成数据的地方
观察者什么时候订阅? 一开始执行MVVM初始化时候根据el遍历dom节点,发现时候时订阅对应主题xxxx
主题什么时候通知更新? 当xxxx改变时,通知观察者更新内容。可以在一开始就监控data通过Object.defineProperty()实现
实现单向数据流(数据=>视图) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 class MVVM { constructor (opts){ this .init(opts) observe(this .$data) 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() node.nodeValue = node.nodeValue.replace(raw, this .$data[key]) new Observer(this , key, function (val, oldVal ) { node.nodeValue = node.nodeValue.replace(oldVal, val) }) } } } let currentObserver = null function observe (data ) { if (!data || typeof data !== 'object' ) return for (var key in data){ let val = data[key] let subject = new Subject() Object .defineProperty(data, key, { enumerable: true , configurable: true , get : function () { console .log('run get' ) if (currentObserver){ currentObserver.subscribeTo(subject) } return val }, set : function (newVal) { val = newVal console .log('run set' ) subject.notify() } }) if (typeof val === 'object' ){ observe(val) } } } class Subject { constructor (){ this .observers = [] } addObserver(observer){ this .observers.push(observer) } notify(){ this .observers.forEach(observer => { observer.update() }) } } class Observer { constructor (vm, key, cb){ this .vm = vm this .key = key this .cb = cb 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(){ let value = this .vm.$data[this .key] return value } } `
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 调用 <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即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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' } } `
调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 调用 <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 >
优化