反应性在 Vue.js 中如何工作?
在前端开发者的世界里,“响应式”这个词人人都在用,但真正理解的人却寥寥无几。其实这没什么错,因为很多人对编程中的响应式有不同的定义。所以在开始之前,我先从前端框架的角度给你一个定义。
“在 JavaScript 框架中,反应性是指应用程序状态的变化会自动反映在 DOM 中的现象。”
Vue.js 中的反应性
Vue.js 中的反应性是软件包自带的。
下面是 Vue.js 中反应性的示例,具有双向绑定(使用v-model
),
在上面的例子中,你可以清楚地看到数据模型层的变化,
new Vue({
el: "#app",
data: {
message: ""
},
})
自动反映在视图层,
<div id="app">
<h1>Enter your message in the box</h1>
<p>{{ message }}</p><br>
<input placeholder="Enter message" v-model="message" />
</div>
如果你熟悉 Vue.js,那么你可能已经习惯了这种方式。但是,你必须记住,在原生 JS 中,事情的运作方式有所不同。让我用一个例子来解释一下。在这里,我用原生 JS 重新创建了上面的 Vue.js 响应式示例。
你会发现 JavaScript 并不是自然响应式的,因为当你输入消息时,你不会看到消息在 HTML 视图中自动重新渲染。为什么会这样?Vue.js 又做了什么?
要回答这个问题,我们必须了解其底层的响应式系统。一旦我们理解清楚,就可以尝试用原生 JavaScript 重新创建我们自己的响应式系统,类似于 Vue.js 的响应式系统。
Vue.js 反应系统
让我从头开始解释一下,
首次渲染
在第一次渲染时,如果“接触”了数据属性(访问数据属性被称为“接触”该属性),则会调用其 getter 函数。
Getter: Getter 函数调用观察器,意图将此数据属性作为依赖项进行收集。
(如果数据属性是依赖项,则意味着每次此属性的值发生变化时,某些目标代码/函数都会运行。)
观察者
每当调用 watcher 时,它会将调用其 getter 的数据属性添加为依赖项。watcher 还负责调用组件的渲染函数。
组件渲染函数
实际上,Vue 的组件渲染功能并不是那么简单,但为了理解,我们只需要知道它返回具有更新的数据属性的虚拟 DOM 树,并将其显示在视图中。
数据发生变化!
这部分基本上是 Vue.js 响应式的核心。因此,当我们对数据属性(作为依赖项收集)进行更改时,会调用其 setter 函数。
Setter: Setter 函数会在数据属性发生每次更改时通知观察者。正如我们所知,观察者会运行组件的渲染函数。因此,数据属性的更改会显示在视图中。
我希望您现在已经清楚了工作流程,因为我们将使用原生 JavaScript 重新创建这个反应系统。
使用原生 JavaScript 重建 Vue.js 反应系统
现在,我们正在重新创建反应系统,最好的方法是逐个理解它的构建块(在代码中),最后我们可以将它们全部组装起来,
数据模型
任务:首先,我们需要一个数据模型。
解决方案:
我们需要什么样的数据?由于我们正在重新创建之前看到的 Vue 示例,因此我们需要一个与之完全相同的数据模型。
let data = {
message: ""
}
目标函数
任务:我们需要有一个目标函数,一旦数据模型发生变化,该函数就会运行。
解决方案:
解释目标函数最简单的方法是:
“嗨,我是一个数据属性message
,我有一个目标函数renderFunction()
。每当我的值发生变化时,我的目标函数就会运行。
PS:我可以有多个目标函数,而不仅仅是renderFunction()
“
因此,让我们声明一个名为的全局变量target
,它将帮助我们为每个数据属性记录一个目标函数。
let target = null
依赖类
任务:我们需要一种方法来收集数据属性作为依赖项。
到目前为止,我们只有数据和目标函数的概念,这些函数在数据值发生变化时运行。但是,我们需要一种方法来为每个数据属性分别记录目标函数,以便当某个数据属性发生变化时,只有那些为该数据属性单独存储的目标函数才会运行。
解决方案:
我们需要为每个数据属性的目标功能提供单独的存储空间。
假设我们有以下数据,
let data = {
x: '',
y: ''
}
然后,我们希望为x
和提供两个单独的存储空间y
。那么,为什么不直接定义一个 Dependency 类,让每个数据属性都有其唯一的实例呢?
这可以通过定义一个 Dependency 类来实现,这样每个数据属性都可以拥有自己的 Dependency 类实例。因此,每个数据属性都可以为目标函数分配自己的存储空间。
class Dep {
constructor() {
this.subscribers = []
}
}
依赖类具有subscribers
数组,它将作为目标函数的存储。
现在,我们还需要两件事来使 Dependency 类完全完成,
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()
。
为了实现这一点,我们将使用 getter 和 setter。Object.defineProperty()
允许我们为任何数据属性添加 getter 和 setter,如下所示,
Object.defineProperty(data, "message", {
get() {
console.log("This is getter of data.message")
},
set(newVal) {
console.log("This is setter of data.message")
}
})
因此,我们将为所有可用的数据属性定义 getter 和 setter,如下所示,
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()
在 getter 中调用了该函数,因为当数据属性被“触摸”时,它的 getter 函数就会被调用。
我们dep.notify()
在 setter 内部有这个函数,因为当数据属性的值发生变化时会调用 setter 函数。
观察者
任务:我们需要一种方法来封装当数据属性的值发生变化时必须运行的代码(目标函数)。
解决方案:
到目前为止,我们已经创建了一个系统,其中数据属性在被“接触”时被添加为依赖项,并且如果该数据属性发生任何变化,其所有目标函数都将被执行。
但是,我们仍然缺少一些东西,我们还没有用任何代码来初始化目标函数的进程。因此,为了封装目标函数的代码并初始化进程,我们将使用观察者。
观察者是一个函数,它接受另一个函数作为参数,然后执行以下三件事,
target
将获取参数的匿名函数赋给全局变量。- 运行
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);
好了,我们完成了!
现在,组装好以上所有代码,我们就成功地用原生 JavaScript 重新创建了 Vue.js 的响应式系统。下面是我展示的第一个示例的实现,使用了这套响应式系统。
鏂囩珷鏉ユ簮锛�https://dev.to/deepsource/how-does-reactivity-work-in-vue-js-3pe2