写点什么

react 常见考点

作者:beifeng1996
  • 2022 年 9 月 27 日
    浙江
  • 本文字数:10031 字

    阅读完需:约 33 分钟

调用 setState 之后发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


  • 在 setState 的时候,React 会为当前节点创建一个 updateQueue 的更新列队。

  • 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。

  • 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。

  • 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。

  • 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。

  • 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。

  • 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素

React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>
复制代码


React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。


除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。


另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault


实现合成事件的目的如下:


  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;

  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。


前端react面试题详细解答

React-Router 的路由有几种模式?

React-Router 支持使用 hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步:


  • BrowserRouter 创建的 URL 格式:xxx.com/path

  • HashRouter 创建的 URL 格式:xxx.com/#/path


(1)BrowserRouter


它使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步。由此可以看出,BrowserRouter 是使用 HTML 5 的 history API 来控制路由跳转的:


<BrowserRouter    basename={string}    forceRefresh={bool}    getUserConfirmation={func}    keyLength={number}/>
复制代码


其中的属性如下:


  • basename 所有路由的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠;


<BrowserRouter basename="/calendar">    <Link to="/today" /></BrowserRouter>
复制代码


等同于


<a href="/calendar/today" />
复制代码


  • forceRefresh 如果为 true,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能;

  • getUserConfirmation 用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理;


// 这是默认的确认函数const getConfirmation = (message, callback) => {  const allowTransition = window.confirm(message);  callback(allowTransition);}<BrowserRouter getUserConfirmation={getConfirmation} />
复制代码


需要配合<Prompt> 一起使用。


  • KeyLength 用来设置 Location.Key 的长度。


(2)HashRouter


使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:


<HashRouter    basename={string}    getUserConfirmation={func}    hashType={string}  />
复制代码


其参数如下


  • basename, getUserConfirmation 和 BrowserRouter 功能一样;

  • hashType window.location.hash 使用的 hash 类型,有如下几种:

  • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops;

  • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops;

  • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops。

React 中发起网络请求应该在哪个生命周期中进行?为什么?

对于异步请求,最好放在 componentDidMount 中去操作,对于同步的状态改变,可以放在 componentWillMount 中,一般用的比较少。


如果认为在 componentWillMount 里发起请求能提早获得结果,这种想法其实是错误的,通常 componentWillMount 比 componentDidMount 早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。


react 的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()


上面这些方法的调用是有次序的,由上而下依次调用。


  • constructor 被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。

  • componentWillMount 方法的调用在 constructor 之后,在 render 之前,在这方法里的代码调用 setState 方法不会触发重新 render,所以它一般不会用来作加载数据之用。

  • componentDidMount 方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用 setState 方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在 constructor 里做,但 constructor 是做组件 state 初绐化工作,并不是做加载数据这工作的,constructor 里也不能 setState,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在 componentDidMount 方法里。


总结:


  • 跟服务器端渲染(同构)有关系,如果在 componentWillMount 里面获取数据,fetch data 会执行两次,一次在服务器端一次在客户端。在 componentDidMount 中可以解决这个问题,componentWillMount 同样也会 render 两次。

  • 在 componentWillMount 中 fetch data,数据一定在 render 后才能到达,如果忘记了设置初始状态,用户体验不好。

  • react16.0 以后,componentWillMount 可能会被执行多次。

用户不同权限 可以查看不同的页面 如何实现?

  1. Js 方式

  2. 根据用户权限类型,把菜单配置成 json, 没有权限的直接不显示

  3. react-router 方式 在 route 标签上 添加 onEnter 事件,进入路由之前替换到首页


<Route path="/home" component={App} onEnter={(nexState,replace)=>{      if(nexState.location.pathname!=='/'){         var  sid = UtilsMoudle.getSidFromUrl(nexState);         if(!sid){            replace("/")         }else{            console.log(sid);         }      }    }}>
复制代码


  1. 自己封装一个 privateRouter 组件 里面判断是否有权限,有的话返回

  2. <Route path={path} component={component} exact={exact}/>

  3. 没有权限的话 component 返回一个提示信息的组件。

  4. 扩展一下,如果是根据用权限来判断是否隐藏组件该怎么做呢?

  5. react 可以使用高阶组件,在高阶组件里面判断是否有权限,然后判断是否返回组件,无权限返回 null

  6. vue 可以使用自定义指令,如果没有权限移除组件


