vue双向数据绑定的原理
预备知识,reduce和Object.defineProperty
这两个知识会在下面大量的用到,其中Object.defineProperty是核心,reduce的话下面大量操作会借助于他,通俗的来说,就是前者具有战术意义,后者具有战略意义
reduce
应用场景:下次操作的初始值,依赖于上一次操作的返回值
1.数组的累加计算
const newarr = arr.reduce((pre,cur)=>{return pre+cur},0)
2.链式获取对象属性的值
const obj = {
name:'zs',
info:{
address:{
location:'北京'
}
}
}
const arrs = ['info','address','location']
const zs_location = arrs.reduce((pre,cur)=>{return pre[cur]},obj)
console.log(zs_location)
升级操作
const obj = {
name:'zs',
info:{
address:{
location:'北京'
}
}
}
const arrStr = 'info.address.location'
const zs_location = arrStr.split('.').reduce((pre,cur)=>pre[cur],obj)
console.log(zs_location)
watch
监听对象的时候,不能直接使用变量,要加上引号
'testdata.count'(){
console.log(this.testdata.count)
this.$store.dispatch('SetCount',this.testdata.count)
this.$store.dispatch('PrintCount')
},
发布订阅模式
1.Dep类:负责进行依赖收集
- 首先,有一个数组,专门来存放所有的订阅信息
- 其次,还要提供一个向数组中追加订阅信息的方法
- 然后,还要提供一个循环,循环触发数组中的每个订阅信息
2.Watch类:负责订阅一些事件
dom相当于订阅者,当数据发生改变的时候,发布者会将消息发布给订阅者,订阅者拿到消息之后进行重新渲染
Object.defineProperty
const obj = {
name:'zs'
}
Object.defineProperty(obj,'name',{
enumerable:true,//当前属性允许被循环
configurable:true,//当前属性,允许被配置 delete
get(){
return '我不是zs'
},
set(newVal){
console.log(newVal)
}
})
console.log(obj.name)//我不是zs,说明结果被拦截了,访问name会被get所拦截
obj.name='ls'//给name赋值的时候会被set所拦截
需要3个类
class Vue{}//通过实例化Vue创建vm对象
class Dep{}//依赖收集的类/收集watcher订阅者的类
class Watcher{}//订阅者的类
两个方法
function Observe(obj){}//定义一个数据劫持的方法
function Compile(el,vm){}//对HTML结构进行模板编译的方法
双向绑定实现:单向数据绑定(能够将vm实例中数据渲染到页面),单向数据动态绑定(更改vm实例中数据会重新渲染对应页面),双向数据(在单项数据绑定的基础上,实现修改页面对应vm的数据能够影响vm实例中的数据)
首先我们来考虑如何实现将vm实例中的数据渲染到页面中,像vue那样,在模板表达式中使用obj.name就可以访问vm实例中的数据,那我们就需要一个模板编译函数,来实现vm实例中数据到页面的渲染
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
}
// 终止递归的条件
return
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
但是这样我们只能第一次刷新页面的时候才可以使用vm中的数据,后面我们修改vm实例中数据,页面并不会变化,也就是说我们只是实现了单项数据的渲染,但是没有动态的进行绑定,如何绑定喃?
这个时候就需要使用到上面所说的订阅发布机制了,当我们创建一个具有数据的DOM的时候,DOM对象可以理解为一个订阅者,我将我的信息存到发布者那里,当发布者监听到DOM对应的vm数据发生变化的时候就发送信息告诉DOM进行页面重新渲染,所以我们在创建一个DOM的时候就要创建一个Watcher类(订阅者类,里面需要拿到对应的vm、自己dom对应的数据key,以及如何更新的回调函数),每次创建一个DOM的时候就实例化一个Watcher类,并将其存储在发布者类里面,所以我们还需要有一个依赖收集类Dep类,里面存放着订阅的信息,这个类的功能:存放订阅信息(watcher实例)、一个添加watcher实例的方法、一个通知每个watcher进行更新的方法,这么看好像Dep要简单点,所以我们首先来实现Dep类
Dep
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
为了方便起见,我们没有做到非常详细只更新某一部分,而是只要数据有更新我们就更新全部,中心在理解数据绑定上面
接下来是Watcher类
// 订阅者的类
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 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
Dep.target = this
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
同时我们还要有一个数据劫持的方法,我们需要去劫持vm实例上data数据,比如某个数据访问的时候我们该做什么操作,修改某个数据的时候该进行什么操作,递归给data中每一个数据都进行数据劫持,我们编译类中只要使用到了vm中的数据,对应操作为get,那么我们就需要实例化一个watcher类,然后存在Dep里面,一旦vm数据发生变化,即触发set,然后dep通知watcher进行更新
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 把 value 这个子节点,进行递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
},
})
})
}
当前还只是实现了单项数据绑定,即vm中data数据变化会引发页面中对应dom重新渲染,但是我们还没有实现双向数据绑定,双向数据绑定其实还较为简单,在编译方法中进行修改,在具有v-model属性的文本框上添加监听函数,即node.addEventListener,数据变化的时候把最新的数据更新到vm上即可 // 对 HTML 结构进行模板编译的方法 function Compile(el, vm) { // 获取 el 对应的 DOM 元素 vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能 const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) { fragment.appendChild(childNode) }
// 进行模板编译 replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法 function replace(node) { // 定义匹配插值表达式的正则 const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
} }
完整代码
<div id="app">
{{name}}
<input type="text" v-model='name'>
</div>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'zs',
age:20,
info:{
a:'a1'
}
}
})
</script>
vue.js
class Vue {
constructor(options) {
this.$data = options.data
// 调用数据劫持的方法
Observe(this.$data)
// 属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
},
})
})
// 调用模板编译的函数
Compile(options.el, this)
}
}
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 把 value 这个子节点,进行递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
},
})
})
}
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.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 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
Dep.target = this
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
\