react篇

常见面试题

  1. 声明式和命令式?
    React是一种声明式的,ReactDom.render(content, dom),把数据渲染到页面得到最后结果就行,不需要知道中间做了什么。而命令式,要一步一步执行,比如先干嘛,再干嘛等等。
  2. jsx和Fiber有什么关系?
    jsx是一种语法糖写法,会被babel转义成createElement()的语法。Fiber是一种链表设计结构,也是react的异步可中断可恢复的的任务调度单元,包括next节点,sbling,return节点。每一个jsx对象都会被转化成Fiber结构,也就是我们所说的虚拟dom。
  3. react17之前jsx文件为什么要声明import React from 'react',之后为什么不需要了?
    jsx经过编译之后编程React.createElement,不引入React就会报错,react17改变了编译方式,变成了jsx.createElement
  4. Fiber是什么,它为什么能提高性能?
    Fiber是一个js对象,能承载节点信息、优先级、updateQueue,同时它还是一个工作单元。
    1. Fiber双缓存可以在构建好wip Fiber树之后切换成current Fiber,内存中直接一次性切换,提高了性能
    2. Fiber的存在使异步可中断的更新成为了可能,作为工作单元,可以在时间片内执行工作,没时间了交还执行权给浏览器,下次时间片继续执行之前暂停之后返回的Fiber
    3. Fiber可以在reconcile的时候进行相应的diff更新,让最后的更新应用在真实节点上
  5. react16为什么要废弃componentWillReceiveProps改成static getDerivedStateFromProps?
    我们知道react16引入的Fiber架构,在render阶段异步可中断,在commit阶段同步执行,而componentWillReceiveProps在render阶段执行,人们如果不小心在componentWillReceiveProps里面做了setState或者调用API的操作,可能会被重复执行。getDerivedStateFromProps的设计为static,而且名字的语义化告诉我们,不能访问到this,仅用来派生父组件props到state,非常安全。

hooks

为什么hooks不能写在条件判断中?

  • hook会按顺序存储在链表中,如果写在条件判断中,就没法保持链表的顺序

useEffect和useLayoutEffect的区别?

  • 就是useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。
  • 通过整体流程可以看出,effect的整个过程涉及到render阶段和commit阶段。render阶段只创建effect链表,commit阶段去处理这个链表。所有实现的细节都是在围绕effect链表。

状态/生命周期

  1. setState是同步的还是异步的?

    legacy模式下:命中batchedUpdates时是异步 未命中batchedUpdates时是同步的

    concurrent模式下:都是异步的

  2. componentWillMount、componentWillMount、componentWillUpdate为什么标记UNSAFE? 
    新的Fiber架构能在scheduler的调度下实现暂停继续,排列优先级,Lane模型能使Fiber节点具有优先级,在高优先级的任务打断低优先级的任务时,低优先级的更新可能会被跳过,所有以上生命周期可能会被执行多次,和之前版本的行为不一致。

组件

  1. react元素$$typeof属性什么?
    用来表示元素的类型,是一个symbol类型
  2. react怎么区分Class组件和Function组件?
    Class组件prototype上有isReactComponent属性
  3. 函数组件和类组件的相同点和不同点?
    相同点:都可以接收props返回react元素,不同点:编程思想:类组件需要创建实例,面向对象,函数组件不需要创建实例,接收输入,返回输出,函数式编程

    函数式组件的优点:

    可测试性:函数组件方便测试

    状态:类组件有自己的状态,函数组件没有只能通过useState

    生命周期:类组件有完整生命周期,函数组件没有可以使用useEffect实现类似的生命周期

    逻辑复用:类组件继承 Hoc(逻辑混乱 嵌套),组合优于继承,函数组件hook逻辑复用

    跳过更新:shouldComponentUpdate PureComponent,React.memo

    发展未来:函数组件将成为主流,屏蔽this、规范、复用,适合时间分片和渲染

开放性问题

