2025-4-15记录面试回顾

52 阅读9分钟

记录一次在职线上面试的经历,时间大概约在下午2点,迎来了我第一次在公司蜗居在微波炉房的难忘经历。又紧张又害怕被人发现,好在自己稳住心态,集中精神全神贯注,这次面试居然要全程讲粤语,oh my god! 平时可以用粤语沟通,但是是第一次用粤语沟通技术,老感觉怪怪的包括自我介绍也要用粤语。最后,问Hr面试评价怎么样,他说还可以,但是还要人事部门进行对比一下,无论结果怎么,面试后应该做好复盘。

1.软件文档在线回答英文翻译中文

有8句话,大概能完全翻译出4.5句的样子。这里还是强调英语对程序员的重要性。

2.vue2 和 vue3 的响应式原理区别

vue2 使用 object.defineProperty对每个属性进行监听,当对属性进行读取的时候,会触发getter函数,对属性进行修改的时候,就会触发setter函数

function defineReactive (data, key, val) {
    // 存储依赖的地方
    const dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 在 getter中收集依赖
            dep.depend()
            return val
        },
        set: function (newVal) {
            val = newVal
            // 在 setter 中触发依赖
            dep.notify()
        }
    })
}

一般属性读取是在watcher里面,watcher可以被认为是依赖,在watcher里面读取数据的时候,会把自己设置到一个全局的变量

class Watcher {
    constructor (vm, exp, cb){
        this.vm = vm
        this.getter = exp
        this.cb = cb
        this.value = this.get()
    }
    
    get () {
        Dep.target = this
        let value = this.getter.call(this.vm, this.vm)
        Dep.target = undefined
        return value
    }
    
    update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

在Watcher读取数据的时候,就会触发getter属性监听。在getter里面就需要进行依赖收集,这些依赖存储的地方就叫Dep,在Dep里面可以把全局变量中的依赖进行收集,收集完毕之后就会把全局依赖设置为空。将来数据发生变化的时候,就把Dep中相关的Watcher拿出来再执行一遍

class Dep {
    constructor () {
        this.subs = []
    }
    
    addSub (sub) {
        this.subs.push(sub)
    }
    
    removeSub (sub) {
        remove(this.subs, sub)
    }
    
    depend () {
        if(Dep.target){
            this.addSub(Dep.target)
        }
    }

    notify () {
        const subs = this.subs.slice()
        for (let i = 0; i < subs.length; i++) {
            subs[i].update()
        }
    }

    function remove (arr, item){
        if(arr.length) {
            const index = arr.indexOf(item)
            if (index > -1) {
                return arr.splice(index, 1)
            }
        }
    }
}

总结:

