先说结论,数据响应式是指通过数据驱动DOM视图的变化,是单向的过程;而双向数据绑定的数据和DOM是一个双向的关系。
Vue的数据响应式
Vue响应式指的是:组件中的data发生变化,立刻触发视图的更新。
Vue响应式的实现主要是基于数据劫持
和发布-订阅者模式
,依赖于Object.defineProperty(vue2)和Proxy(vue3)对象,把data的数据对象转换为getter和sertter。当数据被访问时,触发getter函数进行依赖收集 ;当数据属性被修改时,触发setter函数来通知依赖于此数据的Watcher实例重新计算,从而触发视图的更新。
Vue的双向数据绑定
Vue的双向数据绑定是:基于mvvm思想,数据变化更新视图,视图变化更新数据,使数据在视图和组件之间进行双向的数据流动。
双向数据绑定的效果可以使用的v-model
指令来体现,它是Vue的一个特性,也可以说是一个input事件和value的语法糖。
v-model
v-model本质上是v-bind
和v-on
的语法糖。
-
作用在表单元素上
js<input v-model="data" /> //等价于 <input v-bind:value="data" v-on:input="data=$event.target.value"/> // 动态绑定了 input 的 value 指向了 data 变量,并且在触发 input 事件的时候去动态把 data 设置为当前dom的value值
-
作用在表单元素上
js//父组件中定义子组件 <child :value="data" @input="function(e){data = e}"></child> //子组件中 <input v-bind:value="value" v-on:input="inputChange"></aa-input> props:{value}, methods:{ inputChange(e){ this.$emit('input',e.target.value) } } // 一个组件上的 v-model 会把传入的变量用 prop 当作 value 接收 // 子组件中,js 监听 input 输入框输入数据,触发 input 事件把数据 $emit 出去 // 父组件中,通过同名的 input 事件接收数据
实现双向数据绑定
建议大家在阅读代码之前可以观看配套视频传送门,这个系列的学习视频讲的很清楚,对于初学者难度会大大降,接下来的代码总结仅供有基础的同学食用。
前置技术点(可跳过)
使用reduce链式获取对象属性的值
js
const obj = {
name: 'zs',
info: {
address: {
location: '北京顺义',
},
},
}
// 需求:通过以下字符串在obj中获取相应的属性值
const attrStr = 'info.address.location'
// const location = attrs.reduce((newObj, k) => { return newObj[k] }, obj)
// 第一次 reduce,
// 初始值是 obj 这个对象,
// 当前的 k 项是 info
// 第一次 reduce 的结果是 obj.info 属性对应的对象
// 第二次 reduce,
// 初始值是 obj.info 这个对象,
// 当前的 k 项是 address
// 第二次 reduce 的结果是 obj.info.address 属性对应的对象
// 第三次 reduce,
// 初始值是 obj.info.address 这个对象,
// 当前的 k 项是 location
// 第三次 reduce 的结果是 obj.info.address.location 属性的值
const location = attrStr.split('.').reduce((newObj, k) => newObj[k], obj)
console.log(location) //北京顺义
发布-订阅者模式
js
// 创建Dep类:作用是收集依赖/收集订阅者,并且触发订阅者的回调
class Dep {
constructor() {
// 这个 subs 数组,用来存放所有订阅者的信息
this.subs = []
}
// 向 subs 数组中,添加订阅者的信息
addSub(watcher) {
this.subs.push(watcher)
}
// 发布通知的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
// 创建Watcher类:订阅者的类,每一个订阅者都要有update方法,用于收到通知后作出一些行为
class Watcher {
constructor(cb) {
this.cb = cb
}
// 触发回调的方法
update() {
this.cb()
}
}
const w1 = new Watcher(() => {
console.log('我是第1个订阅者')
})
const w2 = new Watcher(() => {
console.log('我是第2个订阅者')
})
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.notify()
// 我是第1个订阅者
// 我是第2个订阅者
使用Object.defineproperty进行数据劫持
js
const obj = {
name: 'zs',
age: 20,
}
Object.defineProperty(obj, 'name', {
enumerable: true, // 当前属性,允许被循环
configurable: true, // 当前属性,允许被配置
get() {
// getter
console.log("name属性被访问")
return obj.name
},
set(newVal) {
// setter
console.log("name属性被修改")
obj.name = newVal
},
})
const name = obj.name //name属性被访问
obj.name = 'ls' //name属性被修改
双向数据绑定的流程(代码篇)
new Vue()
首先执行初始化,对data执行响应化处理,这个过程发生Observe
中;- 数据初始化完成后,对模板执行编译,这个过程发生
Compile
中; - 进行模板编译的最后阶段,给每一种渲染方式创建watcher的实例,把渲染dom的关键操作封装为watcher的更新函数,这个过程发生在
replaceNode
函数中; - new出来的每一个watcher的实例,都会被
Dep
依次收集起来; - 将来data中数据⼀旦发生变化,会进入setter中,首先找到该属性对应的Dep,执行
notify
方法通知所有watcher执行更新函数。
实现一个简单的双向数据绑定代码如下,可以复制到本地运行,了解Vue是如何将数据劫持与发布订阅者模式结合起来的,其中有很多巧妙的细节,可以搭配视频仔细体会。
html
<body>
<div id="app">
<h3>姓名是:{{name}}</h3>
<h3>年龄是:{{age}}</h3>
<h3>info.a 是:{{info.a}}</h3>
<h3>姓名:<input v-model="name" /></h3>
<h3>年龄:<input v-model="age" /></h3>
<h3>info.a :<input v-model="info.a" /></h3>
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'zs',
age: 20,
info: {
a: 'a1',
c: 'c1'
}
}
})
console.log(vm)
</script>
</body>
js
//vue.js
class Vue{
constructor(options){
this.$data = options.data
//调用数据劫持的方法
Observe(this.$data)
// 把this.$data上的属性代理到vm实例上 vm.$data.name => vm.name
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).forEach(key => {
let value = obj[key]
// 进行递归,给对象类型的数据的子属性也添加getter/setter
Observe(value)
// 把data中的所有数据转换为getter/setter
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
// Dep.target指向watcher实例,如果key属性有对应的订阅者,就把该订阅者收集起来
Dep.target && dep.addSubs(Dep.target)
return value
},
set(newValue){
value = newValue
Observe(value)
// 属性被修改时,向每一个订阅者发出通知,触发渲染dom的回调
dep.notify()
}
})
});
}
function Compile(el,vm){
//获取el对应的DOM元素
vm.$el = document.querySelector(el)
//创建文档碎片,提高DOM操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
//进行模板编译
replaceNode(fragment)
//模板编译结束后,渲染页面
vm.$el.appendChild(fragment)
function replaceNode(node){
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 对文本子节点进行正则的匹配与替换
if(node.nodeType === 3){
const text = node.textContent
const execResult = regMustache.exec(text)
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
}
//如果是DMO节点 并且是输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT'){
//获取节点的所有属性
const attrs = Array.from(node.attributes)
const findResult = attrs.find(attr=>attr.name === 'v-model')
if (findResult){
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)
console.log(obj);
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理 递归获取所有纯文本节点
node.childNodes.forEach(child => replaceNode(child))
}
}
class Dep {
constructor() {
this.subs = []
}
addSubs(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 订阅者的类
class Watcher {
constructor(vm,key,cb) {
this.vm = vm
this.key = key
this.cb = cb
// 下面三行负责把创建的Watcher 实例存到Dep实例的subs数组中
// 这个步骤通过第二行代码访问ledata中的属性,触发了getter,巧妙的将watcher实例收集到dep中
Dep.target = this
key.split('.').reduce((newObj,k)=>newObj[k],vm)
Dep.target = null
}
// 触发回调函数的方法 发布者通知我们更新
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
总结
双向数据绑定 = (数据劫持 + 发布订阅者模式) + 通过js监听dom事件反向给vm属性赋值