70%的人都答错了的面试题,vue3的ref是如何实现响应式的?

news2024/11/16 8:38:16

本文将通过debug的方式带你搞清楚当ref接收的是对象和原始类型时,分别是如何实现响应式的。注:本文中使用的vue版本为3.4.19

看个demo

还是老套路,我们来搞个demo,index.vue文件代码如下:

<template>
  <div>
    <p>count的值为:{{ count }}</p>
    <p>user.count的值为:{{ user.count }}</p>
    <button @click="count++">count++</button>
    <button @click="user.count++">user.count++</button>
  </div>
</template>

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

const count = ref(0);

const user = ref({
  count: 0,
});
</script>

在上面的demo中我们有两个ref变量,count变量接收的是原始类型,他的值是数字0。

count变量渲染在template的p标签中,并且在button的click事件中会count++

user变量接收的是对象,对象有个count属性。

同样user.count也渲染在另外一个p标签上,并且在另外一个button的click事件中会user.count++

接下来我将通过debug的方式带你搞清楚,分别点击count++user.count++按钮时是如何实现响应式的。

开始打断点

第一步从哪里开始下手打断点呢?

既然是要搞清楚ref是如何实现响应式的,那么当然是给ref打断点吖,所以我们的第一个断点是打在const count = ref(0);代码处。这行代码是运行时代码,是跑在浏览器中的。

要在浏览器中打断点,需要在浏览器的source面板中打开index.vue文件,然后才能给代码打上断点。

那么第二个问题来了,如何在source面板中找到我们这里的index.vue文件呢?

很简单,像是在vscode中一样使用command+p(windows中应该是control+p)就可以唤起一个输入框。在输入框里面输入index.vue,然后点击回车就可以在source面板中打开index.vue文件。如下图:

index

然后我们就可以在浏览器中给const count = ref(0);处打上断点了。

RefImpl

刷新页面此时断点将会停留在const count = ref(0);代码处,让断点走进ref函数中。在我们这个场景中简化后的ref函数代码如下:

function ref(value) {
  return createRef(value, false);
}

可以看到在ref函数中实际是直接调用了createRef函数。

接着将断点走进createRef函数,在我们这个场景中简化后的createRef函数代码如下:

function createRef(rawValue, shallow) {
  return new RefImpl(rawValue, shallow);
}

从上面的代码可以看到实际是调用RefImpl类new了一个对象,传入的第一个参数是rawValue,也就是ref绑定的变量值,这个值可以是原始类型,也可以是对象、数组等。

接着将断点走进RefImpl类中,在我们这个场景中简化后的RefImpl类代码如下:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
  get value() {
    trackRefValue(this);
    return this._value;
  }
  set value(newVal) {
    newVal = toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = toReactive(newVal);
      triggerRefValue(this, 4, newVal);
    }
  }
}

从上面的代码可以看到RefImpl类由三部分组成:constructor构造函数、value属性的getter方法、value属性的setter方法。

RefImpl类的constructor构造函数

constructor构造函数中的代码很简单,如下:

constructor(value) {
  this._rawValue = toRaw(value);
  this._value = toReactive(value);
}

在构造函数中首先会将toRaw(value)的值赋值给_rawValue属性中,这个toRaw函数是vue暴露出来的一个API,他的作用是根据一个 Vue 创建的代理返回其原始对象。因为ref函数不光能够接受普通的对象和原始类型,而且还能接受一个ref对象,所以这里需要使用toRaw(value)拿到原始值存到_rawValue属性中。

接着在构造函数中会执行toReactive(value)函数,将其执行结果赋值给_value属性。toReactive函数看名字你应该也猜出来了,如果接收的value是原始类型,那么就直接返回value。如果接收的value不是原始类型(比如对象),那么就返回一个value转换后的响应式对象。这个toReactive函数我们在下面会讲。

_rawValue属性和_value属性都是RefImpl类的私有属性,用于在RefImpl类中使用的,而暴露出去的也只有value属性。