说说你对react的理解/请说一下react的渲染过程

  • 首先jsx经过babel的ast词法解析之后编程React.createElement,React.createElement函数执行之后就是jsx对象,也被称为virtual-dom。
  • 不管是在首次渲染还是更新状态的时候,这些渲染的任务都会经过Scheduler的调度,Scheduler会根据任务的优先级来决定将哪些任务优先进入render阶段,比如用户触发的更新优先级非常高,如果当前正在进行一个比较耗时的任务,则这个任务就会被用户触发的更新打断,在Scheduler中初始化任务的时候会计算一个过期时间,不同类型的任务过期时间不同,优先级越高的任务,过期时间越短,优先级越低的任务,过期时间越长。在最新的Lane模型中,则可以更加细粒度的根据二进制1的位置,来决定任务的优先级,通过二进制的融合和相交,判断任务的优先级是否足够在此次render的渲染。Scheduler会分配一个时间片给需要渲染的任务,如果是一个非常耗时的任务,如果在一个时间片之内没有执行完成,则会从当前渲染到的Fiber节点暂停计算,让出执行权给浏览器,在之后浏览器空闲的时候从之前暂停的那个Fiber节点继续后面的计算,这个计算的过程就是计算Fiber的差异,并标记副作用。详细可阅读往期课件和视频讲解,往期文章在底部。
  • 在render阶段:render阶段的主角是Reconciler,在mount阶段和update阶段,它会比较jsx和当前Fiber节点的差异(diff算法指的就是这个比较的过程),将带有副作用的Fiber节点标记出来,这些副作用有Placement(插入)、Update(更新)、Deletetion(删除)等,而这些带有副作用Fiber节点会加入一条EffectList中,在commit阶段就会遍历这条EffectList,处理相应的副作用,并且应用到真实节点上。而Scheduler和Reconciler都是在内存中工作的,所以他们不影响最后的呈现。
  • 在commit阶段:会遍历EffectList,处理相应的生命周期,将这些副作用应用到真实节点,这个过程会对应不同的渲染器,在浏览器的环境中就是react-dom,在canvas或者svg中就是reac-art等。

 

  • 另外我们也可以从首次渲染和更新的时候看在render和commit这两个子阶段是如果工作
  • mount时:在render阶段会根据jsx对象构建新的workInProgressFiber树,不太了解Fiber双缓存的可以查看往期文章 Fiber架构,然后将相应的fiber节点标记为Placement,表示这个fiber节点需要被插入到dom树中,然后会这些带有副作用的fiber节点加入一条叫做Effect List的链表中。
    在commit阶段会遍历render阶段形成的Effect List,执行链表上相应fiber节点的副作用,比如Placement插入,或者执行Passive(useEffect的副作用)。将这些副作用应用到真实节点上
  • update时:在render阶段会根据最新状态的jsx对象对比current Fiber,再构建新的workInProgressFiber树,这个对比的过程就是diff算法,diff算法又分成单节点的对比和多节点的对比,不太清楚的同学参见之前的文章 diff算法 ,对比的过程中同样会经历收集副作用的过程,也就是将对比出来的差异标记出来,加入Effect List中,这些对比出来的副作用例如:Placement(插入)、Update(更新)、Deletion(删除)等。

在commit阶段同样会遍历Effect List,将这些fiber节点上的副作用应用到真实节点上

如果让你实现一个useState,你会怎么实现。

维护一个state链表和一个游标cursor,我们知道useState的返回值,第一个元素是state,第二个元素是setState。我在useState背后维护一个函数,这个函数每次被调用,就给链表追加一个新state。最好把这个state,和这个函数作为useState的返回值。

let states = []
let setters = []
let firstRun = true
let cursor = 0
 
//  使用工厂模式生成一个 createSetter,通过 cursor 指定指向的是哪个 state
function createSetter(cursor) {
  return function(newVal) { // 闭包
    states[cursor] = newVal
  }
}
 
function useState(initVal) {
  // 首次
  if(firstRun) {
    states.push(initVal)
    setters.push(createSetter(cursor))
    firstRun = false
  }
  let state = states[cursor]
  let setter = setters[cursor]
  // 光标移动到下一个位置
  cursor++
  // 返回
  return [state, setter]
}
使用
  1. 聊聊react生命周期
  2. 简述diff算法
  3. react有哪些优化手段
  4. react为什么引入jsx
  5. 说说virtual Dom的理解
  6. 你对合成事件的理解
    1. 我们写的事件是绑定在dom上么,如果不是绑定在哪里?
    2. 为什么我们的事件手动绑定this(不是箭头函数的情况)
    3. 为什么不能用 return false 来阻止事件的默认行为?
    4. react怎么通过dom元素,找到与之对应的 fiber对象的?
  7. 有react fiber,为什么不需要 vue fiber呢;
    react因为先天的不足——无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
  8. 之前递归遍历虚拟dom树被打断就得从头开始,为什么有了react fiber就能断点恢复呢;react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;

解释结果和现象

1. 点击Father组件的div,Child会打印Child吗

function Child() {
  console.log('Child');
  return <div>Child</div>;
}
    
    
function Father(props) {
  const [num, setNum] = React.useState(0);
  return (
    <div onClick={() => {setNum(num + 1)}}>
      {num}
      {props.children}
    </div>
  );
}
    
    
function App() {
  return (
    <Father>
      <Child/>
    </Father>
  );
}
    
const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);

2.打印顺序是什么

function Child() {
  useEffect(() => {
    console.log('Child');
  }, [])
  return <h1>child</h1>;
}
    
function Father() {
  useEffect(() => {
    console.log('Father');
  }, [])
      
  return <Child/>;
}
    
function App() {
  useEffect(() => {
    console.log('App');
  }, [])
    
  return <Father/>;
}

3. useLayoutEffect/componentDidMount和useEffect的区别是什么

class App extends React.Component {
  componentDidMount() {
    console.log('mount');
  }
}
    
useEffect(() => {
  console.log('useEffect');
}, [])