前言

所谓的前端响应系统,简单的理解可以认为,浏览器根据特定变量显示的数据,在这个变量的值发生变化后,能够自动的刷新页面从而展现出变化后的新值。这也是响应前端框架实现双向绑定的前提。

示例

如下示例:通过setTimeout定时器在2秒后更改变量的值,从而实现界面在2秒后能够自动从“Hello world”变化为“你好,世界”

vue获取响应response的header的cookie_javascript

原理分析

提起响应系统,耳熟能详的就是数据劫持(数据代理),所谓的数据劫持就是指,在访问或者修改对象某个属性时,通过一段代码拦截这个行为,并进行相应的操作返回结果。而前端响应系统,主要就是根据数据劫持这个原理实现的,具体的逻辑可以拆分为以下几步

  1. 初始化读取对象属性值
let obj1 = { text: "Hello world" }
function effect() {
  document.getElementById("toshow").innerHTML = obj1.text
}
  1. 劫持读取行为添加相应操作(将该读取行为暂存起来)
const buckset = new Set()
buckset.add(effect)
  1. 设置对象属性的值
obj1.text = '你好 世界'
  1. 劫持设置行为添加相应操作(设置完成后并调用之前暂存的读取操作)
buckset.forEach(fn => fn())

上述步骤就是响应系统最简易的基础逻辑

代码示例

在 ES2015 以前主要是通过Object.defineProperty()方法修改对象属性的get()方法和set()方法实现劫持读取对象属性值和设置属性值的行为,进而添加额外的逻辑实现主动响应,这也是VUE2的实现方式。在 ES2015版本及以后新增了Proxy代理对象,可以在代理的对象种新增属性的读取和设置行为,从而实现主动响应,VUE3采取的正式这种方式。Object.defineProperty()和Proxy的实现方式分别如下,也可以认为下面两个是两个简易的响应系统:

数据劫持实现方式

<!DOCTYPE html>
<html>
  <body>
    <h3>响应系统基础原理</h3>
    <br />
    <span>
      2秒后自动更换:
      <h4 id="toshow"></h4>
    </span>
    <script>
      // 初始化数据
      let obj1 = { text: 'Hello world' }

	  // 读取操作
      function effect() {
        document.getElementById('toshow').innerHTML = data.text
      }

      // 创建“桶” 用于暂存读取操作
      const buckset = new Set()

      // 劫持读取和操作行为
      function newObj(obj = {}) {
        // 遍历对象键, 根据键劫持对象get和set方法
        const keys = Object.keys(obj)
       
        keys.forEach(k => {
          // 获取初始值
          const val = obj[k]
          Object.defineProperty(obj, k, {
            get: function() {
              // 暂存读取行为
              buckset.add(effect)
              // 读取行为正常返回
              return this.k ? this.k : val
            },

            set: function(value) {
              // 设置行为正常设置
              this.k = value
              // 执行读取行为
              buckset.forEach(fn => fn())
            }
          })
        })
        return obj
      }

	  // 执行劫持行为
      let data = new newObj(obj1)

      // 初始化读取对象
      effect()

      // 修改对象值
      setTimeout(() => {
        data.text = '你好,世界'
      }, 2000)
    </script>
  </body>
</html>

数据代理实现方式

<!DOCTYPE html>
<html>
  <body>
    <h3>响应系统基础原理</h3>
    <br />
    <span>
      2秒后自动更换:
      <h4 id="toshow"></h4>
    </span>
    <script>
      // 初始化数据
      let obj = { text: 'Hello world' }

      // 读取操作
      function effect() {
        document.getElementById('toshow').innerHTML = data.text
      }

      // 创建“桶” 用于暂存读取操作
      const buckset = new Set()

      // 代理读取和操作行为
      const data = new Proxy(obj, {
        get(target, key) {
          // 暂存读取行为
          buckset.add(effect)
          // 读取行为正常返回
          return target[key]
        },

        set(target, key, newVal) {
          // 设置行为正常设置
          target[key] = newVal
          // 执行读取行为
          buckset.forEach(fn => fn())
          return true
        }
      })

      // 初始化读取对象
      effect()

      // 修改对象值
      setTimeout(() => {
        data.text = '你好,世界'
      }, 2000)
    </script>
  </body>
</html>

最后

当然,这仅仅只是最基础的响应系统原理,对于前端框架来说,其功能还是远远不够的,比如如何避免陷入无限递归、如何避免重复调用、如何实现嵌套响应等等,要优化的空间还是非常大。但是通过本文,我们至少有了一个清晰的印象,知道了所谓响应系统的大概实现逻辑,不至于以后在面对响应、数据劫持和数据代理时一头雾水。

如果想对VUE的响应系统有跟深的理解和学习推荐阅读霍春阳编写的《Vue.js设计与实现》第四章响应系统的作用与实现。
如果本文对你有帮助,请动动鼠标点赞加收藏,谢谢😊!