赞
踩
在Javascript里实现数据响应式一般有俩种方案,分别对应着vue2.x 和 vue3.x使用的方式,他们分别是:
对象属性拦截 (vue2.x)
Object.defineProperty
对象整体代理 (vue3.x)
Proxy
其中对象属性拦截,不管使用其中的哪种方式,道理都是相通的。
// 1.字面量定义 let data = { name:'小飞' } Object.defineProperty(data,'name',{ get(){ //当我们访问data的属性时,自动调用的方法. //属性可以不存在,会自动创建 // 并且get函数的返回值就是你返回的值 return '小飞' //不能再写data['age'],这样式写其实又在访问了。会又进入get方法,变成了无限死循环了。 //data['age'] }, set(newValue){ //当我们设置或修改name属性的时候,自动调用的函数 //并且属性最新的值会被当作实参传入进来 //这里数据发生变化后,可以发送ajax,操作一块dom区域都可以。这样就实现了数据响应。 console.log('设置了name属性=' +newValue ) }, })
我们的get方法中返回的值始终是小飞
,是固定的,set中拿到新值之后,我们如何让get中可以得到newVal使我们需要解决的问题。
我们可以 通过一个中间变量 middle
来中转get函数和set函数之间的联动
// 1.字面量定义 let data = { name:'小飞', } let middle = 20 Object.defineProperty(data,'age',{ get(){ //返回20 return middle }, set(newValue){ // middle = newValue console.log('设置了age属性=' +newValue ) }, })
这样每次访问,其实都是返回的那个中间变量middle 的值。设置也是设置的那个middle 的值。
vue中data的数据有很多,是怎么实现的,如何通过劫持的方法把每一个属性都变成setter和getter的形式,通过遍历。
let data = { name: '小飞', age: 18, height:180 } // 遍历每一个属性 Object.keys(data).forEach((key)=>{ // key 属性名 // data[key] 属性值 // data 原对象 defineReactive(data,key,data[key]) }) // 响应式转化方法 function defineReactive(data,key,value){ Object.defineProperty(data,key,{ get(){ return value }, set(newVal){ value = newVal } }) }
这个地方实际上使用了闭包的特性,在每一次的defineReactive
函数执行的时候,都会形成一块独立的函数作用域,传入的value
因为闭包的关系会常驻内存,这样一来,每个defineReactive
函数中的value
会作为各自set
和get
函数操作的局部变量。
Object.defineProperty
方法和Proxy对象代理
。<div id="app"> <p></p> </div> <script> let data = { name: '小飞', age: 18, height:180 } // 遍历每一个属性 Object.keys(data).forEach((key)=>{ // key 属性名 // data[key] 属性值 // data 原对象 defineReactive(data,key,data[key]) }) function defineReactive(data,key,value){ Object.defineProperty(data,key,{ get(){ return value }, set(newVal){ value = newVal // 数据发生变化,操作dom进行更新 document.querySelector('#app p').innerHTML = data.name } }) } // 页面首次加载时候的渲染 document.querySelector('#app p').innerHTML = data.name </script>
这样就能在每次修改数据的时候,响应到视图了。
我们将data中name属性的值作为文本渲染到标记了v-text的p标签内部,在vue中,我们把这种标记式的声明式渲染叫做指令
。
<div id="app"> <p v-text="name"></p> </div> <script> let data = { name: '小飞', age: 18, height: 180 } // 遍历每一个属性 Object.keys(data).forEach((key) => { // key 属性名 // data[key] 属性值 // data 原对象 defineReactive(data, key, data[key]) }) function defineReactive(data, key, value) { Object.defineProperty(data, key, { get() { return value }, set(newVal) { value = newVal // 数据发生变化,操作dom进行更新 compile() } }) } // function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍历所有的子元素 nodes.forEach(node => { // nodeType为1为元素节点 if (node.nodeType === 1) { const attrs = node.attributes // 遍历所有的attrubites找到 v-model // Array.from先将伪数组,转为真数组 Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue //nodeName是属性名 //nodeValue是属性值 //假设 v-text="name" //要是属性名等于v-text, if (dirName === 'v-text') { // 就把这个元素的文本替换为对象的值data[name] node.innerText = data[dataProp] } }) } }) } // 首次渲染 compile() </script>
找标记,把数据绑定到dom的过程,我们称之为binding
。
将data中的name属性对应的值渲染到input上面,同时input值发生修改之后,可以反向修改name的值,在vue系统中,v-model指令就是干这个事情的。
<div id="app"> <input v-model="name" /> </div> <script> let data = { name: "小飞", age: 18, height: 180 } // 遍历每一个属性 Object.keys(data).forEach((key) => { // key 属性名 // data[key] 属性值 // data 原对象 defineReactive(data, key, data[key]) }) function defineReactive(data, key, value) { Object.defineProperty(data, key, { get() { return value }, set(newVal) { // 数据发生变化,操作dom进行更新 //要是值相同,就不变 if (newVal === value) { return } value = newVal compile() } }) } // 编译函数 function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍历所有的子元素 nodes.forEach(node => { // nodeType为1为元素节点 if (node.nodeType === 1) { const attrs = node.attributes // 遍历所有的attrubites找到 v-model Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue if (dirName === 'v-model') { //这里是把数据的值绑定要视图上 node.value = data[dataProp] // 视图变化反应到数据 无非是事件监听反向修改 //这里给元素绑定事件,input事件,将输入框的值绑定到对象的属性上。这样,在输入的时候,就会触发input事件将值绑定到对象属性上面了。 node.addEventListener('input', (e) => { data[dataProp] = e.target.value }) } }) } }) } // 首次渲染 compile() </script>
<div id="app"> <p v-text="name"></p> <p v-text="age"></p> <input v-model="age"></input> <input v-model="name"></input> </div> <script> let data = { name: '小飞', age: 18, height: 180 } // 遍历每一个属性 Object.keys(data).forEach((key) => { // key 属性名 // data[key] 属性值 // data 原对象 defineReactive(data, key, data[key]) }) function defineReactive(data, key, value) { Object.defineProperty(data, key, { get() { return value }, set(newVal) { // 数据发生变化,操作dom进行更新 if (newVal === value) { return } value = newVal compile() } }) } // 编译函数 function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍历所有的子元素 nodes.forEach(node => { // nodeType为1为元素节点 if (node.nodeType === 1) { const attrs = node.attributes Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue console.log( dirName,dataProp) if (dirName === 'v-text') { console.log(`更新了${dirName}指令,需要更新的属性为${dataProp}`) node.innerText = data[dataProp] } if (dirName === 'v-model') { //这里是把数据的值绑定要视图上 node.value = data[dataProp] node.addEventListener('input', (e) => { data[dataProp] = e.target.value }) } }) } }) } // 首次渲染 compile() </script>
通过如上代码可知,每次访问对象的一个属性的时候,都会触发compile函数,打印其他的值,即使他们没发生变化,但其实我们只修改了一个属性。显得不够精准,需要借助设计模式来优化我们的架构,就是发布订阅模式。只变化我们修改的那个属性。
数据更新之后实际上需要执行的代码是
node.innerText = data[dataProp]
为了保存当前的node和dataProp,我们再次设计一个函数执行利用闭包函数将每一次编译函数执行时候的node和dataProp都缓存下来,所以每一次数据变化之后执行的是这样的一个更新函数
。
() => {
node.innerText = data[dataProp]
}
一个响应式数据可能会有多个视图部分都需要依赖,也就是响应式数据变化之后,需要执行的更新函数可能不止一个,如下面的代码所示,name属性有俩个div元素都使用了它,所以当name变化之后,俩个div节点都需要得到更新,那属性和更新函数之间应该是一个一对多的关系。
<div id="app">
<div v-text="name"></div>
<div v-text="name"></div>
<p v-text="age"></p>
<p v-text="age"></p>
</div>
<script>
let data = {
name: 'cp',
age: 18
}
</script>
每一个响应式属性都绑定了相对应的更新函数,是一个一对多的关系,数据发生变化之后,只会再次执行和自己绑定的更新函数。
dom绑定事件的方式,我们学过俩种
这俩种绑定方式的区别是,第二种方案可以实现同一个事件绑定多个回调函数,很明显这是一个一对多的场景,既然浏览器也叫作事件,我们试着分析下浏览器事件绑定实现的思路
事件类型
和回调函数
{
click: ['cb1','cb2',....],
input: ['cb1','cb2',...]
}
() => {
node.innerText = data[dataProp]
}
更新视图。
// 增加dep对象 用来收集依赖和触发依赖 const dep = { map: Object.create(null), // 收集 //dataProp是那个变化的属性名 //updateFn是变化的函数 collect(dataProp, updateFn) { //要是变化的属性名不存在 //就以那个属性名创建一个空数组,用来存放更新视图的不同函数 if (!this.map[dataProp]) { this.map[dataProp] = [] } //将每个函数放到对应的属性名 this.map[dataProp].push(updateFn) }, // 触发 //dataProp是那个变化的属性名 //this.map[dataProp]是对应的那个函数数组 //map属性就是用来存放所有的属性名,属性名又包含着所有更新的函数。 trigger(dataProp) { this.map[dataProp] && this.map[dataProp].forEach(updateFn => { //触发每个this.map[dataProp]函数数组里的每个函数 updateFn() }) } }
在编译函数执行的时候,我们把用于更新dom的更新函数收集起来
// 编译函数 function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍历所有的子元素 nodes.forEach(node => { // nodeType为1为元素节点 if (node.nodeType === 1) { const attrs = node.attributes // 遍历所有的attrubites找到 v-model Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue console.log(dirName, dataProp) if (dirName === 'v-text') { console.log(`更新了${dirName}指令,需要更新的属性为${dataProp}`) node.innerText = data[dataProp] // 收集更新函数 dep.collect(dataProp, () => { //将每个更新的函数放到 dep.collect里对应的属性名数组里。 node.innerText = data[dataProp] }) } }) } }) }
当属性发生变化的时候,我们通过属性找到对应的更新函数列表,然后依次执行即可。
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
// 更新视图
if (newValue === value) return
value = newValue
// 再次编译要放到新值已经变化之后只更新当前的key
dep.trigger(key)
}
})
}
Object.defineProperty
来实现,在vue3中使用Proxy
对象代理方案进行了优化Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。