vue2 在实现响应式时,是根据 object.defineProperty() 这个实现的,vue3 是通过 Proxy 对象实现,但是实现思路是差不多的,响应式其实就是让 函数和数据产生关联,在我们对数据进行修改的时候,可以执行相关的副作用函数来保证数据的响应式。
首先介绍一下 Object.defineProperty()
Object.defineProperty()
对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是一个具有可写或不可写值的属性。访问器描述符是由 getter/setter 函数对描述的属性。描述符只能是这两种类型之一,不能同时为两者。
数据描述符
- configurable: 如果为 true 表示可以再次修改该属性的属性描述符,同时该属性也能从对应的对象上被删除,默认为 false ,通俗来点讲就是,为 true 时,对于该对象指定了一次Object.defineProperty() 后续就不能修改属性描述符所代表的值,但是getter/setter 可以修改
- writable: 如果为 true 表示这个属性运行被写入值,也就是修改,默认为false
- value: 该对象的对应属性的原始值
上面只针对讲了部分数据描述符,访问描述符就是普通的 getter/setter 函数
使用:
然后这个还可以这样使用:
有人会问,那为什么要这样写,而不是直接在属性上修改,我们取上面的 age 属性的值的时候,就会调用到这里面的 set 函数,如果下面这样写:
会栈溢出,因为我们一直不停地调用 set 函数,所以导致了这个情况,所以我们需要借助临时变量。
了解上述之后,我们来看看今天的主题
实现简单的响应式
首先这是我们的界面:
html 代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="box">
<div>
<span>姓:</span>
<span class="lastName"></span>
</div>
<div>
<span>名:</span>
<span class="firstName"></span>
</div>
<div>
<span>年龄:</span>
<span class="age"></span>
</div>
</div>
<input type="text" class="nameInput" placeholder="请输入姓名">
<input type="datetime-local" class="ageInput">
<script src="./index.js"></script>
</body>
</html>
css 代码:
.box{
padding: 30px;
border-radius: 20px;
width: 400px;
margin: 40px auto;
background-image: linear-gradient(to top, #fbc2eb 0%, #a6c1ee 100%);
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
-ms-border-radius: 20px;
-o-border-radius: 20px;
color: white;
font-size: 20px;
font-weight: bold;
line-height: 50px;
}
js 代码:
let doms = {
lastNameDom: document.querySelector('.lastName'),
firstNameDom: document.querySelector('.firstName'),
ageDom: document.querySelector('.age'),
nameInput: document.querySelector('.nameInput'),
ageInput: document.querySelector('.ageInput')
}
let obj = {
name: '李泽言',
age: "2000-1-1"
}
doms.nameInput.addEventListener('input', (e) => {
console.log(e.target.value)
obj.name = e.target.value
})
doms.ageInput.addEventListener('change', (e) => {
obj.age = e.target.value
})
function getFirstName() {
doms.firstNameDom.textContent = obj.name.substring(1)
}
function getLastName() {
doms.lastNameDom.textContent = obj.name[0]
}
function getAge() {
doms.ageDom.textContent = (new Date().getFullYear()) - (new Date(obj.age).getFullYear())
}
以上代码实现了:通过 obj 的 name 属性来获取姓和名 以及 obj 的 age(其实我应该写 birth 的,不要在意这个)来获取年龄。
然后分别调用 getFirstName() 、getLastName()、getAge()
于是我们可以得到一个上述页面,此时我们修改数据,页面上的数据不会因为这个而修改。
我们要实现的就是根据给出的对象,实现对这个对象响应式。
我们先定义一个 obeserve 函数,并且在定义好 obj 后执行这个函数。
function obeserve(obj) {
//需要让里面的属性和上述函数产生依赖
}
结合上面给出的这个示例,我们可以这样写
function obeserve(obj) {
Object.defineProperty(obj,'',{
get:function(){
},
set:function(val){
}
})
}
但是 第二个参数是你所需要访问的属性,我们是希望这个对象的所以属性都需要实现响应式。
所以我们使用 in 来遍历这个对象所有的属性
因此有如下代码:
function obeserve(obj) {
for (const key in obj) {
console.log(key)
let interval = obj[key];
Object.defineProperty(obj, key, {
get: function () {
return interval
},
set: function (val) {
interval = val
}
})
}
}
上面我们使用了 interval 这个临时变量来实现了,我们现在访问相关属性,可以正确的拿到值(至于为什么需要借助其他的变量来实现,可看我上面的阐述)
现在我们有一个想法,就是当我们在进行设置相关属性的值,我们希望 设置好值后,我们能执行与这个值所有关的函数。
那么这个函数我们从那里知道?我该如何知道哪个函数使用了,就是 get 函数,当我们使用了这个属性,一定会在 get 函数这里留下 踪迹。
所以我们目前的代码是:
function obeserve(obj) {
for (const key in obj) {
console.log(key)
let interval = obj[key];
let func = []
Object.defineProperty(obj, key, {
get: function () {
func.push(xxx函数)
return interval
},
set: function (val) {
interval = val
//这里使用了 forEach 来遍历这个存储所有与该属性相关的函数,拿出来执行
func.forEach(value => value())
// 也可以这样写
// for(let i=0;i<func.length;i++){
// func[i]()
// }
}
})
}
}
但是目前会存在一个问题,因为我们很有可能在一个函数里面使用了俩次该属性,会导致我们重复记录该函数,因为本来这个函数只应该执行一次即可。
于是我们需要做出修改,你可以使用 set 容器,当然也可以使用 数组的 includes 函数来判断是否重复。
function obeserve(obj) {
for (const key in obj) {
console.log(key)
let interval = obj[key];
let func = []
Object.defineProperty(obj, key, {
get: function () {
if (!func.includes(xxx函数))
{ func.push(xxx函数) }
return interval
},
set: function (val) {
interval = val
func.forEach(value => value())
}
})
}
}
好,目前我们只需要解决一个问题,就是我们如何知道这个 xxx 函数到底是什么。或者说我们怎么知道当前调用的是那个函数,这里用到了一个非常巧妙的思维。我们定义一个变量,挂载在 window 这个对象的变量,就叫 window.__activeFun,因为定义在 window 上就可以在同一个页面任何地方都可以拿到,即使我们后面需要把这个 obeserve 独立封装起来使用,也不影响。
我们给他赋值为 null
在我们执行某些函数时,我们做这么一个操作:
//初始值
window.__activeFun = null
window.__activeFun = getFirstName
getFirstName()
window.__activeFun = null
于是 obeserve 函数就应该变成这样
function obeserve(obj) {
for (const key in obj) {
console.log(key)
let interval = obj[key];
let func = []
Object.defineProperty(obj, key, {
get: function () {
//判断 这个函数是否为null或者已经存在
if (window.__activeFun !== null && !func.includes(window.__activeFun)) { func.push(window.__activeFun) }
return interval
},
set: function (val) {
interval = val
func.forEach(value => value())
}
})
}
}
但是考虑到函数的可复用性,前面我们所写的 赋值给 window.__activeFun 可以再修改一下
封装成一个函数
window.__activeFun = null
function addToRun(func) {
window.__activeFun = func
func()
window.__activeFun = null
}
于是我们在执行函数时,不直接执行原本的函数
而是这样
addToRun(getFirstName)
addToRun(getLastName)
addToRun(getAge)
将所有的函数都放入这个 addToRun 函数里面走一遭
于是我们就完成了响应式的一个简单应用
当然真实场景会比这个更复杂,我们需要考虑到 浅响应,深响应以及简单类型数据,和数组集合这类数据。
完整 js 代码:
let doms = {
lastNameDom: document.querySelector('.lastName'),
firstNameDom: document.querySelector('.firstName'),
ageDom: document.querySelector('.age'),
nameInput: document.querySelector('.nameInput'),
ageInput: document.querySelector('.ageInput')
}
let obj = {
name: '李泽言',
age: "2000-1-1"
}
doms.nameInput.addEventListener('input', (e) => {
console.log(e.target.value)
obj.name = e.target.value
})
doms.ageInput.addEventListener('change', (e) => {
obj.age = e.target.value
})
obeserve(obj)
function getFirstName() {
doms.firstNameDom.textContent = obj.name.substring(1)
}
function getLastName() {
doms.lastNameDom.textContent = obj.name[0]
}
function getAge() {
doms.ageDom.textContent = (new Date().getFullYear()) - (new Date(obj.age).getFullYear())
}
window.__activeFun = null
function addToRun(func) {
window.__activeFun = func
func()
window.__activeFun = null
}
function obeserve(obj) {
for (const key in obj) {
console.log(key)
let interval = obj[key];
let func = []
Object.defineProperty(obj, key, {
get: function () {
if (window.__activeFun !== null && !func.includes(window.__activeFun)) { func.push(window.__activeFun) }
return interval
},
set: function (val) {
interval = val
func.forEach(value => value())
}
})
}
}
addToRun(getFirstName)
addToRun(getLastName)
addToRun(getAge)