【面试题】面试官:vue的这些原理你了解吗?

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 【面试题】面试官:vue的这些原理你了解吗?

前言

在之前面试的时候我自己也经常会遇到一些vue原理的问题, 我也总结了下自己的经常的用到的,方便自己学习,今天也给大家分享出来, 欢迎大家一起学习交流, 有更好的方法欢迎评论区指出, 后序我也将持续整理总结~

描述 Vue 与 React 区别

说明概念: vue:是一套用于构建用户界面的渐进式框架,Vue 的核心库只关注视图层 react:用于构建用户界面的 JavaScript 库 声明式, 组件化

  1. 定位
  • vue 渐进式 响应式
  • React 单向数据流
  1. 写法 vue:template,jsx react: jsx
  2. Hooks:vue3 和 react16 支持 hook
  3. UI 更新
  4. 文化 vue 官方提供 React 第三方提供,自己选择

整个 new Vue 阶段做了什么?

  1. vue.prototype._init(option)
  2. initState(vm)
  3. Observer(vm.data)
  4. new Observer(data)
  5. 调用 walk 方法,遍历 data 中的每个属性,监听数据的变化
  6. 执行 defineProperty 监听数据读取和设置

数据描述符绑定完成后,我们就能得到以下的流程图

  • 图中我们可以看出,vue 初始化的时候,进行了数据的 get\set 绑定,并创建了一个
  • dep 对象就是用来依赖收集, 他实现了一个发布订阅模式,完后了数据 data 的渲染视图 watcher 的订阅
class Dep {
  // 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型。
  static target: ?Watcher;
  // subs 存放搜集到的 Watcher 对象集合
  subs: Array<Watcher>;
  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    // 搜集所有使用到这个 data 的 Watcher 对象。
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 搜集依赖,最终会调用上面的 addSub 方法
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用对应的 Watcher,更新视图
      subs[i].update();
    }
  }
}
复制代码

描述 vue 的响应式原理

Vue 的三个核心类

  1. Observer :给对象的属性添加 getter 和 setter ,用于依赖收集派发更新
  2. Dep :用于收集当前响应式对象的依赖关系,每个响应式对象都有 dep 实例,dep.subs = watcher[],当数据发生变更的时候,会通过dep.notify()通知各个 watcher
  3. watcher:是一个中介,数据发生变化时通过 watcher 中转,通知组件 观察者对象,render watcher,computed watcher, user watcher
  • 依赖收集
  • 需要用到数据的地方,称为依赖
  • getter中收集依赖,在setter中触发依赖
  1. initState, 对 computed 属性初始化时,会触发computedwatcher 依赖收集
  2. initState, 对监听属性初始化的时候,触发userwatcher 依赖收集
  3. render,触发renderwatcher 依赖收集
  • 派发更新 Object.defindeProperty
  1. 组件中对响应式的数据进行了修改,会触发 setter 逻辑
  2. dep.notify()
  3. 遍历所有 subs,调用每一个 watcher 的 update 方法 总结: 当创建一个 vue 实例时, vue 会遍历 data 里的属性, Objeect.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持 getter:依赖收集 setter:派发更新 每个组件的实例都有对应的 watcher 实例

计算属性的原理

computed watcher 计算属性的监听器,格式化转换,求值等操作

computed watcher 持有一个 dep 实例,通过 dirty 属性标记计算属性是否需要重新求值 当 computed 依赖值改变后,就会通知订阅的 watcher 进行更新,对于 computed watcher 会将 dirty 属性设置为 true,并且进行计算属性方法的调用,

注意

  1. 计算属性是基于他的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值
  2. 意义:比如计算属性方法内部操作非常频繁时,遍历一个极大的数组,计算一次可能要耗时 1s,如果依赖值没有变化的时候就不会重新计算

nextTick 原理

概念

nextTick 的作用是在下一次 DOM 更新循环结束后,执行延迟回调,nextTick 就是创建一个异步任务,要他等到同步任务执行完后才执行

使用

在数据变化后要执行某个操作,而这个操作依赖因数据的改变而改变 dom,这个操作应该放到 nextTick 中

vue2 中的实现

<template>
  <div>{{ name }}</div>
</template>
<script>
export default {
  data() {
    return {
      name: ""
    }
  },
  mounted() {
    console.log(this.$el.clientHeight) // 0
    this.name = "better"
    console.log(this.$el.clientHeight) // 0
    this.$nextTick(() => {
      console.log(this.$el.clientHeight) // 18
    });
  }
};
</script>
复制代码

