Answer Example

88 阅读4分钟
1. 闭包是什么,闭包的用途是什么?
  • 在函数内部定义的变量,返回一个函数,在返回的函数中使用这个变量,使得外面函数在返回后这个变量仍然可以被使用到
  • 用途:维护内部变量、高阶函数等等
2. 如何给项目中的所有async添加try-catch?

利用babel plugin,在AST中寻找async和await,在AST中增加try-catch节点,然后将原本的函数体节点插入到try-catch节点的body中

3. 事件的冒泡和捕获
  • DOM事件的发生分为捕获->target->冒泡的阶段,从window层自上向下传递到实际触发的元素,然后再自下向上传递到window,可以选择监听不同的阶段做回调处理。
  • addEventListener的参数,第三个参数可以设置是监听捕获还是冒泡,true是捕获,false是冒泡,默认是冒泡。
  • 事件委托,不需要给每一个元素绑定事件了,可以在上层元素绑定冒泡阶段监听,然后判断实际触发的target,做不同的处理。
  • 一个场景:在一个历史页面上有很多按钮,想统一做一个封禁点击功能,怎么做呢?直接在顶层元素上,监听捕获阶段,然后拦截(e.stopProgagtion)。
4. 了解浏览器的事件循环么?为什么js在浏览器中有事件循环的机制?
  • js是单线程的,要完成非阻塞的异步任务,就采用事件循环机制
  • 宏任务 & 微任务,为什么一定要区分出来两种任务?
    • 宏任务:setTimeout, setInterval, I/O操作
    • 微任务:Promise, MutationObserver
    • 任务队列先进先出,那么默认就没有优先级的划分,紧急任务有可能被阻塞,所以就引入了两种任务类型
  • 浏览器的事件循环和node中的事件循环有什么区别?
    • Node:timer -> pending callback -> idle -> poll -> check -> close callback,每执行完一个任务阶段,清空一次微任务队列
    • 浏览器:每执行完一个宏任务,就清空一次微任务队列,如果在此过程中有新的微任务加入到微任务队列,则一直执行到清空为止
防抖和节流
  • 防抖:短时间内重复多次触发,只执行最后一次。(输入框输入)
  • 节流:在一个时间间隔内执行一次,但总会执行,不会被一直重置导致长时间不执行(resize,scroll)
  • 手写:
function debounce(func, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func(...args);
    }, delay);
  }
}

function throttle(func, delay) {
    let preTime = Date.now();
    let timer;
    return function (...args) {
        const curTime = Date.now();
        const remain = Math.max(0, delay - (curTime - preTime));
        timer = setTimeout(() => {
            func(...args);
            preTime = Date.now();
        }, remain);
    }
}
虚拟dom是什么?原理?优缺点?

用js模拟真实的dom结构,然后通过一些渲染的方法,再把虚拟dom渲染到真实的dom上
虚拟dom会表示出dom的节点类型、属性、值等 如果频繁的对dom进行修改,很容易引起频繁的回流重绘,导致体验不佳。如果采用虚拟dom可以提前先进行diff,可以把连续多次的变化,合并到一起进行局部的变化渲染。

react和vue里的key是什么?为什么不能用index做key?用了会怎样?不加会怎样?

react在做diff的时候是按照同层作比较的,那么对于list这一列同层的节点加上key标记,可以简化diff的过程,比如在比较时发现key不一样,则直接去掉旧节点生成新节点,如果key相同才继续比较节点type和props等,都相同的话就可以复用旧dom了。
不能用index作为key,因为key的使用本意是想在list结构中新插入或者删除一个节点的时候尽量减少开销,但是如果用index作为key,index前后永远是相等的,这一过程就无法优化,甚至会引发一些bug。比如在最前面新增了一个节点,diff的时候,index(0 === 0), type(input === input),并且props也没有变化的时候,那么react这里就认为是一个更新可以直接复用旧的dom,一直到diff到最后一个节点,index不一样了才发现需要新增一个节点。

