实现一个简简简简简简易版的vue2本质

194 阅读4分钟

背景:

笔者最近在准备面试的东西,脑海中突然蹦出来一个问题,写了这么久的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
            }
       })   
    }
    
}

在控制台则会输出以下内容

image.png

可以看到,此时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()
        })
      }
    })
  }
}