我们发现直接获取最新的 DOM 相关的信息是拿不到的,只有在 nextTick 中才能获取罪行的 DOM 信息

原理分析

在执行 this.name = 'better' 会触发 Watcher 更新, Watcher 会把自己放到一个队列,然后调用 nextTick()函数

使用队列的原因: 比如多个数据变更更新视图多次的话,性能上就不好了, 所以对视图更新做一个异步更新的队列,避免重复计算和不必要的 DOM 操作,在下一轮时间循环的时候刷新队列,并执行已去重的任务(nextTick 的回调函数),更新视图

export function queueWatcher (watcher: Watcher) {
  ...
  // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
  nextTick(flushSchedulerQueue)
}
复制代码

这里的参数flushSchedulerQueue方法就会被放入事件循环中,主线程任务执行完后就会执行这个函数,对 watcher 队列排序,遍历,执行 watcher 对应的 run 方法,然后 render,更新视图 也就是在执行 this.name = 'better'的时候,任务队列可以理解为[flushSchedulerQueue],然后在下一行的 console.log,由于会更新视图任务flushSchedulerQueue在任务队列中没有执行,所以无法拿到更后的视图 然后在执行 this.$nextTick(fn)的时候,添加一个异步任务,这时的任务队列可以理解为[flushSchedulerQueue, fn], 然后同步任务执行完了,接着按顺序执行任务队列里的任务, 第一个任务执行就会更新视图,后面自然能得到更新后的视图了

nextTick 源码

源码分为两个部分:一个是判断当前环境能使用的最合适的 API 并保存异步函数,二是调用异步函数执行回调队列 1 环境判断 主要是判断用哪个宏任务或者微任务,因为宏任务的消耗时间是大于微任务的,所以先使用微任务, 用以下的判断顺序

  • promise
  • MutationObserver
  • setImmediate
  • setTimeout
export let isUsingMicroTask = false; // 是否启用微任务开关
const callbacks = []; // 回调队列
let pending = false; // 异步控制开关,标记是否正在执行回调函数
// 该方法负责执行队列中的全部回调
function flushCallbacks() {
  // 重置异步开关
  pending = false;
  // 防止nextTick里有nextTick出现的问题
  // 所以执行之前先备份并清空回调队列
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  // 执行任务队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
let timerFunc; // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 保存一个异步任务
  const p = Promise.resolve();
  timerFunc = () => {
    // 执行回调函数
    p.then(flushCallbacks);
    // ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
    // 所以用一个空的计时器来强制刷新任务队列
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
  // 如 PhantomJS, iOS7, Android 4.4
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 以上都不支持的情况下,使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
复制代码

环境判断结束就会得到一个延迟回调函数timerFunc 然后进入核心的 nextTick

2 nextTick()函数源码 在使用的时候就是调用 nextTick()这个方法

  • 把传入的回调函数放进回调队列 callbacks
  • 执行保存的异步任务 timerFunc,就会遍历 callbacks 执行相应的回调函数了
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // 把回调函数放入回调队列
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    // 如果异步开关是开的,就关上,表示正在执行回调函数,然后执行回调函数
    pending = true;
    timerFunc();
  }
  // 如果没有提供回调,并且支持 Promise,就返回一个 Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
复制代码

可以看到最后有返回一个 Promise,是可以让我们在不传参的时候用

this.$nextTick().then(()=>{ ... })
复制代码

vue3 中分析

点击按钮更新 DOM 内容, 并获取最新的 DOM 内容

 <template>
     <div ref="test">{{name}}</div>
     <el-button @click="handleClick">按钮</el-button>
 </template>
 <script setup>
     import { ref, nextTick } from 'vue'
     const name = ref("better")
     const test = ref(null)
     async function handleClick(){
         name.value = '掘金'
         console.log(test.value.innerText) // better
         await nextTick()
         console.log(test.value.innerText) // 掘金
     }
     return { name, test, handleClick }
 </script>
复制代码

在使用方式上面有了一些变化,事件循环的原理还是一样的,只是加了几个专门维护队列的方法,以及关联到 effect

vue3 nextTick 源码剖析

const resolvedPromise: Promise<any> = Promise.resolve();
let currentFlushPromise: Promise<void> | null = null;
export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void,
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
复制代码

简单来看就是一个 Promise nextTick 接受一个函数为参数,同时会创建一个微任务,在我们页面调用 nextTick 的时候,会执行该函数,把我们的参数 fn 赋值给 p.then(fn) ,在队列的任务完成后, fn 就执行了 由于加了几个维护队列的方法,所以执行顺序是 queueJob -> queueFlush -> flushJobs -> nextTick 参数的 fn

flushJobs 该方法主要负责处理队列任务,主要逻辑如下

  • 先处理前置任务队列
  • 根据 Id 排列队列
  • 遍历执行队列任务
  • 执行完毕后清空并重置队列
  • 执行后置队列任务
  • 如果还有就递归继续执行

vue Router

路由就是一组 key-value 的对应关系,在前端项目中说的路由可以理解为 url-视图之间的映射关系,这种映射是单向的,url 变化不会走 http 请求,但是会更新切换前端 UI 视图,像 vue 这种单页面应用 就是这样的规则.

路由守卫

  1. 全局路由守卫
  • 前置路由守卫: beforeEach 路由切换之前被调用
  • 全局解析守卫:beforeResolve 在每次导航时就会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后 2,解析守卫就被正确调用,如确保用户可以访问自定义 meta 属性requiresCamera  的路由:
router.beforeResolve(async (to) => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission();
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false;
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error;
      }
    }
  }
});
复制代码

