反应性如何在Vue.js中工作

70 阅读7分钟

在前端开发者的世界里,"反应性 "是每个人都在使用的东西,但很少有人理解。这其实也不是谁的错,因为有几个人对编程中的反应性有不同的定义。所以在开始之前,让我先给你一个前端框架方面的定义。

"在JavaScript框架中,反应性是指应用程序状态的变化会自动反映在DOM中的现象"。

Reactivity

Vue.js中的反应性

Vue.js中的反应性是自带的东西。

下面是Vue.js中反应性的一个例子,有双向绑定(使用v-model )。

在上面的例子中,你可以清楚地看到,数据模型层的变化。

    new Vue({
      el: "#app",
      data: {
        message: ""
      },
    })

会自动反映在视图层中

    
      Enter your message in the box
      {{ message }}
      
    

如果你熟悉Vue.js,那么你可能已经习惯了这一点。但是,你必须记住,在vanilla JS中,事情的运作方式是不一样的。让我用一个例子来解释。在这里,我在vanilla JS中重新创建了上述Vue.js的反应性例子。

你可以看到,JavaScript在这里并不是自然的反应性,因为当你输入信息的时候,你并没有看到信息在HTML视图中自动被重新渲染。为什么会这样呢?Vue.js做的是什么呢?

嗯,为了得到答案,我们必须了解它的底层反应性系统。一旦,我们有了清晰的认识,我们将尝试在vanilla JavaScript中重新创建我们自己的反应性系统,这将与Vue.js的反应性系统相似。

Vue.js的反应性系统

Reactivity System

让我从头开始为你分析一下。

第一次渲染

在第一次渲染时,如果一个数据属性被 "触及"(访问一个数据属性被称为 "触及 "该属性),它的getter函数会被调用。

**获取器。**getter函数调用观察者,意图是将这个数据属性作为依赖关系来收集。

(如果一个数据属性是一个依赖关系,那么它意味着每次这个属性的值发生变化时,一些目标代码/函数就会运行。)

观察者

每当一个观察者被调用,它就会将该数据属性作为一个依赖关系加入到它被调用的获取器中。观察者也负责调用组件的渲染函数。

组件渲染功能

实际上,Vue的组件渲染函数并不那么简单,但为了便于理解,我们只需要知道它返回带有更新数据属性的虚拟DOM树,并在视图中显示。

数据的变化!

这一部分,基本上是Vue.js中反应性的核心。所以,当我们对一个数据属性(作为一个依赖关系被收集)进行更改时,它的setter函数会被调用。

**设置器。**setter函数在数据属性的每一次变化中都会通知观察者。正如我们已经知道的,观察者运行组件的渲染函数。因此,数据属性的变化被显示在视图中。

我希望你现在已经很清楚这个工作流程了,因为我们将在vanilla JavaScript中重新创建这个反应性系统。

在vanilla JavaScript中重新创建Vue.js的反应性系统

现在,我们正在重新创建反应式系统,最好的方法是逐一理解它的构件(在代码中),最后我们可以把它全部组装起来。

数据模型

**任务。**首先,我们需要一个数据模型。

解决方案。

我们需要什么样的数据?由于我们正在重现之前看到的Vue的例子,我们将需要一个与之完全相同的数据模型。

一个目标函数

**任务。**我们需要有一个目标函数,一旦数据模型发生变化就会运行。

解决方案。

解释目标函数的最简单方法是这样的。

"你好,我是一个数据属性message ,我有一个目标函数renderFunction() 。只要我的值发生变化,我的目标函数就会运行。

PS:我可以有一个以上的目标函数,而不仅仅是renderFunction()"

因此,让我们声明一个名为target 的全局变量,它将帮助我们为每一个所有的数据属性记录一个目标函数。

依赖关系类

**任务。**我们需要一种方法来收集数据属性作为一个依赖关系。

现在,我们只有数据和目标函数的概念,当数据的值发生变化时,目标函数会运行。但是,我们需要一种方法来分别记录每一个数据属性的目标函数,这样,当一个数据属性发生变化时,就只有那些为该数据属性单独存储的目标函数会运行。

解决方案。

