Vue的响应式和双向绑定的实现
Vue的响应式和双向绑定的实现
这部分内容学习的是的是B站王红元老师的最后三节课https://www.bilibili.com/video/BV15741177Eh?p=229
- 在老师代码的基础上新增了对input这个元素节点的Watcher,这样就能够实现修改数据可以映射到input的value值,和官方Vue效果一样
- 新增了一些注释
如果有问题还请大佬批评指正
实现效果:
1.刷新后输入框中是data中的数据,当在输入框输入内容后,会改变data中的属性值并映射到页面
2.直接通过按钮修改data的数据,会同步修改页面上的值和input的value值
(不会附动画,只能贴图了。。。)


主体思路

前提概念
这部分代码要用到三个知识:大家查一下就懂了
1.Object.defineProperty()方法
2.Object.keys()方法
3.document.createDocumentFragment() 文档片段的使用
完整代码:可直接复制查看效果
<!DOCTYPE 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">
<input type="text" v-model="msg">
<br>
{{msg}}
<br>
<button id="btn">按钮</button>
</div>
<!-- <script src='./vue-2.4.0.js'></script> -->
<script>
// 一:定义Vue构造函数
class Vue {
constructor(options) {
// 1.将传入的对象保存起来
this.$options = options
this.$data = options.data
this.$el = options.el
// 2.将data添加到响应式系统中
// new Observer(this.$data)
new Observer(this.$data)
// 3.代理this.$data的数据,将其代理到Vue实例对象上
Object.keys(this.$data).forEach(key => {
this._proxy(key)
})
// 4.解析el中的{{}}模板标签
new Compiler(this.$el, this) // 传入#app元素和当前Vue的实例对象
}
_proxy(key) {
// 这里的主要作用就是将this.$data中的变量直接代理到this上
/*
例如 const app = new Vue({
data: {
msg: '举例'
}
})
此时,传入new Vue({})中的对象的data,会被保存为app.$data中,使用方法app.$data.msg
代理的作用就是能够直接通过app.msg来访问这个变量,原Vue也有同样的代理设置,效果也是这样
*/
Object.defineProperty(this, key, { // this是当前的Vue实例,直接将this.$data中的属性名key设为vue实例的属性
enumerable: true,
configurable: true,
get() {
return this.$data[key] // 当访问app.msg的时候,返回的是app.$data.msg的值,并且这一步会触发Observer中设置的对app.$data的get代理
},
set(newValue) {
this.$data[key] = newValue // 当给app.msg = "新的值",是给app.$data.msg = "新的值",并且这一步也会触发Observer中设置的对app.$data的set代理
}
})
}
}
// 二:定义Observer构造函数,监听对象数据的改变
class Observer {
constructor(data) {
this.data = data
// console.log(data)
// console.log(this.data)
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, value) {
// 每一个属性都对应一个dep订阅器对象,用来存放使用这个属性的所有订阅者
const dep = new Dep()
Object.defineProperty(data, key, {
enmuerable: true, // 属性可枚举
configurable: true, // 属性可删除
get() {
// 用Watcher中定义的Dep.target全局属性来判断是否有新的watcher需要添加
if (Dep.target) {
dep.addSub(Dep.target) // 将Dep.target中保存的watcher添加到dep中
}
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
// 当给属性赋新的值时候,触发dep.notify方法
dep.notify()
}
})
}
}
// 三:定义Dep订阅器构造函数,用来存放所有的订阅者watcher
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
// 遍历dep中所有的watcher,调用他们的update函数,去获取新的属性值
this.subs.forEach(item => {
item.update()
})
}
}
// 四:定义Watcher订阅者构造函数
class Watcher {
constructor(node, name, vm) {
this.node = node // 通过正则匹配的{{}}这种文字节点
this.name = name // {{}}节点的内容,也就是{{变量名}}其中的变量名
this.vm = vm //当前Vue的实例对象
// 定义一个Dep.target全局属性用来存放要加入订阅器的watcher,当添加完成后就清空
Dep.target = this //这里新增一个全局属性Dep.target,保存了当前的watcher
this.update()
Dep.target = null
}
// 更新页面数据
update() {
// 1.把页面上{{变量名}}用vm.data.变量名 替代
// 2.update方法在两种情况下被调用
// 2.1 new一个Watcher对象的时候调用this.vm.data[this.name]读取了data中的属性值,触发了defineReactive中的get方法,于是将Dep.target保存的当前的watcher添加到dep中去
// 2.2 data属性被赋新值的时候,触发notify()=>update(),将新的值重新赋给页面上的节点,但此时Dep.target为空,所以不会添加新的watcher到dep
// console.log(this.vm.$data[this.name])
// this.node.nodeValue = this.vm[this.name]
if (this.node.nodeName === 'INPUT') { // 因为input标签的node节点和赋值和{{}}这种文本节点不一样,所以这里加个判断
this.node.value = this.vm[this.name]
}
this.node.nodeValue = this.vm[this.name]
}
}
// 五: 定义Compiler,用来解析模板指令{{}},将模板中的变量替换成数据
const reg = /\{\{(.+)\}\}/ //正则表达式,用来匹配{{}}模板
class Compiler {
constructor(el, vm) {
console.log(el) // #app
this.el = document.querySelector(el) // 这里el中保存的是#app,可以直接选中页面上的#app元素
console.log(this.el)
this.vm = vm // 当前Vue的实例保存在this.vm中
this.frag = this._createFragment()
// 此时frag中保存的是从#app取出来的所有子节点,再将他们添加会#app中
this.el.appendChild(this.frag)
}
_createFragment() {
// 创建一个新的空白的文档片段( DocumentFragment),这个文档片段一般用来添加元素,然后将整个空白文档添加到DOM上去,空白文档本身不会显示
// 因为空白文档是存在于内存中的,所以频繁的添加元素不会影响到DOM重绘和回流,等元素添加完毕将空白文档挂载到DOM上就好了
const frag = document.createDocumentFragment()
let child
while (child = this.el.firstChild) { // 将#app的子节点一个个拿出来
// console.log(child) // 这里有个问题页面上{{msg}}对应的文本节点nodeValue显示是"",不是{{msg}}
// 将节点一个个从#app中取出来去判断
this._compile(child)
// 将取出来的节点添加到frag中,但这样取出的节点在#app中就不会存在了,之后需要将frag再添加回#app中去
frag.appendChild(child)
}
return frag
}
_compile(node) { //判断节点类型
if (node.nodeType === 1) {
//node.attributes是当前元素节点所有属性值的集合,是一个对象,可以通过node.attributes[0]使用第一个属性
// 也能通过node.attributes[属性节点名]获得属性节点
let attrs = node.attributes
if (attrs.hasOwnProperty('v-model')) { // 判断这个节点对象有v-model这个属性
const name = attrs['v-model'].nodeValue // 获得v-model这个属性节点的value值,也就是msg
// console.log(name) // msg
// 匹配到这个属性后,就能新增input这个元素节点的Watcher(订阅者)
new Watcher(node, name, this.vm)
node.addEventListener('input', e => { // 监听当前输入框的input事件
this.vm[name] = e.target.value // 触发时将输入的数据赋给对应的属性
})
}
}
if (node.nodeType === 3) { //文本节点
// console.log(node)
if (reg.test(node.nodeValue)) {
console.log(RegExp.$1)
const name = RegExp.$1.trim() // 取得正则匹配到的内容,去除两边的空格,其实匹配到就是页面上{{}}里面的变量名
// 匹配成功后,就能新增{{}}这个文本节点的Watcher(订阅者)
new Watcher(node, name, this.vm)
}
}
}
}
</script>
<script>
const app = new Vue({
el: '#app',
data: {
msg: '双向绑定'
}
})
document.getElementById('btn').addEventListener('click', () => {
app.msg = '按下手动更改数据'
})
</script>
</body>
</html>

浙公网安备 33010602011771号