// 需要在入口处添加自定义权限指令v-auth,显示可操作组件Vue.directive('auth', {    bind: function (el, binding, vnode) {        // 用户权限表        const rules = auths        for (let i = 0; i < rules.length; i++) {            const item = rules[i]            if(!binding.value || (binding.value == item.auth)){                // 权限允许则显示组件                return true            }        }        // 移除组件        el.parentNode.removeChild(el)    }})// 使用<template>  <div>    <Button v-auth="admin_user_add">添加用户</Button>    <Button v-auth="admin_user_del">删除用户</Button>    <Button v-auth="admin_user_edit">编辑用户</Button>  </div></template>
复制代码

HOC 相比 mixins 有什么优点?

HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:


  • 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖

  • 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。

  • 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本


HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)。

React 的严格模式如何使用,有什么用处?

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。可以为应用程序的任何部分启用严格模式。例如:


import React from 'react';function ExampleApplication() {  return (    <div>      <Header />      <React.StrictMode>                <div>          <ComponentOne />          <ComponentTwo />        </div>      </React.StrictMode>            <Footer />    </div>  );}
复制代码


在上述的示例中,不会对 HeaderFooter 组件运行严格模式检查。但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查。


StrictMode 目前有助于:


  • 识别不安全的生命周期

  • 关于使用过时字符串 ref API 的警告

  • 关于使用废弃的 findDOMNode 方法的警告

  • 检测意外的副作用

  • 检测过时的 context API

高阶组件

高阶函数:如果一个函数接受一个或多个函数作为参数或者返回一个函数就可称之为高阶函数


高阶组件:如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件


react 中的高阶组件


React 中的高阶组件主要有两种形式:属性代理反向继承


属性代理 Proxy


  • 操作 props

  • 抽离 state

  • 通过 ref 访问到组件实例

  • 用其他元素包裹传入的组件 WrappedComponent


反向继承


会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent


反向继承可以用来做什么:


1.操作 state


高阶组件中可以读取、编辑和删除WrappedComponent组件实例中的state。甚至可以增加更多的state项,但是非常不建议这么做因为这可能会导致state难以维护及管理。


function withLogging(WrappedComponent) {        return class extends WrappedComponent {            render() {                return (                    <div>;                        <h2>;Debugger Component Logging...<h2>;                        <p>;state:<p>;                        <pre>;{JSON.stringify(this.state, null, 4)}<pre>;                        <p>props:<p>;                        <pre>{JSON.stringify(this.props, null, 4)}<pre>;                        {super.render()}                    <div>;                );            }        };    }
复制代码


2.渲染劫持(Render Highjacking)


条件渲染通过 props.isLoading 这个条件来判断渲染哪个组件。


修改由 render() 输出的 React 元素树

Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?

Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。


applyMiddleware 源码:


