在理解Object.defineProperty(),我提到了Vue.js 通过Object.defineProperty 方法进行数据劫持从而实现双向数据绑定的,并在Object.defineProperty() 实现双向数据绑定小案例中手动实现了一个简单的双向数据绑定。
其实只通过Object.defineProperty()
已经可以实现数据绑定了,只是结合发布订阅者模式可以做到‘一对多’的模式,效率更高,Vue.js就是通过Object.defineProperty()
+ 发布订阅者模式 来实现双向数据绑定的。
要实现mvvm的双向数据绑定,需要实现以下几点:
- 实现一个观察者Observer,能够对data数据对象的所有的属性都分别创建一个发布者Dep, 并通过Object.defineProperty() 对所有属性进行监听。
- 实现一个发布者Dep,建立一个数组subs保存该data属性的所有订阅者(Watcher),并定义增加订阅者(Watcher) 和更新subs数组中所有订阅者(Watcher)的方法。
- 实现一个订阅者Watcher,记录每个使用data数据对象的属性的地方,并定义了更新视图的update方法。
- 实现一个编译器Compiler,对Vue管理入口元素'#app'中的所有子元素节点的指令进行扫描和解析,根据指令模板替换数据,添加订阅者(Watcher)并更新视图
流程图如下(PS:建议看后面代码的时候都结合这张图看):
接下来就自己来实现双向数据绑定吧,先来看效果:
代码开始了,先准备入口页面和入口文件
index.html
Document
{{message}}
{{message}}
{{message}}
main.js
// 入口文件
import Vue from './vue'
const vue = new Vue({
el: '#app',
data: {
message: '自己实现双向数据绑定'
}
})
window.vue = vue // 绑定到window全局
vue.js
自己写一个vue.js文件,并在index.html 中导入。
// 自己写的vue文件
import Obeserver from './Obeserver'
import Compiler from './Compiler'
class Vue {
constructor(options) {
// 1.保存创建Vue 实例传入的Vue管理入口元素'#app'、data数据
this.$options = options
this.$el = this.$options.el
this.data = this.$options.data
// 2.将创建Vue 实例传入的data数据进行遍历,使用this.message返回this.data.message的值
Object.keys(this.data).forEach(item => {
this._proxy(item)
})
// 3.使用观察者Obeserver监听data中数据的改变
new Obeserver(this.data)
// 4.使用编译器Compiler编译模板
new Compiler(this.$el, this)
}
_proxy(key){
let that = this
Object.defineProperty(this, key, {
get() {
return that.data[key]
},
set(newVal) {
that.data[key] = newVal
}
})
}
}
export default Vue
观察者Observer
实现一个观察者Observer,通过Object.defineProperty()
对所有属性的存取进行监听,当监听到数据的变化之后就需要通知订阅者了,因此可以实现一个发布者Dep,在发布者Dep里面维护一个数组subs,用来收集订阅者,数据变动触发notify就可以通知到订阅者了,订阅者再调用update方法更新视图,data中属性的存取如下:
(1) 当属性值变化时自动调用Object.defineProperty()
的set
方法,拿到最新值并通知发布者Dep,发布者Dep再遍历所有订阅者Watcher修改对应订阅者Watcher对应的节点值,更新视图;
(2) 当获取属性值时自动调用Object.defineProperty()
的get
方法,调用发布者Dep的addSub
方法(判断是否是新的订阅者Watcher,是则添加到发布者Dep的subs数组中)
// 观察者-Obeserver
import Dep from './Dep'
class Obeserver {
constructor(data) {
this.data = data // vue中的data
// 使用Object.defineProperty监听data数据对象的所有属性
Object.keys(this.data).forEach(key => {
let value = this.data[key]
let dep = new Dep(key) // 创建dep对象
Object.defineProperty(this.data, key, {
set(newValue) {
if (value === newValue) return
console.log(`监听到data中${key}改变,新值为:${newValue}`)
value = newValue
dep.notify()
},
//获取this.data中key的值都会触发 get函数, 该方法将Watcher存放到subs数组中
get() {
console.log(`获取到${key}对应的值为:${value}`)
if (Dep.target) {
dep.addSub(Dep.target)
console.log('添加watcher')
}
return value
}
})
})
}
}
export default Obeserver
Dep 发布者
实现一个发布者Dep,建立一个数组subs保存该data属性的所有订阅者(Watcher),并定义了增加订阅者(Watcher) 的addSub
方法和更新subs数组中所有Watcher中对应节点的值的notify
方法。
// Dep-发布者
import Watcher from './Watcher'
class Dep {
constructor(data, value) {
this.subs = []
this.data = data
this.value = value
}
addSub(watcher) {
this.subs.push(watcher)
}
// 遍历subs中存放的所有Watcher,调用update方法,更新视图
notify(value) {
console.log(this.subs)
this.subs.forEach(item => {
item.update(value)
})
}
}
Dep.prototype.target = null
export default Dep
Watcher订阅者
实现一个订阅者Watcher,记录每个使用data数据对象的属性的地方,并定义了更新视图的update
方法。
// Watcher-订阅者
import Dep from "./Dep"
class Watcher {
constructor(node, name, vm, type) {
// 编译器传过来的
this.node = node
this.name = name
this.vm = vm
this.type = type
Dep.target = this
this.update()
Dep.target = null
}
// 更新视图
update(){
/* 执行this.vm[this.name],会调用Observer中Object.defineProperty()的get方法,get方法会判断是否是新的订阅者Watcher,是则将订阅者Watcher 存放到subs数组中
再将this.vm[this.name]的值赋给该节点的值,完成页面渲染
*/
if (this.type === 'twoEle') {
this.node.value = this.vm[this.name] // 元素节点包含v-model更新视图
}else if(this.type === 'ele'){
this.node.innerText = this.vm[this.name] // 元素节点包含插值更新视图
}
else if(this.type === 'text'){
this.node.nodeValue = this.vm[this.name] // 文本节点更新视图
}
}
}
export default Watcher
编译器Compiler
实现一个编译器Compiler,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
import Watcher from './Watcher'
const reg = /\{\{(.*)\}\}/
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el)
this.vm = vm
this.frag = this._createFragment()
this.el.appendChild(this.frag)
}
// appendChild() 方法的作用是从一个元素向另一个元素中移动元素, 因此通过while可以遍历到this.el(#app)中的所有子元素,添加到frag中
_createFragment() {
// createDocumentFragment适合处理有大量dom元素,效率比createElement高
let frag = document.createDocumentFragment()
while(this.el.firstChild) {
this._compile(this.el.firstChild)
frag.appendChild(this.el.firstChild)
}
return frag
}
// 判断节点类型,填加订阅者Watcher并更新视图
_compile(node) {
// node.nodeType: 1元素节点 3文本节点
let type = null
if (node.nodeType === 1) {
console.log(node.attributes)
let attr = node.attributes
/* 元素节点中有v-model: view --> model model --> view
(1) 此处以input输入框为例,监听input 事件,更新data中数据
(2) 设置input输入框的值为data中对应的数据
*/
if(attr.hasOwnProperty('v-model')) {
let name = attr['v-model'].nodeValue
node.addEventListener('input', (e) => {
this.vm[name] = e.target.value
})
type = 'twoEle'
new Watcher(node, name, this.vm, type)
}
// 元素节点中有插值 {{}}
if (reg.test(node.innerText)) {
// 获取元素节点{{}}中的数据
let name = RegExp.$1
name = name.trim()
type = 'ele'
// 将获取到的数据用Watcher订阅者记录并更新视图
new Watcher(node, name, this.vm, type)
}
}else if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
// 测试文本节点是否有插值{{}},则获取{{}}中的数据
let name = RegExp.$1
name = name.trim()
type = 'text'
// 将获取到{{}}中的数据用Watcher订阅者记录
new Watcher(node, name, this.vm, type)
}
}
}
}
export default Compiler
好了,这就实现了一个简单的双向数据绑定了,其思想和原理大都来自vue源码,最后和官方的Vue.js 做一个效果对比。
Document
{{message}}
{{message}}
{{message}}
{{message}}
通过对比可以知道,自己实现的vue.js 和官方的vue.js 双向数据绑定效果一致,收工 。
文中有不足或者读者有疑问或更好的见解,欢迎留言讨论。
如果觉得该篇文章对您有帮助,别忘了留下您的足迹,点个赞❤噢