Vue2 响应式原理简单实现

Vue2 响应式原理简单实现

浅谈Vue2响应式实现原理

其核心思想就是为每一个数据进行劫持和监听,具体怎么做的呢?就是利用

Object.defineProperty(obj, key, descriptor)

为对象中的每一个属性添加get和set方法来进行劫持和监听,当数据被读取时,触发get方法;当数据被修改时,触发set方法。

这仅仅只是把数据劫持监听了,至于如何将修改后的数据重新渲染到页面上,这个时候就需要使用DOM操作命令将数据重新渲染到页面上。

所以在触发set方法的时候,就需要去操作和这个属性相关的方法,即这个属性的依赖

已知,当数据被读取时,触发get方法;在页面上渲染数据的时候也是读取数据的过程,所以可以在set中搜集依赖。

搜集到依赖后,在set中依次调用即可

大致实现流程

  1. 使用Object.defineProperty()将数据对象劫持,添加get和set
  2. get中搜集依赖
  3. set中重新执行属性相关依赖

流程示例:

添加 get set

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>
<body>
   <div class="app">
      <p id="name"></p>
   </div>
   <script>
   let obj = {
       name : '稀土掘金'
   }
   Object.defineProperty(obj, 'name', {
       get: ()=>{
           console.log(`name被读取了`)
       },
       set: (val)=>{
           console.log(`name的值被修改为${val}`)
       }
   })
   let str = obj.name   // 将 obj.name 的值赋值给新的变量 str 则触发 get
   // 执行到这 控制台打印  name被读取了
   obj.name = '稀土掘金1'   // 改变了 obj.name 的值
   // 执行到这 控制台打印  name的值被修改为稀土掘金1
   </script>
</body>
</html>

到这一步,仅仅只是在控制台看到了结果,那么如何将结果渲染到页面上呢?

js 复制代码
// 先写一个方法,用来将数据渲染到页面
function showName() {
      document.querySelector('#name').textContent = `渲染结果:${obj.name}`
}
showName()

理所应当,这个时候应该在屏幕上渲染出来 渲染结果:稀土掘金

但是渲染结果是这样的:

MDN中关于Object.defineProperty()的get方法是这样介绍的:get方法的返回值将被用做该属性的值默认值为undefined

首先肯定不能在get内部直接返回 obj.name,在内部返回renturn obj.name仍然表示你在读取它,就导致get重复调用

完善 添加get返回值

所以代码可以改写成下面这个形式

js 复制代码
   let obj = {
       name: '稀土掘金'
   }
   let internalValue = obj.name
   Object.defineProperty(obj, 'name', {
       get: ()=>{
           console.log(`name被读取了`)
           return internalValue
       },
       set: (val)=>{
           internalValue = val // 下一次读取的时候就是读的新值
           console.log(`name的值被修改为${val}`)
       }
   })
   // let str = obj.name   // 将 obj.name 的值赋值给新的变量 str 则触发 get
   // 执行到这 控制台打印  name被读取了
   // obj.name = '稀土掘金'   // 改变了 obj.name 的值
   // 执行到这 控制台打印  name的值被修改为稀土掘金1
   function showName() {
      document.querySelector('#name').textContent = `渲染结果:${obj.name}`
   }
   showName()

这个时候正常渲染,页面展示:渲染结果:稀土掘金

为对象多个属性添加set get

一般的 一个对象里面可能会有多个属性,那么可以代码改为如下:

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>
  <body>
    <div class="app">
      <p id="name"></p>
      <p id="time"></p>
    </div>
    <script>
      let obj = {
        name: "稀土掘金",
        time: "2023.11.01",
      }
      observe(obj)

      // 为对象每一个属性添加get set
      function observe(obj) {
        for (const key in obj) {
          let internalValue = obj[key]
          Object.defineProperty(obj, key, {
            get: () => {
              console.log(`${key}被读取了,读取的值为:${internalValue}`)
              return internalValue
            },
            set: (val) => {
              internalValue = val // 下一次读取的时候就是读的新值
              console.log(`${key}的值被修改为${val}`)
            },
          })
        }
      }
      function showName() {
        document.querySelector("#name").textContent = `渲染结果:${obj.name}`
      }
      function showTime() {
        document.querySelector("#time").textContent = `时间:${obj.time}`
      }
      showName()
      showTime()
    </script>
  </body>
</html>