经过constructor构造函数的处理后,分别给两个私有属性赋值了:

  • _rawValue中存的是ref绑定的值的原始值。

  • 如果ref绑定的是原始类型,比如数字0,那么_value属性中存的就是数字0。

    如果ref绑定的是一个对象,那么_value属性中存的就是绑定的对象转换后的响应式对象。

RefImpl类的value属性的getter方法

我们接着来看value属性的getter方法,代码如下:

get value() {
  trackRefValue(this);
  return this._value;
}

当我们对ref的value属性进行读操作时就会走到getter方法中。

我们知道template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后由虚拟DOM生成真实DOM。

在执行render函数期间会对count变量进行读操作,所以此时会触发count变量的value属性对应的getter方法。

getter方法中会调用trackRefValue函数进行依赖收集,由于此时是在执行render函数期间,所以收集的依赖就是render函数。

最后在getter方法中会return返回_value私有属性。

RefImpl类的value属性的setter方法

我们接着来看value属性的setter方法,代码如下:

set value(newVal) {
  newVal = toRaw(newVal);
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal;
    this._value = toReactive(newVal);
    triggerRefValue(this, 4, newVal);
  }
}

当我们对ref的value的属性进行写操作时就会走到setter方法中,比如点击count++按钮,就会对count的值进行+1,触发写操作走到setter方法中。

setter方法打个断点,点击count++按钮,此时断点将会走到setter方法中。初始化count的值为0,此时点击按钮后新的count值为1,所以在setter方法中接收的newVal的值为1。如下图:

set

从上图中可以看到新的值newVal的值为1,旧的值this._rawValue的值为0。然后使用if (hasChanged(newVal, this._rawValue))判断新的值和旧的值是否相等,hasChanged的代码也很简单,如下:

const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

Object.is方法大家平时可能用的比较少,作用也是判断两个值是否相等。和==的区别为Object.is不会进行强制转换,其他的区别大家可以参看mdn上的文档。

使用hasChanged函数判断到新的值和旧的值不相等时就会走到if语句里面,首先会执行this._rawValue = newVal将私有属性_rawValue的值更新为最新值。接着就是执行this._value = toReactive(newVal)将私有属性_value的值更新为最新值。

最后就是执行triggerRefValue函数触发收集的依赖,前面我们讲过了在执行render函数期间由于对count变量进行读操作。触发了getter方法,在getter方法中将render函数作为依赖进行收集了。

所以此时执行triggerRefValue函数时会将收集的依赖全部取出来执行一遍,由于render函数也是被收集的依赖,所以render函数会重新执行。重新执行render函数时从count变量中取出的值就是新值1,接着就是生成虚拟DOM,然后将虚拟DOM挂载到真实DOM上,最终在页面上count变量绑定的值已经更新为1了。

看到这里你是不是以为关于ref实现响应式已经完啦?

我们来看demo中的第二个例子,user对象,回顾一下在template和script中关于user对象的代码如下:

<template>
  <div>
    <p>user.count的值为:{{ user.count }}</p>
    <button @click="user.count++">user.count++</button>
  </div>
</template>

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

const user = ref({
  count: 0,
});
</script>

在button按钮的click事件中执行的是:user.count++,前面我们讲过了对ref的value属性进行写操作会走到setter方法中。但是我们这里ref绑定的是一个对象,点击按钮时也不是对user.value属性进行写操作,而是对user.value.count属性进行写操作。所以在这里点击按钮不会走到setter方法中,当然也不会重新执行收集的依赖。

那么当ref绑定的是对象时,我们改变对象的某个属性时又是怎么做到响应式更新的呢?

这种情况就要用到Proxy了,还记得我们前面讲过的RefImpl类的constructor构造函数吗?代码如下:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
}

其实就是这个toReactive函数在起作用。

Proxy实现响应式

