Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给 订阅者,触发相应的监听回调。
需要 observe 的数据对象进行递归遍历,包括子属性对象的属性, 都加上 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 变更的双向绑定效果
<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>Documenttitle>
head>
<body>
<div id="app">
<span>姓名:{{name}}span>
<input type="text" v-model="name">
<span>朋友:{{friend.name}}span>
<input type="text" v-model="friend.name">
div>
<script>
// Vue
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data
observe(this.$data)
dataBroker(this.$data, this)
compile(obj_instance.el, this)
}
}
// 依赖-收集通知订阅者
class Dependency {
constructor() {
this.subscribes = []
}
addSub(sub) {
this.subscribes.push(sub)
}
notify() {
this.subscribes.forEach(el => el.update())
}
}
// 订阅者
class Watcher {
constructor(vm, key, callback) {
// callback 怎么更新自己的文本内容
this.vm = vm
this.key = key
this.callback = callback
// !!! 下面三行代码将watcher加到dep中
Dependency.temp = this
key.split('.').reduce((total, cur) => total[cur], vm)
Dependency.temp = null
}
update() {
this.callback(this.key.split('.').reduce((total, cur) => total[cur], vm))
}
}
// 遍历所有属性
function observe(obj_instance) {
if (!obj_instance || typeof obj_instance !== 'object') {
return
}
Object.keys(obj_instance).forEach(el => {
defineReactive(obj_instance, el, obj_instance[el])
})
}
// 数据劫持
let dep = new Dependency()
function defineReactive(data, key, value) {
observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
Dependency.temp && dep.addSub(Dependency.temp)
return value
},
set(newValue) {
value = newValue
observe(newValue)
// 通知订阅者更新
dep.notify()
}
})
}
// 数据代理
function dataBroker(data, vm) {
Object.keys(data).forEach(key => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
data[key] = newValue
}
})
})
}
// 模板解析
function compile(el, vm) {
vm.$el = document.querySelector(el)
// 创建文档碎片,存储el上的所有子节点
// 在内存中操作dom,避免重绘重排
const fragment = document.createDocumentFragment() // 用来存储文档碎片节点
let child
while (child = vm.$el.firstChild) {
fragment.append(child)
}
// 模板编译
replace(fragment)
function replace(node) {
const regMustance = /\{\{\s*(\S+)\s*\}\}/
// 3为文本节点
if (node.nodeType == 3) {
// 执行替换操作
let text = node.textContent
let regResult = regMustance.exec(text)
if (regResult) {
const result = regResult[1].split('.').reduce((total, cur) => total[cur], vm.$data)
node.textContent = text.replace(regMustance, result)
// 创建Watcher实例
const watcher = new Watcher(vm, regResult[1], (result) => {
node.textContent = text.replace(regMustance, result)
})
}
return
}
if(node.nodeType==1&&node.tagName.toUpperCase()=="INPUT") {
// 注意attributes为伪数组,需要转成数组
const attrs = [...node.attributes]
const findResult = attrs.find(el=>el.name=='v-model')
if(findResult) {
let arr = findResult.value.split('.')
let newValue = arr.reduce((newObj,key)=>newObj[key],vm.$data)
node.value = newValue
const watcher = new Watcher(vm,findResult.value,(newValue)=> {
node.value = newValue
})
}
// 监听文本框的input事件,更改vm上的值
node.addEventListener('input',(e)=> {
let value = e.target.value
const keyArr = findResult.value.split('.')
const obj = keyArr.slice(0,keyArr.length-1).reduce((newObj,key)=>newObj[key],vm)
obj[keyArr[keyArr.length-1]] = value
})
}
node.childNodes.forEach(child => replace(child))
}
// 文档碎片应用到页面上
vm.$el.appendChild(fragment)
}
let vm = new Vue({
el: "#app",
data: {
name: 'wl',
friend: {
name: 'whb'
}
}
})
script>
body>
html>