首先依赖与发布订阅者模式,然后关键技术就是Object.defineProperty()方法来进行数据拦截,然后对于其中属性的属性要用到reduce()循环计算方法,直接上代码。附我自己的注释
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
具体步骤:
第一步: 需要observer(观察者)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步: compile(模板解析器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步: Watcher(订阅者)是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步: MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
以上描述非常正确,来自这里
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<span>名字:{{name}}</span>
<input type="text" v-model="name">
<span>年龄:{{age}}</span>
<input type="text" v-model="age">
<span>更多:{{info.like}}</span>
<input type="text" v-model="info.like">
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app', //这个app就是vue控制的范围,将data中的值全部填充到el的值中去
data: {
name: '小琪',
age: 24,
info: {
like: '吃喝玩乐'
}
}
})
console.log(vm)
</script>
</body>
</html>
下面就是实现的vue.js中的代码
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data
//数据劫持
Observer(this.$data)
//属性代理,主要作用就是当访问vm中的属性时,不用vm.$data.XX,可以直接vm.XX
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, { //为vm定义了$data中的属性
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
})
//调用模板编译的函数。实现将vm中data中的数据渲染到相应控制的dom内部
Compile(obj_instance.el, this)
}
}
//数据劫持-监听实例中的数据
function Observer(data_instance) {
//递归出口:已经没有子属性或者没有检测到对象
if (!data_instance || typeof data_instance !== 'object') return; //递归终止条件
const dep = new Dep()
//这里keys循环数据中的所有属性
Object.keys(data_instance).forEach(key => {
//为当前得到的属性,也就是key值添加getter和setter
let value = data_instance[key]; //先将key对应的值存起来
Observer(value) //递归- 子属性数据劫持
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get() {
//只要执行了下面这一行代码,那么刚刚new的Watcher实例就会被放入dep.subs数组中
Dep.target && dep.addSub(Dep.target)
// console.log(`有人获取了${key}的值`)
return value
},
set(newValue) {
// console.log(`属性${key}的值${value}修改为->${newValue}`)
value = newValue
Observer(value) //监听新传入的对象
//通知每一个订阅者更新自己的文本
dep.notify() //实现了单向数据绑定,改变vm中data的值会同步到dom中
}
})
})
}
//对HTML结构进行解析,实现就是将vm中data中的数据渲染到相应控制的dom内部
//设置两个参数,第一个是元素->vue实例中挂载的元素,也就是需要控制的哪片区域,第二个参数是vue元素
function Compile(el, vm) {
//获取el对应的DOM元素。首先要做的就是将el转换为真实的dom对象,当前传入的el是一个选择器而已
vm.$el = document.querySelector(el)
//文档碎片,存储每个DOM节点。内存中操作文档碎片,不是在页面上操作DOM元素,然后再渲染到页面上。可以避免重绘和重排
//创建文档碎片
let childNode
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
} //while循环之后,页面中所有的dom节点都存在了文档碎片中,此时dom节点为空
//进行模板编译
replace(fragment)
vm.$el.appendChild(fragment) //将dom子节点全部重新渲染到页面中
//定义对dom节点进行编译的方法
function replace(node) {
//定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/ //其中\s表示提取空白字符,\S表示提取非空白字符,+代表至少一个或者多个
//当前的node节点是一个文本子节点,需要进行正则替换
if (node.nodeType === 3) {
//文本子节点也是一个DOM对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
// console.log(node.textContent)//输出所有文本的内容
const text = node.textContent
//进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
// console.log(execResult) //0{{name}} 1name ...
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm) //如果存在属性中的属性就可以直接XX.XX.XX
// console.log(value)
node.textContent = text.replace(regMustache, value) //替换操作噢!!!在这里就实现了dom元素替换,重要的一步
//在这里创建Watcher类的实例
new watcher(vm, execResult[1], (newValue) => { //进入watcher的构造函数中
node.textContent = text.replace(regMustache, newValue)
})
}
//终止递归的条件
return
}
//下面这一段实现视图到vm中数据的绑定
//判断当前的node节点是否为input输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
//得到当前元素的所有属性节点
const attrs = Array.from(node.attributes) //node.attributes是一个伪数组,用Array.from改为真数组
// console.log(attrs)
const findResult = attrs.find(x => x.name === 'v-model')
// console.log(findResult)
if (findResult) {
//获取到当前v-model属性的值 v-model = "name"...
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
// console.log(value) //拿到的值
node.value = value
//创建Watcher的实例,为了当vm里面元素改变时候让input框里面的元素也改变
new watcher(vm, expStr, (newValue) => { //文本框实现了单向数据绑定
node.value = newValue
})
//实现文本框的双向数据绑定
//监听文本框的input输入事件,拿到文本框最新的值,把最新的值更新到vm上即可。
node.addEventListener('input', (e) => {
// console.log(e.target.value)
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
//不是文本节点,可能是一个DOM元素,需要递归该元素的子节点
node.childNodes.forEach((child) => replace(child))
}
}
//发布订阅者模式
//依赖收集的类/收集watcher订阅者的类
class Dep {
constructor() {
//定义一个存储water的数组
this.subs = []
}
//向subs数组中添加watch的方法
addSub(watcher) {
this.subs.push(watcher)
}
//通知订阅的方法
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
//创建订阅者的类
class watcher {
//cb是一个回调函数,记录当前watch是如何更新自己的内容
//但是,只知道如何更新自己还不行,还必须拿到最新的数据
//因此,还需要在new Watcher期间,传入vm,因为vm中保存着最新的数据
//另外,还需要知道在vm身上众多的数据中,自己所需要的数据是哪个。所以,在new Watcher期间,需要指定watcher对应的数据属性名字key
constructor(vm, key, cb) {
this.vm = vm //给当前的watcher挂载上vm属性
this.key = key
this.cb = cb
//下面代码负责把创建的watcher实例存在Dep实例的subs数组中
Dep.target = this //this就是当前new的Watcher实例,给这个实例添加了一个target属性
key.split('.').reduce((newObj, k) => newObj[k], vm) //这里做了一个取值的操作,会触发get(),然后接下来的操作见上面Object.definedProperty。其真实目的不是为了取值,而是执行get(),然后将当前的watcher加入到Dep定义的数组中
Dep.target = null
}
//watcher实例还需要有一个update函数,让发布者能够通知到我们从而进行更新。
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
参考视频链接