81、diff复杂度原理及具体过程画图
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。
Diff算法有三大策略:
- Tree Diff
- Component Diff
- Element Diff
三种策略的执行顺序也是顺序依次执行。
Tree Diff 是对树每一层进行遍历,找出不同,如图1所示。
图1 Tree Diff
Component Diff 是数据层面的差异比较
- 如果都是同一类型的组件(即:两节点是同一个组件类的两个不同实例,比如: 与 ),按照原策略继续比较Virtual DOM树即可
- 如果出现不是同一类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点
Element Diff真实DOM渲染,结构差异的比较
首先进行第一层比较,第一层都是R,不发生变化;然后进入第二层Component Diff,发现A组件没有,则删除A及其子组件B、C;最后比较第三层,创建A及其子组件B、C。
当节点处于同一层级时,Diff提供三种DOM操作:删除、移动、插入。
图2 Element Diff
如图2所示,首先将OldVnode 和 NewVnode的首尾位置分别标记为oldS、oldE、newS、newE。
(1) oldS和newS相同,不发生变化,oldS++,newS++。
oldS = a,oldE = d
newS = a, newE = c
(2) newS与OldVnode不匹配,oldS前面插入f,newS++。
oldS = b,oldE = d
newS = f, newE = c
(3) newS与oldE相同,oldE移动到oldS前面,newS++,oldE–。
oldS = b,oldE = d
newS = d, newE = c
(4) newE与oldE相同,不发生变化,newE–,oldE–。
oldS = b,oldE = c
newS = e, newE = c
(5) 都不相同,oldS前插入newE,删除oldS,oldS++,newS++,newE–,oldE–。
oldS = b,oldE = b
newS = e, newE = e
(6) oldS > oldE,Diff结束,最后结果为:a、f、d、e、c。
最后附上核心源码分析:
patch
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
这个函数做了以下事情:
- 找到对应的真实dom,称为el
- 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
- 如果oldVnode有子节点而Vnode没有,则删除el的子节点
- 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
// oldVnode起始位置小于结束位置并且newVnode起始位置小于结束位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// isUndef 用来判断对象是否等于undefined或者为空,是的话返回true
if (isUndef(oldStartVnode)) {
// oldVnode 起始位置oldS++
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// oldVnode 结束位置oldE--
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// oldS和newS相同,不变化,进行patch,oldS++,newS++
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// oldE和newE相同,不变化,进行patch,oldE--,newE--
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// oldS和newE相同,oldS移动到oldE之后,进行patch,oldS++,newE--
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// oldE和newS相同,oldE移动到oldS之前,进行patch,oldE--,newS++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 全都不相同情况下
// 获取oldVnode->index的key
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
// oldVnode->index为undefined或null,说明没有该元素,创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
// 获取oldVnode
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
// 创建的Vnode和newS相同,插入到oldS之前,进行patch
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// 相同的key但是不一样的element. 被视为新的element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 当oldS>oldE时,将newS至newE间的全部插入
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 当newS>newE,将oldS至oldE间的全部删除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}}
82、什么是虚拟DOM?
虚拟 DOM (VDOM)是真实 DOM 在内存中的表示。UI 的表示形式保存在内存中,并与实际的 DOM 同步。这是一个发生在渲染函数被调用和元素在屏幕上显示之间的步骤,整个过程被称为调和。
83 类组件和函数组件之间的区别是啥?
- 类组件可以使用其他特性,如状态
state
和生命周期钩子。 - 当组件只是接收
props
渲染到页面时,就是无状态组件,就属于函数组件,也被称为哑组件或展示组件。
函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。
区别函数组件类组件是否有 this
没有有是否有生命周期没有有是否有状态 state
没有有
84 React 中 refs 干嘛用的?
Refs
提供了一种访问在render
方法中创建的 DOM 节点或者 React 元素的方法。在典型的数据流中,props
是父子组件交互的唯一方式,想要修改子组件,需要使用新的pros
重新渲染它。凡事有例外,某些情况下咱们需要在典型数据流外,强制修改子代,这个时候可以使用 Refs
。
咱们可以在组件添加一个 ref
属性来使用,该属性的值是一个回调函数,接收作为其第一个参数的底层 DOM 元素或组件的挂载实例。
class UnControlledForm extends Component {
handleSubmit = () => {
console.log("Input Value: ", this.input.value)
}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input) => this.input = input} />
<button type='submit'>Submit</button>
</form>
)
}
}
请注意,input
元素有一个ref
属性,它的值是一个函数。该函数接收输入的实际 DOM 元素,然后将其放在实例上,这样就可以在 handleSubmit
函数内部访问它。
经常被误解的只有在类组件中才能使用 refs
,但是refs
也可以通过利用 JS 中的闭包与函数组件一起使用。
function CustomForm ({handleSubmit}) {
let inputElement
return (
<form onSubmit={() => handleSubmit(inputElement.value)}>
<input
type='text'
ref={(input) => inputElement = input} />
<button type='submit'>Submit</button>
</form>
)
}
85 在 React 中如何处理事件
为了解决跨浏览器的兼容性问题,SyntheticEvent
实例将被传递给你的事件处理函数,SyntheticEvent
是 React 跨浏览器的浏览器原生事件包装器,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
。
比较有趣的是,React 实际上并不将事件附加到子节点本身。React 使用单个事件侦听器侦听顶层的所有事件。这对性能有好处,也意味着 React 在更新 DOM 时不需要跟踪事件监听器。
86 vuex和redux的区别?
vuex是尤雨溪 开发的最佳实践用在VUE框架上的 ,redux并非react独有 ,是一个经典的状态管理设计。
87 Redux遵循的三个原则是什么?
(1)单一事实来源:
整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。
(2)状态是只读的:
改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
(3)使用纯函数进行更改:
为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。
88 如何创建 refs
Refs 是使用 React.createRef()
创建的,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs
分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
或者这样用:
class UserForm extends Component {
handleSubmit = () => {
console.log("Input Value is: ", this.input.value)
}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input) => this.input = input} /> // Access DOM input in handle submit
<button type='submit'>Submit</button>
</form>
)
}
}
89 什么是高阶组件?
高阶组件(HOC)是接受一个组件并返回一个新组件的函数。基本上,这是一个模式,是从 React 的组合特性中衍生出来的,称其为纯组件,因为它们可以接受任何动态提供的子组件,但不会修改或复制输入组件中的任何行为。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC 可以用于以下许多用例
- 代码重用、逻辑和引导抽象
- 渲染劫持
- state 抽象和操作
- props 处理
90 在构造函数调用 super
并将 props
作为参数传入的作用是啥?
在调用 super()
方法之前,子类构造函数无法使用this
引用,ES6 子类也是如此。将 props
参数传递给 super()
调用的主要原因是在子构造函数中能够通过this.props
来获取传入的 props
。
传递 props
class MyComponent extends React.Component {
constructor(props) {
super(props);
console.log(this.props); // { name: 'sudheer',age: 30 }
}
}
没传递 props
class MyComponent extends React.Component {
constructor(props) {
super();
console.log(this.props); // undefined
// 但是 Props 参数仍然可用
console.log(props); // Prints { name: 'sudheer',age: 30 }
}
render() {
// 构造函数外部不受影响
console.log(this.props) // { name: 'sudheer',age: 30 }
}
}
上面示例揭示了一点。props
的行为只有在构造函数中是不同的,在构造函数之外也是一样的。
91 什么是控制组件?
在 HTML 中,表单元素如 <input>
、<textarea>
和<select>
通常维护自己的状态,并根据用户输入进行更新。当用户提交表单时,来自上述元素的值将随表单一起发送。
而 React 的工作方式则不同。包含表单的组件将跟踪其状态中的输入值,并在每次回调函数(例如onChange
)触发时重新渲染组件,因为状态被更新。以这种方式由 React 控制其值的输入表单元素称为受控组件。
92 如何 React.createElement ?
问题:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
)
上述代码如何使用 React.createElement
来实现:
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
93 为什么不直接更新 state
呢 ?
如果试图直接更新 state
,则不会重新渲染组件。
// 错误
This.state.message = 'Hello world';
需要使用setState()
方法来更新 state
。它调度对组件state
对象的更新。当state
改变时,组件通过重新渲染来响应:
// 正确做法
This.setState({message: ‘Hello World’})
94 什么是 React Hooks?
Hooks是 React 16.8 中的新添加内容。它们允许在不编写类的情况下使用state
和其他 React 特性。使用 Hooks,可以从组件中提取有状态逻辑,这样就可以独立地测试和重用它。Hooks 允许咱们在不改变组件层次结构的情况下重用有状态逻辑,这样在许多组件之间或与社区共享 Hooks 变得很容易。
95 React 中的 useState()
是什么?
下面说明useState(0)
的用途:
...
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState(...);
...
const setCount = () => {
setCounter(count + 1);
setMoreStuff(...);
...
};
useState
是一个内置的 React Hook。useState(0)
返回一个元组,其中第一个参数count
是计数器的当前状态,setCounter
提供更新计数器状态的方法。
咱们可以在任何地方使用setCounter
方法更新计数状态-在这种情况下,咱们在setCount
函数内部使用它可以做更多的事情,使用 Hooks,能够使咱们的代码保持更多功能,还可以避免过多使用基于类的组件。
96 React 中的StrictMode(严格模式)是什么??
React 的StrictMode
是一种辅助组件,可以帮助咱们编写更好的 react 组件,可以使用<StrictMode />
包装一组组件,并且可以帮咱们以下检查:
- 验证内部组件是否遵循某些推荐做法,如果没有,会在控制台给出警告。
- 验证是否使用的已经废弃的方法,如果有,会在控制台给出警告。
- 通过识别潜在的风险预防一些副作用。
94 为什么类方法需要绑定到类实例?
在 JS 中,this
值会根据当前上下文变化。在 React 类组件方法中,开发人员通常希望 this
引用组件的当前实例,因此有必要将这些方法绑定到实例。通常这是在构造函数中完成的:
class SubmitButton extends React.Component {
constructor(props) {
super(props);
this.state = {
isFormSubmitted: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit() {
this.setState({
isFormSubmitted: true
});
}
render() {
return (
<button onClick={this.handleSubmit}>Submit</button>
)
}
}
95 什么是 prop drilling,如何避免?
在构建 React 应用程序时,在多层嵌套组件来使用另一个嵌套组件提供的数据。最简单的方法是将一个 prop
从每个组件一层层的传递下去,从源组件传递到深层嵌套组件,这叫做prop drilling。
prop drilling
的主要缺点是原本不需要数据的组件变得不必要地复杂,并且难以维护。
为了避免prop drilling
,一种常用的方法是使用React Context。通过定义提供数据的Provider
组件,并允许嵌套的组件通过Consumer
组件或useContext
Hook 使用上下文数据。
96、描述 Flux 与 MVC?
传统的 MVC 模式在分离数据(Model)、UI(View和逻辑(Controller)方面工作得很好,但是 MVC 架构经常遇到两个主要问题:
数据流不够清晰:跨视图发生的级联更新常常会导致混乱的事件网络,难于调试。
缺乏数据完整性:模型数据可以在任何地方发生突变,从而在整个UI中产生不可预测的结果。
使用 Flux 模式的复杂用户界面不再遭受级联更新,任何给定的React 组件都能够根据 store
提供的数据重建其状态。Flux 模式还通过限制对共享数据的直接访问来加强数据完整性。
97、受控组件和非受控组件区别是啥?
- 受控组件是 React 控制中的组件,并且是表单数据真实的唯一来源。
- 非受控组件是由 DOM 处理表单数据的地方,而不是在 React 组件中。
尽管非受控组件通常更易于实现,因为只需使用refs
即可从 DOM 中获取值,但通常建议优先选择受控制的组件,而不是非受控制的组件。
这样做的主要原因是受控组件支持即时字段验证,允许有条件地禁用/启用按钮,强制输入格式。
98、这段代码有什么问题吗?
这段代码有什么问题:
this.setState((prevState, props) => {
return {
streak: prevState.streak + props.count
}
})
答案:
没有什么问题。这种方式很少被使用,咱们可以将一个函数传递给setState
,该函数接收上一个 state
的值和当前的props
,并返回一个新的状态,如果咱们需要根据以前的状态重新设置状态,推荐使用这种方式。
99、什么是 React Context?
Context
通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props
属性。