我们需要为每个数据属性的目标函数设置一个单独的存储空间。

假设我们有以下的数据。

那么,我们要为xy 两个独立的存储空间。那么,为什么不直接定义一个Dependency类,每个数据属性都可以有其独特的实例?

这可以通过定义一个依赖类来实现,这样每个数据属性都可以有自己的依赖类的实例。因此,每个数据属性都可以为目标函数分配自己的存储空间。

    class Dep {
    	constructor() {
      	this.subscribers = []
      }
    }

Dependency类有subscribers 数组,它将作为目标函数的存储空间。

Dependency Class

现在,我们还需要两样东西来使依赖类完全完整。

  • depend():这个函数将目标函数推送到subscribers 数组中。
  • notify():这个函数运行存储在subscribers 数组中的所有目标函数。
    class Dep {
    	constructor() {
      	this.subscribers = []
      }
      depend() {
      	// Saves target function into subscribers array
      	if (target && !this.subscribers.includes(target)) {
        	this.subscribers.push(target);
        }
      }
      notify() {
      	// Replays target functions saved in the subscribers array
        this.subscribers.forEach(sub => sub());
      }
    }

追踪变化

**任务。**我们需要找到一种方法来自动运行数据属性的目标函数,只要该属性有变化。

解决方案。

到现在我们已经有了。

  • 的数据
  • 数据变化时需要发生什么
  • 依赖性收集机制

接下来我们需要的是。

  • 当一个数据属性被 "触碰 "时,有办法触发depend()
  • 一种方法来跟踪数据属性的任何变化,然后触发notify()

为了实现这一点,我们将使用getters和setters。Object.defineProperty() 允许我们像这样为任何数据属性添加getters和setters。

    Object.defineProperty(data, "message", {
    	get() {
      	console.log("This is getter of data.message")
      },
      set(newVal) {
      	console.log("This is setter of data.message")
      }
    })

所以,我们将为所有像这样的数据属性定义获取器和设置器。

    Object.keys(data).forEach(key => {
    	let internalValue = data[key]

      // Each property gets a dependency instance
      const dep = new Dep()

      Object.defineProperty(data, key, {
      	get() {
        	console.log(`Getting value, ${internalValue}`)
        	dep.depend() // Saves the target function into the subscribers array
          return internalValue
        },
        set(newVal) {
        	console.log(`Setting the internalValue to ${newVal}`)
        	internalValue = newVal
          dep.notify() // Reruns saved target functions in the subscribers array
        }
      })
    })

另外,你可以看到上面的dep.depend() 在获取器中被调用,因为当一个数据属性被 "触及 "时,它的获取器函数被调用。

我们在setter里面有dep.notify() ,因为当该数据属性的值发生变化时,setter函数会被调用。

观察者

**任务。**我们需要一种方法来封装当数据属性的值发生变化时必须运行的代码(目标函数)。

解决方案。

到目前为止,我们已经创建了一个系统,当数据属性被 "触碰 "时,它们就被添加为依赖关系,如果该数据属性有任何变化,它的所有目标函数都将被执行。

但是,还有一些不足之处,我们还没有用目标函数的任何代码来初始化这个过程。因此,为了封装目标函数的代码,然后初始化进程,我们将使用观察者。

观察者是一个函数,它接收另一个函数作为参数,然后做以下三件事。

  • 将golbaltarget 变量与它在参数中得到的匿名函数赋值。
  • 运行target() 。(这样做是为了初始化进程。)
  • 重新赋值target = null
    let watcher = function(func){
      // Here, a watcher is a function that encapsulates the code
      // that needs to recorded/watched.
      target = func // Then it assigns the function to target
      target() // Run the target function
      target = null // Reset target to null
    }

现在,如果我们把一个函数传给观察者,然后运行它,反应性系统将完成,进程将得到初始化。

    let renderFunction = () => {
    	// Function that renders HTML code.
    	document.getElementById("message").innerHTML = data.message;
    }

    watcher(renderFunction);

然后,我们就完成了!

现在,将上述所有代码集合起来,我们已经成功地在vanilla JavaScript中重新创建了Vue.js的反应性系统。下面是我向你展示的第一个例子的实现,使用这个反应式系统。