到这个时候,重新修改属性值obj.name = '稀土掘金2' set触发 打印name的值被修改为稀土掘金2 然后再重新执行一次showName(), 此时页面 渲染结果:稀土掘金2,同时set处打印出name被读取了,读取的值为:稀土掘金2

js 复制代码
obj.name = '稀土掘金2'  // log name的值被修改为稀土掘金2
showName()   // log name被读取了,读取的值为:稀土掘金2

当然,可以把showName()放在set中执行

js 复制代码
            set: (val) => {
              internalValue = val // 下一次读取的时候就是读的新值
              showName()
              console.log(`name的值被修改为${val}`)
            },

依赖挂载全局

这样的话代码就写死了,所以这个时候就需要将依赖收集起来,也就是将和这个属性相关的方法收集起来

js 复制代码
// 将依赖挂载到全局对象上, 比如挂载到window上
window.__func = showName()
// 执行依赖 
showName()  // 这一步的时候就已经触发了 get  所以可以在get中收集一下当前依赖
// 清空 __func
window.__func = null

逻辑有了,那么就封装一下该流程

js 复制代码
function autorun(fn){
    window.__func = fn
    fn()
    window.__func = null
}
// 后续执行就可以直接
autorun(showName)
autorun(...)

在get中搜集依赖 get中使用

接下来就需要在get中收集依赖、set中使用依赖

js 复制代码
      function observe(obj) {
        for (const key in obj) {
          let funs = new Set() // 创建一个数组用于收集依赖 一个方法可能被执行多次
          let internalValue = obj[key]
          Object.defineProperty(obj, key, {
            get: () => {
              if (window.__func) {  // 避免 null 的影响
                funs.add(window.__func)
              }
              console.log(`${key}被读取了,读取的值为:${internalValue}`)
              return internalValue
            },
            set: (val) => {
              internalValue = val // 下一次读取的时候就是读的新值
              // 使用依赖
              for (const key of funs.keys()) {
                key()
              }
              console.log(`name的值被修改为${val}`)
            },
          })
        }
      }

说明

上述只是非常非常简单的演示了一下vue2的响应式原理,实际使用中,对于一些深层嵌套对象是,还需要递归去逐层添加get和set。有兴趣的可以去看一下源码,源码地址在下方。

附:源码+个人解读

这段地址是Vue2响应式核心实现

这里主要分为5个主要部分

  1. Observer 类:Observer主要结合defineReactive将数据对象转化为响应式对象。使用Object.defineProperty来劫持属性的读取和写入操作也是在defineReactive方法中进行(index.ts)
  2. Dep类: 用于管理依赖于数据的组件或表达式。Dep(dep.ts)
  3. getter和setter: 也就是Object.definePropertygetset用于在属性读取和写入时触发依赖的更新。当一个属性被访问/读取时,它会将当前Watcher添加到该属性的依赖列表中。当属性被修改时,它会通知所有依赖于该属性的Watcher执行更新操作。
  4. Watcher类:观察者,它会订阅一个或多个数据的变化,当数据变化时,Watcher会执行相应的回调函数。在Vue中,组件的渲染过程本质上就是一个Watcher。(watcher.ts)
  5. 还有一个数组响应式:通过重写数组的变异方法(例pushpopshiftunshift等)来实现数组的响应式。当这些方法被调用时,Vue就会派发更新。
相关推荐
树上有只程序猿4 分钟前
3分钟,了解一下Vue3中的插槽到底是个啥
vue.js
潜龙在渊灬26 分钟前
前端 UI 框架发展史
javascript·vue.js·react.js
写不出代码真君2 小时前
Proxy和defineProperty
前端·javascript
乐坏小陈2 小时前
TypeScript 和 JavaScript:2025 年应该选择哪一个?【转载】
前端·javascript
Clrove.112 小时前
JavaWeb——Ajax
前端·javascript·ajax
Epicurus2 小时前
DOM节点类型列举
前端·javascript
Cutey9162 小时前
Vue2 vs Vue3 的 props 对比
vue.js·面试
鸿是江边鸟,曾是心上人2 小时前
react+ts+eslint+prettier 配置教程
前端·javascript·react.js
hyyyyy!2 小时前
《从事件冒泡到处理:前端事件系统的“隐形逻辑”》
前端·javascript·react.js
A-Kamen3 小时前
前端数据模拟利器 Mock.js 深度解析
开发语言·前端·javascript