export default function applyMiddleware(...middlewares) {    return createStore => (...args) => {        // 利用传入的createStore和reducer和创建一个store        const store = createStore(...args)        let dispatch = () => {            throw new Error()        }        const middlewareAPI = {            getState: store.getState,            dispatch: (...args) => dispatch(...args)        }        // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍        const chain = middlewares.map(middleware => middleware(middlewareAPI))        // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch        dispatch = compose(...chain)(store.dispatch)        return {            ...store,            dispatch        }    }}
复制代码


从 applyMiddleware 中可以看出∶


  • redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。

  • 柯里化函数两端一个是 middewares,一个是 store.dispatch

区分状态和 props

在 React 中,refs 的作用是什么

Refs 可以用于获取一个 DOM 节点或者 React 组件的引用。何时使用 refs 的好的示例有管理焦点/文本选择,触发命令动画,或者和第三方 DOM 库集成。你应该避免使用 String 类型的 Refs 和内联的 ref 回调。Refs 回调是 React 所推荐的。

React 16.X 中 props 改变后在哪个生命周期中处理

在 getDerivedStateFromProps 中进行处理。


这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。


两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。


需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:


static getDerivedStateFromProps(nextProps, prevState) {    const {type} = nextProps;    // 当传入的type发生变化的时候,更新state    if (type !== prevState.type) {        return {            type,        };    }    // 否则,对于state不进行任何操作    return null;}
复制代码

React 中 props.children 和 React.Children 的区别

在 React 中,当涉及组件嵌套,在父组件中使用props.children把所有子组件显示出来。如下:


function ParentComponent(props){    return (        <div>            {props.children}        </div>    )}
复制代码


如果想把父组件中的属性传给所有的子组件,需要使用React.Children方法。


比如,把几个 Radio 组合起来,合成一个 RadioGroup,这就要求所有的 Radio 具有同样的 name 属性值。可以这样:把 Radio 看做子组件,RadioGroup 看做父组件,name 的属性值在 RadioGroup 这个父组件中设置。


首先是子组件:


//子组件function RadioOption(props) {  return (    <label>      <input type="radio" value={props.value} name={props.name} />      {props.label}    </label>  )}
复制代码


然后是父组件,不仅需要把它所有的子组件显示出来,还需要为每个子组件赋上 name 属性和值:


//父组件用,props是指父组件的propsfunction renderChildren(props) {
//遍历所有子组件 return React.Children.map(props.children, child => { if (child.type === RadioOption) return React.cloneElement(child, { //把父组件的props.name赋值给每个子组件 name: props.name }) else return child })}//父组件function RadioGroup(props) { return ( <div> {renderChildren(props)} </div> )}function App() { return ( <RadioGroup name="hello"> <RadioOption label="选项一" value="1" /> <RadioOption label="选项二" value="2" /> <RadioOption label="选项三" value="3" /> </RadioGroup> )}export default App;
复制代码


以上,React.Children.map让我们对父组件的所有子组件又更灵活的控制。


**

React 与 Vue 的 diff 算法有何不同?

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。


React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。


  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。

  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。

  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。


以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。


Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

高阶组件的应用场景

权限控制


利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别页面元素级别


// HOC.js    function withAdminAuth(WrappedComponent) {        return class extends React.Component {            state = {                isAdmin: false,            }            async componentWillMount() {                const currentRole = await getCurrentUserRole();                this.setState({                    isAdmin: currentRole === 'Admin',                });            }            render() {                if (this.state.isAdmin) {                    return <WrappedComponent {...this.props} />;                } else {                    return (<div>您没有权限查看该页面,请联系管理员!</div>);                }            }        };    }
// 使用// pages/page-a.js class PageA extends React.Component { constructor(props) { super(props); // something here... } componentWillMount() { // fetching data } render() { // render page with data } } export default withAdminAuth(PageA);
复制代码


可能你已经发现了,高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。


什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为


可以提高代码的复用性和灵活性


再对高阶组件进行一个小小的总结:


  • 高阶组件 不是组件 一个把某个组件转换成另一个组件的 函数

  • 高阶组件的主要作用是 代码复用

  • 高阶组件是 装饰器模式在 React 中的实现


封装组件的原则


封装原则


1、单一原则:负责单一的页面渲染


2、多重职责:负责多重职责,获取数据,复用逻辑,页面渲染等


3、明确接受参数:必选,非必选,参数尽量设置以_开头,避免变量重复


4、可扩展:需求变动能够及时调整,不影响之前代码


5、代码逻辑清晰


6、封装的组件必须具有高性能,低耦合的特性


7、组件具有单一职责:封装业务组件或者基础组件,如果不能给这个组件起一个有意义的名字,证明这个组件承担的职责可能不够单一,需要继续抽组件,直到它可以是一个独立的组件即可

constructor

答案是:在 constructor 函数里面,需要用到props的值的时候,就需要调用 super(props)
复制代码


  1. class 语法糖默认会帮你定义一个 constructor,所以当你不需要使用 constructor 的时候,是可以不用自己定义的

  2. 当你自己定义一个 constructor 的时候,就一定要写 super(),否则拿不到 this

  3. 当你在 constructor 里面想要使用 props 的值,就需要传入 props 这个参数给 super,调用 super(props),否则只需要写 super()

解释 React 中 render() 的目的。

每个 React 组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 <form><group><div> 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。

何为受控组件(controlled component)

在 HTML 中,类似 <input>, <textarea><select> 这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange 会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。

React key 是干嘛用的 为什么要加?key 主要是解决哪一类问题的

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。


在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。


注意事项:


  • key 值一定要和具体的元素—一对应;

  • 尽量不要用数组的 index 去作为 key;

  • 不要在 render 的时候用随机数或者其他操作给元素加上不稳定的 key,这样造成的性能开销比不加 key 的情况下更糟糕。

Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别


  • Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可

  • Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可

  • Vuex 数据流的顺序是∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)


通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;


(2)共同思想


  • 单—的数据源

  • 变化可以预测


本质上∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。

用户头像

beifeng1996

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
react常见考点_前端_beifeng1996_InfoQ写作社区