v-model双向数据绑定原理
话不多说直接上代码
我是看了 黑马的彬哥讲解的 超级详细 保姆级别教学 记录下来 方便自己查阅
视频地址:https://www.bilibili.com/video/BV1Dr4y1c7xS?p=24&t=473.1
这是封装的js
class Vue {
constructor(options) {
this.$data = options.data
// 数据劫持
Observer(this.$data)
// 属性代理 为了方便用户 直接this.xxx就可操作数据 不用this.$data.xxx
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
})
// 编译模板
Compile(options.el, this)
}
}
// 定义一个数据劫持的方法
function Observer(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
// 创建一个Dep实例
const dep = new Dep()
// 通过Object.keys获取该对象的每个键以数组方式存储
Object.keys(obj).forEach(key => {
// 当前被循环的key所对应的值 get中不能直接return obj[key]会报错
let value = obj[key]
// 再次调用该函数 防止对象中还有对象
Observer(value)
// 为当前的key添加getter和setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行代码 那么刚才new的watcher实列
// 就被放到了dep.sub这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
// 添加新值的时候递归一次 为其添加setter和getter
// 这里不能用arguments.callee 因为这是一个新函数
Observer(value)
// 通知每个订阅者更新自己的文本
dep.notify()
}
})
})
}
// 对HTML结构经行模板编译
function Compile(el, vm) {
// 获取el对应的元素
vm.$el = document.querySelector(el)
// 创建文档碎片 防止页面过度重排重绘
// 将页面的元素都先暂存到fragment中(页面元素会消失,页面元素被放进fragment了)
const fragment = document.createDocumentFragment()
// 如果vm.$el有第一个直接子元素就追加
while (childNode = vm.$el.firstChild) {
fragment.appendChild(childNode)
}
//
replace(fragment)
// 将文档碎片的内容重新加载到页面
vm.$el.appendChild(fragment)
// 负责对dom模板进行编译的方法
function replace(node) {
// 定义匹配插值的reg
var regMustache = /\{\{\s*(\S+)\s*\}\}/
// 判断当前node节点是否是文本节点 是的话经行替换
// 3 表示文本节点
if (node.nodeType === 3) {
// 文本子节点也是一个dom对象,获取其内容用textContent属性
const text = node.textContent
// 经行字符串的正则提取与匹配
// execRes第二项是跟当前名字一样的值 可作为key
const execRes = regMustache.exec(text)
console.log(execRes);
// 匹配成功的操作
if (execRes) {
// 拿到vm中当前key的值
const value = execRes[1].split('.').reduce((acc, k) => acc[k], vm)
node.textContent = text.replace(regMustache, value)
// --------------------------
// 在这个时候创建watcher类的实例
new Watcher(vm, execRes[1], (newVal) => {
node.textContent = text.replace(regMustache, newVal)
})
// -----------------------------
}
// 终止递归的条件
return
}
// 判断当前节点是否是input输入框
// 值 1 表示元素节点
if (node.nodeType === 1 && node.nodeName.toUpperCase() === 'INPUT') {
const attrs = Array.from(node.attributes)
const findRes = attrs.find(elem => elem.name === 'v-model')
// console.log(findRes);
if (findRes) {
// 获取当前v-model的属性值
const expStr = findRes.value
const value = expStr.split('.').reduce((acc, key) => acc[key], vm)
node.value = value
// ================
// 创建订阅者实例
new Watcher(vm, expStr, (newVal) => {
node.value = newVal
})
// =================
// 监听文本框的input事件,拿到输入的最新值 把最新值 更新到vm上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
// 为了防止 info.a.b 这样的数据 我们只需要获取a 给a[b] 赋值
// 如果keyArr只要一个值 那么截取的时候不就是空数组了嘛
// 答: [].reduce(()=>{},obj123) 会返回obj123
const obj = keyArr.slice(0, keyArr.length - 1).reduce((acc, key) => acc[key], vm)
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
// 如果不是文本节点 经行递归处理
// 虽然childNodes返回的是类数组对象但是 nodeList也有forEach方法
node.childNodes.forEach(child => replace(child))
}
}
// 创建信息搜集者、发布者的类
class Dep {
constructor() {
this.sub = []
}
// 添加订阅者
addSub(watcher) {
this.sub.push(watcher)
}
// 发布
notify() {
this.sub.forEach(watcher => watcher.update())
}
}
// 创建订阅者类
class Watcher {
// cb回调函数中 记录着当前Watcher如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据
// 因此,还需要再new Watcher期间 把vm也传递进来(因为vm中保存着最新的数据)
// 除此之外 还需要知道 在vm众多数据中 哪一个才是当前所需要的数据
// 因此 必须在 new Watcher期间 指定watcher对应的数据名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 下面三行代码 负责把创建的Watcher实例存到subs数组中
// 创建一个类的自定义属性target 值是当前Watcher实例
Dep.target = this
// 获取vm[k]的值 目的是为了触发vm[k]的get方法
key.split('.').reduce((acc, k) => acc[k], vm)
// 最后再值清空
Dep.target = null
}
// 让发布者调用此函数 通知自己进行更新
update() {
var newVal = this.key.split('.').reduce((acc, k) => acc[k], this.vm)
this.cb(newVal)
}
}
这是HTML片段
<body>
<div id="app">
<h1>a的值是:{{a}}</h1>
<h1>b的值是:{{b}}</h1>
<h1>info.c的值是:{{info.c}}</h1>
<div>
a的值是:
<input type="text" v-model="a">
</div>
<div>
info.c的值是:
<input type="text" v-model="info.c">
</div>
</div>
<script src="./vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
a: 'a1',
b: 'b1',
info: {
c: 'c1',
d: 'd1'
}
}
})
console.log(vm);
</script>
</body>