高级前端面试题(react + 原生js + es6)

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  if (lane === SyncLane) {
    // legacy或blocking模式
    if (
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime); // 注册回调任务
      if (executionContext === NoContext) {
        flushSyncCallbackQueue(); // 取消schedule调度 ,主动刷新回调队列,
      }
    }
  } else {
    // concurrent模式
    ensureRootIsScheduled(root, eventTime);
  }
}

lane === SyncLane也就是 legacy 或 blocking 模式中, 注册完回调任务之后(ensureRootIsScheduled(root, eventTime)), 如果执行上下文为空, 会取消 schedule 调度, 主动刷新回调队列flushSyncCallbackQueue().

这里包含了一个热点问题(setState到底是同步还是异步)的标准答案:

  • 如果逻辑进入flushSyncCallbackQueue(executionContext === NoContext), 则会主动取消调度, 并刷新回调, 立即进入fiber树构造过程. 当执行setState下一行代码时, fiber树已经重新渲染了, 故setState体现为同步.
  • 正常情况下, 不会取消schedule调度. 由于schedule调度是通过MessageChannel触发(宏任务), 故体现为异步.

React hooks相关:
       hook本质是将直接修改组件的state,改成了一个消息队列,通过useState中的set方法将修改事件推到队列中,react会进行合并和修改,然后自动触发setState。       
        useEffect在使用时候需要注意不能在外部使用判断,因为如果有判断的话,会导致react无法找到它而引发报错。第一个参数是回调函数,当不传递第二个参数的时候,每次render后都会执行,传递空数组的话,生命周期和componentDidMount一致,只会在组件挂载后执行一次,传递相关变量,那么会在第一次挂载和每次变量变化时执行。回调函数返回一个函数的话,这个函数对应的是componentWillUnmount钩子。
        useMemo(),缓存计算函数的结果,只有当第二个参数关心的变量发生变化时候才会再次执行计算,因为每次render都会让整个函数重新运行,如果不进行useMemo()缓存结果的话,那么每次render都会进行运算,会造成性能上的浪费。
        useCallback(),缓存回调函数,和useMemo的区别就是一个是缓存计算结果,一个是缓存函数,同样如果不进行缓存的话,那么每次都会重新生成函数。
        useContext主要是解决组件间多层传递props的问题,与class方式中的Context是一样的功能,只是在获取context值上的方式有一些不同,class方式需要声明静态属性static contextType = MyContext,这样在组件中就可以通过this.context访问。在函数中使用useContext(MyContext)进行获取。
        useReduce(),类似redux中的reduce,有两个参数,第一个是一个描述reduce怎么更新state的函数,第二个是初始的state,返回值数组中第一项是默认state的值,第二项是dispatch函数。与redux一样,useReduce只接受dispatch触发,并传递一个actions来描述应当如何更新。调用dispatch方法后会进入到reduce函数中,对actions进行解析处理后返回新的state来进行更新。

React class和生命周期相关:
1. 避免不必要的渲染。
        React.memo(),PureComponent会对props进行浅比较,所以如果props未发生变化则不会进行更新,引用类型无法判断。需要使用shouldComponentUpdate来进行引用类型的判断,返回false则可以放弃更新。
        对于函数式的组件,未使用任何props,如果未使用memo创建的方式,则可以在使用组件的时候传递一个key,这样react更新的时候对比节点key未发生变化则不会被销毁重建。

2. 错误边界
        在没有错误边界的时候,react组件很容易被某一个组件的错误属性访问或者后端异常返回导致页面白屏,所以可以使用静态方法 static getDerivedStateFromError来返回设置state error的状态,并在componentDidCatch的钩子中对错误进行上传或打印,这样能够保证页面可以按照error的组件去渲染内容,而不至于白屏。

3.生命周期
        constructor做state初始化。
        componentDidMount做ajax请求和事件监听,启动定时器。
        componentWillUnmount取消事件监听,停止定时器。
        componentWillReceiveProps检查props是否变化然后触发ajax或者setState等操作。
        componentDidUpdate做一些动画,获取真实dom节点的属性,做一些更新。或者对比两次state变化,然后将变化的内容回调给父组件。

React新特性:
1. createRoot和ReactDOM.render的区别
        ReactDOM.render是会遍历整个虚拟dom树后进行更新,过程不可中断,这样是能保证所有节点都会执行到,但是也由于可能部分节点更新缓慢,导致用户看起来感受到卡顿。
        createRoot是基于fiber模式,通过createRoot会创建出来fiber的根节点,fiber会将每个组件作为一个节点记录到队列中,并且会记录每个组件的父节点、子节点、兄弟节点,以此可以实现不需要遍历整颗虚拟dom树,就可以进行局部更新,ReactDOM是一次遍历全部后多次重绘,fiber会对某个节点多次比较重绘,可能会导致其他组件的变化被延迟响应。
        在react18版本中,推出了Concurrent特性,Concurrent是基于fiber实现的一种新的渲染模式,它将局部更新分为多个等级的任务,并且可以对正在执行的任务进行中断并去执行更高等级的任务,以此保证用户可以获得更好的体验,这样也会避免掉高等级任务迟迟得不到响应的情况。

2. lazy()和Suspense
        lazy是基于import动态加载实现,例如页面地址不同导致需要不同的模块,这个时候就可以使用lazy进行按需加载。
        在react中,对于组件的加载是有状态的,所以可以使用上Suspense来包裹通过lazy加载的模块,fallback属性作为未加载模块时的渲染。需要注意的一个问题是,如果使用了Suspense包裹lazy的话,那么对于子组件嵌套lazy模块的时候,任何子组件lazy的时候都会导致外层的Suspense重新触发fallback,所以为了避免这种情况,最好是在每个使用lazy加载的模块外,都套用上一层Suspense。

原生相关:
1. 事件循环机制
        因为js是运行在浏览器环境中的,浏览器只给js一个线程,也是我们常说的主线程,所以当我们执行setTimeout和ajax的时候,就需要借助浏览器的定时进程和网络进程。
        当浏览器的其他进程处理好后,在将结果告诉js线程,因为js只有单线程,所以不能立马执行,由此产生了一个事件队列的机制,其他进程的结果都会放到事件队列中,等待js主线程执行完毕后,在从事件队列中取出相应的事件开始执行,不断循环这个过程就是事件循环。
        需要注意的是由于主线程的任务什么时候能执行完是不确定的,所以对于setTimeout事件的定时一定是会有偏差的。
        另外在事件队列中对事件还分为两种等级,setTImeout和ajax是宏任务,promise.then是微任务,微任务总是要快于宏任务执行。不过微任务严格意义上并不算是异步,因为只是修改了调用顺序,而没有借助其他的线程执行。

2. 闭包
        闭包本质是个作用域链条,简单一句话就是在函数体内部能够访问函数体外部的变量。常见的写法大部分都是一个函数内,返回另外一个函数,在返回的这个函数中又引用了外部函数所定义的变量。比较常用的一些场景是模块封装,避免变量污染。在多个请求中保存一些中间结果,方便后续合并数据等等。
        可能会导致的问题就是内存泄漏,因为js的回收机制对于还存在引用的变量是无法回收的,所以为了避免内存泄漏,我们在用完引用变量的时候,需要对这个变量置为null,这样下次垃圾回收的时候,就会被销毁掉了。

3. 垃圾回收
        一种是计数方式,变量每多一次引用计数就+1,减少一次引用就-1,当为0的时候就进行销毁,可能会存在的问题是两个对象互相引用,那计数就永远不会为0,所以要避免这种互相循环引用的实现。
        另外一种是标记状态,对象被调用的时候标记为活跃状态,调用结束后标记为结束状态,对所有结束状态的变量进行删除。同时没有被引用的变量因为没有状态,所以也会被回收。

4. 防抖和节流
        防抖简单说是在一定时间内,触发了执行事件,如果在这个时间内,又再次或者多次触发事件,那么时间会继续顺延,直到最后一次触发满足了设定的时间才会执行。
        节流是在一定时间内,无论触发多少次事件,最终都只会在到期时间后,执行一次。防抖和节流都是比较常见的闭包实现例子。

5. 作用域和作用域链
        其实本质上就是个闭包问题,作用域分为全局作用域和局部作用域,也就是说在window上定义的变量和函数(window就是global就是self也就是根),在全局任何地方是都能够访问的,但是如果在函数内部定义的变量和方法,在函数外部是无法访问的。
        当我们在函数内部访问一个函数体内不存在的变量时,会去当前函数所在的环境中查找这个变量,一直查找到根,这个过程就是作用域链。