还是同样的套路,这次我们给绑定对象的名为user的ref打个断点,刷新页面代码停留在断点中。还是和前面的流程一样最终断点走到RefImpl类的构造函数中,当代码执行到this._value = toReactive(value)时将断点走进toReactive函数。代码如下:

const toReactive = (value) => (isObject(value) ? reactive(value) : value);

toReactive函数中判断了如果当前的value是对象,就返回reactive(value),否则就直接返回value。这个reactive函数你应该很熟悉,他会返回一个对象的响应式代理。因为reactive不接收number这种原始类型,所以这里才会判断value是否是对象。

我们接着将断点走进reactive函数,看看他是如何返回一个响应式对象的,在我们这个场景中简化后的reactive函数代码如下:

function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

从上面的代码可以看到在reactive函数中是直接返回了createReactiveObject函数的调用,第三个参数是mutableHandlers。从名字你可能猜到了,他是一个Proxy对象的处理器对象,后面会讲。

接着将断点走进createReactiveObject函数,在我们这个场景中简化后的代码如下:

function createReactiveObject(
  target,
  isReadonly2,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

在上面的代码中我们终于看到了大名鼎鼎的Proxy了,这里new了一个Proxy对象。new的时候传入的第一个参数是target,这个target就是我们一路传进来的ref绑定的对象。第二个参数为baseHandlers,是一个Proxy对象的处理器对象。这个baseHandlers是调用createReactiveObject时传入的第三个参数,也就是我们前面讲过的mutableHandlers对象。

在这里最终将Proxy代理的对象进行返回,我们这个demo中ref绑定的是一个名为user的对象,经过前面讲过函数的层层return后,user.value的值就是这里return返回的proxy对象。

当我们对user.value响应式对象的属性进行读操作时,就会触发这里Proxy的get拦截。

当我们对user.value响应式对象的属性进行写操作时,就会触发这里Proxy的set拦截。

getset拦截的代码就在mutableHandlers对象中。

Proxysetget拦截

在源码中使用搜一下mutableHandlers对象,看到他的代码是这样的,如下:

const mutableHandlers = new MutableReactiveHandler();

从上面的代码可以看到mutableHandlers对象是使用MutableReactiveHandler类new出来的一个对象。

我们接着来看MutableReactiveHandler类,在我们这个场景中简化后的代码如下:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = Reflect.set(target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

在上面的代码中我们看到了set拦截了,但是没有看到get拦截。

MutableReactiveHandler类是继承了BaseReactiveHandler类,我们来看看BaseReactiveHandler类,在我们这个场景中简化后的BaseReactiveHandler类代码如下:

class BaseReactiveHandler {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, "get", key);
    return res;
  }
}

BaseReactiveHandler类中我们找到了get拦截,当我们对Proxy代理返回的对象的属性进行读操作时就会走到get拦截中。

前面讲过了经过层层return后user.value的值就是这里的proxy响应式对象,而我们在template中使用user.count将其渲染到p标签上,在template中读取user.count,实际就是在读取user.value.count的值。

同样的template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后将虚拟DOM转换为真实DOM渲染到浏览器上。在执行render函数期间会对user.value.count进行读操作,所以会触发BaseReactiveHandler这里的get拦截。

get拦截中会执行track(target, "get", key)函数,执行后会将当前render函数作为依赖进行收集。到这里依赖收集的部分讲完啦,剩下的就是依赖触发的部分。

我们接着来看MutableReactiveHandler,他是继承了BaseReactiveHandler。在BaseReactiveHandler中有个get拦截,而在MutableReactiveHandler中有个set拦截。