router.beforeResolve  是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。

  • 后置路由守卫 :afterEach 路由切换之后被调用requiresCamera  的路由:
  1. 独享路由守卫
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      },
    },
  ],
});
复制代码
  1. 组件內路由守卫 可以在组件内使用者两个钩子
  • 通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
}
复制代码
  • 通过路由规则,离开该组件时调用
beforeRouteLeave (to, from, next) {
}
复制代码

完整的导航解析过程

  1. 导航被触发。
  2. 在失活的组件里调用  beforeRouteLeave  守卫。
  3. 调用全局的  beforeEach  守卫。
  4. 在重用的组件里调用  beforeRouteUpdate  守卫(2.2+)。
  5. 在路由配置里调用  beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用  beforeRouteEnter
  8. 调用全局的  beforeResolve  守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的  afterEach  钩子。
  11. 触发 DOM 更新。
  12. 调用  beforeRouteEnter  守卫中传给  next  的回调函数,创建好的组件实例会作为回调函数的参数传入。

路由模式

  1. history 模式 /: 使用pushStatereplaceState,通过这两个 API 可以改变 url 地址不发生请求,popState事件
  2. hash 模式# :
    hash 是 URL 中 hash(#)及后面的那部分,常用作锚点在页面内进行导航,改变 hash 值不会随着 http 请求发送给服务器,通过hashChange事件监听 URL 的变化,可以用他来实现更新页面部分内容的操作

vueRouter 的实现

剖析 VueRouter 本质

通过使用 vueRouter 可以知道

  1. 通过 new Router() 获得一个 router 实例,我门引入的 VueRouter 其实就是一个类
class VueRouter {}
复制代码
  1. 使用 Vue.use(),而 Vue.use 的一个原则就是执行对象的 install 这个方法,所有,我们可以再一步假设 VueRouter 有 install 这个方法 所以得出
//myVueRouter.js
class VueRouter {}
VueRouter.install = function () {};
export default VueRouter;
复制代码

分析 Vue.use

Vue.use(plugin) 用法: 用于安装 vue.js 插件,如果插件是一个对象,必须提供 install 方法,如果插件是一个函数,它会被作为 install 方法,调用 install 方法的时候,会将 vue 作为参数传入,install 方法被同一个插件多次调用时,插件也只会被安装一次

作用: 注册插件,此时只需要调用 install 方法并将 Vue 作为参数传入 1. 插件的类型,可以是 install 方法,也可以是一个包含 install 方法的对象 2. 插件只能被安装一次,保证插件列表中不能有重复的插件

需要将 Vue 作为 install 方法第一个参数传入,先将 Vue 保存起来,将传进来的 Vue 创建两个组件 router-link 和 router-view

//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
  Vue = v;
  console.log(v);
  //新增代码
  Vue.component('router-link', {
    render(h) {
      return h('a', {}, '首页');
    },
  });
  Vue.component('router-view', {
    render(h) {
      return h('h1', {}, '首页视图');
    },
  });
};
export default VueRouter;
复制代码

install 一般是给每个 vue 实例添加东西的,路由中就是添加$router$route,注意:每个组件添加的$router是同一个和$route 是同一个,避免只是根组件有这个 router 值,使用代理的思想

