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

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

参考

相关推荐
stayong17 分钟前
市面主流跨端开发框架对比
前端
武昌库里写JAVA18 分钟前
C语言 函数指针和指针函数区别 - C语言零基础入门教程
vue.js·spring boot·sql·layui·课程设计
庞囧30 分钟前
大白话讲 React 原理:Scheduler 任务调度器
前端
东华帝君43 分钟前
react 虚拟滚动列表的实现 —— 动态高度
前端
CptW1 小时前
手撕 Promise 一文搞定
前端·面试
温宇飞1 小时前
Web 异步编程
前端
腹黑天蝎座1 小时前
浅谈React19的破坏性更新
前端·react.js
东华帝君1 小时前
react组件常见的性能优化
前端
第七种黄昏1 小时前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ1 小时前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript