Skip to content

面试问的问题

一、*DOMContentLoaded 和 load 的区别

DOMContentLoaded 和 load 是 JavaScript 中两个重要的页面事件。

DOMContentLoaded 事件在页面的 HTML 解析完毕后触发,此时页面所有的 DOM 节点都已经生成。这个时候,图片、视频、样式等资源可能还没有加载完成。通常情况下,如果需要在页面的 DOM 节点生成后执行一些 JavaScript 操作,我们会将这些操作放在 DOMContentLoaded 事件的回调函数中。这样可以保证在页面所有的 DOM 节点都准备就绪后,再执行 JavaScript 操作,从而避免出现找不到 DOM 节点的错误。

load 事件在页面所有资源(包括图片、视频、样式等)都已经加载完成后触发。此时,页面已经完全加载完成,并且所有资源都已经准备就绪。如果需要在页面完全加载完成后执行一些 JavaScript 操作,我们可以将这些操作放在 load 事件的回调函数中。这样可以保证在页面完全加载完成后,再执行 JavaScript 操作,从而避免出现找不到资源的错误。

综上所述,DOMContentLoaded 事件在 DOM 节点生成后就会触发,而 load 事件需要等到所有资源都加载完成后才会触发。因此,如果需要在 DOM 节点生成后执行 JavaScript 操作,我们应该使用 DOMContentLoaded 事件。如果需要等到所有资源都加载完成后执行 JavaScript 操作,我们应该使用 load 事件。

二、如何使用 DOMContentLoaded 和 load

要使用 DOMContentLoaded 事件和 load 事件,可以通过 JavaScript 来监听这些事件并添加回调函数。

监听 DOMContentLoaded 事件的代码如下:

javascript
document.addEventListener('DOMContentLoaded', function () {
  // 在 DOM 节点生成后执行一些 JavaScript 操作
  console.log('DOMContentLoaded');
});

在这个示例中,我们通过 document.addEventListener 方法监听 DOMContentLoaded 事件,并将回调函数作为第二个参数传递给它。当 DOM 节点生成后,回调函数就会被调用,从而执行一些 JavaScript 操作。在这个示例中,我们简单地输出一条日志信息。

监听 load 事件的代码如下:

javascript
window.addEventListener('load', function () {
  // 在所有资源都加载完成后执行一些 JavaScript 操作
  console.log('load');
});

在这个示例中,我们通过 window.addEventListener 方法监听 load 事件,并将回调函数作为第二个参数传递给它。当所有资源都加载完成后,回调函数就会被调用,从而执行一些 JavaScript 操作。在这个示例中,我们简单地输出一条日志信息。

需要注意的是,DOMContentLoaded 事件和 load 事件是互斥的,也就是说,当 DOMContentLoaded 事件触发时,load 事件还没有触发,而当 load 事件触发时,DOMContentLoaded 事件已经触发过了。因此,如果需要在页面加载完成后执行一些 JavaScript 操作,我们应该使用 load 事件,而不是 DOMContentLoaded 事件。

三、*Vue3 reactive 实现原理,如果让你去做你会如何思考

在线例子 vue3-reactive.html

Vue3 的 reactive 实现原理可以概括为以下几个步骤:

  1. reactive 函数接受一个普通对象作为参数,创建一个响应式代理对象 proxy,并返回该代理对象。
  2. proxy 对象内部通过 Proxy 实现拦截对代理对象的访问和修改。
  3. proxy 对象内部维护一个 WeakMap 对象 rawToReactive,用于将原始对象映射到响应式代理对象。
  4. proxy 对象内部维护一个 WeakMap 对象 reactiveToRaw,用于将响应式代理对象映射到原始对象。
  5. 在访问代理对象属性时,get 拦截器会根据需要将属性的值转换为响应式对象并返回。如果属性的值是对象,则递归地将其转换为响应式对象。
  6. 在修改代理对象属性时,set 拦截器会触发更新。如果新值和旧值不相等,则将新值也转换为响应式对象并将其赋值给代理对象。 在触发更新时,会根据 reactiveToRaw 找到对应的原始对象,然后通过 WeakMap 找到依赖于该原始对象的所有响应式对象,并触发更新。 如果要手动实现 reactive 函数,可以按照以上步骤进行实现。其中最核心的是使用 Proxy 实现对代理对象的拦截,以及通过 WeakMap 来维护响应式对象和原始对象之间的映射关系。具体实现可以参考以下代码:
js
let activeEffect = null;

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

function reactive(obj) {
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      trigger(target, key);
    }
  });
  return observed;
}

const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => effect());
  }
}

使用自己手动实现的 reactive 函数可以像使用 Vue 3 的 reactive 函数一样,可以把一个对象传入 reactive 函数中,获取到一个响应式的对象,通过对响应式对象的修改,可以触发相关的依赖更新。以下是一个示例:

javascript
// 定义一个普通对象
const obj = {
  count: 0
};

// 使用 reactive 函数创建一个响应式对象
const state = reactive(obj);

// 定义一个 effect 函数,用于监听 count 的变化
effect(() => {
  console.log('count 值变化了:', state.count);
});

// 修改 state 对象中的 count 属性,会触发 effect 函数的执行
state.count++; // 输出:count 值变化了: 1

在上述示例中,我们定义了一个普通的对象 obj,通过调用 reactive 函数,获取到了一个响应式对象 state。之后,我们使用 effect 函数监听 state.count 属性的变化,在执行 state.count++ 时,因为 state.count 是响应式的,所以会触发 effect 函数的执行,从而输出 count 值变化了: 1。

需要注意的是,自己手动实现的 reactive 函数只是简单的演示原理,并不完整,还有很多情况没有考虑到,实际使用时建议使用 Vue 3 提供的 reactive 函数。

四、Vue3 内部的 reactive 方法绑定的对象是如何把数据展示到视图上的,原理

Vue 3 内部的 reactive 方法是通过使用 ES6 的 Proxy 对象来实现的,它会创建一个代理对象,将这个代理对象返回给调用者。这个代理对象能够拦截针对原始对象的访问和修改,并且会跟踪对这个代理对象的所有访问。这就是 Vue 3 响应式系统的核心。

当你通过模板或者代码中的响应式对象访问到这个代理对象的属性时,它就会触发 get 拦截器函数。在 get 函数中,Vue 3 的响应式系统会将这个依赖收集起来,然后返回被访问的属性的值。这样,当这个属性的值发生改变时,Vue 3 就知道哪些地方需要被更新。

当你通过模板或者代码中的响应式对象修改这个代理对象的属性时,它就会触发 set 拦截器函数。在 set 函数中,Vue 3 的响应式系统会将这个变化通知给所有依赖它的组件,然后再修改这个属性的值。

在实现响应式对象之后,Vue 3 会通过将组件中的模板转换成一个渲染函数来将数据展示到视图上。这个渲染函数会将组件中的所有响应式对象的属性访问转换成对响应式对象的代理对象的访问。当数据发生变化时,渲染函数会重新执行,然后更新视图。Vue 3 的响应式系统使用了这些代理对象来追踪所有的数据依赖,以此实现了高效的视图更新机制。

更详细一点

当我们使用 Vue 3 内部的 reactive 方法将一个对象转换为响应式对象时,Vue 实际上会使用 ES6 Proxy 对象来包装这个对象。这个包装后的对象,我们称之为代理对象,它是原始对象的一个镜像,但它拥有比原始对象更多的能力。

代理对象的 get 拦截器函数会被触发当我们读取代理对象的属性值时。在这个拦截器函数中,Vue 的响应式系统会进行依赖收集。依赖收集的意思是,Vue 会记录下当前组件正在访问哪些响应式对象的哪些属性,并建立响应式依赖关系。

依赖收集完成之后,代理对象会将被访问的属性值返回给调用者。因为访问的是代理对象,而非原始对象,所以可以在这个拦截器函数中进行依赖收集的操作,这也是 Vue 实现响应式的重要原理。

当我们通过模板或代码修改代理对象的属性值时,代理对象的 set 拦截器函数会被触发。在这个拦截器函数中,Vue 的响应式系统会将属性的新值与旧值进行比较,如果不同,则会触发更新操作。这个更新操作包括重新渲染组件,并将新的属性值渲染到视图上。

在实际的更新操作中,Vue 会使用虚拟 DOM 和 Diff 算法来进行高效的视图更新。这样,即使我们的数据发生了很多次变化,Vue 也可以快速地计算出需要更新的部分,并且只更新这些部分,而不是全部重新渲染。这就是 Vue 响应式系统的优势所在。

总结一下,Vue 3 的响应式系统的原理是,使用 ES6 Proxy 对象来包装原始对象,建立响应式依赖关系,并在数据变化时,通过虚拟 DOM 和 Diff 算法来进行高效的视图更新。这个响应式系统是 Vue 框架的核心之一,也是 Vue 框架能够实现高效的组件化开发的重要原因。

五、*Vue2 Vue3 Nexttick 原理,为什么会有这个东西存在