数组转树
const arr = [{
        id: 2,
        name: '部门B',
        parentId: 0
    },
    {
        id: 3,
        name: '部门C',
        parentId: 1
    },
    {
        id: 1,
        name: '部门A',
        parentId: 2
    },
    {
        id: 4,
        name: '部门D',
        parentId: 1
    },
    {
        id: 5,
        name: '部门E',
        parentId: 2
    },
    {
        id: 6,
        name: '部门F',
        parentId: 3
    },
    {
        id: 7,
        name: '部门G',
        parentId: 2
    },
    {
        id: 8,
        name: '部门H',
        parentId: 4
    }
]

function array2Tree(arr) {
    const root = arr.find(item => item.parentId === 0);
    root.children = findChildren(root);
    function findChildren(node) {
        const children = [];
        for (let i = 0; i < arr.length; i++) {
            if (arr[i].parentId === node.id) {
                arr[i].children = findChildren(arr[i]);
                children.push(arr[i]);
            }
        }
        return children;
    }
    return root;
}
async和await原理

和generator+yield很像,generator让函数可以被中断挂起和恢复执行; 那么async也可以被暂停,就是遇到await就被同步暂停住执行promise,等到promise完成后在恢复执行gen.next

微前端
  • qiankun
    • js隔离:3种方式,snapsandbox采用的是卸载的时候保存快照,再次加载的时候恢复出来;proxysandbox采用是proxy代理,把window上原生的属性单独保管,子应用自己挂载的属性通过window的代理访问
    • css隔离:严格沙箱直接用shadow dom的特性来做隔离;实验性沙箱是动态为style标签中的样式加应用的前缀特征来区分;BEM;CSS in js;
    • 路由:劫持路由,然后加载子应用的钩子;或者就是把子应用的路由以hash的形式加到主应用路由上
    • 通信方式:postmessage或者event bus
    • 预加载:利用requestIdleCallback做预加载
react-router原理
  • 通过onpopstate监听路由变化,然后同步到一个路由状态上,比如浏览器的后退、前进
  • 但是我们SPA应用如果想手动改路由,通常就要执行pushstate或者replacestate,这个时候相当于手动改路由状态
  • router是根据维护的这个路由状态,来决定渲染哪些组件的
脚手架去做哪些事情
  • 工程化的一些事情
    • git hooks
    • eslint配置
    • 一系列的通用配置,nginx等等
  • 代码结构,一些默认固定的文件生成
  • 组件库的选择
错误日志、性能日志采集
  • 异常上报,sendbeacon方式、requestIdleCallback、gif等
    • window.onerror
    • unHandledRejection
    • 重写setTimeout
    • 框架报错,比如componentDidCatch
    • 手动上报错误
  • 性能
    • yahoo的boomrang回旋镖
    • performance.timing performance.getEntries 手动mark等等
    • LCP、FID、CLS
useEffect和useLayoutEffect都在什么时机执行,哪个有可能阻塞渲染

useEffect执行是在commit并且更新渲染完成之后,所以感知上是异步的,不会阻塞渲染。
useLayoutEffect则是在commit之后,实际更新到渲染之前,所以在这个阶段执行耗时同步任务的话,是会阻塞渲染的。

React DOM Diff

深度优先遍历对新旧两棵虚拟DOM树的同层级节点进行比较;
具体diff策略:

  • tree diff:即便是跨层级移动的节点,在diff的时候也仅考虑创建和删除。
  • component diff:对同层级的react组件进行diff时,先比较是不是同一类型的组件,如果是的话再比较组件的props,子节点及属性等等。只要组件类型不同就进行删除&创建操作,不再进行深层次比较
  • element diff:比较同层级的节点类型和属性,如果有key属性的话,则被允许通过移动操作来代替删除&创建操作。
context穿透
webpack热更新原理
小程序双线程,以及为什么不能用DOM API

首先js是运行在单独的线程里的,本身没有BOM和DOM,另外每次操作渲染层变化都是通过jsbridge,所以每次DOM的增删改都通过DOM API来完成,额外的通信造成性能会非常差

xss如何防范

输入过滤,输出转移,对于盗取token的情况使用http only

作用域链
缓存优先级

service worker -> memory -> disk -> 网络请求