Table of Contents
这两天在看数据双向绑定的文章。主要还是看不懂vue源码,只能找别人的文章看看。有一篇文章是《用proxy实现一个更优雅的vue》。看完后把代码抄了一遍,画了个简单的解析图(可能有误)。然后又自己实现了一遍,做了简单的扩展。
github:为了理解MVVM而创建的项目
src: 通过Object.defineProperty实现数据的双向绑定
src-proxy-simple: 抄写的代码
src-proxy: 简单扩展
扩展后的功能:
单向绑定
指令 | 可用属性 | 例子 |
m-bind | ‘text’, ‘innerText’, ‘html’, ‘innerHTML’, ‘src’, ‘value’, ‘href’, ‘class’, ‘style’, ‘alt’, ‘title’ | m-bind:text = “query.tip” |
双向绑定
指令 | 例子 |
m-model | m-model = “query.query” |
事件绑定
指令 | 可用事件 | 例子 |
m-on | ‘click’, ‘dbclick’, ‘input’, ‘focus’, ‘blur’, ‘change’, ‘load’, ‘select’ | m-on:click=”add” |
总结
功力还是不够,看不懂vue源码,也实现不了什么功能。
代码:
// index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="../css/style.css"> <script src="../js/mvvm.js"></script> </head> <body> <div class="container"> <section id="app" class="app"> <div class="query"> <span class="query-tip" m-bind:text="query.tip"></span> <input class="query-input" m-bind:style="style" m-model="query.query" m-on:change="changeStyle" type="text"> <span class="query-text" m-bind:title="query.query" m-bind:text="query.query"></span> </div> <div class="actions"> <span class="number" m-bind:innerText="number"></span> <button m-on:click="add">add</button> <button m-on:click="sub">sub</button> </div> </section> </div> </body> <script src="../js/app.js"></script> </html>
// style.css .number { display: inline-block; width: 30px; text-align: center; }
// mvvm.js /** * 类:M */ class M { // 初始化 constructor(options) { this.$el = document.querySelector(options.el) this.$data = options.data this.$methods = options.methods this._observe(this, '$data') // 监听数据 this._compile(this.$el) // 编译html,监听事件,收集依赖 this._initFill(this.$data) // 数据填充 } // 监听数据————对set进行拦截 _observe(parent, child) { const data = parent[child] parent[child] = new Proxy(data, { set(target, key, value) { let res = Reflect.set(target, key, value) // 数据变化时更新视图 if (target._ob[key]) { target._ob[key].map(item => { item.update() }) } return res } }) // 遍历对象,如果元素是数组或对象,递归监听 const keys = Object.keys(data) keys.map(item => { if (data[item] !== null && (typeof data[item]) === 'object') { this._observe(data, item) } }) } // 编译 _compile(el) { const nodes = Array.prototype.slice.call(el.children) nodes.map(node => { if (node.children.length > 0) this._compile(node) let attrs = null // 解析m-bind if (attrs = this._hasBind(node)) { attrs.map(attr => { this._pushWatcher(node, attr.attr, attr.key) }) } // 解析m-model if (node.hasAttribute('m-model')) { const key = node.getAttribute('m-model') this._pushWatcher(node, 'value', key) const data = this._resolveValue(key) if (data !== false) { const tempData = data.data const tempKey = data.key node.addEventListener('input', () => {tempData[tempKey] = node.value}) } } let events = null // 解析m-on if (events = this._hasOn(node)) { events.map(event => { const handler = this.$methods[event.key].bind(this.$data) console.log(event) node.addEventListener(event.event, handler) }) } }) } // 判断m-bind _hasBind(node) { let i = null let attr = null let key = null const attrs = [] // 可以bind的属性 const _bindAttr = ['text', 'innerText', 'html', 'innerHTML', 'src', 'value', 'href', 'class', 'style', 'alt', 'title'] _bindAttr.map((item, index) => { if (node.hasAttribute('m-bind:' + item)) { i = index attr = item key = node.getAttribute('m-bind:' + item) if (attr && key) { if (attr === 'text') attr = 'innerText' if (attr === 'html') attr = 'innerHTML' attrs.push({ attr, key }) attr = null key = null } } }) if (attrs.length) { return attrs } else return false } // 添加watcher _pushWatcher(node, attr, key) { const data = this._resolveValue(key) if (data !== false) { const tempData = data.data const tempKey = data.key if (tempData._ob === null || tempData._ob === undefined) { tempData._ob = Object.create(null) } console.log(tempKey) if (tempData._ob[tempKey] === null || tempData._ob[tempKey] === undefined) { tempData._ob[tempKey] = [] } tempData._ob[tempKey].push(new Watcher(node, attr, tempKey, tempData)) } } // 解析值所在对象 _resolveValue(key) { const arr = key.split('.') if (arr.length) { let data = this.$data for (let i = 0, len = arr.length - 1; i < len; i++) { if (data[arr[i]]) { data = data[arr[i]] } } return { data, key: arr[arr.length - 1] } } else { return false } } // 判断m-on _hasOn(node) { let event = null let key = null const events = [] // 可以监听的事件 const _event = ['click', 'dbclick', 'input', 'focus', 'blur', 'change', 'load', 'select'] _event.map((item, index) => { if (node.hasAttribute('m-on:' + item)) { event = item key = node.getAttribute('m-on:' + item) if (event && key) { events.push({event, key}) event = null key = null } } }) if (events.length) { return events } else return false } // 数据填充初始化 _initFill(data) { if (data._ob) { const keys = Object.keys(data._ob) keys.map(item => { data._ob[item].map(watcher => { watcher.update() }) }) } const dataKeys = Object.keys(data) dataKeys.map(item => { if ((typeof data[item]) === 'object' && item !== '_ob') { this._initFill(data[item]) } }) } } // 类:Watcher。更新视图 class Watcher { constructor(node, attr, key, data) { this.node = node this.attr = attr this.key = key this.data = data } update() { console.log(this) this.node[this.attr] = this.data[this.key] } }
// app.js window.m = new M({ el: '#app', data: { number: 0, query: { tip: '输入:', query: '' }, style: 'color: #000' }, methods: { add() { this.number++ }, sub() { this.number-- }, changeStyle() { let color = '' let number = Number(this.query.query) if (!number) { color = 'red' } else if(number < 10){ color = 'green' } else if (number < 100) { color = 'blue' } else { color = 'gray' } this.style = 'color: ' + color } } })