共计 10817 个字符,预计需要花费 28 分钟才能阅读完成。
React diff 算法的原理是什么?
实际上,diff 算法探讨的就是虚构 DOM 树发生变化后,生成 DOM 树更新补丁的形式。它通过比照新旧两株虚构 DOM 树的变更差别,将更新补丁作用于实在 DOM,以最小老本实现视图更新。具体的流程如下:
- 实在的 DOM 首先会映射为虚构 DOM;
- 当虚构 DOM 发生变化后,就会依据差距计算生成 patch,这个 patch 是一个结构化的数据,内容蕴含了减少、更新、移除等;
-
依据 patch 去更新实在的 DOM,反馈到用户的界面上。
一个简略的例子:
import React from 'react'
export default class ExampleComponent extends React.Component {render() {if(this.props.isVisible) {return <div className="visible">visbile</div>;}
return <div className="hidden">hidden</div>;
}
}
这里,首先假设 ExampleComponent 可见,而后再扭转它的状态,让它不可见。映射为实在的 DOM 操作是这样的,React 会创立一个 div 节点。
<div class="visible">visbile</div>
当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写外部的 innerText 为 hidden。这样一个生成补丁、更新差别的过程统称为 diff 算法。
diff 算法能够总结为三个策略,别离从树、组件及元素三个层面进行复杂度的优化:
策略一:疏忽节点跨层级操作场景,晋升比对效率。(基于树进行比照)
这一策略须要进行树比对,即对树进行分层比拟。树比对的解决手法是十分“暴力”的,即两棵树只对同一档次的节点进行比拟,如果发现节点曾经不存在了,则该节点及其子节点会被齐全删除掉,不会用于进一步的比拟,这就晋升了比对效率。
策略二:如果组件的 class 统一,则默认为类似的树结构,否则默认为不同的树结构。(基于组件进行比照)
在组件比对的过程中:
- 如果组件是同一类型则进行树比对;
- 如果不是则间接放入补丁中。
只有父组件类型不同,就会被从新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 能够进步性能的起因。
策略三:同一层级的子节点,能够通过标记 key 的形式进行列表比照。(基于节点进行比照)
元素比对次要产生在同层级中,通过标记节点操作生成补丁。节点操作蕴含了插入、挪动、删除等。其中节点从新排序同时波及插入、挪动、删除三个操作,所以效率耗费最大,此时策略三起到了至关重要的作用。通过标记 key 的形式,React 能够间接挪动 DOM 节点,升高内耗。
React.forwardRef 是什么?它有什么作用?
React.forwardRef 会创立一个 React 组件,这个组件可能将其承受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特地有用:
- 转发 refs 到 DOM 组件
- 在高阶组件中转发 refs
React 的事件和一般的 HTML 事件有什么不同?
区别:
- 对于事件名称命名形式,原生事件为全小写,react 事件采纳小驼峰;
- 对于事件函数解决语法,原生事件为字符串,react 事件为函数;
- react 事件不能采纳 return false 的形式来阻止浏览器的默认行为,而必须要地明确地调用
preventDefault()
来阻止默认行为。
合成事件是 react 模仿原生 DOM 事件所有能力的一个事件对象,其长处如下:
- 兼容所有浏览器,更好的跨平台;
- 将事件对立寄存在一个数组,防止频繁的新增与删除(垃圾回收)。
- 不便 react 对立治理和事务机制。
事件的执行程序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为须要冒泡到 document 上合成事件才会执行。
父子组件的通信形式?
父组件向子组件通信:父组件通过 props 向子组件传递须要的信息。
// 子组件: Child
const Child = props =>{return <p>{props.name}</p>
}
// 父组件 Parent
const Parent = ()=>{return <Child name="react"></Child>}
子组件向父组件通信:: props+ 回调的形式。
// 子组件: Child
const Child = props =>{
const cb = msg =>{return ()=>{props.callback(msg)
}
}
return (<button onClick={cb("你好!")}> 你好 </button>
)
}
// 父组件 Parent
class Parent extends Component {callback(msg){console.log(msg)
}
render(){return <Child callback={this.callback.bind(this)}></Child>
}
}
constructor
答案是:在 constructor 函数外面,须要用到 props 的值的时候,就须要调用 super(props)
- class 语法糖默认会帮你定义一个 constructor,所以当你不须要应用 constructor 的时候,是能够不必本人定义的
- 当你本人定义一个 constructor 的时候,就肯定要写 super(),否则拿不到 this
- 当你在 constructor 外面想要应用 props 的值,就须要传入 props 这个参数给 super,调用 super(props),否则只须要写 super()
react 生命周期
初始化阶段:
- getDefaultProps: 获取实例的默认属性
- getInitialState: 获取每个实例的初始化状态
- componentWillMount:组件行将被装载、渲染到页面上
- render: 组件在这里生成虚构的 DOM 节点
- componentDidMount: 组件真正在被装载之后
运行中状态:
- componentWillReceiveProps: 组件将要接管到属性的时候调用
- shouldComponentUpdate: 组件承受到新属性或者新状态的时候(能够返回 false,接收数据后不更新,阻止 render 调用,前面的函数不会被继续执行了)
- componentWillUpdate: 组件行将更新不能批改属性和状态
- render: 组件从新描述
- componentDidUpdate: 组件曾经更新
销毁阶段:
- componentWillUnmount: 组件行将销毁
shouldComponentUpdate 是做什么的,(react 性能优化是哪个周期函数?)
shouldComponentUpdate 这个办法用来判断是否须要调用 render 办法从新描述 dom。因为 dom 的描述十分耗费性能,如果咱们能在 shouldComponentUpdate 办法中可能写出更优化的 dom diff 算法,能够极大的进步性能。
在 react17 会删除以下三个生命周期
componentWillMount,componentWillReceiveProps,componentWillUpdate
参考 前端进阶面试题具体解答
React 中的高阶组件使用了什么设计模式?
应用了装璜模式,高阶组件的使用:
function withWindowWidth(BaseComponent) {
class DerivedClass extends React.Component {
state = {windowWidth: window.innerWidth,}
onResize = () => {
this.setState({windowWidth: window.innerWidth,})
}
componentDidMount() {window.addEventListener('resize', this.onResize)
}
componentWillUnmount() {window.removeEventListener('resize', this.onResize);
}
render() {return <BaseComponent {...this.props} {...this.state}/>
}
}
return DerivedClass;
}
const MyComponent = (props) => {return <div>Window width is: {props.windowWidth}</div>
};
export default withWindowWidth(MyComponent);
装璜模式的特点是不须要扭转 被装璜对象 自身,而只是在里面套一个外壳接口。JavaScript 目前曾经有了原生装璜器的提案,其用法如下:
@testable
class MyTestableClass {}
**
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 中其余的场景简直都能够应用防抖和节流去进步响应性能。
setState 到底是异步还是同步?
先给出答案: 有时体现出异步, 有时体现出同步
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的setState
的“异步”并不是说外部由异步代码实现,其实自身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用程序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,造成了所谓的“异步”,当然能够通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的后果setState
的批量更新优化也是建设在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout
中不会批量更新,在“异步”中如果对同一个值进行屡次setState
,setState
的批量更新策略会对其进行笼罩,取最初一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新
Redux 外部原理 外部怎么实现 dispstch 一个函数的
以
redux-thunk
中间件作为例子,上面就是thunkMiddleware
函数的代码
// 局部转为 ES5 代码,运行 middleware 函数会返回一个新的函数,如下:return ({dispatch, getState}) => {
// next 理论就是传入的 dispatch
return function (next) {return function (action) {
// redux-thunk 外围
if (typeof action === 'function') {return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
}
redux-thunk
库外部源码十分的简略,容许action
是一个函数,同时反对参数传递,否则调用办法不变
redux
创立Store
:通过combineReducers
函数合并reducer
函数,返回一个新的函数combination
(这个函数负责循环遍历运行reducer
函数,返回全副state
)。将这个新函数作为参数传入createStore
函数,函数外部通过 dispatch,初始化运行传入的combination
,state 生成,返回 store 对象redux
中间件:applyMiddleware
函数中间件的次要目标就是批改dispatch
函数,返回通过中间件解决的新的dispatch
函数redux
应用:理论就是再次调用循环遍历调用reducer
函数,更新state
Redux Thunk 的作用是什么
Redux thunk 是一个容许你编写返回一个函数而不是一个 action 的 actions creators 的中间件。如果满足某个条件,thunk 则能够用来提早 action 的派发(dispatch),这能够解决异步 action 的派发(dispatch)。
如何配置 React-Router 实现路由切换
(1)应用<Route>
组件
路由匹配是通过比拟 <Route>
的 path 属性和以后地址的 pathname 来实现的。当一个 <Route>
匹配胜利时,它将渲染其内容,当它不匹配时就会渲染 null。没有门路的 <Route>
将始终被匹配。
// when location = {pathname: '/about'}
<Route path='/about' component={About}/> // renders <About/>
<Route path='/contact' component={Contact}/> // renders null
<Route component={Always}/> // renders <Always/>
(2)联合应用 <Switch>
组件和 <Route>
组件
<Switch>
用于将 <Route>
分组。
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
<Switch>
不是分组 <Route>
所必须的,但他通常很有用。一个 <Switch>
会遍历其所有的子 <Route>
元素,并仅渲染与以后地址匹配的第一个元素。
(3)应用 <Link>、<NavLink>、<Redirect>
组件
<Link>
组件来在你的应用程序中创立链接。无论你在何处渲染一个<Link>
,都会在应用程序的 HTML 中渲染锚(<a>
)。
<Link to="/">Home</Link>
// <a href='/'>Home</a>
是一种非凡类型的 当它的 to 属性与以后地址匹配时,能够将其定义为 ” 沉闷的 ”。
// location = {pathname: '/react'}
<NavLink to="/react" activeClassName="hurray">
React
</NavLink>
// <a href='/react' className='hurray'>React</a>
当咱们想强制导航时,能够渲染一个 <Redirect>
,当一个<Redirect>
渲染时,它将应用它的 to 属性进行定向。
高阶组件的利用场景
权限管制
利用高阶组件的 条件渲染 个性能够对页面进行权限管制,权限管制个别分为两个维度: 页面级别 和 页面元素级别
// 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、组件具备繁多职责:封装业务组件或者根底组件,如果不能给这个组件起一个有意义的名字,证实这个组件承当的职责可能不够繁多,须要持续抽组件,直到它能够是一个独立的组件即可
React 中有应用过 getDefaultProps 吗?它有什么作用?
通过实现组件的 getDefaultProps,对属性设置默认值(ES5 的写法):
var ShowTitle = React.createClass({getDefaultProps:function(){
return{title : "React"}
},
render : function(){return <h1>{this.props.title}</h1>
}
});
React 如何进行组件 / 逻辑复用?
抛开曾经被官网弃用的 Mixin, 组件形象的技术目前有三种比拟支流:
-
高阶组件:
- 属性代理
- 反向继承
- 渲染属性
- react-hooks
componentWillReceiveProps 调用机会
- 曾经被废除掉
- 当 props 扭转的时候才调用,子组件第二次接管到 props 的时候
调用 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 和 Vue 的了解,它们的异同
相似之处:
- 都将注意力集中放弃在外围库,而将其余性能如路由和全局状态治理交给相干的库
- 都有本人的构建工具,能让你失去一个依据最佳实际设置的我的项目模板。
- 都应用了 Virtual DOM(虚构 DOM)进步重绘性能
- 都有 props 的概念,容许组件间的数据传递
- 都激励组件化利用,将利用分拆成一个个性能明确的模块,进步复用性
不同之处:
1)数据流
Vue 默认反对数据双向绑定,而 React 始终提倡单向数据流
2)虚构 DOM
Vue2.x 开始引入 ”Virtual DOM”,打消了和 React 在这方面的差别,然而在具体的细节还是有各自的特点。
- Vue 声称能够更快地计算出 Virtual DOM 的差别,这是因为它在渲染过程中,会跟踪每一个组件的依赖关系,不须要从新渲染整个组件树。
- 对于 React 而言,每当利用的状态被扭转时,全副子组件都会从新渲染。当然,这能够通过 PureComponent/shouldComponentUpdate 这个生命周期办法来进行管制,但 Vue 将此视为默认的优化。
3)组件化
React 与 Vue 最大的不同是模板的编写。
- Vue 激励写近似惯例 HTML 的模板。写起来很靠近规范 HTML 元素,只是多了一些属性。
- React 举荐你所有的模板通用 JavaScript 的语法扩大——JSX 书写。
具体来讲:React 中 render 函数是反对闭包个性的,所以咱们 import 的组件在 render 中能够间接调用。然而在 Vue 中,因为模板中应用的数据都必须挂在 this 上进行一次直达,所以 import 完组件之后,还须要在 components 中再申明下。
4)监听数据变动的实现原理不同
- Vue 通过 getter/setter 以及一些函数的劫持,能准确晓得数据变动,不须要特地的优化就能达到很好的性能
- React 默认是通过比拟援用的形式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的从新渲染。这是因为 Vue 应用的是可变数据,而 React 更强调数据的不可变。
5)高阶组件
react 能够通过高阶组件(Higher Order Components– HOC)来扩大,而 vue 须要通过 mixins 来扩大。
起因高阶组件就是高阶函数,而 React 的组件自身就是纯正的函数,所以高阶函数对 React 来说大海捞针。相同 Vue.js 应用 HTML 模板创立视图组件,这时模板无奈无效的编译,因而 Vue 不采纳 HOC 来实现。
6)构建工具
两者都有本人的构建工具
- React ==> Create React APP
- Vue ==> vue-cli
7)跨平台
- React ==> React Native
- Vue ==> Weex
react-router 里的 <Link>
标签和 <a>
标签有什么区别
比照
<a>
,Link
组件防止了不必要的重渲染
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 是指父组件的 props
function 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
让咱们对父组件的所有子组件又更灵便的管制。