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

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 等渲染流程, 彼此异同等. 是一道非常关键的前端面试题

参考

相关推荐
前端Hardy9 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
李老头探索17 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
web Rookie39 分钟前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生1 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js