前端知识点大纲汇总


优化相关

一次加载大量DOM

  1. 类似的问题在其它语言最佳的解决方案是使用多线程,JavaScript虽然没有多线程,但是setTimeout和setInterval两个函数却能起到和多线程差不多的效果。 因此,要解决这个问题, 其中的setTimeout便可以大显身手。 setTimeout函数的功能可以看作是在指定时间之后启动一个新的线程来完成任务。

    function loadAll(response) {
    	//将10万条数据分组, 每组500条,一共200组
    	var groups = group(response);
    	for (var i = 0; i < groups.length; i++) {
    		//闭包, 保持i值的正确性
    		window.setTimeout(function () {
    			var group = groups[i];
    			var index = i + 1;
    			return function () {
    				//分批渲染
    				loadPart( group, index );
    			}
    		}(), 1);
    	}
    }
    
    //数据分组函数(每组500条)
    function group(data) {
    	var result = [];
    	var groupItem;
    	for (var i = 0; i < data.length; i++) {
    		if (i % 500 == 0) {
    			groupItem != null && result.push(groupItem);
    			groupItem = [];
    		}
    		groupItem.push(data[i]);
    	}
    	result.push(groupItem);
    	return result;
    }
    var currIndex = 0;
    //加载某一批数据的函数
    function loadPart( group, index ) {
    	var html = “”;
    	for (var i = 0; i < group.length; i++) {
    		var item = group[i];
    		html += “title:” + item.title + index + " content:" +item.content+ index + “”;
    	}
    //保证顺序不错乱
    while (index - currIndex == 1) {
    	$(“#content”).append(html);
    		currIndex = index;
    	}
    }

    window.requestAnimationFrame 接受参数为函数,比起setTimeout和setInterval有以下优点: 1.把每一帧中的所有DOM操作集中起来,在一次的重排/重绘中完成。每秒60帧。 2.在隐藏或者不可见的元素中,requestAnimationFrame将不会重绘/重排。

  2. document.createDocumentFragment() 用来创建一个虚拟的节点对象,节点对象不属于文档树。 当需要添加多个DOM元素时,可以先把DOM添加到这个虚拟节点中。然后再统一将虚拟节点添加到页面,这会减少页面渲染DOM的次数。

    //总数据
    const total = 10000;
    //每次插入的数据
    const once = 20;
    //需要插入的次数
    const times = Math.ceil(total/once)
    //当前插入的次数
    let curTimes = 0;
    //需要插入的位置
    const ul = document.querySelector('ul');
    function add(){
        let frag = document.createDocumentFragment();
        for(let i = 0; i < once; i++){
            let li = document.createElement('li');
            li.innerHTML = Math.floor(i + curTimes * once);
            frag.appendChild(li);
        }
        curTimes++;
        ul.appendChild(frag);
        if(curTimes < times){
            window.requestAnimationFrame(add);
        }
    }

知识链:setTimeout和setInterval相关知识点🔜setTimeout和setInterval的互相实现🔜requestAnimationFrame相关知识点🔜$nextTick()的实现原理🔜JavaScript事件循环

Webpack和Vite的区别

运行原理

Webpack

当我们使用webpack启动项目时,webpack会根据我们配置文件(webpack.config.js) 中的入口文件(entry),分析出项目项目所有依赖关系,然后打包成一个文件(bundle.js),交给浏览器去加载渲染。

这样就会带来一个问题,项目越大,需要打包的东西越多,启动时间越长。

Vite

<script type="module">中,浏览器遇到内部的import引用时,会自动发起http请求,去加载对应的模块。

vite也正是利用了ES module这个特性,使用vite运行项目时,首先会用esbuild进行预构建,将所有模块转换为es module,不需要对我们整个项目进行编译打包,而是在浏览器需要加载某个模块时,拦截浏览器发出的请求,根据请求进行按需编译,然后返回给浏览器。

这样一来,首次启动项目(冷启动)时,自然也就比webpack快很多了,并且项目大小对vite启动速度的影响也很小。

构建方式

webpack

webpack是基于nodejs运行的,但js只能单线程运行,无法利用多核CPU的优势,当项目越来越大时,构建速度也就越来越慢了。

vite

vite预构建按需编译的过程,都是使用esbuild完成的。

esbuild是用go语言编写的,可以充分利用多核CPU的优势,所以vite开发环境下的预构建按需编译速度,都是非常快的。

http2

vite充分利用了http2可以并发请求的优势,这也是速度快的一个主要原因。 接下来,我们了解一下http2的来龙去脉。

在之前http1的时候,浏览器对同一个域名的请求,是有并发限制的,一般为6个,如果并发请求6个以上,就会造成阻塞问题,所以在http1的时代,我们要减少打包产物的文件数量,减少并发请求,来提高项目的加载速度。

2015年以后,http2出现了,他可以并发发送多个请求,不会出现http1的并发限制。这时候,将打包产物分成多个小模块,并行去加载,反而会更快。

vite也充分利用了这一优势,对项目资源进行了合理的拆分,访问项目时,同时加载多个模块,来提升项目访问速度。

热更新

我们首先来了解一下什么是HMR。

模块热替换(hot module replacement - HMR),该功能可以实现应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面,也就是我们常说的热更新

vite与webpack虽然都支持HMR,但两个工具的实现原理是不一样的。

webpack

webpack项目中,每次修改文件,都会对整个项目重新进行打包,这对大项目来说,是非常不友好的。

这里对整个项目,我理解的是整个项目的模块依赖关系重新梳理编译,这个是最花时间的,至于更新,应该都是对目前正在编辑的模块更新。

虽然webpack现在有了缓存机制,但还是无法从根本上解决这个问题。

vite

vite项目中,监听到文件变更后,会用websocket通知浏览器,重新发起新的请求,只对该模块进行重新编译,然后进行替换。

并且基于es module的特性,vite利用浏览器的缓存策略,针对源码模块(我们自己写的代码)做了协商缓存处理,针对依赖模块(第三方库)做了强缓存处理,这样我们项目的访问的速度也就更快了。

知识链:webpack实现原理🔜ES module相关知识🔜http协议相关🔜websocket实现原理🔜JavaScript事件循环

路由懒加载

  1. 因为懒加载是对子模块(子组件)进行延后加载。如果子模块(子组件)不单独打包,而是和别的模块掺和在一起,那其他模块加载时就会将整个文件加载出来了,这样子模块(子组件)就被提前加载出来了。所以, 要实现懒加载,就得先将进行懒加载的子模块(子组件)分离出来。

    懒加载实现的前提:ES6的动态加载模块 - import()

    调用import()之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。 ---- 来自《webpack - 模块方法》的import()小节

  2. 小知识点:import()中的注释。 我们注意到,import()括号里面有一串注释,如:

    import(/* webpackChunkName: "con" */ './con.js')

    通过这个注释,再配合webpacj.config.jsoutput.chunkFilename,我们可以设置打包生成的文件(chunk)的名字。例如,上面例子的Webpack的配置:

    module.exports = {
        entry:'./src/main.js', //入口文件
        output: {
            path: path.resolve(__dirname, 'dist'),
            chunkFilename: '[name].bundle.js',
            filename: 'bundle.js',
        }
    }
  3. 无论使用函数声明还是函数表达式创建函数,函数被创建后并不会立即执行函数内部的代码,只有等到函数被调用之后,才执行内部的代码。 相信对于这个函数特性,大家都十分清楚地。看到这里,大家对于懒加载的实现可能已经有了思路。

    只要将需要进行懒加载的子模块文件(children chunk)的引入语句(本文特指import())放到一个函数内部。然后再需要加载的时候执行该函数。这样就可以实现懒加载(按需加载)。这也就是是懒加载(按需加载)的原理了

  4. webpack提供的require.ensure()实现懒加载:

    1:vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。
    2:这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
    3:require.ensure可实现按需加载资源,包括js,css等。他会给里面require的文件单独打包,不会和主文件打包在一起。
    4:第一个参数是数组,表明第二个参数里需要依赖的模块,这些会提前加载。
    5:第二个是回调函数,在这个回调函数里面require的文件会被单独打包成一个chunk,不会和主文件打包在一起,这样就生成了两个chunk,第一次加载时只加载主文件。
    6:第三个参数是错误回调。
    7:第四个参数是单独打包的chunk的文件名

路由级权限

简单角色动态路由

配置路由文件,定义路由信息
import Vue from "vue";
import VueRouter from "vue-router";
import store from "@/store";
Vue.use(VueRouter);
// 默认静态路由
const routes = [
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login/index.vue"),
  },
];
// 动态配置的路由(配置权限)
export const asyncRoutes = [
  {
    path: "/test1",
    name: "Test1",
    component: () => import("@/views/test1/index.vue"),
    meta: {
      roles: ["admin", "editor"], //  拥有这些角色权限的用户才能访问
      title: "测试1",
    },
    children: [
      {
        path: "children1",
        name: "Test1Children1",
        component: () => import("@/views/test1/test1-children1/index.vue"),
        meta: { roles: ["admin"], title: "测试1子路由1" },
      },
      {
        path: "children2",
        name: "Test1Children2",
        component: () => import("@/views/test1/test1-children2/index.vue"),
        meta: { roles: ["editor"], title: "测试1子路由2" },
      },
      {
        path: "children3",
        name: "Test1Children3",
        component: () => import("@/views/test1/test1-children3/index.vue"),
        meta: { roles: ["admin"], title: "测试1子路由3" },
      },
    ],
  },
  {
    path: "/test2",
    name: "Test2",
    component: () => import("@/views/test2/index.vue"),
    meta: { roles: ["admin"],  title: "测试2" },
    children: [],
  },
];
// 创建一个路由器实例
const createRouter = () =>
  new VueRouter({
    mode: 'hash',
    routes,
  });
// 创建路由器
const router = createRouter();
// 重置路由器
// 在Vue Router中,一旦路由器被创建并且路由被添加,你就不能再添加更多的路由。这可能会在某些情况下造成问题,例如,当你需要基于用户角色动态添加路由时。
export function resetRouter() {
  const newRouter = createRouter()
  // 重置router对象
  router.matcher = newRouter.matcher;
  // 提交用户路由的清除状态的mutation
  store.commit("user/CLEAR_ROUTERS");
}
export default router;
permission模块,集成封装根据权限过滤路由的vuex方法

写入generateRoutes方法,方便项目中全局调用,传入角色权限数组来进行路由过滤(一般是请求到的数组

根据 权限角色过滤动态配置路由 得到需要生成的路由信息

/src/store/modules/permission.js

import { asyncRoutes } from "@/router";
const state = {
  routes: [], // 动态配置的路由
};
const mutations = {
  SET_ROUTES: (state, routes) => {
    state.routes = routes;
  },
};
const actions = {
  /**
   * 生成路由
   * @param {Object} commit - 提交方法
   * @param {Array} roles - 用户角色权限数组
   * @returns {Promise} - 返回一个Promise对象,用于异步处理
   */
  generateRoutes({ commit }, roles) {
    return new Promise((resolve) => {
      let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); // 传入动态配置路由数组,角色权限数组
      console.log(accessedRoutes, 'accessedRoutes')
      commit("SET_ROUTES", accessedRoutes);
      resolve(accessedRoutes);
    });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

在action异步 generateRoutes 方法中调用 filterAsyncRoutes 方法对 asyncRoutes 数组进行过滤

filterAsyncRoutes 方法使用递归进行过滤直到没有children字段为止

/**
 * 使用 meta.role 确定当前用户是否具有权限
 * @param {Array} roles - 用户的角色列表
 * @param {Object} route - 路由对象
 * @returns {boolean} - 如果用户具有访问权限则返回true,否则返回false
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
      // 如果路由的roles属性包含用户的任何一个角色,那么some方法就会返回true,表示用户有权限访问这个路由。
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}
/**
 * 通过递归筛选异步路由表
 * @param routes - asyncRoutes 动态配置的路由
 * @param roles - 用户的角色列表
 */
export function filterAsyncRoutes(routes, roles) {
  // 初始化结果数组
  const res = []
  // 遍历路由数组
  routes.forEach(route => {
    // 创建临时变量,复制route对象
    const tmp = { ...route }
    // 判断是否有权限
    if (hasPermission(roles, tmp)) {
      // 如果有权限,继续遍历子路由
      if (tmp.children) {
        // 递归调用filterAsyncRoutes函数,过滤子路由
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      // 将符合条件的路由添加到结果数组中
      res.push(tmp)
    }
  })
  // 返回结果数组
  return res
}
user模块,模拟获取用户信息方法

创建vuex模块 user.js ,用于模拟接口获取用户角色信息并保存状态

/src/store/modules/user.js

const state = {
  userInfo: {}, // 用户信息
  roles: [], // 角色权限数组
};
const mutations = {
  SET_USERINFO(state, userInfo) {
    state.userInfo = userInfo;
  },
  SET_ROLES(state, roles) {
    state.roles = roles;
  },
};
const actions = {
  // 获取用户信息方法
  getInfo({ commit }) {
    return new Promise((resolve) => {
      const data = {
        name: "admin",
        avatar: "",
        roles: ["admin"],
        introduction: "I am a super administrator",
        status: 1,
        email: "",
        phone: "1234567890",
        id: 1,
        createTime: "2016-11-22 10:30:30",
        lastLoginTime: "2019-05-27 10:30:30",
        lastLoginIp: "127.0.0.1",
      };
      commit("SET_USERINFO", data); // 设置用户信息
      commit("SET_ROLES", data.roles); // 设置角色权限
      // 延时模拟异步
      setTimeout(() => {
        resolve(data);
      }, 500);
    });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};
创建全局路由守卫,控制路由权限并动态生成路由

定义好了上述的一系列方法,那么在什么时机调用,并且添加到路由中呢?

1 => 正确的时机应当是 用户已经登录并取得得用户信息后,再根据用户角色过滤路由,最后生成路由

2 => 当用户退出登录或者未登录时,清空用户信息以及路由,恢复为初始状态,跳转回登录页

使用 vue-router的全局路由守卫 定义一个路由权限文件 进行权限控制

/src/router/permission.js

import router, { resetRouter } from "@/router";
import store from "@/store";

const whiteList = ["/login"]; // 路由白名单,无需登录便可以访问的路由

router.beforeResolve(async (to, from, next) => {
  // 获取用户登录的token
  const hasToken = "xxxxxxxx"; // getToken() // 这里用虚拟值代替,表示已登录

  // 判断当前用户是否登录
  if (hasToken) {
    if (to.path === "/login") {
      // 如果当前用户已经登录,则跳转到首页
      next({ path: "/" });
    } else {
      // 从store user模块中获取用户权限角色,如果有角色则代表已经获取了用户信息登录中,直接放行
      const hasRoles = store.state.user.roles && store.state.user.roles.length > 0;
      if (hasRoles) {
        next();
      } else {
        // 获取了token,但是没有角色,则调用获取用户信息接口获取角色权限数组
        try {
          // 调用获取存储用户信息,并取得角色权限数组,例如 ['admin'] or ,['developer','editor']
           const { roles } = await store.dispatch("user/getInfo");
          // 获取用户角色权限数组后调用vuex的generateRoutes方法过滤掉没有权限的路由表并返回
          const accessRoutes = await store.dispatch("permission/generateRoutes", roles);
          // 使用addRoute将 动态配置的路由 添加到 layout 首页子级中
          router.addRoute({
            path: "/",
            name: "home",
            redirect: accessRoutes[0].path,
            component: () => import("@/views/layout"),
            meta: { title: "首页" }, // 路由元信息
            children: accessRoutes,
          });
          // 然后再添加404导航路由,防止刷新后默认导向404页
          router.addRoute({
            path: "/404",
            redirect: "/404",
            hidden: true,
            component: () => import("@/views/notFoundPage.vue"),
          });
          router.addRoute({ path: "*", redirect: "/404", hidden: true });
          // 设置replace:true,这样导航就不会留下历史记录
          next({ ...to, replace: true });
        } catch (error) {
          // 捕获错误异常退出登录清除用户信息
          resetRouter(); // 重置路由
          // await store.dispatch("user/resetToken");
          next(`/login?redirect=${to.path}`);
        }
      }
    }
  } else {
    // 用户未登录
    resetRouter(); // 重置路由
    if (whiteList.indexOf(to.path) !== -1) {
      // 需要跳转的路由是否是白名单whiteList中的路由,若是,则直接跳转
      next();
    } else {
      // 需要跳转的路由不是白名单whiteList中的路由,直接跳转到登录页
      next(`/login?redirect=${to.path}`);
    }
  }
});

为防止页面刷新时会重置路由后找不到对应的路由地址会导向404页,所以将404页放在等待接口返回用户信息获取完成,动态添加路由后再添加。将404页面的路由放在动态添加路由之后,那么在动态添加路由的过程中,用户正在访问的路由就有可能被添加进来,从而避免被重定向到404页面。因为一旦用户正在访问的路由被添加,那么它就不再是未定义的路由,就不会被404页面的路由匹配。

按钮级权限

Vue的自定义指令通过Vue.directive方法来创建:

  • bind:指令第一次绑定到元素时调用。在这里可以执行一次性的初始化设置。
  • inserted:被绑定元素插入父元素时调用。注意,父元素可能还未存在,所以不能进行DOM操作。
  • update:被绑定元素所在的组件更新时调用,但是可能发生在其子组件更新之前。可以比较更新前后的值,执行相应的操作。
  • componentUpdated:被绑定元素所在的组件及其子组件全部更新后调用。可以执行操作,例如更新DOM。
  • unbind:指令与元素解绑时调用。可以执行清理操作。
Vue.directive('my-directive', {
  bind(el, binding, vnode) {
    // 初始化设置
  },
  inserted(el, binding, vnode) {
    // 元素插入父元素时调用
  },
  update(el, binding, vnode) {
    // 组件更新时调用
  },
  componentUpdated(el, binding, vnode) {
    // 组件及子组件更新后调用
  },
  unbind(el, binding, vnode) {
    // 解绑时调用
  }
});

一般常见的方案有:

  • 将按钮封装成组件,然后通过 props 传递参数,来控制是否显示or可操作
  • 封装一个组件,通过插槽形式传递按钮,再通过prop传值控制
  • 写一个自定义指令,通过 value 控制元素是否显示

相比 v-if 的好处有以下几点:

  1. 可以轻松地扩展权限控制功能。
  2. 支持多个权限码、支持异步获取权限等。
  3. 语义化效果更加明显。
// 全局自定义指令
Vue.directive("permission", {
  // 在元素被插入到 DOM 中时触发
  inserted(el, binding) {
    // 如果绑定值为 false,则从父节点中移除元素
    if (!binding.value) {
      el.parentNode.removeChild(el); // 移除元素
    }
  },
  // 在元素更新时触发
  update(el, binding) {
    // 如果绑定值为 true
    if (binding.value) {
      // 如果元素没有父节点(即之前被移除了)
      if (!el.parentNode) {
        // 将元素插入到原来的位置
        el.__v_originalParent.insertBefore(el, el.__v_anchor || null);
      }
    } else {
      // 如果元素有父节点
      if (el.parentNode) {
        // 创建一个注释节点作为替换元素的占位符
        el.__v_anchor = document.createComment("");
        el.__v_originalParent = el.parentNode;
        // 用注释节点替换原来的元素
        el.parentNode.replaceChild(el.__v_anchor, el); 
      }
    }
  },
});

加入请求后的改进代码:

Vue.directive("permission", {
  async inserted(el, binding) {
    el.style.display = "none";
    try {
      const hasPermission = await checkPermission(binding.value);
      if (hasPermission) {
        el.style.display = "";
      } else if (el.parentNode) {
        el.parentNode.removeChild(el);
      }
    } catch (error) {
      console.error('Error checking permission:', error);
      if (el.parentNode) {
        el.parentNode.removeChild(el);
      }
    }
  },
  ...
});

图片懒加载

使用原生JavaScript实现

  1. HTML结构调整:在<img>标签的src属性中不直接设置图片地址,而是使用自定义属性(如data-src)来存储图片地址。
<img class="lazy-load" data-src="path/to/image.jpg" alt="示例图片">
  1. 编写JavaScript代码:监听滚动事件,检查图片是否进入可视区域,如果是,则将data-src的值赋给src属性,触发图片加载。
document.addEventListener("DOMContentLoaded", function() {
    var lazyImages = [].slice.call(document.querySelectorAll("img.lazy-load"));

    function lazyLoad() {
        lazyImages.forEach(function(img) {
            if (img.offsetTop < window.innerHeight + window.scrollY) { // offsetTop:表示元素的顶部边缘相对于其offsetParent元素顶部的距离(最近的具有定位的祖先元素,如果没有则为body)
                img.src = img.getAttribute("data-src");
                img.classList.remove("lazy-load");
            }
        });
    }

    lazyLoad();
    window.addEventListener("scroll", lazyLoad);
});

使用Intersection Observer API

Intersection Observer API提供了一种异步监听目标元素与其祖先元素或顶级文档viewport交叉状态的方式。当被观察的元素进入或离开视口时,会触发一个回调函数。

document.addEventListener("DOMContentLoaded", function() {
    var lazyImages = [].slice.call(document.querySelectorAll("img.lazy-load"));
    var imageObserver = new IntersectionObserver(function(entries, observer) {
        entries.forEach(function(entry) {
            if (entry.isIntersecting) {
                var image = entry.target;
                image.src = image.getAttribute("data-src");
                image.classList.remove("lazy-load");
                imageObserver.unobserve(image);
            }
        });
    });

    lazyImages.forEach(function(image) {
        imageObserver.observe(image);
    });
});

首屏加载时间

对首屏的理解

字节

首屏时间是指从用户打开网页到首屏内容完全显示出来的时间。这是一个非常重要的性能指标,因为它直接影响到用户的体验。

首屏时间包括以下几个阶段:

  1. DNS 解析时间:浏览器将网站的域名解析为 IP 地址的时间。
  2. TCP 连接时间:浏览器与服务器建立 TCP 连接的时间。
  3. HTTP 请求和响应时间:浏览器发送 HTTP 请求并接收到 HTTP 响应的时间。
  4. DOM 解析时间:浏览器解析 HTML 文档,构建 DOM 树的时间。
  5. CSSOM 构建时间:浏览器解析 CSS 文件,构建 CSSOM 树的时间。
  6. JavaScript 执行时间:浏览器执行 JavaScript 代码的时间。
  7. 渲染时间:浏览器将 DOM 树和 CSSOM 树合并为渲染树,计算布局,绘制页面的时间。

首屏时间的计算方法有很多种,不同的方法可能会得到不同的结果。一般来说,首屏时间应该包括上述所有阶段的时间。

获取首屏加载时间

在浏览器控制台中使用 performance.timing.loadEventEnd -performance.timing.navigationStart 来获取页面加载所需的总时间

  • performance.timing.loadEventEnd 表示浏览器加载完页面上的所有资源(如图像、CSS、JavaScript 等)的时间点。它表示浏览器完成了页面加载的所有工作,并且可以开始对页面进行渲染和交互。
  • performance.timing.navigationStart 表示浏览器开始加载页面的时间点。它表示浏览器接收到请求并开始加载页面的第一个字节的时间点。

gzip压缩

nginx 开启

在部署前端页面的nginx服务中,修改server项配置,以开启gzip压缩

server {
    listen       8080;
    server_name  ************;
 
    # 开启gzip
    gzip on;
    # 进行压缩的文件类型。
	gzip_types text/plain text/css application/javascript application/json;
	# 是否在http header中添加Vary: Accept-Encoding,建议开启
	gzip_vary on;
	# 允许使用预压缩的 .gz 文件
	gzip_static on;
	# 设置 Gzip 压缩级别
	gzip_comp_level 5;
	# 设置响应数据的最小长度以启用 Gzip 压缩
	gzip_min_length 256;
	# 指定启用压缩的代理列表
	gzip_proxied any;
	# 禁用 Gzip 压缩的浏览器 User-Agent 列表
	gzip_disable "msie6";
	# 配置内存缓冲区的数量和大小
	gzip_buffers 16 8k;
    
}
vue2 + vue-cli 项目中配置
vue3 + vite 项目中配置

安装 vite 压缩插件 vite-plugin-compression

npm i vite-plugin-compression --save-dev

vite.config.js 文件中添加以下配置:

import compression from 'vite-plugin-compression'

export default {
  plugins: [
    compression({
      algorithm: 'gzip', // 压缩算法,可选['gzip','brotliCompress','deflate','deflateRaw']
      threshold: 10240, // 如果体积大于10kb阈值,则进行压缩,参数单位为b
    }),
  ],
}

配置好之后,打开浏览器访问,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip 标识,表示浏览器支持并且启用了gzip压缩的资源

cdn

CDN(内容分发网络)是一种用于加速网络内容传输的技术和架构。它通过将内容分布到全球各地的服务器节点,使得用户可以从离他们更近的服务器上获取所需的内容,从而减少了网络延迟和提高了内容的传输速度。

CDN 的使用一般是通过将静态资源(如图片、样式表、脚本等)部署到 CDN 提供商的服务器上,并在网页中引用 CDN 上的资源链接。

使用 CDN 可以加速网站的加载速度,改善用户体验,并减轻服务器的负载压力。

异步加载方案

路由懒加载
按需异步加载组件

全局注册使用:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

局部注册使用:

components: {
    'my-component': () => import('./my-async-component')
}
依赖按需引入

图片压缩

在前端实现图片压缩,可以使用 HTML5 的 File API 和 Canvas API。以下是一个简单的步骤:

  1. 使用 File API 读取用户上传的图片文件。
  2. 使用 Image 对象加载图片。
  3. 创建一个 Canvas 对象,设置其大小为目标压缩大小。
  4. 使用 Canvas 的 drawImage 方法将图片绘制到 Canvas 上。
  5. 使用 Canvas 的 toDataURL 方法将 Canvas 转换为 data URL,这个 data URL 就是压缩后的图片。

以下是一个简单的实现:

function compressImage(file, maxWidth, maxHeight, quality = 0.8) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = URL.createObjectURL(file);
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      let width = img.width;
      let height = img.height;

      // 如果图片大于目标尺寸,进行等比例缩放
      if (width > height) {
        if (width > maxWidth) {
          height *= maxWidth / width;
          width = maxWidth;
        }
      } else {
        if (height > maxHeight) {
          width *= maxHeight / height;
          height = maxHeight;
        }
      }

      canvas.width = width;
      canvas.height = height;
      ctx.drawImage(img, 0, 0, width, height);

      // 将 canvas 转换为 data URL
      const dataUrl = canvas.toDataURL('image/jpeg', quality);
      resolve(dataUrl);
    };
    img.onerror = reject;
  });
}

白屏检测方案

web-see 前端监控方案,提供了 采样对比+白屏修正机制 的检测方案,兼容有骨架屏、无骨架屏这两种情况,来解决开发者的白屏之忧

import webSee from 'web-see';

Vue.use(webSee, {
  dsn: 'http://localhost:8083/reportData', // 上报的地址
  apikey: 'project1', // 项目唯一的id
  userId: '89757', // 用户id
  silentWhiteScreen: true, // 开启白屏检测
  skeletonProject: true, // 项目是否有骨架屏
  whiteBoxElements: ['html', 'body', '#app', '#root'] // 白屏检测的容器列表
});

白屏检测方案的实现流程

详见:前端白屏的检测方案,让你知道自己的页面白了 - 掘金 (juejin.cn)

技术方案调研

  • 检测根节点是否渲染

  • Mutation Observer监听DOM变化

  • 页面截图检测

  • 采样对比

    该方法是对页面取关键点,进行采样对比,在准确性、易用性等方面均表现良好,也是最终采用的方案

    对于有骨架屏的项目,通过对比前后获取的 dom 元素是否一致,来判断页面是否变化(这块后面专门讲解)

    采样对比代码:

    // 监听页面白屏
    function whiteScreen() {
      // 页面加载完毕
      function onload(callback) {
        if (document.readyState === 'complete') {
          callback();
        } else {
          window.addEventListener('load', callback);
        }
      }
      // 定义外层容器元素的集合
      let containerElements = ['html', 'body', '#app', '#root'];
      // 容器元素个数
      let emptyPoints = 0;
      // 选中dom的名称
      function getSelector(element) {
        if (element.id) {
          return "#" + element.id;
        } else if (element.className) {// div home => div.home
          return "." + element.className.split(' ').filter(item => !!item).join('.');
        } else {
          return element.nodeName.toLowerCase();
        }
      }
      // 是否为容器节点
      function isContainer(element) {
        let selector = getSelector(element);
        if (containerElements.indexOf(selector) != -1) {
          emptyPoints++;
        }
      }
      onload(() => {
        // 页面加载完毕初始化
        for (let i = 1; i <= 9; i++) {
          let xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2);
          let yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10);
          isContainer(xElements[0]);
          // 中心点只计算一次
          if (i != 5) {
            isContainer(yElements[0]);
          }
        }
        // 17个点都是容器节点算作白屏
        if (emptyPoints == 17) {
          // 获取白屏信息
          console.log({
            status: 'error'
          });
        }
      }
    }

白屏修正机制

若首次检测页面为白屏后,任务还没有完成,特别是手机端的项目,有可能是用户网络环境不好,关键的JS资源或接口请求还没有返回,导致的页面白屏

需要使用轮询检测,来确保白屏检测结果的正确性,直到页面的正常渲染,这就是白屏修正机制

轮询代码:

// 采样对比
function sampling() {
  let emptyPoints = 0;
  ……
  // 页面正常渲染,停止轮询
  if (emptyPoints != 17) {
    if (window.whiteLoopTimer) {
      clearTimeout(window.whiteLoopTimer)
      window.whiteLoopTimer = null
    }
  } else {
    // 开启轮询
    if (!window.whiteLoopTimer) {
      whiteLoop()
    }
  }
  // 通过轮询不断修改之前的检测结果,直到页面正常渲染
  console.log({
    status: emptyPoints == 17 ? 'error' : 'ok'
  });
}
// 白屏轮询
function whiteLoop() {
  window.whiteLoopTimer = setInterval(() => {
    sampling()
  }, 1000)
}

Vue性能优化之虚拟列表

需要解决那些问题?

  • 容器的高度(列表的高度)
  • 列表项的高度
  • 可视区域展示多少条数据
  • 可视区域展示哪部分数据
  • 让可视区域可以一直滚动

逐个分析上面的问题

  • 容器的高度(列表的高度):这个可以自己设置一个高度

  • 列表项的高度,也可以自己设置一个高度

  • 可视区域展示多少条数据:可以通过计算得到,公式为:容器的高度/列表项的高度

  • 可视区域展示哪部分数据:我们可以通过设置开始下标结束下标,来截取数据

    假设初始时,开始下标是0,那结束下标就是 开始下标+可视区域的条数结束下标是随开始下标可视区域的条数变化而变化的;然后随着滚动,开始下标要发生变化,只要更新开始下标,结束下标就能计算出来,那我们怎么去更新开始下标呢?开始下标怎么计算?

    如果我们知道被滚动条卷进去了多少个列表项,那就能知道现在的开始下标是多少了;我们知道,列表项的高度,如果能再知道滚动条卷进去了多少高度,用卷进去的高度/列表项高度就可以得到卷进去多少个了;scrollTop属性刚好可以得到滚动条卷入的高度

  • 让可视区域可以一直滚动:我们是通过滚动去改变可视区域的数据,而不是增加数据,所以我们需要想办法让可视区域可以滚动,直到没有数据了才停止滚动;可以padding进行占位,比如我们有1000条数据,每条数据占40px高度,可视区域显示20条数据,那我们初始的时候,padding-top为0,padding-bottom为(1000-20)*40;随着滚动,上下padding也会发生变化,当padding-top为0代表在滚动到顶部了,padding-bottom为0代表滚动到底部了。pading-top随着开始下标变大而变大(开始下标乘以40),开始下标为0,则padding-top为0;padding-bottom随着结束下标变大而减小((数据总条数-结束下标)乘以40),数据总条数等于结束下标,则padding-bottom为0;也就是让可视区域随着滚动条一起移动

代码实现

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';

const containHeight = ref<number>(800)  // 容器高度
const listContainerRef = ref<HTMLElement>() // 容器引用
const listRef = ref<HTMLElement>() // 列表引用

const itemHeight = ref<number>(40) // 列表项高度

const dataList = reactive<Array<string>>([]) // 所有数据
let startIndex = ref<number>(0) // 开始下标

const containHeightpx = computed<string>(() => { //容器高度,用于样式
  return containHeight.value + 'px'
})

const itemHeightpx = computed<string>(() => { //列表项高度,用于样式
  return itemHeight.value + 'px'
})

// 可视区域数量
const showNum = computed(() => {
  return ~~(containHeight.value / itemHeight.value)  // ~~转换成数字类型,有向下取整的妙用
})

// 结束下标 = 开始下标 + 可视区域数量
const endIndex = computed(() => {
  return startIndex.value + showNum.value
})

// 展示的列表
const showList = computed(() => {
  return dataList.slice(startIndex.value, endIndex.value)
})

// 列表的padding
const listStyle = computed(() => {
  return {
    paddingTop: startIndex.value * itemHeight.value + 'px',
    paddingBottom: (dataList.length - endIndex.value) * itemHeight.value + 'px'
  }
})

// 初始化加载数据
onMounted(() => {
  for (let index = 1; index <= 1000; index++) {
    dataList.push(`列表项---${index}`)
  }
})

// 监听滚动条的变化
const listContainerScroll = () => {
  console.log(startIndex.value, endIndex.value);

  // 获取滚动条卷入的高度
  let scrollTop = listContainerRef.value!.scrollTop

  // 更新开始下标
  startIndex.value = Math.floor(scrollTop / itemHeight.value);

  // 剩下的会通过计算属性自动变化

}

</script>

<template>
  <div ref="listContainerRef" class="listContainerClass" @scroll="listContainerScroll">
    <div ref="listRef" :style="listStyle">
      <div v-for="(item, index) in showList" :key="index" class="itemClass">
        {{ item }}
      </div>
    </div>
    <div>没有更多了....</div>
  </div>
</template>

<style lang="scss" scoped>
.listContainerClass {
  height: v-bind(containHeightpx);
  overflow: auto;
  border: 1px solid black;

  .itemClass {
    height: v-bind(itemHeightpx);
  }
}
</style>

封装优化一下

  • 把它变成一个公用组件,可以通过传值的方式使用
  • 给滚动事件加上防抖功能(加了防抖,所以渲染的数量增大点,防止出现空白)
<script setup lang="ts">


import { debounce } from 'lodash';
import { computed, PropType, ref } from 'vue';

const props = defineProps({
  // 容器高度
  containHeight: {
    type: Number,
    default: 800
  },
  // 列表项高度
  itemHeight: {
    type: Number,
    default: 40
  },

  dataList: {
    type: Object as PropType<any>,
    default: () => []
  }

})


const listContainerRef = ref<HTMLElement>() // 容器引用
const listRef = ref<HTMLElement>() // 列表引用

let startIndex = ref<number>(0) // 开始下标

const containHeightpx = computed<string>(() => { //容器高度,用于样式
  return props.containHeight + 'px'
})

const itemHeightpx = computed<string>(() => { //列表项高度,用于样式
  return props.itemHeight + 'px'
})

// 可视区域数量
const showNum = computed(() => {
  return ~~(props.containHeight / props.itemHeight) * 2  // 由于加了防抖,所以渲染的数量增大点,防止出现空白
})

// 结束下标 = 开始下标 + 可视区域数量
const endIndex = computed(() => {
  return startIndex.value + showNum.value
})

// 展示的列表
const showList = computed(() => {
  return props.dataList.slice(startIndex.value, endIndex.value)
})

// 列表的padding
const listStyle = computed(() => {
  return {
    paddingTop: startIndex.value * props.itemHeight + 'px',
    paddingBottom: (props.dataList.length - endIndex.value) * props.itemHeight + 'px'
  }
})


// 监听滚动条的变化
const scrollEvent = () => {

  console.log(startIndex.value, endIndex.value);

  // 获取滚动条卷入的高度
  let scrollTop = listContainerRef.value!.scrollTop

  // 更新开始下标
  startIndex.value = Math.floor(scrollTop / props.itemHeight);

  // 剩下的会通过计算属性自动变化

}


const listContainerScroll = debounce(scrollEvent, 20)

</script>

<template>
  <div ref="listContainerRef" class="listContainerClass" @scroll="listContainerScroll">
    <div ref="listRef" :style="listStyle">
      <div v-for="(item, index) in showList" :key="index" class="itemClass">
        <slot :item="item"></slot>
      </div>
    </div>
    <div>没有更多了....</div>
  </div>
</template>

<style lang="scss" scoped>
.listContainerClass {
  height: v-bind(containHeightpx);
  overflow: auto;
  border: 1px solid black;

  .itemClass {
    height: v-bind(itemHeightpx);
  }
}
</style>
<script setup lang="ts">
import VirtualList from '@/components/virtualList/index.vue'
import { onMounted, reactive } from 'vue';

const dataList = reactive<any>([])

onMounted(() => {
  for (let index = 0; index < 1000; index++) {
    dataList.push({
      id: index,
      title: '列表项'
    })

  }
})
</script>

<template>
  <VirtualList :dataList="dataList" :containHeight="600" :itemHeight="30">
    <template #default="{ item }">
      {{ item.title }}-{{ item.id }}
    </template>
  </VirtualList>
</template>

<style lang="scss" scoped>

</style>

登录鉴权相关

答案:Node.js中的登录鉴权 | QT-7274 (qblog.top)

什么是cookie

字节

提示:

  • HTTP无状态:维护一个状态用来告知服务端前后两个请求是否来自同一浏览器
  • cookie存储在客户端
  • cookie不可跨域
  • 属性值都有哪些

cookie的问题

提示:

  • 因为存储在客户端,使用前需要验证合法性
  • 能存储的容量有限
  • 无法跨域
  • 移动端对cookie支持不是很好,session需要基于cookie实现

什么是session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了session是一种特殊的cookie

cookie和session的联系

session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

cookie和session的区别

  • 安全性SessionCookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是Token

JWT的优势

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT: 将 TokenPayload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

对比传统的session认证方式,JWT的优势是:

  • 简洁:JWT Token数据量小,传输速度也很快
  • 因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
  • 不需要在服务端保存会话信息,也就是说不依赖于cookiesession,所以没有了传统session认证的弊端,特别适用于分布式微服务
  • 单点登录友好:使用session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题
  • 适合移动端应用:使用session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到cookie(需要 cookie 保存 sessionId),所以不适合移动端

setTimeout、setInterval、requestAnimationFrame

setTimout

运行机制

执行该语句时,立即把当时定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值将传入的函数加入任务队列,之后的执行就交给任务队列负责。

如果此时的任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间。

使用SetTimeout进行优化

详见本文——优化相关章节中的【一次加载大量DOM】

setTimeout的定义和用法

  • 定义:用于在指定的毫秒数调用函数或计算表达式。
  • 参数:
    • 第一个参数function,必填的,回调函数,可以是一个函数,也可以是一个函数名。
    • 第二个参数delay,可选的,延迟时间,单位是ms。
    • 第三个参数param1,param2,param3...,可选的,是传递给回调函数的参数,比较不常用到,在IE9 及其更早版本不支持该参数。
  • 返回值:返回一个 ID(数字),可以将这个ID传递给clearTimeout()来取消执行。

setTimeout的最短延迟时间

第二个参数delay未设置的时候,默认为0,意味着“马上”执行,或者尽快执行。

但是有一个规定如下

If timeout is less than 0, then set timeout to 0. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

上面的意思是说,如果延迟时间短于0,则将延迟时间设置为0。如果嵌套级别大于5,延迟时间短于4ms,则将延迟时间设置为4ms。

还有另外一种情况。为了节电,对于那些不处于当前窗口的页面,浏览器会将最短延时限制扩大到1000ms。

setInterval

setInterval的定义和用法

  • 定义:可按照指定的周期(以毫秒计)来调用函数或计算表达式。
  • 参数:
    • 第一个参数function,必填的,回调函数,可以是一个函数,也可以是一个函数名。
    • 第二个参数delay,可选的,间隔时间,单位是ms。
    • 第三个参数param1,param2,param3...,可选的,是传递给回调函数的参数,比较不常用到,在IE9 及其更早版本不支持该参数。
  • 返回值:返回一个 ID(数字),可以将这个ID传递给clearInterval()来取消执行。

setInterval的最短间隔时间

在John Resig的新书《Javascript忍者的秘密》一书中提到

Browsers all have a 10ms minimum delay on OSX and a(approximately) 15ms delay on Windows.

在苹果机上的最短间隔时间是10毫秒,在Windows系统上的最短间隔时间大约是15毫秒。

大多数电脑显示器的刷新频率是60HZ,大概相当于每秒钟重绘60次。因此,最平滑的动画效的最佳循环间隔是1000ms/60,约等于16.6ms。

综上所述,我认为setInterval的最短间隔时间应该为16.6ms。

setInterval的间隔时间设置问题

setInterval的间隔时间一定要比回调函数的执行时间大

setInterval函数的工作方式是,每隔指定的间隔时间,就将回调函数添加到任务队列中。如果回调函数的执行时间大于这个间隔时间,那么回调函数的执行可能会和下一次的调度重叠。

这是因为JavaScript是单线程的,一次只能执行一个任务。如果回调函数的执行时间过长,那么在它执行结束之前,下一个间隔时间可能已经到达,此时新的回调函数就会被添加到任务队列中。如果前一个回调函数还没有执行完,那么新的回调函数就必须等待,这就可能导致实际的间隔时间小于预期。

但是在很多情况下,我们并不能清晰的知道回调函数的执行时间,为了能按照一定的间隔周期性的触发定时器,可以用以下方法实现:

setTimeout(function handlerInterval(){
    // do something
    setTimeout(handlerInterval,100); 
    // 执行完处理程序的内容后,在末尾再间隔100毫秒来调用该程序,这样就能保证一定是100毫秒的周期调用
},100)

setTimeout为什么不准时

  1. 如果嵌套级别大于5,延迟时间短于4ms,则将延迟时间设置为4ms。

  2. 对于那些不处于当前窗口的页面,浏览器会将最短延时限制扩大到1000ms。

  3. setTimeout 在 JavaScript 中并不是完全准确的。这是因为 JavaScript 是单线程的,setTimeout 只是将回调函数放入任务队列等待执行,而实际执行时间取决于当前执行栈中的任务何时完成。

    这使得 requestAnimationFrame 在动画场景下比 setTimeout 更准确,因为它能确保在每一帧刷新时都执行,而 setTimeout 可能会因为 JavaScript 的事件循环或其他原因而有所延迟。

    然而,requestAnimationFrame 并不能用来替代 setTimeout 来实现一个准确的定时器,因为它的执行频率是固定的,并且当标签页不在前台时,requestAnimationFrame 的运行会被暂停。

    function _timerSetInterval(fn, delay, ...args) {
        let current = Date.now();
        let timer = null;
    
        const task = () => {
            current += delay;
            timer = setTimeout(() => {
                fn.apply(this, args);
                task();
            }, Math.max(0, current - Date.now()));
        };
    
        task();
        return () => clearTimeout(timer);
    }
    
    _timerSetInterval(() => {console.log(new Date())}, 1000);

requestAnimationFrame

requestAnimationFrame()是一个浏览器提供的API,用于在下一次重绘之前执行一段代码。这通常发生在屏幕刷新的时候,也就是每秒60次。这个API主要用于动画效果的实现,因为它可以确保回调函数在屏幕刷新的每一帧中都被执行一次,从而实现流畅的动画效果。

以往的JS动画通常是用setTimeout或setInterval定时器来实现,这种实现方法存在的问题,一是上面提到的“掉帧”,二是无法掌握回调函数的执行时机,三是系统性能的浪费,当页面转为后台运行时并不会自动停止。

当然,requestAnimationFrame并非完美,因为是在主线程上执行的,当主线程非常繁忙时,requestAnimationFrame的效果就大打折扣。

requestAnimationFrame和 $nextTick()的区别

$nextTick()的回调函数会在当前宏任务结束后,下一个宏任务开始前执行,也就是在微任务队列完成后立即执行。这是因为$nextTick()的内部实现使用了微任务队列。

requestAnimationFrame()的回调函数会在下一次重绘之前执行,这通常在下一个宏任务开始之前,但在所有微任务完成之后。然而,这并不意味着它会立即在微任务队列完成后执行,因为浏览器还需要处理其他的任务,如渲染和垃圾回收,这可能会导致requestAnimationFrame()的回调函数延迟执行。

因此,如果你在同一个宏任务中调用了$nextTick()requestAnimationFrame(),那么$nextTick()的回调函数会先执行,然后是requestAnimationFrame()的回调函数。

浏览器相关

HTTP和HTTPS

腾讯

提示:

  • 概念:是....通过....并使用...使得...
  • 区别:优缺点
  • https的工作原理:建立ssl-传输(公钥)-协商-加密-解密-通信

答案:

相关拓展:

  • 对称加密和非对称加密
  • HTTP请求过程

HTTP请求过程

腾讯

对称加密和非对称加密

腾讯

提示:

  • 对称加密:对称加密使用同一个密钥进行加密和解密。这种加密方式的优点是加密和解密的速度快,适合于大量数据的加密;缺点是密钥的分发和管理比较困难,不适合于大规模的网络通信安全。
  • 非对称加密:非对称加密使用一对密钥,一个是公钥,另一个是私钥。公钥是公开的,任何人都可以使用公钥进行加密,但只有对应的私钥才能进行解密。这种加密方式的优点是安全性高,密钥的管理也相对容易;缺点是加密和解密的速度慢,不适合于大量数据的加密。

常见状态码

腾讯

答案:

TCP和UDP的区别和应用场景

答案:面试官:如何理解UDP 和 TCP? 区别? 应用场景? | web前端面试 - 面试官系列 (vue3js.cn)

如何理解OSI七层模型

答案:面试官:如何理解OSI七层模型? | web前端面试 - 面试官系列 (vue3js.cn)

DNS协议 是什么?说说DNS 完整的查询过程?

答案:面试官:DNS协议 是什么?说说DNS 完整的查询过程? | web前端面试 - 面试官系列 (vue3js.cn)

如何理解CDN?说说实现原理?

答案:面试官:如何理解CDN?说说实现原理? | web前端面试 - 面试官系列 (vue3js.cn)

GET和POST的区别

答案:面试官:说一下 GET 和 POST 的区别? | web前端面试 - 面试官系列 (vue3js.cn)

HTTP请求头及其作用

答案:面试官:说说 HTTP 常见的请求头有哪些? 作用? | web前端面试 - 面试官系列 (vue3js.cn)

跨域

腾讯、字节

提示:

  • 概念:什么是跨域?即什么是同源?(三点:协议|主机|端口)
  • 三种方法:JSONP|CORS|Proxy
  • CORS的服务器端设置:Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-Methods
  • Proxy(网络代理)的客户端设置(Vue常用):代理标识,targetchangeOriginpathRewrite

知识链:JSONP的实现

答案:面试官:Vue项目中你是如何解决跨域的呢? | web前端面试 - 面试官系列 (vue3js.cn)

Cookie、sessionStorage、localStorage 的区别

答案:浏览器相关细节题(1) | QT-7274 (qblog.top)

在地址栏里输入一个 URL,到这个页面呈现出来,中间会发生什么?

提示:

  • 查找缓存中的ip - 构造http请求 - 返回页面 - 参考答案1
  • 根据HTML和CSS构造树并合并 - 布局和渲染 - 并行(缓存)下载资源 - 绘制 - 参考答案2

答案:

相关拓展:

  • 浏览器缓存
  • 浏览器中的重绘和重排
  • 浏览器是如何渲染页面的
  • 什么是reflow
  • 什么是repaint
  • 为什么transform效率高

后五个问题答案:浏览器相关细节题(3) | QT-7274 (qblog.top)

重绘和重排

减少页面的重绘和重排

  • 尽量减少使用 CSS 属性的快捷方式:例如,使用 border-width、border-style 和 border-color 而不是 border。CSS 属性的快捷方式会将所有值初始化为“初始值”,因此避免使用它们可以最小化重绘和回流(在实际工作中,CSS 快捷方式的性能影响微乎其微,并且使用快捷方式可以简化样式并解决一些样式覆盖问题)。
  • 在 GPU 上渲染动画:浏览器已经优化了 CSS 动画,使其适用于触发动画属性的重绘(因此也包括回流)。为了提高性能,将具有动画效果的元素移动到 GPU 上。可以触发 GPU 硬件加速的 CSS 属性包括 transform、filter、will-change 和 position:fixed。动画将在 GPU 上处理,提高性能,特别是在移动设备上(但避免过度使用,因为可能会导致性能问题)。
  • 使用 will-change CSS 属性来提高性能:它通知浏览器元素需要修改的属性,使浏览器能够在实际更改之前进行优化(但避免过度使用 will-change;在动画中遇到性能问题时考虑使用它)。
  • 通过更改 className 批量修改元素样式。
  • 将复杂的动画元素定位为 fixed 或 absolute 以防止回流。
  • 避免使用表格布局:因为在表格元素上触发回流会导致其中所有其他元素的回流。
  • 使用 translate 而不是修改 top 属性来上下移动 DOM 元素。
  • 创建多个 DOM 节点时,使用 DocumentFragment 进行一次性创建。
  • 必要时为元素定义高度或最小高度:没有显式高度,动态内容加载可能会导致页面元素移动或跳动,从而导致回流(例如,为图像定义宽度和高度以防止布局变化并减少回流)。
  • 尽量减少深度嵌套或复杂选择器的使用,以提高 CSS 渲染效率。
  • 对元素进行重大样式更改时,暂时使用 display:none 隐藏它们,进行更改,然后将它们设置回 display:block。这样可以最小化回流,只需两次即可。
  • 使用 contain CSS 属性将元素及其内容与文档流隔离,防止其边界框外意外副作用的发生。

答案:浏览器中的重绘和重排 | QT-7274 (qblog.top)

什么是reflow

提示:

  • 本质
  • 合并
  • 异步还是同步?
  • 布局抖动

答案:浏览器相关细节题(3) | QT-7274 (qblog.top)

Web攻击方式

腾讯

提示:

  • 全称:XSS,CSRF,SQL注入

  • XSS:

    • 攻击目标:盗取敏感信息

    • 攻击类型:存储型(数据库中),反射型(URL中),DOM型(由浏览器端取出)

    • 预防措施:

      • 输入检查:为了防止潜在的安全威胁,我们需要对输入内容进行转义或过滤,特别是像 script<iframe> 这样的标签。

      • 设置 httpOnly:大部分 XSS 攻击的目标是窃取用户的 cookie 以伪造身份认证。通过设置 httpOnly 属性,我们可以防止脚本获取用户的 cookie。

        当设置了 httpOnly 标志后,这个 Cookie 就不能通过客户端的 JavaScript 来访问了。这意味着,即使攻击者能够通过 XSS 攻击在用户的浏览器中执行恶意脚本,他们也无法读取到标记为 httpOnly 的 Cookie。这样就可以防止攻击者通过 XSS 攻击窃取用户的 Cookie 信息。

      • 开启 CSP:CSP,也就是内容安全策略,可以帮助我们阻止非白名单资源的加载和运行。通过开启 CSP,我们可以进一步提高网站的安全性。

        CSP 通过指定哪些内容是可信的,可以限制网页中可以加载和执行的内容。例如,你可以指定只允许加载和执行来自特定源的脚本,或者禁止执行内联脚本。

        CSP 通过 HTTP 的 Content-Security-Policy 响应头来设置。例如,以下的 CSP 设置只允许从当前源加载脚本:

        Content-Security-Policy: script-src 'self'

        如果网页中有试图加载或执行不符合 CSP 的内容,浏览器会阻止这个操作,并在控制台中报告错误。

  • CSRF:

    • 攻击目标:冒充用户对被攻击的网站执行操作
    • 攻击流程:登录a网站 - 进入b网站 - 冒充用户向a网站发起请求 - a网站误认为是用户请求 - 执行自定义操作
    • 攻击特点:发起位置 - 不是直接窃取 - 冒用而不是获取 - 各种方式
    • 预防措施:
      • 服务端添加 X-Frame-Options 响应头:这是一个 HTTP 响应头,主要用于防御通过 <iframe> 嵌套进行的点击劫持攻击。设置了这个响应头后,浏览器会阻止非法嵌入的网页渲染。
      • 使用 JavaScript 判断顶层视口的域名:我们可以通过比较 top.location.hostnameself.location.hostname 来判断顶层视口的域名是否与当前页面的域名一致。如果不一致,我们就不允许进行操作。
      • 对敏感操作使用更复杂的步骤:对于一些敏感的操作,我们可以通过增加操作的复杂性来提高安全性,例如,使用验证码,或者在删除项目之前需要输入项目名称等。

答案:面试官:web常见的攻击方式有哪些?如何防御? | web前端面试 - 面试官系列 (vue3js.cn)

相关拓展:

  • 同源策略
  • 鉴权

浏览器缓存

腾讯、滴滴

提示:

  • 强缓存:Expires Cache-Control
  • 协商缓存:Etag If-None-Match
  • 为什么还需要 Etag

答案:浏览器缓存详解 | QT-7274 (qblog.top)

四次挥手

字节

四次挥手是 TCP(传输控制协议)用来终止一个已经被建立的连接的过程。它包括以下四个步骤:

  1. 第一次挥手:客户端发送一个 FIN 包给服务器,表示它已经完成了数据的发送,要关闭连接。此时,客户端进入 FIN-WAIT-1 状态。
  2. 第二次挥手:服务器收到 FIN 包后,会发送一个 ACK 包给客户端,表示它知道客户端要关闭连接了。此时,服务器进入 CLOSE-WAIT 状态,客户端收到 ACK 包后,进入 FIN-WAIT-2 状态。
  3. 第三次挥手:服务器完成最后的数据发送后,也会发送一个 FIN 包给客户端,告诉客户端,我也没有数据要发送了,准备关闭连接。此时,服务器进入 LAST-ACK 状态。
  4. 第四次挥手:客户端收到 FIN 包后,也会发送一个 ACK 包给服务器,然后进入 TIME-WAIT 状态,等待一段时间后,如果没有收到服务器的再次请求,就关闭连接。服务器收到 ACK 包后,就关闭连接。

这个过程被称为四次挥手,是因为在这个过程中,有四次重要的数据包的发送:两次 FIN 包的发送和两次 ACK 包的发送。

三次握手

提示:

  • 三次握手的过程:
  • 每一次握手的作用:接收能力/发送能力
  • 为什么不是两次握手?

答案:面试官:说说TCP为什么需要三次握手和四次挥手? | web前端面试 - 面试官系列 (vue3js.cn)

Content-Type参数

HTTP协议头中的Content-Type参数用于指示请求或响应中携带的实体正文(body)的MIME类型(Multipurpose Internet Mail Extensions)。它告诉客户端或服务器如何解释正文的内容。

Content-Type参数值的MIME类型通常以type/subtype的形式表示,其中type表示主类型(Top-Level Type),subtype表示子类型(Subtype)。type是广义的数据类型,而subtype则更具体地描述了数据的类型。

以下是Content-Type参数的一些常见取值及其含义:

  1. text/plain:纯文本,没有指定任何特定的格式。
  2. text/html:HTML格式的文档,用于网页内容。
  3. text/css:Cascading Style Sheets (CSS) 文件。
  4. application/json:JSON格式的数据。
  5. application/xml:XML格式的数据。
  6. application/octet-stream:未知的二进制数据,没有指定特定的格式。
  7. multipart/form-data:用于HTML表单上传文件等多部分数据。
  8. image/jpeg, image/png, image/gif:JPEG、PNG、GIF等图片格式。
  9. audio/mpeg, audio/wav:MPEG、WAV等音频格式。
  10. video/mp4, video/mpeg:MP4、MPEG等视频格式。

通常情况下,HTTP协议头中的Content-Type参数值可以包括一个可选的字符集编码部分,用于指定正文的字符集。如果未指定字符集编码,则使用默认的字符集编码。

默认情况下,如果未指定字符集编码,则根据MIME类型的约定使用一些常见的默认字符集编码:

  • 对于text/*类型的文本数据,默认字符集编码是ISO-8859-1(也称为Latin-1)。
  • 对于application/*类型的数据,默认字符集编码是没有定义的,因为这种类型通常包含二进制数据或者不依赖于字符集编码。
  • 对于其他类型,也可能有默认的字符集编码规则。

如果需要明确指定字符集编码,可以在Content-Type参数值中使用; charset=语法

由于参数设置的问题经常会出现错误,导致调用失败,最常见的就是由于Content-Type参数值设置不准确导致了415错误。

大文件上传如何做断点续传

腾讯

提示:

  • 实现方式:服务器端返回,告知从哪里返回;浏览器端自行处理
  • 如果中途上传中断过怎么办?(以及其他多个问题)
  • 实现思路

答案:大文件断点续传问题解决方案 | QT-7274 (qblog.top)

Webpack系列

答案:面试官:说说你对webpack的理解?解决了什么问题? | web前端面试 - 面试官系列 (vue3js.cn)

axios的封装与原理

答案:

浏览器的存储

答案:浏览器相关细节题(1) | QT-7274 (qblog.top)

Nginx原理及相关

  • 正向代理:它代理的是客户端,是一个位于客户端和原始服务器(Origin Server)之间的服务器,为了从原始服务器获取内容,客户端向代理发送一个请求并指定目标(原始服务器)。

    正向代理最大的特点是客户端非常明确要访问的服务器地址;服务器只清楚请求来自哪个代理服务器,而不清楚来自哪个具体的客户端。

  • 反向代理:它代理的是服务端,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。

    反向代理的作用:

    • 保证内网的安全,通常将反向代理作为公网访问地址,Web 服务器是内网。

    • 负载均衡,通过反向代理服务器来优化网站的负载。

  • 负载均衡:

    Nginx 支持的负载均衡调度算法方式如下:

    weight 轮询(默认):接收到的请求按照顺序逐一分配到不同的后端服务器,即使在使用过程中,某一台后端服务器宕机,Nginx 会自动将该服务器剔除出队列,请求受理情况不会受到任何影响。这种方式下,可以给不同的后端服务器设置一个权重值(weight),用于调整不同的服务器上请求的分配率。

    权重数据越大,被分配到请求的几率越大;该权重值,主要是针对实际工作环境中不同的后端服务器硬件配置进行调整的。

    ip_hash:每个请求按照发起客户端的 ip 的 hash 结果进行匹配,这样的算法下一个固定 ip 地址的客户端总会访问到同一个后端服务器,这也在一定程度上解决了集群部署环境下 Session 共享的问题。

    fair:智能调整调度算法,动态的根据后端服务器的请求处理到响应的时间进行均衡分配。

    响应时间短处理效率高的服务器分配到请求的概率高,响应时间长处理效率低的服务器分配到的请求少,它是结合了前两者的优点的一种调度算法。但是需要注意的是 Nginx 默认不支持 fair 算法,如果要使用这种调度算法,请安装 upstream_fair 模块。

    url_hash:按照访问的 URL 的 hash 结果分配请求,每个请求的 URL 会指向后端固定的某个服务器,可以在 Nginx 作为静态服务器的情况下提高缓存效率。注意 Nginx 默认不支持这种调度算法,要使用的话需要安装 Nginx 的 hash 软件包。

答案:

JavaScript相关

for...offor...in

for...offor...in都是JavaScript中的循环结构,但它们的用途和行为是不同的。

for...of循环用于遍历可迭代的对象(如数组,字符串,Map,Set等)的元素。它会按照可迭代对象的顺序遍历元素。

let arr = [1, 2, 3];

for (let value of arr) {

 console.log(value); *// 输出 1, 2, 3*

}

for...in循环用于遍历对象的可枚举属性。它会按照对象属性的顺序遍历属性名。

let obj = {a: 1, b: 2, c: 3};

for (let key in obj) {

 console.log(key); *// 输出 'a', 'b', 'c'*

}

需要注意的是,for...in会遍历对象的原型链,所以它可能会遍历到你不期望的属性。如果你只想遍历对象自身的属性,你可以使用Object.prototype.hasOwnProperty()方法进行检查。

let obj = {a: 1, b: 2, c: 3};

for (let key in obj) {
 if (obj.hasOwnProperty(key)) {
  console.log(key); *// 输出 'a', 'b', 'c'*
 }

}

总的来说,for...offor...in都是遍历结构,但for...of主要用于遍历可迭代的对象,而for...in主要用于遍历对象的可枚举属性。

var let const,const声明的对象可以修改属性吗?

varletconst都是JavaScript中的变量声明关键字,但它们的行为是不同的。

var是最早的变量声明关键字,它没有块级作用域的概念,只有函数级作用域。

letconst是ES6引入的新的变量声明关键字,它们都有块级作用域的概念。

const用于声明常量,一旦声明,其值就不能改变。但是,如果const声明的是一个对象,你可以修改这个对象的属性。这是因为const只保证变量的引用不变,而不保证对象的内容不变。

new操作符的工作

  1. 创建一个新的空对象。
  2. 将新对象的原型链指向构造函数的原型对象。
  3. 将构造函数的 this 指向新对象,并执行构造函数。
  4. 如果构造函数没有显式返回一个对象,则返回新对象。

答案:JavaScript中new操作符详解 | QT-7274 (qblog.top)

script的async和defer属性

字节

  • defer属性用于异步加载外部JavaScript文件,当异步加载完成后,该外部JavaScript文件不会立即执行,而是等到整个HTML文档加载完成才会执行。
  • async属性用于异步加载外部JavaScript文件,当异步加载完成后,该外部JavaScript文件会立即执行,即使整个HTML文档还没有加载完成。

数据类型判断

字节

提示:

  • typeof 的返回值,存在的缺点(注意null的特殊性
  • instanceof 的返回值,存在的缺点
  • 通用方法

答案:

为什么Object.prototype.toString方法使用时要使用call方法

在上面的执行步骤中特别强调了this,当直接使用Object.prototype.toString方法时,this的值是Object本身,执行结果也就是"[object Object]"字符串。

而当使用call方法时,就修改了Object.prototype.toString方法的this值,此时this的值就是call方法的参数。

数据相同判断

  1. 相等比较(==):

    1. 如果两个操作数类型相同,执行严格比较。
    2. 如果两个操作数类型不同,则进行类型转换后再进行比较:
      • 如果一个操作数是数值(number),另一个操作数是字符串,则将字符串转换为数值,然后进行比较
      • 如果一个操作数是布尔值,则将布尔值转换为数值,然后进行比较
      • 如果一个操作数是对象,另一个操作数是数值或字符串,则将对象转换为原始值,然后进行比较
        • 会首先调用对象的 valueOf() 方法,将对象转化为基本类型,再进行比较
        • valueOf() 返回的不是基本类型时,才会调用 toString() 方法
  2. 相等比较(===):

    • 基本数据类型

      基本数据类型(如字符串、数字、布尔值、nullundefinedsymbol)在比较时,是比较它们的值:

      console.log(1 === 1); // 输出:true
      console.log('abc' === 'abc'); // 输出:true
    • 引用数据类型

      引用数据类型(如对象、数组和函数)在比较时,是比较它们的引用。也就是说,即使两个对象的内容完全相同,它们也可能被认为是不相等的,因为它们在内存中的位置可能不同:

      console.log({} === {}); // 输出:false
      console.log([] === []); // 输出:false
      console.log((() => {}) === (() => {})); // 输出:false
  3. Object.is

    Object.is 方法用于判断两个值是否是相同的值。它的行为与严格相等运算符(===)类似,但有两个主要的区别:

    1. Object.is 对于 NaN 的比较返回 trueNaN === NaN 返回 false)。
    2. Object.is 认为 +0-0 是不同的(+0 === -0 返回 true)。

    如果我们要实现一个类似 Object.is 的函数,我们可以这样做:

    function is(x, y) {
      // 针对 `NaN` 的情况
      if (x !== x) {
        return y !== y;
      }
      // 针对 `+0` 和 `-0` 的情况
      if (x === 0 && y === 0) {
        return 1 / x === 1 / y;
      }
      // 其他情况
      return x === y;
    }

    在这个函数中,我们首先检查 x 是否是 NaN,如果是,我们返回 y 是否也是 NaN。然后我们检查 xy 是否都是 0,如果是,我们返回 1 / x1 / y 的比较结果(这可以区分 +0-0)。最后,我们返回 xy 的严格相等比较结果。

数组相关

数组的常用方法

滴滴

提示:

  • 增添方法(是否影响原数组
    • push():参数+作用+返回值
    • unshift():参数+作用+返回值
    • splice():参数+作用+返回值
    • concat():参数+作用+返回值
  • 删除方法
    • pop()
    • shift()
    • splice()
    • slice()
  • 查询方法
    • indexOf()
    • includes()
    • find()
  • 迭代方法
    • some()
    • every()
    • forEach()
    • filter()
    • map()

答案:JavaScript相关细节题 | QT-7274 (qblog.top)

数组的索引

在JavaScript中,数组的索引通常是非负整数。然而,由于JavaScript的数组实际上是特殊的对象,所以你可以使用任何字符串作为属性名,包括对象、函数和Symbol的字符串表示形式。

// 对象作为索引
let arr = [];
let obj = { name: 'test' };
arr[obj] = 'value'; // 这里实际上是将对象转换为字符串'[object Object]',然后作为属性名
console.log(arr['[object Object]']); // 输出'value'

// 函数作为索引
let func = function() {};
arr[func] = 'value2'; // 这里实际上是将函数转换为字符串,然后作为属性名
console.log(arr[func.toString()]); // 输出'value2'

// Symbol作为索引
let sym = Symbol('test');
arr[sym] = 'value3'; // 这里Symbol作为属性名
console.log(arr[sym]); // 输出'value3'

但是,这些"索引"不会被计算在数组的长度中,也不会影响数组的迭代。当我们迭代数组时,只有数字索引的元素被迭代。

let arr = [];
arr[0] = 'zero';
arr['1'] = 'one'; // 这是一个数字索引,因为'1'可以被转换为数字1
arr['foo'] = 'foo';
arr[{name: 'test'}] = 'object';
arr[function() {}] = 'function';
arr[Symbol('test')] = 'symbol';

console.log(arr.length); // 输出2,因为最大的数字索引是1,所以长度是1+1=2

arr.forEach((value, index) => {
    console.log(index, value); // 只会输出0 'zero'和1 'one',不会输出'foo' 'foo'、'[object Object]' 'object'、'function() {}' 'function'和Symbol(test) 'symbol'
});

私有属性的实现

  1. 使用 Symbol

    let nameSymbol = Symbol("name");
    let obj = {
      [nameSymbol]: "codereasy",
      getName: function () {
        return this[nameSymbol];
      },
    };
    console.log(obj.getName()); //输出"codereasy" 
    
    //试图直接访问nameSymbol属性会失败,除非你具有Symbol 引用
    console.log(obj[nameSymbol]); //输出"codereasy",也就是如果我们知道引用,也是可以拿到私有属性的值的
    console.log(obj["name"]); // 输出undefined
  2. 使用闭包:闭包可以用来创建私有变量。这是因为闭包允许函数记住并访问其词法作用域,即使当函数在其原始作用域之外执行时也是如此。

    function createObject() {
        let privateVar = 'I am private';
        return {
            getPrivateVar: function() {
                return privateVar;
            }
        };
    }
    
    let obj = createObject();
    console.log(obj.getPrivateVar()); // 输出 'I am private'
    console.log(obj.privateVar); // 输出 undefined
  3. 使用类的私有字段:在ES2020中,JavaScript引入了类的私有字段,你可以在类中使用#前缀来定义私有字段。

    class MyClass {
        #privateField = 'I am private';
    
        getPrivateField() {
            return this.#privateField;
        }
    }
    
    let obj = new MyClass();
    console.log(obj.getPrivateField()); // 输出 'I am private'
    console.log(obj.#privateField); // SyntaxError
  4. 使用WeakMap:WeakMap可以用来存储私有属性,因为WeakMap的键是不可枚举的。

    let privateProperty = new WeakMap();
    
    class MyClass {
        constructor() {
            privateProperty.set(this, 'I am private');
        }
    
        getPrivateProperty() {
            return privateProperty.get(this);
        }
    }
    
    let obj = new MyClass();
    console.log(obj.getPrivateProperty()); // 输出 'I am private'
    console.log(privateProperty.get(obj)); // 输出 'I am private'

原型链

字节

提示:

  • 当访问一个对象的属性时,不仅仅...
  • proto属性:对象实例和它的构造器(工厂)之间的桥梁
  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

答案:面试官:JavaScript原型,原型链 ? 有什么特点? | web前端面试 - 面试官系列 (vue3js.cn)

DOM和BOM

字节

DOM:

  • 全称
  • 作用:它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容
  • 操作

BOM:

  • 全称

  • 作用:它提供了独立于内容与浏览器窗口进行交互的对象 —— 其作用就是和浏览器做一些交互效果等

    浏览器的全部内容可以看成DOM,整个浏览器可以看成BOM

  • BOM的核心对象:window(表示浏览器的一个实例)

  • location:除 hash 外,只要修改 location 的一个属性,就会导致页面重新加载新URL

  • navigatorscreenhistory

答案:

DOM事件流

DOM 事件流描述的是从页面接收事件的顺序。DOM2 级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

  1. 事件捕获阶段:事件开始时,浏览器会从根节点开始,向下传播到目标元素。在这个阶段中,父节点在子节点之前接收到事件。
  2. 处于目标阶段:事件到达实际的目标元素。在这个阶段,事件已经被派发到目标元素,目标元素处理这个事件。
  3. 事件冒泡阶段:事件从目标元素开始,向上回传到根节点。在这个阶段中,父节点在子节点之后接收到事件。

在这三个阶段中,你可以使用 addEventListener 方法注册事件处理程序,并通过第三个参数来指定是在捕获阶段还是冒泡阶段处理事件。如果第三个参数为 true,则在捕获阶段处理事件;如果为 false 或不提供,那么在冒泡阶段处理事件。

优点

事件委托,也称为事件代理,是一种在父元素上设置监听器来管理一种或多种子元素的事件监听的技术。它的优点包括:

  1. 内存占用减少:不需要为每个子元素都添加事件监听器,只需要在其父元素上添加一个监听器就可以管理所有的子元素。这大大减少了内存占用,提高了性能。
  2. 动态元素的管理:如果你的应用中会动态添加或删除元素,那么事件委托就非常有用。你不需要为新添加的元素添加新的监听器,也不需要在元素被删除时移除监听器。父元素的监听器会自动处理所有的子元素,无论它们何时被添加或删除。
  3. 简化代码:事件委托可以简化你的代码,使其更易于管理和维护。你只需要在一个地方添加监听器,而不是在多个地方添加和移除监听器。
  4. 提高性能:由于浏览器不需要为每个元素都绑定事件,因此可以减少事件绑定的次数,从而提高性能。

例如:

<!-- HTML -->
<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <!-- 可能会动态添加更多的列表项 -->
</ul>

// JavaScript
document.getElementById('myList').addEventListener('click', function(e) {
  if(e.target && e.target.nodeName == 'LI') {
    alert('List item ' + e.target.innerText + ' was clicked!');
  }
});

addEventListener的第三个参数的应用场景

addEventListener 的第三个参数决定了事件处理函数是在事件的“捕获阶段”还是“冒泡阶段”被触发。这两种方式的选择主要取决于你的具体需求。

  1. 捕获阶段(第三个参数为 true:在某些情况下,你可能希望在事件到达其实际目标之前就进行处理。例如,你可能想在点击链接之前进行一些操作,如验证或动画等。在这种情况下,你可以在捕获阶段处理事件。

    document.body.addEventListener('click', function(event) {
      alert('Body clicked during capturing phase');
    }, true);

    在这个例子中,无论你点击页面的任何位置,都会首先弹出警告框,然后才会执行其他的点击事件处理函数。

  2. 冒泡阶段(第三个参数为 false 或不提供):大多数情况下,我们会在冒泡阶段处理事件,因为这样可以确保事件的默认行为(如链接的跳转)已经发生。此外,由于大多数的浏览器都支持事件冒泡,所以在冒泡阶段处理事件可以提高代码的兼容性。

    document.body.addEventListener('click', function(event) {
      alert('Body clicked during bubbling phase');
    }, false);

    在这个例子中,只有当所有的点击事件处理函数都执行完毕后,才会弹出警告框。

target currentTarget relateTarget具体指向什么目标?

在事件处理中,targetcurrentTargetrelatedTarget 是常用的三个属性,它们的含义和用法如下:

  1. targetevent.target 指向直接触发事件的元素,也就是事件最初发生的地方。

    例如,如果你在一个按钮上点击,那么 event.target 就是这个按钮。

    button.addEventListener('click', function(event) {
     console.log(event.target); // 输出:button元素
    });
  2. currentTargetevent.currentTarget 指向绑定事件监听器的元素。

    例如,如果你在一个列表上设置了事件监听器,而用户点击了其中的一个列表项,那么 event.target 是被点击的列表项,而 event.currentTarget 是列表本身。

    list.addEventListener('click', function(event) {
    
     console.log(event.target); // 输出:被点击的li元素
    
     console.log(event.currentTarget); // 输出:ul元素
    
    });
  3. relatedTargetevent.relatedTarget 在某些特定的事件中使用,如 mouseovermouseout 事件,表示鼠标刚刚离开的元素(对于 mouseover 事件)或者鼠标即将进入的元素(对于 mouseout 事件)。

    例如,如果你将鼠标从一个按钮移动到另一个按钮,那么 event.target 是你即将进入的按钮,event.relatedTarget 是你刚刚离开的按钮。

    button.addEventListener('mouseover', function(event) {
    
     console.log(event.target); // 输出:鼠标进入的button元素
    
     console.log(event.relatedTarget); // 输出:鼠标离开的元素
    
    });

变量提升

  • 变量提升:指变量和函数声明在代码编译阶段被提升到它们所在的作用域的顶部。

  • 只有在非严格模式下才会变量提升。

  • 先提升函数,再提升变量:也就是说如果在同一个作用域中同时声明了一个函数和一个变量,并且它们的名字相同,那么函数声明会覆盖变量声明

  • 只有声明的变量会提升,初始化不会提升:

    console.log(a); // 输出:undefined
    var a = 5;
    console.log(a); // 输出:5
    
    // 因为a的声明被提升到了代码的顶部,但是初始化并没有提升,所以第一次打印是undefined
  • 只有声明的函数才会提升,初始化不会提升。

相关拓展:

  • JavaScript变量命名规则

变量命名规则

  1. varvar 是最早的声明变量的方式,它的作用域是函数作用域。如果在函数外部声明,那么它就是全局变量。var 声明的变量会发生变量提升,即在声明之前就可以使用,但是值为 undefined
  2. letlet 是 ES6 引入的新的声明变量的方式,它的作用域是块级作用域let 声明的变量不会发生变量提升,如果在声明之前使用会报错。
  3. constconst 也是 ES6 引入的,用于声明常量,一旦声明,其值就不能改变const 的作用域和 let 一样,也是块级作用域const 声明的常量也不会发生变量提升,如果在声明之前使用会报错。

练习题

function trickyQuestion() {
  console.log(a); // undefined
  console.log(foo()); // 2
  
  var a = 1;
  function foo() {
    return 2;
  }

  if(false) {
    var a = 3;
    function foo() {
      return 4;
    }
  }

  console.log(a); // 1 声明会被覆盖,但是初始值没有被覆盖
  console.log(foo()); // 2
}

trickyQuestion();
let a = 0, b = 0;
function fn(a) {
  fn = function fn2(b) {
    console.log(a, b)
    console.log(++a+b)
  }
  console.log('a', a++)
}
fn(1); // a, 1
fn(2); // 2, 2   5
// 不带修饰符的变量的赋值会沿着向上查找作用域中的值,直到window,所以第一次执行fn后,fn内部的fn现在成了全局变量
var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()

var b = {
    a,
    c: b
}
console.log(b.c);
//在立即执行函数中,首先 console.log(a) 输出 undefined,因为在函数作用域内有 var a 的声明,所以 a 在整个函数作用域内都存在,但在赋值之前,其值为 undefined,这就是 JavaScript 的变量提升(hoisting)。

//然后 a = 5,但这个 a 是函数作用域内的 a,并不影响全局的 a,所以 console.log(window.a) 输出的是全局的 a,即 10。

//接着 var a = 20,这个 a 也是函数作用域内的 a,所以 console.log(a) 输出的是 20。

//对于 var b = { a, c: b },这是 ES6 的对象字面量属性值简写,等价于 var b = { a: a, c: b },但在 b 被赋值之前,b 是 undefined,所以 b.c 的值是 undefined,console.log(b.c) 输出的是 undefined。

答案:彻底解决 JS 变量提升| 一题一图,超详细包教包会😉 - 掘金 (juejin.cn)

this的绑定规则

滴滴

提示:

  • 默认绑定
  • 隐式绑定
    • 一般的对象调用
    • 对象属性引用链
    • 隐式丢失
  • 显示绑定
  • new绑定
  • 绑定的优先级
  • 绑定的例外情况

答案:this的绑定规则详解 | QT-7274 (qblog.top)

深拷贝和浅拷贝

滴滴

答案:JavaScript深拷贝和浅拷贝 | QT-7274 (qblog.top)

如何实现数组的浅拷贝

小鹅通

  1. 使用 Array.prototype.slice() 方法

    let arr = [1, 2, 3];
    
    let copy = arr.slice();
  2. 使用 Array.prototype.concat() 方法

    let arr = [1, 2, 3];
    let copy = [].concat(arr);
  3. 使用扩展运算符(...

    let arr = [1, 2, 3];
    let copy = [...arr];
  4. 使用 Array.from() 方法

    let arr = [1, 2, 3];
    let copy = Array.from(arr);

事件循环

字节、小鹅通、腾讯

答案:JavaScript事件循环 | QT-7274 (qblog.top)

练习题

console.log('script start'); 

setTimeout(function () {
  console.log('setTimeout'); 
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2'); 
});

async function async1() {
  console.log('async1 start'); 
  await async2(); 
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

async1(); 

new Promise(function (resolve) {
  console.log('promise3'); 
  resolve('foo');
}).then(function () {
  console.log('promise4'); 
}).then(new Promise(function (resolve) {
  console.log('promise8')
  resolve()
}).then(function(){
  console.log('promise5')
}));

console.log('script end'); 
  1. 先执行同步代码:

    • console.log('script start')
    • async1()
      • console.log('async1 start')
      • await async2()
        • console.log('async2')
    • console.log('promise3')
    • console.log('promise8')
    • console.log('script end')
  2. 在执行微队列任务:

    • console.log('promise1')

    • console.log('async1 end');

    • console.log('promise4');

    • console.log('promise5')

      注意因为promise8的优先级(同步任务)是比promise1(微任务)高的,所以promise5自然在promise2之前进入微队列。

    • console.log('promise2')

  3. 最后执行宏任务:

    • console.log('setTimeout')

JavaScript垃圾回收机制

腾讯

提示:

  • 标记清除
  • 引用计数

答案:JavaScript中的垃圾回收机制 | QT-7274 (qblog.top)

闭包

腾讯

答案:通俗易懂介绍下JS闭包 | QT-7274 (qblog.top)

事件冒泡及经历的阶段

答案:JavaScript代码题目 | QT-7274 (qblog.top)

发布订阅者模式

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知。

  • 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
  • 发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。

观察者模式:在观察者模式中,被观察的对象(称为主题)维护一个观察者列表,当主题的状态发生变化时,它会通知所有的观察者。观察者模式通常用于实现低耦合的系统,一个对象的状态改变不需要直接影响其他对象。

寄生组合式继承

提示:

  1. 创建对象,创建父类原型的一个副本
  2. 增强对象,弥补因重写原型而失去的默认的constructor属性
  3. 指定对象,将新创建的对象赋值给子类的原型

答案:JavaScript常用八种继承方案 - 掘金 (juejin.cn)

版本控制

node模块解析机制

Node.js 的模块解析机制主要包括以下几个步骤:

  1. 内置模块:如果请求的模块是 Node.js 的内置模块(例如 fspath 等),那么直接返回内置模块,并结束查找。
  2. 文件模块:如果请求的模块以 ./..// 开头,那么 Node.js 会将其视为文件模块。Node.js 会按照 .js.json.node 的顺序,尝试添加这些扩展名,然后在文件系统中查找。
  3. 目录模块:如果请求的模块是一个目录,那么 Node.js 会查找该目录下的 package.json 文件。如果 package.json 文件存在,那么 Node.js 会查找 package.jsonmain 字段指定的文件。如果 package.json 不存在,或者 main 字段不存在,那么 Node.js 会查找该目录下的 index.js 文件。
  4. node_modules:如果以上步骤都没有找到模块,那么 Node.js 会在当前目录的父目录中查找 node_modules 文件夹,并尝试在其中查找模块。Node.js 会一直向上查找,直到找到模块或者到达根目录。

npm扁平安装机制如何处理版本冲突

提示:

  • 幽灵依赖
  • pnpm包管理器

答案:聊一聊前端包管理中的幽灵依赖 - 掘金 (juejin.cn)

package.json 与 package-lock.json 的关系

提示:

  • ~ 会匹配最近的小版本依赖包,比如 ~1.2.3 会匹配所有 1.2.x 版本,但是不包括 1.3.0

    ^ 会匹配最新的大版本依赖包,比如 ^1.2.3 会匹配所有 1.x.x 的包,包括 1.3.0,但是不包括 2.0.0

    * 安装最新版本的依赖包,比如 *1.2.3 会匹配 x.x.x

  • package-lock.json:对整个依赖树进行版本锁死

  • package-lock.json 是不会无缘无故被更改的,一定是因为 package.json 或者 node_modules 被更改了

答案:package.json 与 package-lock.json 的关系 - 掘金 (juejin.cn)

npm i 和 npm ci 的区别

  1. npm install:这个命令会根据 package.jsonpackage-lock.json(或 yarn.lock)来安装依赖。如果 package-lock.json 不存在,npm install 会创建它。此外,npm install 会自动更新 package-lock.json,如果有任何依赖的新版本符合 package.json 中的版本规则。
  2. npm ci:这个命令主要用于持续集成(Continuous Integration)环境。它会严格根据 package-lock.jsonnpm-shrinkwrap.json 安装依赖,不会更新这两个文件。此外,npm ci 会在安装前删除 node_modules 目录,确保从干净的状态开始安装。

操作系统相关

进程和线程

腾讯

提示:

  • 概念
  • 是否可以直接访问?用什么方法实现?
  • 区别

答案:浏览器相关细节题(3) | QT-7274 (qblog.top)

进程间通信的方式

进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息1。以下是常见的进程间通信方式:

  1. 管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。管道分为无名管道和命名管道(FIFO)。无名管道通常用于父子进程或兄弟进程之间的通信,而命名管道则可以用于无亲缘关系进程间的通信234
  2. 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点234
  3. 共享内存(Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的234
  4. 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源234
  5. 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生34
  6. 套接字(Socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机上的两个进程间的通信214

僵尸进程和孤儿进程

腾讯

提示:

  1. 僵尸进程(Zombie Process):当一个子进程比它的父进程先结束,而父进程还没有调用 wait() 或 waitpid() 等函数来读取子进程的退出状态,那么子进程的进程描述符在系统中仍然存在,这种状态的进程就被称为僵尸进程。
  2. 孤儿进程(Orphan Process):当一个父进程结束,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)接管,并由 init 进程对它们完成状态收集工作。

死锁

腾讯

提示:

  1. 概念:死锁是指两个或更多的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉它们将无法继续执行下去。
  2. 死锁的四个必要条件:
    • 互斥条件:一个资源每次只能被一个进程使用。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  3. 解决死锁的基本方法:
    • 预防死锁:破坏死锁的四个必要条件中的一个或多个。例如,只允许进程在没有其他资源的情况下请求资源,或者只允许进程一次请求所有的资源。
    • 避免死锁:在资源动态分配过程中,使用一些避免算法,防止系统进入不安全状态。例如,银行家算法。
    • 检测死锁:当系统中存在两个或两个以上的进程在等待已被其他进程占用的资源时,就可能存在死锁。系统可以提供一种机制来检测和恢复死锁。
    • 解除死锁:当检测到死锁后,可以通过剥夺资源、撤销进程等方式解除死锁。

端口是什么?

字节

端口是一个抽象的概念,用于区分不同的服务或进程。每个网络服务或进程都会绑定到一个特定的端口号上,这样当网络数据到达计算机时,就可以根据端口号将数据分发到对应的服务或进程。

端口的主要作用如下:

  1. 区分服务:同一台计算机上可以运行多个网络服务,例如 HTTP 服务、FTP 服务、数据库服务等。这些服务都会监听在不同的端口上,例如 HTTP 服务通常监听在 80 端口,HTTPS 服务监听在 443 端口,MySQL 数据库服务监听在 3306 端口等。
  2. 区分连接:在 TCP 或 UDP 协议中,一个完整的连接由源 IP 地址、源端口、目标 IP 地址和目标端口四个元素组成。这样即使从同一个 IP 地址发出多个连接请求,只要源端口不同,就可以被视为不同的连接。
  3. 数据分发:当网络数据到达计算机时,操作系统会根据数据包的目标端口,将数据分发给监听在该端口上的服务或进程。

CSS相关

CSS选择器以及优先级

CSS选择器用于选择你想要样式化的元素。以下是一些常见的CSS选择器:

  1. 元素选择器:选择HTML元素类型。例如,p选择所有的<p>元素。
  2. 类选择器:选择具有特定类的元素。例如,.myClass选择所有具有myClass类的元素。
  3. ID选择器:选择具有特定ID的元素。例如,#myID选择ID为myID的元素。
  4. 属性选择器:选择具有特定属性的元素。例如,[href]选择所有具有href属性的元素。
  5. 伪类选择器:选择处于特定状态的元素。例如,:hover选择鼠标悬停在上面的元素。
  6. 伪元素选择器:选择元素的特定部分。例如,::before选择元素的内容之前的位置。
  7. 组合选择器:结合多个选择器,选择满足所有条件的元素。例如,p.myClass选择所有既是<p>元素又具有myClass类的元素。

CSS选择器的优先级(也称为特异性)决定了当多个规则应用于同一个元素时,哪个规则会生效。优先级由选择器的组成部分决定:

  1. !important
  2. 内联样式(1000):在HTML元素的style属性中定义的样式具有最高的优先级。
  3. ID选择器(0100):ID选择器的优先级高于类选择器、属性选择器、伪类选择器和元素选择器。
  4. 类选择器、属性选择器和伪类选择器(0001):这些选择器的优先级高于元素选择器和伪元素选择器。
  5. 元素选择器和伪元素选择器(0000):这些选择器具有最低的优先级。

如果优先级相同,那么在CSS中后出现的规则会覆盖先出现的规则。如果你想要一个规则无论如何都生效,你可以在规则后面添加!important关键字,但这应该尽量避免,因为它会破坏CSS的优先级规则。

伪类和伪元素的区别

伪类和伪元素是用来修饰不在文档树中的部分,比如,一句话中的第一个字母,或者是列表中的第一个元素。

  • 伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。比如说,当用户悬停在指定的元素时,我们可以通过:hover来描述这个元素的状态。

    虽然它和普通的css类相似,可以为已有的元素添加样式,但是它只有处于dom树无法描述的状态下才能为元素添加样式,所以将其称为伪类。

  • 伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before来在一 个元素前增加些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。

区别

  • 伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档树外的元素。 因此,伪类与伪元素的区别在于:有没有创建一个文档树之 外的元素。
  • CSS3规范中的要求使用双冒号表示伪元素,以此来区分伪元素和伪类,比如:before和:after等伪元素使用双冒号,:hover和:active等伪类使用单冒号。 除了一些低于IE8版本的浏览器外,大部分浏览器都支持伪元素的双冒号表示方法。

rem em vh vw

  • em:定义字体大小时以父级的字体大小为基准;定义长度单位时以当前字体大小为基准。
  • rem:以根元素的字体大小为基准

答案:响应式布局的常用解决方案对比 | QT-7274 (qblog.top)

z-index

z-index 是 CSS 属性,用于控制元素在页面的堆叠顺序(即哪个元素在前,哪个元素在后)。使用 z-index 时,有几点需要注意:

  1. 定位上下文z-index 只对定位元素(即 position 属性为 relativeabsolutefixedsticky 的元素)有效。对于 positionstatic 的元素,z-index 不会产生任何效果。
  2. 堆叠上下文z-index 的值只在同一个堆叠上下文中比较。每个定位元素和根元素都会创建一个新的堆叠上下文。在一个堆叠上下文中,具有较大 z-index 值的元素会覆盖具有较小 z-index 值的元素,但它们都不会覆盖任何在父级堆叠上下文中的元素。
  3. z-index 是整数z-index 的值必须是整数。你可以使用正数、零或负数。正数的元素会覆盖零和负数的元素,零的元素会覆盖负数的元素。
  4. 自动值:如果 z-index 的值是 auto,那么元素的堆叠顺序由其在 HTML 中的位置决定(后出现的元素覆盖先出现的元素)。
  5. 透明度和混合模式:设置 opacitytransformfiltermix-blend-mode 属性的值也会创建新的堆叠上下文。
  6. 子元素和父元素:子元素永远不会覆盖其父元素,无论 z-index 如何设置。这是因为子元素是在父元素的堆叠上下文中。

z-index为0和auto的区别?

z-index 的默认值是 auto。这意味着元素的堆叠顺序由其在 HTML 中的位置决定(后出现的元素覆盖先出现的元素)。

auto0 在大多数情况下的效果是相同的,都表示元素处于正常的文档流中。然而,它们在创建新的堆叠上下文时有所不同。

对于 positionrelativeabsolutefixed 的元素,如果 z-index 的值为 auto,那么它不会创建新的堆叠上下文;如果 z-index 的值为 0 或其他数字,那么它会创建新的堆叠上下文。

创建新的堆叠上下文的元素会与其兄弟元素分开,它和其子元素会形成一个独立的层次,这个层次的 z-index 会被视为一个整体来与其他层次比较,而不是单独的元素。这意味着,即使堆叠上下文中的一个元素的 z-index 很大,它也不能覆盖其父级堆叠上下文中的其他元素。

例如:

<div style="position: relative; z-index: 1;">
  <div style="position: relative; z-index: 2;">A</div>
</div>
<div style="position: relative; z-index: 0;">
  <div style="position: relative; z-index: 3;">B</div>
</div>

虽然元素 B 的 z-index 值(3)大于元素 A 的 z-index 值(2),但元素 B 不会覆盖元素 A,因为它们不在同一个堆叠上下文中。元素 A 和 B 分别在它们各自的父元素的堆叠上下文中,而这两个父元素的 z-index 值分别为 1 和 0,所以元素 A 的父元素(以及其中的所有内容)将覆盖元素 B 的父元素。

盒模型

字节

提示:盒子的大小=?

答案:CSS中的盒子模型 | QT-7274 (qblog.top)

align-contentalign-self 的区别

金山云

  1. align-content:这个属性用于多行的 flex 容器(当 flex 容器的 flex-wrap 属性不是 nowrap,并且有多行 flex 项目时)。它控制的是行与行之间的对齐,可以将其视为在 flex 容器的交叉轴上对齐 flex 行。可能的值包括:flex-startflex-endcenterspace-betweenspace-aroundstretch
  2. align-self:这个属性允许你覆盖某个特定 flex 项目的 align-items 属性。它控制的是单个项目在交叉轴上的对齐方式。可能的值包括:autoflex-startflex-endcenterbaselinestretch

答案:CSS中的布局汇总 | QT-7274 (qblog.top)

flex 布局实现垂直置顶,靠右对齐

金山云

.container {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: flex-start;
}
  • flex-direction: column 设置主轴方向为从上到下
  • justify-content: flex-start 就可以使项目垂直置顶
  • align-items: flex-end 则使项目在交叉轴(从左到右)上靠右对齐。

BFC

字节、小鹅通、腾讯

提示:

  • 全称 及 一句话概括作用
  • BFC的渲染规则:六种
  • BFC的创建条件:五种

答案:前端面试汇总 | QT-7274 (qblog.top)

如何实现垂直水平居中

字节

提示:

  • 宽高已知:一种
  • 宽高未知:定位两种+布局三种

答案:CSS水平垂直居中详解 | QT-7274 (qblog.top)

修改ElementUI的样式

滴滴

  • 全局样式覆盖

    在你的全局样式文件(如 App.vuemain.js 引入的 CSS 文件)中,写入针对 ElementUI 组件的 CSS 样式。由于 CSS 的特性,后面的样式会覆盖前面的样式,所以你的自定义样式会覆盖 ElementUI 的默认样式。

    注意,由于 ElementUI 的样式是通过类选择器应用的,所以你可能需要使用 !important 来覆盖默认样式。

  • 局部样式覆盖

    在你的 Vue 组件中,你可以在 <style> 标签中写入针对 ElementUI 组件的 CSS 样式。如果你只想在这个组件中覆盖样式,你需要使用 scoped 属性。

    注意,如果你使用 scoped,你的样式只会应用到这个组件中的 ElementUI 组件。如果你想覆盖全局的 ElementUI 样式,你需要去掉 scoped

  • ::deep修改

    这是因为 ElementUI 的样式是在组件的 Shadow DOM 中,而 ::v-deep 可以帮助我们穿透 Shadow DOM,修改其内部的样式。

    注意,::v-deep 只能在 Vue 组件的 <style> 标签中使用,并且需要配合 scoped 使用。如果你在全局样式文件中使用 ::v-deep,可能会无法生效。

  • 修改ElementUI的 Sass 变量

    ElementUI 的样式是基于 Sass 的,你可以通过修改 Sass 变量来改变 ElementUI 的默认样式。首先,你需要在你的项目中安装 sass-loadernode-sass,然后在你的样式文件中引入 ElementUI 的样式文件,并修改你想要改变的变量。

  • 直接修改源代码

盒模型

滴滴

提示:

  • 盒子的分类
  • 盒子的组成
  • 盒子的大小
  • 四个属性的特点

答案:CSS中的盒子模型 | QT-7274 (qblog.top)

粘性定位的原理

小鹅通

它是相对定位和固定定位的混合体:元素在滚动到一定位置之前表现为相对定位,当元素滚动到指定位置时,它会变为固定定位。

工作原理如下:

  • 当元素在视口(viewport)内,且未达到指定的偏移位置时,元素表现为相对定位。此时,元素会按照正常的文档流进行布局,不会脱离文档流。
  • 当页面滚动,使元素达到或超过指定的偏移位置时,元素会变为固定定位。此时,元素会固定在视口的某个位置,不会随着页面的滚动而移动。
  • 当页面继续滚动,使元素的父容器滚出视口时,元素会再次变为相对定位,恢复到正常的文档流中。
.sticky {
  position: sticky;
  top: 0;
}

HTML的块级元素和行内元素

腾讯

块级元素包括:

  • <div>
  • <h1><h6>
  • <p>
  • <form>
  • <header>, <footer>, <section>

块级元素的特点:

  • 默认情况下,块级元素会独占一行,即使设置了宽度,其后的元素也会换行显示。
  • 可以设置宽度、高度、外边距和内边距。

行内元素包括:

  • <span>
  • <a>
  • <img>
  • <button>
  • <input>
  • <label>
  • <em>, <strong>

行内元素的特点:

  • 行内元素不会独占一行,多个行内元素会在一行内从左到右依次排列。
  • 默认情况下,不能设置宽度和高度(除非设置了 display: inline-block),只能设置左右的外边距和内边距。

行内块元素包括:

  • <img>
  • <input>

行内块元素的特点:

行内块级元素是块级元素和行内元素的混合体。它像行内元素一样,不会独占一行,可以和其他元素并排显示。同时,它又像块级元素一样,可以设置宽度和高度。

positon定位及区别

腾讯

在 CSS 中,position 属性有 4 个取值:static、fixed、relative、absolute。各个取值的定位对象如下表所示。

  • 原因

    答案

属性 说明
static 静态定位,元素出现在正常文档中(默认值)
fixed 固定定位,相对于浏览器窗口定位
relative 相对定位,相对于元素初始位置定位
absolute 绝对定位,相对于值不为 static 的祖先元素定位

ES6相关

箭头函数和普通函数的区别

金山云

  • this 绑定

    箭头函数this 值取决于所在定义的位置普通函数this 值取决于被调用的位置

  • arguments 对象

    箭头函数不绑定 arguments 对象,如果在箭头函数中访问 arguments,它将取自包含箭头函数的最近的非箭头函数。普通函数则会在每次函数调用时创建自己的 arguments 对象。

  • 构造函数

    箭头函数不能用作构造函数,你不能使用 new 关键字调用箭头函数。如果你这样做,JavaScript 会抛出一个错误。

  • prototype 属性

    箭头函数没有 prototype 属性,而普通函数有这个属性。

  • yield 关键字

    箭头函数不能包含 yield 关键字,所以你不能在箭头函数中使用 yield 关键字来定义生成器函数。普通函数则可以。

相关拓展:

  • this 指向相关题目
  • 构造函数
  • 生成器函数

CommonJS和ES6模块的区别

  1. 加载方式的差异:CommonJS 模块是运行时加载,即只有在运行时才能确定模块的依赖关系和输出值。而 ES6 Modules 是编译时输出接口,即在代码的静态解析阶段就能确定模块的依赖关系和输出值。
  2. 输出值的差异:CommonJS 输出的是值的拷贝,即一旦输出值,就与模块内部的值无关。而 ES6 Modules 输出的是值的引用,即模块内部的值改变会影响到输出值。
  3. 模块路径的差异:CommonJS 导入的模块路径可以是一个表达式,因为它使用的是 require 方法。而 ES6 Modules 的模块路径只能是字符串。
  4. this 的指向差异:在 CommonJS 中,this 指向当前模块。而在 ES6 Modules 中,this 指向 undefined
  5. 顶层变量的差异:在 ES6 Modules 中,没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

Promise

金山云

提示:

  • 状态:三种
  • 特点:不受外界影响;状态一旦改变
  • 实例方法:三种
  • 构造函数方法:五种

答案:ES6经典数据结构详解 | QT-7274 (qblog.top)

class

金山云

提示:

  • ES6的完全可以看做ES5的构造函数Point === Point.prototype.constructor,即在ES6的类的实例上调用方法 == 调用原型上的方法。

  • constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法,如果没有显式定义,一个空的constructor方法会被默认添加。

  • 类的方法内部如果含有 this,它将默认指向类的实例,但如果单独取出方法使用,this 会指向调用它的环境。

    解决方法:

    1. 在构造方法中绑定 thisthis.printName() = this.printName.bind(this)
    2. 使用箭头函数
    3. 使用Proxy,在获取方法的时候自动绑定 this

和ES5的区别:

  • ES6类的内部定义的所有方法都是不可枚举的,这和ES5不同:Object.keys(Point.prototype)

  • ES6类和模块的内部默认使用严格模式

  • ES6类必须使用new调用,普通构造函数不用new也可以执行

  • ES6类不存在变量提升,因为必须保证子类在父类之后定义

  • ES6 为 new 命令引入了 new.target 属性,(在构造函数中)返回 new 命令所作用的构造函数。 如果构造函数不是通过 new 命令调用的,那么 new.target 会返回 undefined ,因此这个属性可用于确定构造函数是怎么调用的 。

    注意:

    1. 子类继承父类时 new.target 会返回子类。利用这个特点,可以写出不能独立使用而必须继承后才能使用的类。
    2. 在函数外部,使用 new.target 会报错。

和ES5的相同:

  • 实例对象自身的属性(定义在this上),hasOwnProperty() 方法返回 true ;原型对象的属性(定义在类上),hasOwnProperty() 方法返回 false
  • 类的所有实例对象共享一个原型对象:p1._ proto _ === p2._ proto _
  • 类的内部都可以使用 getset 关键字对某个属性设置存值函数和取值函数,拦截该属性的存取行为

构造函数

金山云

ECMAScript的原生构造函数大致有:Boolean()Number()String()Array()Date()Function()RegExp()Error()Object()

  • ES5先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数
  • ES6先新建父类的实例对象 this,再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。

注意:继承 Object 的子类有一个行为差异,即无法通过super方法向父类 Object 传参,这是因为ES6改变了 Object 构造函数的行为,一旦发现 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数。

项目相关

Three.js

  • 在页面关闭销毁跳转离开时清除代码中 定时器事件监听动画帧等相关方法。释放场景中的材质内存,清除场景和模型相关信息

    // 清除模型数据
    onClearModelData(){
    	cancelAnimationFrame(this.rotationAnimationFrame)
    	cancelAnimationFrame(this.renderAnimation)
    	cancelAnimationFrame(this.animationFrame)
    	this.container.removeEventListener('click', this.onMouseClickModel)
    	this.container.removeEventListener('mousedown', this.onMouseDownModel)
    	this.container.removeEventListener('mousemove', this.onMouseMoveModel)
    	window.removeEventListener("resize", this.onWindowResize)
    	// 材质释放内存
     	this.scene.traverse((v) => {
    		  if (v.type === 'Mesh') {
    		     v.geometry.dispose();
    		     v.material.dispose();
    	    	}
    	  })
    	  // 清除场景和模型相关信息
             this.model.clear()
              this.scene.clear()
    }
  • 只在需要的时候渲染 如果在没有操作的时候,让循环一直渲染属于浪费资源,接下来我来带给大家一个只在需要时渲染的方法。

    • 首先在循环渲染中加入一个判断,如果判断值为true时,才可以循环渲染:

      var renderEnabled;
      function animate() {
       
          if (renderEnabled) {
              renderer.render(scene, camera);
          }
       
          requestAnimationFrame(animate);
      }
       
      animate();
    • 然后设置一个延迟器函数,每次调用后,可以将renderEnabled设置为true,并延迟三秒将其设置为false,这个延迟时间大家可以根据需求来修改:

      //调用一次可以渲染三秒
      let timeOut = null;
      function timeRender() {
      	//设置为可渲染状态
          renderEnabled = true;
          //清除上次的延迟器
          if (timeOut) {
              clearTimeout(timeOut);
          }
       
          timeOut = setTimeout(function () {
              renderEnabled = false;
          }, 3000);
      }
    • 接下来,我们在需要的时候调用这个timeRender()方法即可,比如在相机控制器更新后的回调中:

      controls.addEventListener('change', function(){
          timeRender();
      });

tailwindCSS

  1. 原子化 CSS (Atomic CSS)

CSS 原子化是指定义一组表示单一用途样式单元的类。

另外还有 CSS 组件化,了解两者可参考文章 「CSS 思维」组件化 VS 原子化

TailwindCss 将类名拆到了最小的单位,我们只需要用到一定数量的原子类,就能完成一个复杂的页面样式。这也是为啥使用 TailwindCsstree-shake 后,压缩后的 css 体积可以降低到 10kb 以下的原因。

  1. 较好的语义化

使用 TailwindCss 你不用花精力来定义类名,你可以使用内置具有良好语义化的类名,实现样式效果。你也可以一定程度定义符合你自己规则的类名,例如加上统一的前缀。

TailwindCss 语义化也并不完美,默认的命名方案有一定的记忆成本。在后面最佳实践,会具体谈谈这个问题,提供一些其他解决方案。

  1. 约束性

使用 TailwindCss 功能类,是从预定义的设计系统中选择样式,这使得构建统一的 UI 变得更加容易。这也是 CSS 原子化与直接使用内联样式有着明显差异,TailwindCss 具有约束性。

  1. 响应式

TailwindCss 中的每个功能类都可以有条件的应用于不同的断点(breakpoints),在不同分辨率设备上,可以轻松切换属性。内联样式中,无法使用媒体查询。

答案:TailwindCSS 基本介绍与最佳实践 - 知乎 (zhihu.com)

解决了哪些问题?

  • CSS类命名

  • 样式冲突

    样式冲突这个问题其实起因还要归结于 CSS 类命名。为了复用,我们在一个 class 上挂一些常用的规则,比如说:

    .left { float: left }
  • 模块复用

这个实现版本,对比第一版,有以下区别:

  1. 我们没有自定义任何的 css class,使用的所有的 css class 都直接来源于 Tailwind CSS,这样就没有了命名的困扰问题,同时也解决了 css 膨胀的问题。当然 html 体积也变大了,但是因为 class 中使用的是有限集合内的、高度重复的 class 名称,在 Gzip、Brotli 这些压缩算法的作用下,是可以基本忽略的。
  2. 每一个 class 一般只对应一条 css 规则,如 p-6 对应 padding: 1.5rem,h-12对应 height: 3rem,原子性的 class 颗粒度自然更容易在其他地方复用,而且原子化的 css 规范/思想,强制开发人员在为 html 标签定义样式时,写全所有需要的 class ,大大减少了不同 html 标签的 class 之间的相互影响。
  3. 「Atomic/Utility-First CSS」的使用,让样式重构/整体修改变得更加容易。我们可以通过覆盖原子颗粒度的 class ,变更应用的整体样式,例如,覆盖 text-xl 为 2rem,这样所以使用到 text-xl class 的字体大小都会变成 2rem。

答案:tailwindcss 面试题-掘金 (juejin.cn)

图片上传

// HTML
<form id="uploadForm" enctype="multipart/form-data">
    <input id="fileInput" type="file" name="image" accept="image/png, image/jpeg" />
    <button type="submit">上传</button>
</form>

// JavaScript
document.getElementById('uploadForm').addEventListener('submit', function(event) {
    event.preventDefault();

    var fileInput = document.getElementById('fileInput');
    var file = fileInput.files[0];

    // 检查文件类型
    var validTypes = ['image/jpeg', 'image/png'];
    if (!validTypes.includes(file.type)) {
        alert('上传图片只能是 JPG、PNG 格式!');
        return;
    }

    // 检查文件大小
    if (file.size / 1024 / 1024 > 5) {
        alert('上传图片大小不能超过 5MB!');
        return;
    }

    var formData = new FormData();
    formData.append('image', file);

    fetch('你的上传URL', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        console.log('图片上传成功', data);
    })
    .catch(error => {
        console.error('图片上传失败', error);
    });
});

文件上传的二进制具体是怎么处理的

image-20240423172522888

在JavaScript中,处理文件上传的二进制数据通常使用 FileReader 对象。FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。

以下是一个简单的例子,展示了如何读取用户选择的文件并将其作为二进制数据处理:

var file = ...; // 获取用户选择的文件
var chunkSize = 1024 * 1024; // 设置块大小为1MB
var start = 0;

while (start < file.size) {
  var chunk = file.slice(start, start + chunkSize);

  var reader = new FileReader();
  reader.onload = function(event) {
    var binaryString = event.target.result;
    // 这里可以处理二进制字符串
  };
  reader.readAsBinaryString(chunk);

  start += chunkSize;
}

断点续传相关

var file = ...; // 获取用户选择的文件
var chunkSize = 1024 * 1024; // 设置块大小为1MB
var totalChunks = Math.ceil(file.size / chunkSize); // 计算总切片数
var maxRetries = 3; // 设置最大重试次数为3
var parallelUploads = 3; // 设置同时上传的切片数为3
var cancelUpload = false; // 设置一个标志,表示用户是否取消上传

// 从 localStorage 中恢复上传进度
var uploadProgress = localStorage.getItem('uploadProgress');
if (uploadProgress) {
  uploadProgress = JSON.parse(uploadProgress); // 如果有上传进度,解析为对象
} else {
  uploadProgress = { // 否则,初始化上传进度
    uploadedChunks: [], // 已上传的切片索引
    totalChunks: totalChunks, // 总切片数
  };
}

// 定义一个函数,用于上传文件切片
function uploadChunk(i, retries = 0) {
  if (cancelUpload) {
    return Promise.reject(new Error('Upload cancelled')); // 如果用户取消上传,拒绝 Promise
  }

  var start = i * chunkSize; // 计算切片的开始位置
  var end = Math.min(start + chunkSize, file.size); // 计算切片的结束位置
  var chunk = file.slice(start, end); // 获取文件切片

  // 创建一个新的 FormData 对象,添加文件切片
  var formData = new FormData();
  formData.append('file', chunk, 'chunk-' + i);

  // 使用 fetch API 上传文件切片
  return fetch('/upload', {
    method: 'POST',
    body: formData,
  })
    .then(response => {
      if (!response.ok) {
        throw new Error('Upload failed'); // 如果响应不是 ok,抛出错误
      }

      // 更新上传进度
      uploadProgress.uploadedChunks.push(i);
      localStorage.setItem('uploadProgress', JSON.stringify(uploadProgress)); // 保存上传进度到 localStorage

      if (uploadProgress.uploadedChunks.length === totalChunks) {
        // 如果所有切片都已上传,通知服务器合并切片
        return fetch('/merge', { method: 'POST' });
      }
    })
    .catch(error => {
      if (retries < maxRetries) {
        // 如果失败,且重试次数小于最大重试次数,重试上传
        return uploadChunk(i, retries + 1);
      } else {
        throw error; // 否则,抛出错误
      }
    });
}

// 并行上传文件切片
var chunkIndex = 0;
var uploadPromises = [];
while (chunkIndex < totalChunks && uploadPromises.length < parallelUploads) {
  if (!uploadProgress.uploadedChunks.includes(chunkIndex)) {
    uploadPromises.push(uploadChunk(chunkIndex)); // 如果这个切片还没有上传,添加到上传 Promise 数组
  }
  chunkIndex++;
}

Promise.all(uploadPromises)
  .then(() => {
    console.log('Upload complete'); // 当所有 Promise 都完成,打印 "Upload complete"
  })
  .catch(error => {
    console.error('Error:', error); // 如果有任何 Promise 失败,打印错误
  });

Node.js相关

Node.js的加载机制

Node.js 的模块加载机制主要基于 CommonJS 规范,其主要步骤如下:

  1. 路径分析:Node.js 会分析模块路径,确定模块的位置。如果模块路径是相对路径或绝对路径,Node.js 会直接使用这个路径。如果模块路径是模块名,Node.js 会在 node_modules 目录中查找模块。
  2. 文件定位:Node.js 会根据模块路径定位到具体的文件。如果模块路径没有文件扩展名,Node.js 会按 .js.json.node 的顺序尝试添加扩展名。如果模块路径是目录,Node.js 会尝试加载该目录下的 package.json 文件或 index.js 文件。
  3. 编译执行:Node.js 会根据文件类型进行编译执行。.js 文件会被转换为 JavaScript 代码并执行,.json 文件会被解析为 JSON 对象,.node 文件会被视为编译过的插件加载。
  4. 缓存:为了提高性能,Node.js 会缓存已加载的模块。如果后续再次加载同一个模块,Node.js 会直接从缓存中获取,而不会再次执行上述步骤。

需要注意的是,Node.js 的模块加载是同步的,也就是说,当 require() 函数被调用时,Node.js 会立即加载和执行模块,然后返回模块的 exports 对象。这意味着模块的加载顺序会影响程序的执行顺序。

module.exportsexports 的区别

  1. module.exports 是真正的接口:Node.js 模块系统中,module.exports 是真正用于导出模块公开接口的对象。
  2. exportsmodule.exports 的一个引用:在每个模块的开头,Node.js 都会添加这样一行代码:exports = module.exports。这样,exports 就成了 module.exports 的一个快捷方式,你可以通过 exports 添加属性和方法来导出接口。
  3. exports 不能改变 module.exports 的指向:如果你给 exports 赋一个新的值,例如 exports = function() {}exports = new SomeClass(),这时 exports 就不再指向 module.exports 了。但是,Node.js 仍然会返回 module.exports,而不是 exports。所以,如果你想导出一个函数或对象,你应该直接赋值给 module.exports,而不是 exports

如果你想导出的是一个对象或者函数,你必须使用 module.exports。因为 exports 只是 module.exports 的一个引用,你不能通过改变 exports 来改变 module.exports 的值。

WebSocket.js相关

WebSocket的理解

答案:面试官:说说对WebSocket的理解?应用场景? | web前端面试 - 面试官系列 (vue3js.cn)

为什么使用WebScocket

在这个项目中,我使用 WebSocket 来实现实时通信,主要是因为 WebSocket 提供了全双工的通信机制,这意味着服务器和客户端可以同时发送和接收数据,而无需等待对方的响应。这对于我们的项目来说非常重要,因为我们需要在用户上传图像后立即开始图像识别,并将识别结果实时反馈给用户。

我选择 WebSocket 而不是 AJAX、Server-Sent Events 或者长轮询,主要是因为这些技术都有一些限制。例如,AJAX 是一种单向通信技术,它只能由客户端发起请求,而服务器不能主动向客户端发送数据。Server-Sent Events 也是一种单向通信技术,虽然它允许服务器主动向客户端发送数据,但客户端不能向服务器发送数据。长轮询虽然可以实现类似于实时通信的效果,但它需要不断地发送请求,这会增加服务器的负载。

在实现 WebSocket 通信时,我使用了 Python 的 websockets 库在服务器端创建 WebSocket 服务,然后在 Vue 的前端使用原生的 WebSocket API 来连接服务器。当用户上传图像后,我会将图像数据通过 WebSocket 发送到服务器,然后服务器会立即开始图像识别,并将识别结果通过 WebSocket 发送回客户端。这样,用户就可以实时看到图像识别的结果。

WebScoket的工作原理

WebSocket 的工作原理可以分为以下几个步骤:

  1. 建立连接:首先,客户端会向服务器发送一个 HTTP 请求,这个请求被称为握手请求。这个请求的特点是,它的 Upgrade 头字段的值为 websocket,表示客户端希望将连接升级为 WebSocket 连接。

  2. 服务器响应:如果服务器支持 WebSocket,它会返回一个 HTTP 101 Switching Protocols 响应,表示同意升级连接。这个响应的 Upgrade 头字段的值也为 websocket

  3. 数据传输:一旦连接被升级为 WebSocket 连接,客户端和服务器就可以通过这个连接进行全双工的数据传输。这意味着客户端和服务器可以同时发送和接收数据,而无需等待对方的响应。

    在数据传输过程中,WebSocket 使用了一种称为帧的数据单位。每个帧都包含了一些元数据,例如帧的长度、类型(文本或二进制)和是否是消息的最后一个帧等。这使得 WebSocket 可以支持多种数据类型,包括文本和二进制数据,也可以支持大于 64KB 的数据。

  4. 关闭连接:客户端或服务器可以在任何时候发送一个关闭帧来关闭连接。一旦连接被关闭,就不能再发送数据了。

WebSocket的安全措施

  1. 使用 WSS(WebSocket Secure):WSS 是 WebSocket 的安全版本,它使用了 TLS(传输层安全协议)来加密数据。这可以防止数据被窃听或篡改。在实际应用中,我们应该尽可能使用 WSS 而不是非加密的 WS。
  2. 验证和过滤输入:为了防止跨站脚本攻击(XSS),我们需要对所有从客户端接收到的数据进行验证和过滤。例如,我们可以使用白名单来过滤掉所有非法的输入,或者使用 HTML 实体编码来转义所有的 HTML 特殊字符。
  3. 使用 Same-Origin Policy:为了防止跨站请求伪造(CSRF),我们可以使用同源策略。同源策略是一种安全策略,它限制了一个文档或脚本从哪些源可以加载资源。在 WebSocket 中,服务器可以检查请求的 Origin 头字段,只接受来自合法源的请求。
  4. 使用认证和授权:为了防止未经授权的访问,我们可以在 WebSocket 连接上实施认证和授权。例如,我们可以在握手请求中包含一个令牌(例如 JWT),然后在服务器端验证这个令牌。只有当令牌有效时,才允许建立连接。

WebSocket 连接断开

当 WebSocket 连接断开时,可以通过监听 close 事件来处理。在 close 事件的处理函数中,可以进行必要的清理工作,例如取消所有的订阅,清除所有的状态等。

如果需要实现自动重连的机制,可以在 close 事件的处理函数中启动一个定时器,定时尝试重新连接。

let socket = new WebSocket('wss://example.com');

socket.onclose = function(event) {
  console.log('WebSocket is closed. Reconnect will be attempted in 1 second.', event.reason);
  setTimeout(function() {
    connect();
  }, 1000);
};

socket.onerror = function(err) {
  console.error('WebSocket encountered error: ', err.message, 'Closing socket');
  socket.close();
};

function connect() {
  socket = new WebSocket('wss://example.com');
  socket.onclose = ... // 重新设置事件处理函数
  socket.onerror = ...
}

Vue相关

Vue通信方式

金山云

提示:

  • 通信的分类
  • 父子组件的传递方式
  • ref的特点

答案:Vue组件间通信方式详解 | QT-7274 (qblog.top)

Vuex

提示:

  • 原理:
    1. Vue组件接收交互行为,调用dispatch触发对应的aciton(提供了Promise的封装)
    2. action通过commit提交操作方法,对mutation进行提交,是唯一能执行mutation的方法
    3. mutations 状态改变操作方法,通过其改变状态,即state
    4. getters返回最新state值,是state对象的读取方法,响应数据或者状态给Vue组件,界面随之更新

答案:详解Vuex | QT-7274 (qblog.top)

vue-router的两种模式以及实现原理

字节

提示:

  • hash
    • hash值在哪
    • 发送请求时?
    • hash值的改变,都会?
  • history
    • 两个API
    • 在使用 history 模式时,需要通过服务端支持允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。
  • 原理
    • 通常构建一个Vue应用的时候, 我们会使用Vue.use以插件的形式安装VueRouter,同时会在Vue的实例上挂载router的实例。
    • 在vueRouter这个插件中有一个公共的方法install,这个方法的第一个参数是Vue构造器,第二个参数是一个可选的参数对象,其中在install文件中,混入了mixin,给每一个组件创建beforeCreate钩子,在Vue的实例上初始化了一些私有属性,其中_ router指向了VueRouter的实例,_ root指向了Vue的实例。
    • 在Vue中利用数据劫持defineProperty在原型prototype上初始化了一些getter,分别是:
      • router 代表当前Router的实例。
      • route 代表当前Router的信息。
    • 在install中也全局注册了router-view,router-link,其中的Vue.util.defineReactive, 这是Vue里面观察者劫持数据的方法,劫持_ route,当_ route触发setter方法的时候,则会通知到依赖的组件。
    • 接下来在init中,会挂载判断是路由的模式,是history或者是hash,点击行为按钮,调用hashchange或者popstate的同时更新_ route, _ route的更新会触发route-view的重新渲染。

答案:Vue相关细节题 | QT-7274 (qblog.top)

SPA

SPA(Single Page Application)单页应用,是一种只有一个 HTML 页面的应用,所有的用户交互都通过 JavaScript 来更新当前页面,而不是通过网络请求新的 HTML 页面。这种模式的应用包括 Gmail、Google Maps、Facebook 或 Twitter 等。

优点

  1. 用户体验好:由于大部分资源(HTML/CSS/JS)都在第一次加载时就已经获取,所以页面之间的切换不需要重新从服务器加载资源,响应速度快,用户体验好。
  2. 前后端分离:前端通过 API 与后端进行交互,前后端可以分别开发和优化,提高开发效率。
  3. 减轻服务器压力:服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍。

缺点

  1. 首屏加载慢:由于一开始需要加载的资源较多,所以首屏的加载时间可能会较长。
  2. SEO 不友好:由于所有的内容都在一个页面中,且内容的改变是通过 JavaScript 动态替换的,所以搜索引擎可能无法正确索引到页面的信息。
  3. 前端工作量大:前端需要处理路由、视图以及数据等各种问题,工作量相对较大。

SSR

字节

提示:

  • 全称
  • 优点(和SPA比较):两点
  • 缺点:两点

答案:Vue相关细节题 | QT-7274 (qblog.top)

Vue和React区别

字节

  1. 模板 vs JSX:Vue.js 使用基于 HTML 的模板语法,这使得 Vue.js 的学习曲线相对较平缓,尤其是对于那些已经熟悉 HTML 的开发者。而 React 使用 JSX,这是一种将 JavaScript 和 HTML 混合在一起的语法,对于那些喜欢在 JavaScript 中处理一切的开发者来说,这可能更有吸引力。
  2. 数据绑定:Vue.js 提供了双向数据绑定,这意味着当你在输入框中输入内容时,你的数据模型会自动更新,反之亦然。而 React 只提供了单向数据流,这意味着你需要手动处理输入框的变化事件,并更新你的数据模型。
  3. 状态管理:React 通常与 Redux 一起使用,以提供集中式的状态管理。而 Vue.js 有一个官方的状态管理库 Vuex,它的 API 设计更加简洁,更易于理解和使用。
  4. 社区和生态系统:React 的社区和生态系统更加成熟,有许多高质量的第三方库可以使用。而 Vue.js 的社区和生态系统虽然也在快速发展,但相比 React 还是稍微小一些。

Computed和Watch的区别

滴滴

提示:

  • 作用
  • 应用场景

答案:Vue相关细节题 | QT-7274 (qblog.top)

v-if和v-show的区别

滴滴

提示:

  • 触发条件带来的影响
  • 生命周期函数

答案:尚品汇-性能优化 | QT-7274 (qblog.top)

生命周期

滴滴

提升:

  • 流程
  • 各阶段的使用场景
  • 数据请求在created和mounted的区别

答案:Vue核心 Vue生命周期 | QT-7274 (qblog.top)

Vue2和Vue3的区别

滴滴

提示:

  • 速度更快
  • 体积减小
  • 更容易维护
  • 更好的TS支持
  • 其他细节改变

答案:

相关拓展:

  • Vue3的性能优化
  • Vue3的Tree shaking算法

Vue3的性能优化

提示:

  • vue2的性能浪费
  • diff算法优化:静态标记
  • 静态提升
  • 事件监听缓存

答案:Vue3和Vue2的区别 | QT-7274 (qblog.top)

相关拓展:

  • Vue3的Tree shaking

Vue3的Tree shaking

提示:

  • 传统方法和Tree shaking 的区别
  • Vue2的缺点:生产代码+无法检测
  • Vue3的优点:分块
  • Tree shaking的优点

答案:Vue3.0中Treeshaking特性 | QT-7274 (qblog.top)

Vue的双向绑定的原理

滴滴

提示:

  • 双向绑定的概念
  • 双向绑定的组成:MVVM
  • 编译 -- 获取 -- 定义 -- 需要 -- 通知

答案:Vue相关细节题 | QT-7274 (qblog.top)

虚拟DOM

滴滴

提示:

  • 为什么要用到虚拟DOM
  • 什么是虚拟DOM
  • 如何实现虚拟DOM
    • 用JS对象模拟DOM树
    • 渲染用JS表示的DOM对象
    • 比较两棵虚拟DOM树的差异
    • 将两个虚拟DOM对象的差异应用到真正的DOM树

答案:Vue虚拟DOM详解 | QT-7274 (qblog.top)

$nextTick()的原理

小鹅通

提示:

  • 身份:它是一个生命周期钩子
  • 作用
  • 使用场景
  • 实现原理
    • 实现原理主要涉及到JavaScript 的事件循环(Event Loop)和微任务队列(Microtask Queue)。【详见:JavaScript事件循环】
    • Vue.js 的 DOM 更新是异步的,当数据改变时,Vue.js 不会立即更新 DOM,而是将需要更新的组件添加到一个异步队列中。然后,在事件循环的微任务队列中,Vue.js 会清空异步队列,更新所有需要更新的组件。这个过程被称为 "异步更新队列"。
    • $nextTick() 方法就是基于这个异步更新队列实现的。当你调用 $nextTick() 方法时,Vue.js 会将你提供的回调函数添加到一个单独的队列中,这个队列会在异步更新队列清空后执行。这样,你就可以确保你的代码在 DOM 更新后执行。

答案:Vue核心 $nextTick 过渡与动画 | QT-7274 (qblog.top)

相关拓展:

  • JavaScript中的事件循环

Vue的响应式原理及更新迭代

答案:

Vite打包工具的构建流程

  1. 读取配置:Vite 首先会读取用户的配置文件(默认为 vite.config.js),并根据配置文件中的设置初始化构建环境。
  2. 模块解析:Vite 会解析入口文件(默认为 index.html),找出所有的模块依赖。Vite 使用了 ES Module 的静态导入特性,可以在不执行代码的情况下解析出所有的模块依赖。
  3. 模块转换:对于 JavaScript 模块,Vite 会使用 esbuild 进行转换,这比传统的 Babel 转换要快得多。对于 Vue、React 等框架的模块,Vite 会使用对应的插件进行转换。
  4. 模块优化:Vite 会预编译依赖模块,将其转换为可以直接在浏览器中运行的 ES Module 格式,大大提高了页面的加载速度。
  5. 代码拆分:Vite 会根据模块的依赖关系进行代码拆分,生成多个 chunk,以实现代码的按需加载。
  6. 静态资源处理:Vite 会处理和优化静态资源,如图片、样式文件等。
  7. 构建输出:最后,Vite 会将转换和优化后的代码输出到构建目录(默认为 dist)。

Vue部署出现404问题

答案:Vue项目开发与部署 | QT-7274 (qblog.top)

计算器

function calculate(s) {
  //去空格
  s = s.replace(/ /g, "");
  if (s.length === 0) return 0;
  let stack = [];//数据栈
  let sign = '+'; //符号
  let res = 0, pre = 0, i = 0;
  while (i < s.length) {
      let ch = s.charAt(i);
      //处理两位数的问题
      if (!isNaN(ch)) {
          pre = pre*10+(ch-'0');
      }
      //碰到左括号 就把括号里面当成一个 新的被加数
      if (ch === '(') {
          let j = findClosing(s.substring(i)); // 找到右括号的位置,返回右括号的索引位置
          pre = calculate(s.substring(i+1, i+j)); // 计算括号内的式子
          i += j; // 跳过已经计算过
      }
      if (i === s.length-1 || isNaN(ch)) {
          //将所有的结果压栈 最后统一加起来
          switch (sign) {
              case '+':
                  stack.push(pre); break;
              case '-':
                  stack.push(-pre); break;
              case '*':
                  stack.push(stack.pop()*pre); break; // 弹出运算然后运算
              case '/':
                  stack.push(stack.pop()/pre); break;
          }
          pre = 0;
          //记录当前的符号
          sign = ch;
      } 
      i++;
  }
  // 本质上说全都是加法
  while (stack.length !== 0) res += stack.pop();
  return res;
}

//删除所有的括号对,并返回右括号的位置
function findClosing(s) {
  let level = 0, i = 0;
  for (i = 0; i < s.length; i++) {
      if (s.charAt(i) === '(') level++;
      else if (s.charAt(i) === ')') {
          level--;
          if (level === 0) break;
      } else continue;
  }
  return i;
}

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录