前端热门面试题鉴定-响应式

Q: 我们常说 React, Vue 等实现以"响应式"的逻辑进行前端开发, 请讲一下你对"响应式"的理解

分析

指令式

首先我们需要明确, 单纯的 JS 代码本身是一行一行执行的指令. 在没有封装逻辑的前提下, JS 代码对 X 的操作就局限于 X, 并不会影响到 Y 或 Z

js 复制代码
let X = 1
let Y = 2
let Z = X + Y

console.log(Z) // 3

X = 2
console.log(Z) // 依然是 3, 因为我们并没有发出对 Z 进行操作的指令

操作 DOM 的原生 API 也是这样"指令式"风格

html 复制代码
<html>
  <button>+1</button>
  <output>1</output>
</html>

<script>
// 获取 <button> 元素
const btn = document.querySelector("button");
// 获取 <output> 元素
const output = document.querySelector("output");

function addCount() {
  output.innerText++;
}

// 当点击 <button> 元素时, 让 <output> 元素内部的数字 +1
btn.addEventListener("click", addCount)
</script>

响应式

我们再回到上文中 X, Y 和 Z 的代码:

js 复制代码
let X = 1
let Y = 2
let Z = X + Y

虽然 X, Y 和 Z 的类型都是数字(number), 但 Z 可以被视作 X 和 Y 经过加和逻辑处理后的结果, 也可以说加和逻辑从 X 和 Y 获取数据后得到了 Z

这时我们可以认为 X 和 Y 是加和逻辑的依赖 (dependencies), 因为加和逻辑获得结果 Z 需要 X 和 Y; 也可以说加和逻辑订阅(subscribe)了 X 和 Y

在"指令式"风格下, 如果我们希望维持依赖组 (dependencies)和结果组(results)之间的绑定关系, 就必须在每一次依赖组被修改后调用一次绑定逻辑(BindLogic)来更新结果组

js 复制代码
dependencies = changeDependencies()
result = bindLogic(dependencies)

出于代码简洁性(懒🤪), 我们希望当依赖组被修改时, 绑定逻辑可以自动响应 来更新结果组. 这时绑定逻辑就好像"修改依赖组"这一行为的副作用(side effect)一样

为了实现绑定逻辑的响应式 , 我们需要一个位于更高层的监听逻辑. 当监听到依赖组内的对象被修改时, 调用相应的绑定逻辑

js 复制代码
let X = 1
let Y = 2
let Z

function update() {
  // 依赖组(X, Y)和结果组(Z)之间以加和逻辑绑定
  Z = X + Y
}

whenDepsChange(update)

console.log(Z) // 3

X = 2
console.log(Z) // 4, whenDepsChange() 监听到 X 的变化后调用 update() 来更新 Z, 加和关系保持成立

更进一步, 当依赖组内部对象被读取 (get), 这就表明某一逻辑依赖了该对象, 此时应追踪 (track)该逻辑; 当依赖组内的对象被赋值 (set), 此时应根据被赋值对象的追踪情况, 触发 (trigger)所有依赖该被赋值对象的逻辑. 这时我们就实现了一个响应式系统

Vue

Vue 提供了相应 API 来构建上述响应式逻辑:

Vue API 作用
reactive() | ref() 声明将被放入依赖组或结果组的对象
watchEffect() 声明绑定逻辑并指明相应依赖组和结果组
computed() 利用绑定逻辑和依赖组对象来声明结果组对象
js 复制代码
import { ref, watchEffect } from "vue"

const X = ref(0)
const Y = ref(1)
const Z = ref()

watchEffect(() => {
  // 绑定逻辑为"加和(+)"
  // X 和 Y 放入依赖组, Z 放入结果组
  Z.value = X.value + Y.value
})

// Vue 将自动维护 X, Y 和 Z 之间的加和逻辑, 此时 Z 的值变为 3
X.value = 2

computed() 改写上述代码:

js 复制代码
import { ref, computed } from "vue"

