在学习尚硅谷禹神的最新的vue3时时有这样一个疑问:
就是一开始使用如下代码,在页面中读取data.name时会产生死循环:
然后后来使用observe构造函数创建一个obs对象,然后再把vm._data和data和obs连等,为什么这样在读取data.name时不会产生死循环,我的想法如下图:
我没怎么学过前端,还请各位解答谢谢。
在学习尚硅谷禹神的最新的vue3时时有这样一个疑问:
就是一开始使用如下代码,在页面中读取data.name时会产生死循环:
我没怎么学过前端,还请各位解答谢谢。
组件的思想是复用,定义组件当然是把通用的公共的东西抽出来复用。
以下代码中定义了两个组件cpn1
和cpn2
,都是定义了两个计数器,con1
的data虽然使用了函数,但是为了模拟data:{count:0}
,使用了常量obj
来返回count。
<div id="app">
<h2>data不使用函数</h2>
<cpn1></cpn1>
<cpn1></cpn1>
<hr>
<h2>data使用函数</h2>
<cpn2></cpn2>
<cpn2></cpn2>
<hr>
</div>
<script src="../js/vue.js"></script>
<template id="cpn1">
<div>
<button @click="count--">-</button>
当前计数:{{count}}
<button @click="count++">+</button>
</div>
</template>
<template id="cpn2">
<div>
<button @click="count--">-</button>
当前计数:{{count}}
<button @click="count++">+</button>
</div>
</template>
<script>
const obj = {
count:0
};
const app = new Vue({
el: "#app",
components: { //局部组件创建
cpn1: {
template: '#cpn1',
data() {
return obj;
}
},
cpn2: {
template: '#cpn2',
data() {
return {
count: 0
}
}
}
}
})
</script>
图中可以看到,不使用函数data
的好像共用一个count
属性,而使用函数的data
的count是各自用各自的,像局部变量一样有块级作用域,这个块级就是vue组件的作用域。
我们在复用组件的时候肯定希望,各自组件用各自的变量,如果确实需要都用一样的,可以全局组件注册,也可以是用vuex来进行状态管理。
针对问题: 当页面中读取data.name时产生死循环的问题该如何解决?
解决方案: 1. 确认是因为该问题导致的死循环,可以通过console.log()等打印信息,定位代码中出现问题的位置。 2. 将组件中的data定义成函数形式,确保每个组件实例都有一个独立的数据源。 3. 使用观察者模式解决数据监听问题。可以通过创建一个Observer类,并在该类中使用Object.defineProperty()为每个属性添加getter和setter监听,从而实现数据劫持。在创建组件实例时,使用observe构造函数创建Observer对象,并将vm._data和data和obs进行连等。具体代码实现如下:
class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
function defineReactive(obj, key, val) {
const dep = new Dep(); //定义一个dep依赖实例
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
val = obj[key];
// 继续递归核心算法,判断val是不是对象,如果是,就递归下去
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = property.get ? property.get.call(obj) : val;
if (Dep.target) { // 当Dep.target存在时,添加订阅进来
dep.depend();
if (childOb) { // 如果子组件不为空,则将新添加的Dep对象添加进去
childOb.dep.depend();
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = property.get ? property.get.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) { //如果要设置的值等于现在的值,或者设置的值是NaN,并且现在的值也是NaN,则直接结束递归
return
}
if (property && property.set) {
property.set.call(obj, newVal);
} else {
val = newVal;
}
childOb = observe(newVal);
dep.notify(); // 如果值发生了改变,则通知订阅者更新组件视图
}
});
}
function observe(value) {
if (!value || typeof value !== 'object') {
return
}
if (value.__ob__) {
return value.__ob__;
}
return new Observer(value);
}
function initState(vm) {
const data = vm.$options.data;
vm._data = data.call(vm);
// 执行observe函数
observe(vm._data);
}
let vm = new Vue({
el: "#box",
data() {
return {
num: 0
}
},
components: {
temp: {
template: "#temp",
data() {
return {
num: 2
}
}
}
}
});
以上是使用observe构造函数创建obs对象声明方式来通过观察者模式解决死循环问题的代码实现。其中,Observer类是对整个数据对象的分类,递归地为对象中每个属性定义get/set方法进行数据监听。而在getter方法中,使用Dep类的depend方法来将订阅者添加到dep实例的subs队列中,将该属性与订阅者联系起来。在setter方法中,使用Dep类的notify方法,通知其subs队列中所有的订阅者进行更新操作。
最后,对于问题“为什么要使用observe构造函数创建obs对象,并将vm._data和data和obs连等?”,这是因为observe构造函数创建的obs对象可以进行递归监听,实现对整个对象的监听,而连等可以将_data和data和obs的引用指向同一块内存地址,这样修改_data和data中的值时,都指向一个地址,可以互相影响,实现对data数据的监听。