6. 原型和原型链
        涉及到几个方面,首先是构造函数,我们可以通过function和class关键字来创建构造函数,每个构造函数都有自己的原型,可以通过指定prototype和extends的方式来实现继承。
        其次是实例化对象,通过new关键字创建,每个实例化对象都会有原型上的属性和方法,并且可以定义自己的属性和方法。当我们通过调用或访问实例化对象的方法和属性时,如果实例化对象自身没有这个方法或者属性,那么就会沿着这个实例化对象的原型上去查找,一直找到Object。这个向上查找得过程就是原型链。(ObjectFunction为构造函数)
        常用的几个方法是Object.getPrototypeof(obj),返回这个实例化对象的原型。
        ObjectFunction.isPrototypeof(obj),判断这个实例化对象是否含有这个构造函数的原型。
        obj instanceof ObjectFunction,判断实例化对象的原型是否等于构造函数的原型。等价于Object.getPrototypeof(obj) === ObjectFunction.prototype;

7. Set和Map
        Set是一个不会重复的集合,可以通过Array.from和结构[...new Set()]的方式转换成数组,在设置值和取值上需要通过add和values方法,判断是否存在需要用has,如果只是要一个不会变的集合,那么数组要简单。
        Map和Object最主要的区别是遍历顺序是固定的,并且可以使用变量作为key,增加方法是set,读取是get。

ES6新增特性:
1.  箭头函数
        箭头函数没有args,另外的一个区别是this的指向。普通函数的this取决于是谁调用的它,也就是谁调用那么this就指向谁。但是箭头函数是在一开始就确定下来的,箭头函数中的this值是在定义函数的时候确定,因为箭头函数会捕获起上下文的`this`值,因此`this`值在箭头函数中是不可变的,这个行为其实是和闭包一样的行为。

2. 字符串模板
        使用``来拼接字符串,变量可以直接使用${},比传统的字符串 + 字符串看起来要更直观书写上更加方便。

3. 解构和扩展运算符
        解构会让取值的过程更加的方便,扩展运算符算是浅拷贝的一种方式,对于一些需要重新生成变量并且含有自身属性的赋值过程会更简单。

4. class
        可以告别function的方式来写构造函数,也可以写自己的静态方法,在方法前增加static标记,静态方法实例化是无法继承的,只能通过构造类的名字调用。私有属性目前还未正式发布,在预案上增加#关键字,目前大家约定俗称的规矩是在变量前增加_来区分,虽然其实还是能在实例化的对象中访问到的。

5. Promies
        可以告别以前的回调地狱,书写起来更加直观,接受一个函数包含resolve和reject参数,主要用来做异步的操作,对于一个请求依赖更外一个请求的结果可以通过.then的方式编写,多个请求并发执行可以通过promise.all实现。
        Promise最强大的功能是可以通过在then里面返回一个promise构造一个复杂的执行序列。

6. async和await
        async是一个装饰性的关键字,用于声明这是一个异步函数,本身上没有什么实际作用,但是不写的话,在函数内使用await会有错误提示。
        想使用await首先需要一个Promise,对于使用上await的promise,会通知系统当前执行序列挂起,直到promise被完成,这样会让执行顺序看起来像是同步执行,本质上是改变了代码的调用顺序。

7. 数组相关的一些方法
        find,findIndex,参数都是接收一个函数,给定条件,满足条件既返回对应的一项或者角标。
        some和every,参数是一个函数,给定条件,返回值是布尔类型,some只需要有一项满足就为true,every需要全部满足。
        map会重新生成新的数组,参数函数中必须要有return。
        includes判断是否包含某一项,比indexOf要更方便一些。
        filter对原数组进行过滤,并生成新的数组。
        reduce对所有子项进行累加器的函数,第二个参数是初始值,并返回最后累加后的结果。

8.深拷贝的方式
        JSON.parse(JSON.stringify(obj))可能存在的问题是如果有值是undefined会消失,对于值是函数的情况会直接报错,因为函数是不能stringify的。
        递归来实现。
        

function deepClone(obj) {
    if(obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    let clone = Array.isArray(obj) ? [] : {};
    for(let key of obj) {
        clone[key] = deepClone(obj[key])
    }

    return clone
}

css3相关:
        flex布局、@keyframes动画、transform、transition、border、多个背景、渐变色。


性能优化相关:
        webpack压缩代码,删除未使用的模块和代码,loader相关插件。
        对于一些第三方库可以使用cdn缓存引用。
        对图片素材进行压缩。
        css提取公共样式表。
        组件载入对于非必须的组件使用import动态载入。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值