const X = ref(0)
const Y = ref(1)
// 直接用依赖组对象 X 和 Y, 以及加和逻辑来声明结果组对象 Z
const Z = computed(() => X.value + Y.value)

X.value = 2

平时我们使用 Vue, 最常见的绑定关系是状态 -渲染-> DOM

js 复制代码
import { ref, watchEffect } from "vue"

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `count is: ${count.value}`
})

// 依赖组 count 发生改变, 渲染逻辑将被触发
count.value++

这就是开发者只需要管理好状态, Vue 会自动将其反映到 DOM 上 的内部逻辑. 但在具体实现上, Vue 对渲染逻辑进行了大量优化, 所以更推荐将状态和 DOM 之间的具体渲染逻辑放在 <template>

html 复制代码
<template>
  <button @click="addCount">+1</button>
  <output>{{count}}</output>
</template>

<script setup>
import { ref } from "vue";

const count = ref(0);

function addCount() {
  count.value += 1;
}
</script>

回答

在分析"响应式"之前, 我们应首先明确: 不进行任何逻辑封装的前提下, JS 是指令式 的, 即对 X 的操作就局限于 X, 并不会影响到 Y 或 Z. 原生 DOM 操作也沿袭了指令式 风格. 因此在为网站添加动态性 , 也就是维护数据 -渲染-> DOM 这一关系时, 数据变动 指令其后必须跟随渲染指令

出于代码的简洁性, 让数据变动摆脱掉渲染这条"小尾巴", 前端开发引入了响应式 的概念. 具体表现为通过使用 React 或 Vue 等来自动维护数据DOM 之间的渲染 逻辑. 当数据发生改变, React 或 Vue 将响应变化并依据改变后的结果执行渲染

此时我们可以将数据分到依赖组 (dependencies), DOM分到结果组 (results), 渲染逻辑从依赖组中获取数据, 经过执行后得到结果组. 那么泛化来说, 依赖组 中可以放入任意数据; 结果组 中也可以不局限于 DOM; 依赖和结果之间的绑定逻辑 也可以自由指定, 且当依赖发生改变时自动执行, 来对结果进行更新, 维护依赖和结果之间的对应关系. 这就实现了响应式

更进一步, 当依赖组内部对象被读取 (get), 这就表明某一逻辑依赖了该对象, 此时应追踪 (track)该逻辑; 当依赖组内的对象被赋值 (set), 此时应根据被赋值对象的追踪情况, 触发 (trigger)所有依赖该被赋值对象的逻辑. 这时我们就实现了一个响应式系统

补充

这道题是一道非常适合面试切入的题. 既可以深入提问响应式 在代码层面如何实现, 又可以问响应式关联的两端: 从具体依赖什么出发, 深入到前端状态管理, 第三方状态集成等; 从到底如何得到结果出发, 深入到 Vue, React 等渲染流程, 彼此异同等. 是一道非常关键的前端面试题

参考

相关推荐
Mryan20056 分钟前
Angular | 利用 `ChangeDetectorRef` 解决 Angular 动态显示输入框的聚焦问题
前端·javascript·angular.js
郭尘帅6668 分钟前
Vue3中实现轮播图
开发语言·前端·javascript
众乐乐_200838 分钟前
Java 后端给前端传Long值,精度丢失的问题与解决
java·前端·状态模式
一叶茶1 小时前
VsCode和AI的前端使用体验:分别使用了Copilot、通义灵码、iflyCode和Trae
前端·vscode·gpt·ai·chatgpt·copilot·deepseek
熊猫钓鱼>_>1 小时前
基于MCP的桥梁设计规范智能解析与校审系统构建实践
前端·easyui·设计规范
若初&1 小时前
文件上传Ⅲ
前端·web安全
若愚67921 小时前
前端取经路——前端安全:构建坚不可摧的Web应用防线
前端·安全
邪恶的贝利亚1 小时前
定时器设计
java·linux·前端
工业互联网专业1 小时前
基于springboot+vue的机场乘客服务系统
java·vue.js·spring boot·毕业设计·源码·课程设计·机场乘客服务系统