当我们点击user.count++按钮时,会对user.value.count进行写操作。由于对count属性进行了写操作,所以就会走到set拦截中,set拦截代码如下:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = Reflect.set(target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

我们先来看看set拦截接收的4个参数,第一个参数为target,也就是我们proxy代理前的原始对象。第二个参数为key,进行写操作的属性,在我们这里key的值就是字符串count。第三个参数是新的属性值。

第四个参数receiver一般情况下是Proxy返回的代理响应式对象。这里为什么会说是一般是呢?看一下MDN上面的解释你应该就能明白了:

假设有一段代码执行 obj.name = "jen", obj 不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set() 处理器会被调用,而此时,obj 会作为 receiver 参数传进来。

接着来看set拦截函数中的内容,首先let oldValue = target[key]拿到旧的属性值,然后使用Reflect.set(target, key, value, receiver)

Proxy中一般都是搭配Reflect进行使用,在Proxyget拦截中使用Reflect.get,在Proxyset拦截中使用Reflect.set

这样做有几个好处,在set拦截中我们要return一个布尔值表示属性赋值是否成功。如果使用传统的obj[key] = value的形式我们是不知道赋值是否成功的,而使用Reflect.set会返回一个结果表示给对象的属性赋值是否成功。在set拦截中直接将Reflect.set的结果进行return即可。

还有一个好处是如果不搭配使用可能会出现this指向不对的问题。

前面我们讲过了receiver可能不是Proxy返回的代理响应式对象,所以这里需要使用if (target === toRaw(receiver))进行判断。

接着就是使用if (hasChanged(value, oldValue))进行判断新的值和旧的值是否相等,如果不相等就执行trigger(target, "set", key, value, oldValue)

这个trigger函数就是用于依赖触发,会将收集的依赖全部取出来执行一遍,由于render函数也是被收集的依赖,所以render函数会重新执行。重新执行render函数时从user.value.count属性中取出的值就是新值1,接着就是生成虚拟DOM,然后将虚拟DOM挂载到真实DOM上,最终在页面上user.value.count属性绑定的值已经更新为1了。

这就是当ref绑定的是一个对象时,是如何使用Proxy去实现响应式的过程。

看到这里有的小伙伴可能会有一个疑问,为什么ref使用RefImpl类去实现,而不是统一使用Proxy去代理一个拥有value属性的普通对象呢?比如下面这种:

const proxy = new Proxy(
  {
    value: target,
  },
  baseHandlers
);

如果是上面这样做那么就不需要使用RefImpl类了,全部统一成Proxy去使用响应式了。

但是上面的做法有个问题,就是使用者可以使用delete proxy.valueproxy对象的value属性给删除了。而使用RefImpl类的方式去实现就不能使用delete的方法去将value属性给删除了。

总结

这篇文章我们讲了ref是如何实现响应式的,主要分为两种情况:ref接收的是number这种原始类型、ref接收的是对象这种非原始类型。

  • 当ref接收的是number这种原始类型时是依靠RefImpl类的value属性的gettersetter方法中去实现的响应式。

    当我们对ref的value属性进行读操作时会触发value的getter方法进行依赖收集。

    当我们对ref的value属性进行写操作时会进行依赖触发,重新执行render函数,达到响应式的目的。

  • 当ref接收的是对象这种非原始类型时,会调用reactive方法将ref的value属性转换成一个由Proxy实现的响应式对象。

    当我们对ref的value属性对象的某个属性进行读操作时会触发Proxy的get拦截进行依赖收集。

    当我们对ref的value属性对象的某个属性进行写操作时会触发Proxy的set拦截进行依赖触发,然后重新执行render函数,达到响应式的目的。

最后我们讲了为什么ref不统一使用Proxy去代理一个有value属性的普通对象去实现响应式,而是要多搞个RefImpl类。

因为如果使用Proxy去代理的有value属性的普通的对象,可以使用delete proxy.valueproxy对象的value属性给删除了。而使用RefImpl类的方式去实现就不能使用delete的方法去将value属性给删除了。

文章转载自:前端欧阳

原文链接:https://www.cnblogs.com/heavenYJJ/p/18328847

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2110381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

顶升搬运小车与激光叉车在卡板物流及仓储效率提升中的应用

一、项目概述 本项目旨在实现卡板的自动化运输与仓储管理&#xff0c;通过引入顶升搬运小车&#xff08;AGV&#xff09;与激光叉车两种自动化设备&#xff0c;提高运输效率&#xff0c;减少人工干预&#xff0c;并确保流程的顺畅与安全。 二、产品特性与需求分析 考虑到产品…

fastadmin 清除插件缓存报错

Argument 1 passed to Symfony\Component\VarExporter\VarExporter::export() must be an instance of Symfony\Component\VarExporter\mixed, array given, called in F:\work\awebsite\oeob\vendor\karsonzhang\fastadmin-addons\src\addons\Service.php on line 404 我用的…

在DevEco Studio中安装通义灵码

下载插件离线包 离线压缩包 打开设置 打开插件 安装 5.安装后找到右侧图标&#xff0c;登录阿里账号。

Qt TabWidget添加多个窗口,实现分页窗体布局

Qt TabWidget添加多个窗口窗体&#xff0c;可关闭与打开 点击按钮可判断是否打开&#xff0c;避免重复打开 使用Qt中的TabWidget组件创建一个简单的分页窗体布局。点击按钮时&#xff0c;会新增一个窗体并添加到TabWidget中。每个子窗体能动态获取父窗体指针以进行操作 分别…

SpringBoot2:请求处理原理分析-接口参数解析原理

一、知识回顾 我们知道&#xff0c;接口的参数&#xff0c;一般都要配上注解来一起使用。 不同的参数注解&#xff0c;决定了传参的方式不同。 为什么会这样&#xff1f; 如果让你设计接口参数解析&#xff0c;你会怎么做&#xff1f; 本篇就来探究springboot底层是如何通过参…

又发现一个国内超好用的 AI 开放平台!

首先&#xff0c;我认为一个好的人工智能开放平台应该具备以下关键特性&#xff1a;提供多种AI模型和工具&#xff0c;涵盖自然语言处理、计算机视觉、语音识别、数据分析等多个领域。 满足不同应用领域的需求&#xff1b;需具备强大的计算资源&#xff0c;以支持大规模模型的…

K线图新玩法:利用Pin Bar精准捕捉市场反转

对于交易者来说&#xff0c;K线图是必备工具之一&#xff0c;所谓K线图就是由一系列的蜡烛形状组成&#xff0c;每个蜡烛代表一定时间周期内的价格变动情况。而有一种交易策略就是依据蜡烛形态预测市场趋势情况&#xff0c;这种交易策略被叫做Pin Bar交易法。 Pin Bar是什么&am…

高效智能 | 客户运营与知识库管理系统的融合策略

在当今快速变化的商业环境中&#xff0c;企业不仅要关注产品的创新和市场的拓展&#xff0c;更需要深耕客户运营&#xff0c;以提升客户满意度和忠诚度。而知识库管理系统作为支撑客户运营的重要工具&#xff0c;其与企业客户运营策略的深度融合&#xff0c;正成为企业提升核心…

畅捷通如何远程访问

畅捷通如何远程访问 越来越多的企业选择了畅捷通ERP来提升管理效率与资源整合能力。然而&#xff0c;随之而来的远程访问问题却成为了不少用户的困扰。作为一名畅捷通ERP的使用者&#xff0c;我深刻体会到&#xff0c;如何高效、便捷地进行远程访问是实现企业数字化管理的关键。…

软件测试之UI自动化测试

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、about自动化测试 定义&#xff1a;把人为驱动的测试转化为机器执行的一种过程&#xff0c;重点在于持续集成这个概念&#xff1b; 优势&#xff1a;节约人力和…

DDoS对策是什么?详细解说DDoS攻击难以防御的理由和对策方法

攻击规模逐年增加的DDoS攻击。据相关调查介绍&#xff0c;2023年最大的攻击甚至达到了700Gbps。 为了抑制DDoS攻击的危害&#xff0c;采取适当的对策是很重要的。 特别是在网站显示花费时间或频繁出现504错误的情况下&#xff0c;可能已经受到了DDoS攻击&#xff0c;需要尽早采…

代理IP中的API接口:解锁高效与自动化的网络访问新方式

“在当今数字化时代&#xff0c;网络数据的获取与分析已成为各行各业不可或缺的一部分。随着网络环境的日益复杂和网站反爬机制的升级&#xff0c;直接使用原始IP地址进行数据采集或网络访问变得越来越困难。此时&#xff0c;代理IP作为一种有效的解决方案&#xff0c;成为了众…

网络编程 0905作业

作业 1、流式域套接字敲一遍。 服务器 server.c 代码 #include <myhead.h> #define BACKLOG 10int main(int argc, const char *argv[]) {//1、创建流式域套接字int oldfd socket(AF_UNIX,SOCK_STREAM,0);if(oldfd -1){perror("socket");return -1;}//2、…

盘点2024年热门的4款高效的AI写作软件

AI 写作的魅力在于它可以在短时间内完成一篇结构完成整&#xff0c;逻辑清晰的文章内容&#xff1b;并且能够理解用户的需求&#xff0c;然后使用丰富的词汇和句式帮助表达出来。现在各种AI写作软件有很多&#xff0c;为了能够帮助大家更好的解决写作问题&#xff0c;我找了4个…

[001-07-001].第1节:Redis中的BigKey使用分析

我的后端学习大纲 我的Redis学习大纲 1、MorKey分析&#xff1a; 1.1.数据准备&#xff1a; a.大批量往Redis中插入100W数据: 1.Linux Bash下面执行如下命令&#xff0c;是将100万条命令写入到temp目录下的一个临时文件中&#xff1a; 2.通过redis提供的管道–prpe命令插入1…

期权时间价值是什么?期权时间价值的影响分析

今天带你了解期权时间价值是什么&#xff1f;期权时间价值的影响分析。期权会随时间的流逝价值递减。这个特点有利月义务仓(卖出开仓)&#xff0c;不利于权利仓(买入开仓)。远期合约消耗慢&#xff0c;近期合约消耗快。实值合约消耗慢&#xff0c;虚值合约消耗快。 期权管理时…

企业精英引领未来,大数据讲座照亮中职学子职业航道

为深化校企合作模式&#xff0c;加速产教融合步伐&#xff0c;搭建起职业教育与职场世界的无缝对接桥梁&#xff0c;唯众企业携手武汉市第二职业教育中心&#xff08;简称“武汉二职”&#xff09;&#xff0c;共同举办了一场别开生面的专题讲座。此次活动不仅促进了企业与学生…

MOS管烧毁的原因

MOS管作为电子设备中常用的功率开关器件&#xff0c;因其高效能、低导通损耗和快速开关速度&#xff0c;广泛应用于各类电源、电机控制和电力电子系统中。然而&#xff0c;在实际应用中&#xff0c;MOS管烧毁的现象时有发生&#xff0c;通常伴随着电路故障或设计问题。 1. 过…

Circuitjs 的选项设置

选项设置位于 “菜单–选项” 下, 包含显示, 定位, 编辑, 颜色等方面的设置项. 显示电流 勾选后, 导线及元件中会有小点在不断移动, 以模拟电流的移动. 小点移动的速度还反应了电流的大小. 移动快说明电流大, 移动慢说明电流小. 另: 小点移动的速度还受右侧栏 “电流速度” …

Nginx location 和 proxy_pass 配置详解

概述 Nginx 配置中 location 和 proxy_pass 指令的不同组合方式及其对请求转发路径的影响。 配置效果 1. location 和 proxy_pass 都带斜杠 / location /api/ {proxy_pass http://127.0.0.1:8080/; }访问地址&#xff1a;www.hw.com/api/upload转发地址&#xff1a;http://…