一、Compiler
解析器
解析器的作用一方面是解析出视图中相关的指令,将数据填充到视图中,另一方面也是添加新的订阅者,在数据发生更新时,能同步更新到视图中。
有了解析器以后,整个模型就算是完整了,参考下图:
Vue中的模板指令非常多,而且也做了很多的兼容,所以我们这只做例如v-model
, {{}}
, v-on
。
基本思路:
1、把真实DOM元素转换为文档片段;
2、遍历文档片段中所有的节点,解析出双括号指令和v-xxx的指令;
3、根据不同的指令,添加不同的操作:
v-model: 初始化数据,添加订阅器,同时添加input事件;
v-text: 初始化数据,添加订阅器;
v-on: 为当前元素节点添加对应的事件和回调;
定义Compiler
解析器类:
function Compiler(vm, el){
this.vm = vm // Vue对象
this.el = document.querySelector(el) // 挂载点
this.fragment = null // 文档片段
this.init() // 初始化
}
初始化操作:
Compiler.prototype = {
init(){
if (this.el) {
this.fragment = this.nodeToFragment(this.el) // 文档片段
this.compileElement(this.fragment) // 解析
this.el.appendChild(this.fragment) // 将文档片段添加到视图中
} else {
console.log('Dom元素不存在');
}
},
}
文档片段处理:
Compiler.prototype = {
...
nodeToFragment(el){ // 文档片段处理
var fragment = document.createDocumentFragment()
var child = el.firstChild
while (child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
}
解析操作,对应v-model
、v-on
、{{}}
是不有不同操作的,所以要进行判断处理。
v-text
、v-on
是元素属性中的,而{{}}
是在元素中的`innerHTML。
Compiler.prototype = {
...
compileElement(fragment){ // 解析元素节点
var childNodes = fragment.childNodes;
[].slice.call(childNodes).forEach(node=>{
var reg = /\{\{(.*)\}\}/
var text = node.textContent
if(this.isElementNode(node)) { // 元素节点
this.compileAttr(node)
} else if (this.isTextNode(node) && reg.test(text)) { // 文本节点
this.compileText(node, reg.exec(text)[1])
}
// 继续递归遍历当前节点的子节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
},
isElementNode(node){ // 元素节点
return node.nodeType === 1
},
isTextNode(node){ // 文本节点
return node.nodeType === 3
},
}
先处理{{}}
,只需要先将数据更新到界面中,并创建新的订阅者(会添加到订阅器中):
Compiler.prototype = {
...
compileText(node, exp){ // {{}}的处理
var val = this.vm[exp] // 获取属性值
node.textContent = val // 更新到页面中
new Watcher(this.vm, exp, value=>{ // 新的订阅者(绑定更新函数即可)
node.textContent = value
})
},
}
对于元素中的属性v-model
、v-on
绑定处理的,又需要判断出是那种,v-on
这只需要绑定上对应的事件即可;而v-model
会不一样,所有需要区分处理:
Compiler.prototype = {
...
compileAttr(node){ // v-xxx的处理
// 获取当前节点上所有的属性节点
var nodeAttrs = node.attributes;
var self = this;
// 遍历属性,找到是否有v-xxx
[].slice.call(nodeAttrs).forEach(attr=>{
var attr_name = attr.name
var reg = /v\-/;
if(reg.test(attr_name)){ // v-xxx指令
var exp = attr.value // 对应的事件
let dir = attr_name.substring(2) // 字符串切分 v- xxx
reg = /on\:/;
if(reg.test(dir)){ // v-on事件绑定
self.compileEvent(node, self.vm, exp, dir)
} else{ // v-model双向数据绑定 [备注: 我们只做几个指令操作]
self.compileModel(node, self.vm, exp, dir)
}
// 操作完成后,删除对应的属性
node.removeAttribute(attr_name)
}
})
},
}
v-on
事件绑定的处理,根据属性值,获取事件句柄,绑定上即可:
Compiler.prototype = {
...
compileEvent(node, vm, exp, dir){ // v-on的处理
// 例如 v-on:click='xxxx'
// 获取事件类型
var eventType = dir.split(':')[1]
// 根据事件名,获取函数句柄
var callback = vm.methods && vm.methods[exp]
if(eventType && callback){ // 有事件名且定义有对应事件处理
node.addEventListener(eventType, callback.bind(vm), false)
} else {
console.log('请在methods中添加对应的方法: ' + exp)
}
},
}
v-model
双向数据绑定,数据先要更新到界面中,接着是创建新的订阅者(会添加到订阅器中),最后还要对输入事件的处理:
Compiler.prototype = {
...
compileModel(node, vm, exp, dir){ // v-model的处理,为model绑定input事件
var val = this.vm[exp] // 获取属性值
node.value = val // 更新到页面中
new Watcher(this.vm, exp, value=>{ // 新的订阅者(绑定更新函数即可)
node.value = value
})
node.addEventListener('input', e => {
let newVal = e.target.value
if (val == newVal) {
return false
}
this.vm[exp] = newVal
val = newVal
}, false)
},
}
二、Vue类的创建
添加上监听器,并对DOM进行指令解析,根据不同指令添加不同的操作(例如事件处理、订阅服务…)
function Vue(options) {
this.el = options.el
this.data = options.data
this.methods = options.methods
Object.keys(this.data).forEach(key => { // 数据代理
this.proxyKeys(key)
})
new Observer(this.data) // 监听器
new Compiler(this, this.el) // DOM解析
}
Vue.prototype = {
proxyKeys(key) {
var self = this
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get() {
return self.data[key]
},
set(newVal) {
self.data[key] = newVal
}
})
},
}
三、测试
代码参考: https://github.com/iphone3/VueDataBinging
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<h1 v-on:click="onAlert">Vue双向数据绑定之原理及实现</h1>
<h3>名字: {{name}}</h3>
<div>
请输入你的名字:<input type="text" class="text" v-model='name' />
</div>
</div>
<script src="dep.js" type="text/javascript" charset="utf-8"></script>
<script src="watcher.js" type="text/javascript" charset="utf-8"></script>
<script src="observer.js" type="text/javascript" charset="utf-8"></script>
<script src="compiler.js" type="text/javascript" charset="utf-8"></script>
<script src="Vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
var app = new Vue({
el:'#app',
data: {
name: 'atom'
},
methods: {
onAlert: function(){
alert('恭喜你完成了学习!!!')
}
}
})
</script>
</body>
</html>