Vue2 和 Vue3 的 nextTick 都是为了解决异步更新 DOM 时的问题而存在的。

在 Vue 中,当我们修改数据时,Vue 会把要更新的 DOM 先打上标记,然后把这些标记放到一个队列里,最后异步地更新队列中的所有标记,以达到减少 DOM 操作的目的。这样的机制称为异步更新队列。

由于 JavaScript 的单线程机制,DOM 更新的异步操作需要等到所有同步操作(如事件处理、setTimeout 等)执行完毕后才会执行。如果没有异步更新队列,Vue 在同步修改数据后,立即进行 DOM 更新,此时可能得到的还是旧数据,从而导致问题。

在 Vue2 中,nextTick 的实现机制是将要更新的操作放到一个队列中,通过宏任务 setTimeout 或 setImmediate 实现异步更新。

而在 Vue3 中,nextTick 已经被移除了,取而代之的是 queuePostFlushCb,它是一个内部实现机制,当异步队列中所有标记都已更新后,就会执行 queuePostFlushCb 中的回调函数。这种实现方式比 Vue2 中的 nextTick 更加高效,并且能够更好地控制 DOM 的更新时机。

总的来说,nextTick 机制的存在是为了解决 DOM 更新异步化的问题,而实现机制则是通过将要更新的操作放到异步队列中,等待同步操作执行完毕后再执行。

六、模拟实现 Nexttick

Vue 2 版本的 nextTick 实现:

html
<div id="app">{{ message }}</div>
js
new Vue({
  el: '#app',
  data: {
    message: 'Hello World'
  },
  mounted() {
    this.$nextTick(() => {
      console.log('updated');
    });
  }
});
js
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this);
};

const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

let timerFunc;

if (typeof Promise !== 'undefined') {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== 'undefined') {
  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);
  };
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

function nextTick(cb, ctx) {
  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();
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

Vue 3 版本的 nextTick 实现:

html
<div id="app">{{ message }}</div>
js
const app = Vue.createApp({
  data() {
    return {
      message: 'Hello World'
    };
  },
  mounted() {
    this.$nextTick(() => {
      console.log('updated');
    });
  }
});

app.mount('#app');
js
let isFlushPending = false;
let isPending = false;

let queue = [];
let flushIndex = 0;

function nextTick(fn) {
  queue.push(fn);

  if (!isPending) {
    isPending = true;
    Promise.resolve().then(flushJobs);
  }
}

function flushJobs() {
  isPending = false;
  isFlushPending = true;

  queue.sort((a, b) => a.id - b.id);

  for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
    const job = queue[flushIndex];
    job();
  }

  queue.length = 0;
  isFlushPending = false;
}

let id = 0;

function createJob(fn) {
  return {
    id: ++id,
    fn
  };
}

Vue.prototype.$nextTick = function (fn) {
  const job = createJob(fn);
  nextTick(job.fn);
};

七、*写一个方法,如果接口请求后缓存第一次请求的结果,以后的请求都直接返回第一次的结果,如果刷新就不管了

在线例子 缓存接口请求的结果并返回.html

js
const fetchCache = {};

async function fetchApiWithCache(url) {
  const cacheName = 'my-api-cache';
  const cache = await caches.open(cacheName);
  const cachedResponse = await cache.match(url);

  if (cachedResponse) {
    // 如果有缓存,则返回缓存的结果
    const data = await cachedResponse.json();
    return data;
  } else if (fetchCache[url]) {
    // 如果正在请求,则等待请求结果
    return fetchCache[url];
  } else {
    // 如果没有缓存,则发起新的请求,并将结果缓存到本地
    const fetchPromise = fetch(url)
      .then((response) => response.json())
      .then((data) => {
        console.log(111);
        cache.put(url, new Response(JSON.stringify(data)));
        delete fetchCache[url];
        return data;
      })
      .catch((error) => {
        delete fetchCache[url];
        throw error;
      });

    fetchCache[url] = fetchPromise;
    return fetchPromise;
  }
}

这个示例中,我们创建了一个全局的 fetchCache 对象,用于缓存正在请求的 URL 及其对应的 promise。在函数中,我们首先检查是否有缓存结果,如果有,则直接返回结果。如果正在请求中,则等待请求结果。如果没有缓存结果,也没有正在请求中,则发起一个新的请求,并将 promise 添加到 fetchCache 对象中。

这样,在多次调用 fetchApiWithCache 时,只有第一次会真正向服务器发起请求,后续的请求都会等待第一次请求的结果,并从缓存中获取结果。