背景:
笔者最近在准备面试的东西,脑海中突然蹦出来一个问题,写了这么久的vue,vue2的本质是什么?以前或许从文章或许从同事的口中听到过,vue2的本质其实就是Object.defineProperty罢了,监听数据,如果监听到的数据发生改变,则把监听到的数据渲染到页面中。
带着这个问题,笔者也再去看了一些文章,发现如果只是实现依赖收集更新页面的话,其实代码量也不太大,那就自己手动实现一个吧。话不多说,show code!
实现
首先需要明白一件事,当时尤老师想实现vue的时候,他会一下子实现这么复杂的功能吗?答案是自然不会,需要一个循序渐进的过程。假设我现在需要在页面上实现姓名和年龄,我肯定是会这样写:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.warp {
background-color: aqua;
width: 300px;
height: 150px;
}
</style>
<body>
<div style="display: flex; justify-content: center;">
<div class="warp">
<p id="firstName">姓</p>
<p id="lastName">名</p>
<p id="age">年龄</p>
</div>
</div>
</body>
</html>
这样确实能实现我们的功能,但是如果需要从js中拿到姓名以及年龄数据呢?那可以定义一个变量,将值加入到页面中,页面代码可以写成这个样子:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.warp {
background-color: aqua;
width: 300px;
height: 150px;
}
</style>
<body>
<div style="display: flex; justify-content: center;">
<div class="warp">
<p id="firstName"></p>
<p id="lastName"></p>
<p id="age"></p>
</div>
</div>
<div style="display: flex; justify-content: center;">
<input type="text" oninput="user.name=this.value"/>
</div>
<script src="./index.js"></script>
</body>
</html>
index.js
var user = {
name: '张三',
age: 32
}
function getFirstName() {
document.querySelector('#firstName').textContent = '姓:' + user.name[0]
}
function getLastName() {
document.querySelector('#lastName').textContent = '名:' + user.name.slice(1)
}
function getAge() {
document.querySelector('#age').textContent = '年龄:' + user.age
}
getFirstName()
getLastName()
getAge()
这样写的话,问题就来了,如果我需要对user里面的数据进行修改,页面不会进行同步渲染,那我要怎样做才能达到修改数据,然后页面也进行同步渲染呢?这个时候Object.defineProperty就派上用场了。 这个时候只要通过Object.defineProperty去监听user里面的每个数据,只要你数据发生了变化,我就通知页面重新渲染新的数据,按照这个思路,可以写出如下监听函数:
mue.js
function observe(obj) {
for(const key in obj) {
let tempValue = obj[key]
Object.defineProperty(obj, key, {
get() {
return tempValue
},
set(val) {
console.log('我发生了改变')
tempValue = val
}
})
}
}
在控制台则会输出以下内容
可以看到,此时user.name更新了,但是页面没有进行渲染,那是因为变量更新了,但是并没有调用相应的函数进行数据更新,这个时候只要把相应的函数(订阅者)收集起来,只要触发了set()函数,再将所有的订阅者都执行一遍,是不是就实现了页面渲染的目的呢。可以重写observe函数
function observe(obj) {
for(const key in obj) {
let tempValue = obj[key]
let func = []
Object.defineProperty(obj, key, {
get() {
//收集相应依赖
func.push()
return tempValue
},
set(val) {
tempValue = val
func.forEach(item => item())
}
})
}
}
改写成这样,就能收集到相应的依赖,而且变量发生变化的时候,页面自动去调用相应的依赖去更新页面。这样写会出现两个问题,1、怎么去收集依赖呢。2、收集完会有重复问题
为了解决上述两个问题,可以将需要执行的函数(依赖)赋值给一个变量,等调用set方法时,去执行这个变量,可以进行如下改写
index.js
var user = {
name: '张三',
age: 32
}
observe(user)
function getFirstName() {···}
function getLastName() {···}
function getAge() {···}
// 将需要执行的依赖赋值给window.__func
window.__func = getFirstName
getFirstName()
window.__func = null // 如果不置空,会出现收集无关依赖问题
window.__func = getLastName
getLastName()
window.__func = null
window.__func = getAge
getAge()
window.__func = null
mue.js
function observe(obj) {
for(const key in obj) {
let interValue = obj[key]
let func = []
Object.defineProperty(obj, key, {
get() {
// 依赖收集
if(window.__func && !func.includes(window.__func)) {
func.push(window.__func)
}
func.push()
return interValue
},
set(val) {
interValue = val
func.forEach(item => {
item()
})
}
})
}
}
如上,在控制台修改user.name/age时,页面也会及时更新,达到双向绑定的目的。
最后
至此,一个缩减版的vue2也算是完成了。当然,相对于其他作者写的那些,本文显得微不足道,只是突然想到这个问题,然后查阅资料,进行实现,也算是自己对vue的一个深入吧。笔者在html文件中加入了一个input框,直接在框内修改数据,姓名会随着修改的数据同步发生变化,所有代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.warp {
background-color: aqua;
width: 300px;
height: 150px;
}
</style>
<body>
<div style="display: flex; justify-content: center;">
<div class="warp">
<p id="firstName"></p>
<p id="lastName"></p>
<p id="age"></p>
</div>
</div>
<div style="display: flex; justify-content: center;">
<input type="text" oninput="user.name=this.value"/>
</div>
<script src="./mue.js"></script>
<script src="./index.js"></script>
</body>
</html>
index.js
var user = {
name: '张三',
age: 32
}
observe(user)
function getFirstName() {
document.querySelector('#firstName').textContent = '姓:' + user.name[0]
}
function getLastName() {
document.querySelector('#lastName').textContent = '名:' + user.name.slice(1)
}
function getAge() {
document.querySelector('#age').textContent = '年龄:' + user.age
}
window.__func = getFirstName
getFirstName()
window.__func = null
window.__func = getLastName
getLastName()
window.__func = null
window.__func = getAge
getAge()
window.__func = null
mue.js
function observe(obj) {
for(const key in obj) {
let interValue = obj[key]
let func = []
Object.defineProperty(obj, key, {
get() {
// 依赖收集
if(window.__func && !func.includes(window.__func)) {
func.push(window.__func)
}
func.push()
return interValue
},
set(val) {
interValue = val
func.forEach(item => {
item()
})
}
})
}
}