这里通过讲解model 指令的实现来学习双向绑定的实现过程
首先将整个实现的思路顺下。
- 在初始化中,首先 new一个vue 实例,通过调用的过程我们可以知道,
- 先将参数options 对象存为$options
- 要想实现双向绑定,需要将传过来的data 参数进行 observe
- 然后将data 中的值绑定到当前实例上
- 然后根据传入的el 参数,编译模板
- 接着探讨编译模板的过程,我们这里做一个简单版的,用fragment来实现,实际上使用的是ATS
- 调用函数 让节点变为 fragment
- 编译模板
- 将编译好的模板挂载到dom
- 第二步骤中的1和3.思路比较简单,这里重点讲第二个编译模板。这里也只是简单实现,不使用递归。
- 通过根节点,获取子节点,然后遍历子节点。遍历过程中根据节点的nodeType 不同,来处理元素节点,和文本节点
- 元素节点的处理:获取元素节点的attributes 值, 然后遍历它。遍历过程找到v-model 这个指令
- 文本节点的处理:通过正则获取{{}}里面的值。
- 找到v-model 的指令后,获取绑定的值 比如v-model="a" 中的a,对这个找到的值做处理
- 对找到的a添加 watcher
- 获取a这个属性的值,并给绑定的元素赋值
- 给input 添加监听事件,来实时更新最新的输入值
- 文本节点的处理,找到{{}}中的值后,通过添加watcher 来添加处理
下面是实现过程中的代码粘贴,有一部分代码参考之前讲过的 数据响应式原理,juejin.cn/post/699458…
代码结构如下图
index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
你好{{a}}
<input v-model="a" type="text">
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
<button onclick="add()">点我加1</button>
</div>
<script src='/xuni/bundle.js'></script>
<script>
var vm = new Vue({
el: 'app',
data :{
a:'33'
}
})
function add () {
vm._data.a++
}
console.log(vm)
</script>
</body>
</html>
index.js
import Compile from './Compile'
import { observe } from './initdata/data/observer'
import Watcher from './initdata/data/watcher.js'
export default class Vue {
constructor(options) {
// 把参数options 对象存为$options
this.$options = options || {}
this._data = options.data || undefined;
observe(this._data)
this._initData()
new Compile(options.el, this)
}
_initData() {
let self = this;
Object.keys(this._data).forEach(key => {
Object.defineProperty(self, key,{
get() {
return self._data[key]
},
set(newVal) {
self._data[key] = newVal
}
})
})
}
}
window.Vue = Vue
compile.js
import Watcher from './initdata/data/watcher.js'
export default class Compile {
constructor (el, vue) {
this.$vue = vue
this.$el = document.querySelector('#'+el)
if (this.$el) {
// 如果传入了挂载点 调用函数 让节点变为 fragment 实际上用的AST 这里就是轻量级的
let fragment = this.node2Fragment(this.$el)
// 编译模板
this.compile(fragment)
// 将编译完的模板上树
this.$el.appendChild(fragment)
}
}
node2Fragment (el) {
console.log(el)
var fragment = document.createDocumentFragment();
var child
// 让所有的dom 都进入 fragment
while (child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
}
compile (el) {
var childNodes = el.childNodes;
var self = this;
let reg = /\{\{(.*)\}\}/
childNodes.forEach(node => {
var text = node.textContent;
if (node.nodeType==1) {
self.compileElement(node)
} else if (node.nodeType==3 && reg.test(text)) {
let name = text.match(reg)[1]
self.compileText(node, name)
}
})
}
compileElement (node) {
let self = this;
// 这里的方便之处是在 操作真正的属性列表 结构比较像虚拟节点
var nodeAttrs = node.attributes;
[].slice.call(nodeAttrs).forEach(attr => {
// 这里分析指令 不用之前的虚拟dom 是因为这样比较简单, 思量逻辑是一致的
var attrName = attr.name;
var value = attr.value;
// 指令是从 v-开头的
var dir = attrName.substring(2)
// 看看是不是指令
if (attrName.indexOf('v-') == 0) {
// 这里去判断是哪个指令
if (dir=='model') {
console.log('发现是指令' + dir)
// 对指令添加watch
new Watcher(self.$vue, value, newVal => {
node.value = newVal
})
// 获取该值,赋值
var val = this.getVueVal(this.$vue, value)
node.value = val
// 添加input 事件来触发
node.addEventListener('input',(e) => {
let newValue = e.target.value
console.log(newValue)
self.setVueVal(self.$vue, value, newValue)
})
}else if (dir =='if') {
console.log('发现是指令' + dir)
}
}
})
}
compileText(node, name) {
node.textContent = this.getVueVal(this.$vue, name)
new Watcher(this.$vue, name, value => {
node.textContent = value
})
}
getVueVal(vue, name) {
console.log(vue,name)
let val = vue._data
let keys = name.split('.')
keys.forEach(key => {
val = val[key]
})
return val
}
setVueVal(vue, name, value) {
let val = vue
let keys = name.split('.')
keys.forEach((key,index) => {
if (index==keys.length-1) {
val[key] = value
}else {
val = val[key]
}
})
}
}
引用的响应式代码就不粘贴了,可以参考 juejin.cn/post/699458…