V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GrapeCityChina
V2EX  ›  推广

前端必读: Vue 响应式系统大 PK

  •  
  •   GrapeCityChina · 2021-05-19 11:55:11 +08:00 · 744 次点击
    这是一个创建于 1044 天前的主题,其中的信息可能已经有所发展或是发生改变。

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

    响应式系统( Reactivity systems )是现代前端框架的关键部分之一。应用系统的的高度交互性、动态性和响应能力全靠它支持。每个 Web 开发人员而言都应该了解这一系统的功能和实践操作。

    原理

    响应系统是一种使自动使数据源(模型)与数据表示(视图)层自动保持同步的机制。每次模型更改时,都会重新渲染视图。

    以一个简单的 Markdown 编辑器为例。通常编辑器有两个窗格:一个窗格用于编写 Markdown 代码(用于修改基础模型),另一个窗格用于预览已编译的 HTML (显示已更新的视图)。当我们在书写窗格中写东西时,它会立即在预览窗格中自动预览。这个例子比较简单,在实际情况中会复杂很多。

    在许多情况下,我们要显示的数据取决于其他数据。在这种情况下,需要跟踪相关数据,并根据跟踪情况来更新数据。例如,我们有一个 fullName,该属性由 firstName 和 lastName 属性组成。修改其任何依赖项后,fullName 将自动重新评估,并在视图中显示结果。

    了解什么是响应式系统后,在了解 Vue 3 中的响应系统如何工作以及如何在实践中使用之前,让我们一起来快速回顾一下 Vue 2 中的响应系统内容及其注意事项。

    Vue 2 的响应式系统简介

    Vue 2 中的响应或多或少会被“隐藏”。无论我们放置在 data 对象中的是什么,Vue 都会使其隐式反应( reactive implicitly )。这样虽然可以使开发人员的工作更加轻松,但灵活度却会不可避免的降低。 在幕后,Vue 2 使用 ES5 Object.defineProperty 将 data 对象的所有属性转换为 getter 和 setter 。对于每个组件实例,Vue 创建一个依赖关系观察程序实例,观察者会记录组件渲染期间依赖收集 /跟踪的任何属性。当属性触发依赖项的设置器时,将通知观察者,并将组件重新渲染并更新视图。但是却也会有一些问题存在。

    变更检测警告

    由于 Object.defineProperty 方法的限制,Vue 无法检测到某些数据更改。包括:

    • 给对象添加属性或把对象移除属性(例如 obj.newKey = value )
    • 按索引设置数组项(例如 arr[index] = newValue )
    • 修改数组的长度(例如 arr.length = newLength ) 不过为了解决这些问题,Vue 为提供了 Vue.set API 方法,该方法向响应对象添加了一个属性,确保新属性也是响应性的,从而触发了视图更新。

    用下述实例讨论该情况:

      <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
      <button @click="addAgeProperty">Add "age" property</button>
      <p>Here are my favorite activities:</p>
      <ul>
        <li v-for="item, index in activities" :key="index">
          {{ item }}
          <button @click="editActivity(index)">Edit</button>
        </li>
      </ul>
      <button @click="clearActivities">Clear the activities list</button>
    </div>
    
    
      el: '#app',
      data: {
        person: {
          name: "David"
        },
        activities: [
          "Reading books",
          "Listening music",
          "Watching TV"
        ]
      },
      methods: { 
        // 1. Add a new property to an object
        addAgeProperty() {
          this.person.age = 30
        },
        // 2. Setting an array item by index
        editActivity(index) {
          const newValue = prompt('Input a new value')
          if (newValue) {
            this.activities[index] = newValue
          }
        },
        // 3. Modifying the length of the array
        clearActivities() { 
          this.activities.length = 0 
        }
      }
    });
    
    

    在上面的示例中,我们会发现这三种方法都不起作用。我们不能向该 person 对象添加新属性,无法使用 activities 的索引来编辑数组中的项目,也不能修改 activities 数组的长度。

    优化如下:

      el: '#app',
      data: {
        person: {
          name: "David"
        },
        activities: [
          "Reading books",
          "Listening music",
          "Watching TV"
        ]
      },
      methods: { 
        // 1. Adding a new property to the object
        addAgeProperty() {
          Vue.set(this.person, 'age', 30)
        },
        // 2. Setting an array item by index
        editActivity(index) {
          const newValue = prompt('Input a new value')
          if (newValue) {
            Vue.set(this.activities, index, newValue)
          }
        },
        // 3. Modifying the length of the array
        clearActivities() { 
          this.activities.splice(0)
        }
      }
    });
    
    

    在此示例中,我们用 Vue.setAPI 方法将新 age 属性添加到 person 对象,并从活动数组中选择 /修改特定项目。在最后一种情况下,使用 JavaScript 内置 splice 方法。

    这个做法完全可行但却略显笨拙,而且会导致前后代码不一致。而 Vue 3 就解决了这个问题。 我们用下面示例继续看:

      data() {
        return {
          person: {
            name: "David"
          },
          activities: [
            "Reading books",
            "Listening music",
            "Watching TV"
          ]
        }
      },
      methods: { 
        // 1. Adding a new property to the object
        addAgeProperty() {
          this.person.age = 30
        },
        // 2. Setting an array item by index
        editActivity(index) {
          const newValue = prompt('Input a new value')
          if (newValue) {
            this.activities[index] = newValue
          }
        },
        // 3. Modifying the length of the array
        clearActivities() { 
          this.activities.length = 0 
        }
      }
    }
    
    Vue.createApp(App).mount('#app')
    
    

    可以看到在 Vue 3 中,所有方法都可以正常工作。

    在 Vue 2.6 中,引入的 Vue.observable API 方法,一定程度的公开了响应式系统,使开发人员可以体验到响应式系统的内容。实际上,这和 Vue 内部用来包装 data 对象是完全相同的方法,对于在简单场景创建小的跨组件状态存储很有用。但依旧没办法和 Vue3 的响应式系统相比,接下来就为大家详细介绍。

    注意:由于 Object.defineProperty 方法是仅限 ES5 且不可调整的功能,因此 Vue 2 不支持 IE8 及以下版本。

    Vue 3 响应式系统如何工作

    为了充分利用 ES6 Proxy and Reflect API,Vue 3 中的响应式系统已被完全重写。新版本新增响应式 API,该 API 使系统比以前更加灵活和强大。

    Proxy API 允许开发人员拦截和修改目标对象上的更低级对象操作。代理( proxy )是对象的克隆 /包装( clone/wrapper ),并提供特殊功能(称为 target ),这些功能响应特定的操作并覆盖 JavaScript 对象的内置行为(称为 traps )。如果仍然需要使用默认行为,则可以使用相应的 Reflection API,其名称顾名思义就是反映 Proxy API 的方法。这里有一个示例,用来了解如何在 Vue 3 中使用这些 API:

      name: "David",
      age: 27
    };
    
    const handler = {
      get(target, property, receiver) {
        // track(target, property)
        console.log(property) // output: name
        return Reflect.get(target, property, receiver)
      },
      set(target, property, value, receiver) {
        // trigger(target, property)
        console.log(`${property}: ${value}`) // output: "age: 30" and "hobby: Programming"
        return Reflect.set(target, property, value, receiver)
      }
    }
    
    let proxy = new Proxy(person, handler);   
    
    console.log(person)
    
    // get (reading a property value)
    console.log(proxy.name)  // output: David
    
    // set (writing to a property)
    proxy.age = 30;
    
    // set (creating a new property)
    proxy.hobby = "Programming";
    
    console.log(person) 
    
    

    要创建一个新的代理,使用 new Proxy(target, handler)构造函数。它带有两个参数:目标对象( person 对象)和处理程序对象,该对象定义将拦截哪些操作( get 和 set 操作)。在 handler 对象中,get 和 set 陷阱来跟踪何时读取属性以及何时修改 /添加属性。设置控制台语句以确保运行正确。

    在 get 和 set 陷阱采用下列参数:

    • target:代理包装的目标对象
    • property:属性名称
    • value:属性值(此参数仅用于设置操作)
    • receiver:进行操作的对象(通常是代理)

    Reflect API 方法与其相应的代理方法接受相同的参数

    注释中 track 函数和 trigger 函数特定用于 Vue,用于跟踪何时读取属性以及何时修改 /添加属性。

    在示例的最后一部分,用控制台语句输出原始 person 对象。然后用另一份声明中读取属性 name 的 proxy 对象。接下来,修改 age 属性并创建一个新 hobby 属性。最后,再次输出该对象以查看它是否正确更新。

    以上就是 Vue3 响应式系统的完整工作流程,但在实际工作中会复杂得多。

    使用 Vue 3 响应式系统,还有一些注意事项:

    • 仅适用于支持 ES6 +的浏览器
    • 响应代理不等于原始对象

    总结

    以上我们将 Vue2 和 Vue3 中响应式系统部分进行了比较,并对响应式系统的工作原理进行了说明,在后面的文章中,我们会进一步为大家介绍 Vue3 中响应式系统的 API,敬请期待。

    GrapeCityChina
        1
    GrapeCityChina  
    OP
       2021-05-20 15:06:55 +08:00
    扩展阅读:

    如果你已经熟练掌握 Vue3, 不妨让我们通过实际搭建一款基于 Vue 3 组件的表格编辑系统( https://www.grapecity.com.cn/developer/spreadjs/vue ),更加深入学习
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1435 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 23:48 · PVG 07:48 · LAX 16:48 · JFK 19:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.