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
}
}
})