  1. 通过Object.defineProperty 监听对象的每一个属性,当读取数据会触发getter函数,修改数据时会触发setter
  2. 然后在getter中进行依赖收集(构造器),当setter被触发的时候,就在getter中收集到的依赖进行相关操作(执行的是回调函数)
  3. 收集依赖通过Dep类进行存储,并且可以对相关的依赖添加或者删除并且通知相关依赖进行相关操作
  4. 在Vue2依赖指的就是Watcher,只有Watcher触发的getter才会进行依赖收集,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当响应式数据发生改变的时候,就把收集到的Watcher进行通知

Vue3 使用proxy对数据实现getter/setter方法进行代理,从而实现响应式数据,在副作用函数中读取响应式数据的时候,就会触发proxy的getter,在getter里面把当前的副作用函数保存起来,将来对响应式数据发生更改的时候,将之前保存卡里的副作用函数取出来执行。

// 使用一个全局变量存储被注册的副作用函数
let activeEffect

// 注册副作用函数
function effect (fn) {
    activeEffect = fn
    fn()
}

const obj = new Proxy(data,{
    // getter拦截读取操作
    get (target, key) {
        // 将副作用函数 activeEffect 添加到 存储副作用函数的全局变量的targetMap 中
        track(target, key)

        // 返回读取的属性值
        return Reflect.get(target, key)
    },

    // Reflect是一个内置的对象,用于拦截和操作对象行为,不能被构造函数来创建新的对象实例

    // setter 拦截设置操作
    set (target, key, val) {
        // 设置属性值
        const result = Reflect.set(target, key, val)
        // 把之前存储的副作用函数取出来并执行
        trigger(target, key)
        return result
    }
})

// 存储副作用函数的全局变量
//WeakMap 的键值必须是对象,不能是原始值,否则会抛出错误。键值是弱引用的,当一个键对象没有没引用的时候,会直接被垃圾回收机制自动清除
// WeakMap 不可迭代。你不能通过任何方法遍历它的内容,因为它的键是弱引用的,可能在任何时候被垃圾回收。
const targetMap = new WeakMap()

// 在getter拦截器内追踪
function track (target, key) {
    // 没有activeEffect,直接返回
    if(!activeEffect) return
    // 根据target从全局变量targetMap 中获取 depsMap
    let depsMap = targetMap.get(target)
    if(!depsMap) {
        // 如果 depsMap 不存,那么需要新建一个 Map 并且与 target 关联
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    // 将当前活动的副作用函数保存起来
    deps.add(activeEffect)
}

// 在setter拦截器中触发相关依赖
function trigger(target, key) {
    // 根据target 从全局变量targetMap取出depsMap
    const depsMap = targetMap.get(target)
    if(!depsMap) return
    // 根据key取出相关联的所有副作用函数
    const effects = depsMap.get(key)
    // 执行所有的副作用函数
    effects && effects.forEach(fn => fn())
}

上述方法只实现了对引用类型的响应式处理,proxy的代理目标必须是非原始值,原始值是按值传递的,而非引用传递。一个函数接受原始值作为参数,形参与实参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。

vue3对原始值做了一层包裹的方式来实现对原始值变成响应式数据,实现方式也是通过属性访问器getter/setter来实现的

class RefImpl{
    private _value
    public dep
    // 表示这是一个 Ref 类型的响应式数据
    private _v_isRef = true
    constructor(value) {
        this._value = value
        // 依赖存储
        this.dep = new Set()
    }
	// getter 访问拦截
    get value() {
        // 依赖收集
        trackRefValue(this)
        return this._value
    }
	// setter 设置拦截
    set value(newVal) {
        this._value = newVal
        // 触发依赖
        triggerEffect(this.dep)   
    }
}

ref 本质上是一个实例化之后的 “包裹对象”,因为 Proxy 无法提供对原始值的代理,所以我们需要使用一层对象作为包裹,间接实现原始值的响应式方案。 由于实例化之后的 “包裹对象” 本质与普通对象没有任何区别,所以为了区分 ref 与 Proxy 响应式对象,我们需要给 ref 的实例对象定义一个 _v_isRef 的标识,表明这是一个 ref 的响应式对象。

Vue3 则是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。

总结

Vue3 对数组实现代理时,用于代理普通对象的大部分代码可以继续使用,但由于对数组的操作与对普通对象的操作存在很多的不同,那么也需要对这些不同的操作实现正确的响应式联系或触发响应。这就需要对数组原型上的一些方法进行重写。

比如通过索引为数组设置新的元素,可能会隐式地修改数组的 length 属性的值。同时如果修改数组的 length 属性的值,也可能会间接影响数组中的已有元素。另外用户通过 includes、indexOf 以及 lastIndexOf 等对数组元素进行查找时,可能是使用代理对象进行查找,也有可能使用原始值进行查找,所以我们就需要重写这些数组的查找方法,从而实现用户的需求。原理很简单,当用户使用这些方法查找元素时,先去响应式对象中查找,如果没找到,则再去原始值中查找。

另外如果使用 push、pop、shift、unshift、splice 这些方法操作响应式数组对象时会间接读取和设置数组的 length 属性,所以我们也需要对这些数组的原型方法进行重新,让当使用这些方法间接读取 length 属性时禁止进行依赖追踪,这样就可以断开 length 属性与副作用函数之间的响应式联系了。

vue2和vue3差异

  • 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
  • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
  • 无法监听到数组元素的变化,只能通过劫持重写了几个数组方法
  • 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
  • 不支持 Map、Set 等数据结构

3. axios如何阻止请求重复

import axios from 'axios'

const pendingRequests = new Map() //请求对象

const getRequestKey = (config) => {
    return `${config.url}-${JSON.stringify(config.params || config.data)}`
}

// 添加请求
const addPendingRequest = (config) => {
    const requestKey = getRequestKey(config)
    if (!pendingRequests.has(requestKey)) {
        const controller = new AbortController()
        config.signal = controller.signal;
        pendingRequests.set(requestKey, controller)
    }
}

// 移除请求
const removePendingRequest = (config) => {
    const requestKey = getRequestKey(config)
    if (pendingRequests.has(requestKey)) {
        pendingRequests.delete(requestKey)
    }
}

// 取消重复请求
const cancelDuplicateRequest = (config) => {
    const requestKey = getRequestKey(config)
    if (pendingRequests.has(requestKey)) {
        const controller = pendingRequests.get(requestKey)
        controller.abort('请求重复,已取消')
        pendingRequests.delete(requestKey)
    }
}

instance.interceptors.request.use((config) => {
    cancelDuplicateRequest(config); // 取消重复请求
    addPendingRequest(config); // 添加当前请求
    return config;
}, (error) => {
    return Promise.reject(error);
});


instance.interceptors.response.use((response) => {
    removePendingRequest(response.config); // 移除已完成的请求
    return response;
}, (error) => {
    if (error.code === 'ERR_CANCELED' && error.message.includes('请求重复')) {
        console.warn('请求被取消:', error.message);
    } else {
        removePendingRequest(error.config || {}); // 移除失败的请求
    }
    return Promise.reject(error);
});

代码的整体执行过程

用户发起请求 
    ↓ 
进入请求拦截器
    ↓ 
检查是否有重复请求(cancelDuplicateRequest) 
    ↓ 
如果有重复请求 → 取消之前的请求(AbortController.abort())
    ↓ 
如果没有重复请求 → 将当前请求添加到 pendingRequests 中(addPendingRequest) 
    ↓ 
发送请求 
    ↓ 
请求完成(成功或失败)
    ↓ 
进入响应拦截器 
    ↓
从 pendingRequests 中移除已完成的请求(removePendingRequest)

4. 权限控制

当前的系统主要是包括了按钮权限,路由权限,数据权限。前两个主要是在用户登录之后,请求对应的角色接口,那么它的角色接口响应返回对应的按钮和路由标识存储在全局仓库的permission字段里面,当permission有值的情况下,会通过自定义指令去判断系统所有的按钮是否有权限,如果没有权限的情况下,我在组件未挂载的时候清除掉,而路由权限会通过递归的方式去不断添加路由,使用的api主要是router.addRoute。那么数据权限的话,主要是用于控制数据面板的数据,只要后端返回的数据为null的情况下,我们自动给它标注符号为***

5. 说一说大文件上传,断点续传,以及分片上传。假设上传的时候超出setTimeOut时间如何解决?