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

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

分析

指令式

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

let X = 1
let Y = 2
let Z = X + Y

console.log(Z) // 3

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

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

<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 的代码:

let X = 1
let Y = 2
let Z = X + Y

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

logic.png

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

bind.png

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

dependencies = changeDependencies()
result = bindLogic(dependencies)

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

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

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()利用绑定逻辑和依赖组对象来声明结果组对象
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() 改写上述代码:

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

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>

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

参考

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务