//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
  Vue = v;
  // 新增代码
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        // 如果是根组件
        this._root = this; //把当前实例挂载到_root上
        this._router = this.$options.router;
      } else {
        //如果是子组件
        this._root = this.$parent && this.$parent._root;
      }
      Object.defineProperty(this, '$router', {
        get() {
          return this._root._router;
        },
      });
    },
  });
  Vue.component('router-link', {
    render(h) {
      return h('a', {}, '首页');
    },
  });
  Vue.component('router-view', {
    render(h) {
      return h('h1', {}, '首页视图');
    },
  });
};
export default VueRouter;
复制代码

完善 VueRouter 类 首先明确下是实例化的时候传了 v 的参数为 mode(路由模式), routes(路由表),在类的构造器中传参

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash';
    this.routes = options.routes || []; //你传递的这个路由是一个数组表
  }
}
复制代码

但是我们直接处理 routes 的十分不方便的,所以我们先要转换成 key:value 的格式

createMap(routes) {
    return routes.reduce((pre,current) => {
        pre[current.path] = current.component
        return pre
    },{})
}
复制代码

手写 vueRouter

vue 模板编译的原理

vue 中模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 html 语法,所有需要将 template 转换成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 html 元素,就可以让视图跑起来了,这个过程就叫做模板编译。模板编译又分为三个阶段,解析parse, 优化optimize, 生成generate,最终生成可执行函数render

  • 解析阶段 : 使用大量的正则表达式对 template 字符串进行解析,将标签,指令,属性等转化为抽象语法树 AST
  • 优化阶段: 遍历 AST,找打其中的一些静态节点进行标记, 方便在页面重渲染的时候进行 diff 比较时,直接跳过这些静态节点,优化 runtime 的性能
  • 生成阶段: 将最终的 AST 转化为 render 函数字符串

给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

相关文章
|
1天前
|
消息中间件 存储 缓存
大厂面试高频:Kafka 工作原理 ( 详细图解 )
本文详细解析了 Kafka 的核心架构和实现原理,消息中间件是亿级互联网架构的基石,大厂面试高频,非常重要,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka 工作原理 ( 详细图解 )
|
3天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
3天前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
26天前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
26天前
|
SQL 存储 关系型数据库
美团面试:binlog、redo log、undo log的底层原理是什么?它们分别实现ACID的哪个特性?
老架构师尼恩在其读者交流群中分享了关于 MySQL 中 redo log、undo log 和 binlog 的面试题及其答案。这些问题涵盖了事务的 ACID 特性、日志的一致性问题、SQL 语句的执行流程等。尼恩详细解释了这些日志的作用、所在架构层级、日志形式、缓存机制以及写文件方式等内容。他还提供了多个面试题的详细解答,帮助读者系统化地掌握这些知识点,提升面试表现。此外,尼恩还推荐了《尼恩Java面试宝典PDF》和其他技术圣经系列PDF,帮助读者进一步巩固知识,实现“offer自由”。
美团面试:binlog、redo log、undo log的底层原理是什么?它们分别实现ACID的哪个特性?
|
14天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
31 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
25天前
|
负载均衡 算法 Java
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
40岁老架构师尼恩分享了关于SpringCloud核心组件的底层原理,特别是针对蚂蚁集团面试中常见的面试题进行了详细解析。内容涵盖了Nacos注册中心的AP/CP模式、Distro和Raft分布式协议、Sentinel的高可用组件、负载均衡组件的实现原理等。尼恩强调了系统化学习的重要性,推荐了《尼恩Java面试宝典PDF》等资料,帮助读者更好地准备面试,提高技术实力,最终实现“offer自由”。更多技术资料和指导,可关注公众号【技术自由圈】获取。
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
|
25天前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
25天前
|
消息中间件 Java Linux
得物面试:什么是零复制?说说 零复制 底层原理?(吊打面试官)
尼恩,40岁老架构师,专注于技术分享与面试辅导。近期,尼恩的读者群中有小伙伴在面试一线互联网企业如得物、阿里、滴滴等时,遇到了关于零复制技术的重要问题。为此,尼恩系统化地整理了零复制的底层原理,包括RocketMQ和Kafka的零复制实现,以及DMA、mmap、sendfile等技术的应用。尼恩还计划推出一系列文章,深入探讨Netty、Kafka、RocketMQ等框架的零复制技术,帮助大家在面试中脱颖而出,顺利拿到高薪Offer。此外,尼恩还提供了《尼恩Java面试宝典》PDF等资源,助力大家提升技术水平。更多内容请关注尼恩的公众号【技术自由圈】。
得物面试:什么是零复制?说说 零复制 底层原理?(吊打面试官)