网站首页 > 技术文章 正文
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的语法糖。
- 作用在表单元素上
<input v-model="data" />
//等价于
<input v-bind:value="data" v-on:input="data=$event.target.value"/>
// 动态绑定了 input 的 value 指向了 data 变量,并且在触发 input 事件的时候去动态把 data 设置为当前dom的value值
作用在表单元素上
//父组件中定义子组件
<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链式获取对象属性的值
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) //北京顺义
发布-订阅者模式
// 创建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进行数据劫持
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是如何将数据劫持与发布订阅者模式结合起来的,其中有很多巧妙的细节,可以搭配视频仔细体会。
<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>
//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属性赋值
猜你喜欢
- 2024-10-12 学习Vue的数据绑定语法,实现动态的用户界面(四)
- 2024-10-12 Vue前端框架:绑定单个复选框。 vue复选框默认选中
- 2024-10-12 vue2响应式原理 vue2.0响应式原理
- 2024-10-12 vue数据绑定语法 vue样式绑定语法
- 2024-10-12 【Vue】第5章 Vue数据绑定和计算属性详解
- 2024-10-12 Vue数据及事件绑定、filter过滤器
- 2024-10-12 Vue开发:组件绑定自定义事件及其应用
- 2024-10-12 vue.js实战之表单绑定-单选按钮 vue表格单选
- 2024-10-12 跟神一起学前端-初识vue、数据绑定
- 2024-10-12 vue.js 动态绑定class的几种方式 vue动态绑定类
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)