面试题
1.说说对 React 的理解?有哪些特性?
-
raect是什么
-
React
是一个简单的javascript UI
库,用于构建高效、快速的用户界面。
它是一个轻量级库,因此很受欢迎。它遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。
它使用虚拟DOM
来有效地操作DOM
。
它遵循从高阶组件到低阶组件的单向数据流。
特性
-
React
特性有很多,如:JSX
语法- 单向数据绑定
- 虚拟
DOM
- 声明式编程
Component
优势
-
高效灵活
声明式的设计,简单使用
组件式开发,提高代码复用率
单向响应的数据流会比双向绑定的更安全,速度更快
2.说说Real diff算法是怎么运作的?
diff算法
在某一时间节点调用
React
的render
() 方法,会创建一棵由React
元素组成的树。在下一次state
或props
更新时,相同的render
() 方法会返回一棵不同的树。React
需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前UI
与最新的树保持同步。
React diff策略
Web UI
中DOM
节点跨层级的移动操作特别少,可以忽略不计。【永远只比较同层节点,不会跨层级比较节点。】- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一
key
进行区分
基于以上三个前提策略,React 分别对 tree diff
、component diff
** 以及 element diff
进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
tree层:
tree
层对DOM
节点的跨层级移动的操作忽略不计,只对相同层级的DOM
节点进行比较(即同一个父节点下的所有子节点),一旦发现节点不存在,直接删除掉该节点以及之下的所有子节点。
component层:
component
层是组件间的比较,有三种策略:
- 遇到同一类型的组件遵循
tree diff
,进行层级对比 - 遇到不同类型的组件,直接将这个不同的组件判断为脏组件,并替换该组件和之下所有的子节点
- 在同一类型的两个组件中,当知道这个组件的虚拟
dom
没有任何变化,就可以手动使用shouldComponentUpdate
()来判断组件是否需要进行diff
,进一步的提升了diff
效率和性能
注意:
避免使用结构相同但是类型不同的组件,由于类型不同的原因,
diff
会直接销毁该组件并重建,会造成的性能浪费;
对于同一类型并且没有变化的组件,合理使用shouldComponentUpdate
() 进行优化
element层:
element层对同一层级的元素节点进行比较,有三种情况:
- 面对全新的节点时,执行插入操作
- 面对多余的节点时,执行删除操作
- 面对换位的节点时,执行移动操作
react中key值的作用
key值就是每个元素节点对应的唯一标识,要进行数据的更新,需要进行新旧虚拟dom的对比,就需要对比子元素的key值是否有匹配项,如果有则会进行数据的更新;如果没有就需要进行删除和重新创建
3.说说React生命周期有哪些不同的阶段?每个阶段对应的方法是?
生命周期: React整个组件生命周期包括从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程
一共有三个阶段,分别为
-
挂载阶段(Mounting):已插入真实的Dom阶段
-
更新阶段(Updating):正在被重新渲染的阶段
-
卸载阶段(Unmounting):已移出真是dom阶段
旧版生命周期
新版生命周期
挂载阶段:
constructor(): 在 React 组件挂载之前,会调用它的构造函数。
componentWillMount(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用
更新运行阶段:
componentWillReceiveProps(): 在接受父组件改变后的props需要重新渲染组件时用到的比较多,外部组件传递频繁的时候会导致效率比较低
shouldComponentUpdate():用于控制组件重新渲染的生命周期,state发生变化,组件会进入重新渲染的流程,在这里return false可以阻止组件的更新
render(): render() 方法是 class 组件中唯一必须实现的方法。
componentWillUpdate(): shouldComponentUpdate返回true以后,组件进入重新渲染完成之前进入这个函数。
componentDidUpdate(): 每次state改变并重新渲染页面后都会进入这个生命周期
卸载或销毁阶段:
componentWillUnmount (): 在此处完成组件的卸载和数据的销毁。
4.说说你对react hook的理解?
React 中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
优势
- 代码逻辑聚合,逻辑复用
- HOC嵌套地狱
- 代替class
好处:
- 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
- 类定义更为复杂
---- 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
---- 时刻需要关注this的指向问题;
---- 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿; - 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离
注意:
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
- 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
- 不能在useEffect中使用useState,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
常用的 hooks
- useState()
- useEffect()
- useCallback()
- useMemo()
- useRef()
- useContext()
- useReducer()
Hooks的用法
useState():状态钩子
纯函数组件没有状态,useState()
用于设置和使用组件的状态属性。
const [state, setState] = useState(initialValue);
// state:初始的状态属性,指向状态当前值,类似this.state
// setState:修改状态属性值的函数,用来更新状态,类似setState
// initialValue:状态的初始值,该值会赋给state
注意:setState
的命名为:set+State(初始状态名),并且采用小驼峰命名法。例如[count, setCount]
、[name, setName]
示例:使用Hooks重写计数器
const Count = () => {
const [count, setCount] = useState(0); // 将0设置为count的初始值
const addCount = () => {
let newCount = count;
setCount(newCount += 1);
}
return (
<div>
<p>{count}</p>
<button onClick={addCount}>加1</button>
</div>
)
}
用函数组件实现了一个功能完全一样的计数器,代码看起来更加的轻便简洁,没有了继承,没有了渲染逻辑,没有了生命周期等。这就是hooks存在的意义。
useEffect():副作用钩子
useEffect()
是副作用的钩子,可以实现特定的功能,如异步请求。语法如下:
useEffect(() => {
// 回调函数,其中是要进行的异步操作代码
}, [array])
// [array]:useEffect执行的依赖,当该数组的值发生改变时,回调函数中的代码就会被指向
// 如果[array]省略,则表示不依赖,在每次渲染时回调函数都会执行
// 如果[array]是空数组,即useEffect第二项为[],表示只执行一次
示例:通过useEffect()
模拟异步加载数据。
const AsyncPage = () => {
// 首先设置loading状态为true
const [loading, setLoading] = useState(true);
useEffect(() => {
// 2秒后将loading状态设置为false
setTimeout(() => {
setLoading(false);
}, 2000);
})
return (
// 判断loading是否为true,是就显示loading,不是就显示异步请求完成
loading ? <p>loading...</p> : <p>异步请求完成</p>
)
}
示例:useEffect()
依赖第二项数组变化
const AsyncPage = ({name}) => {
const [loading, setLoading] = useState(true); // 设置loading状态为true
const [person, setPerson] = useState({}); // 设置person状态为空对象
useEffect(() => {
// 首先设置loading为true,2秒后改为false,name改成传过来的参数
setLoading(true);
setTimeout(() => {
setLoading(false);
setPerson({name});
}, 2000);
}, [name]); // 表示当name修改才会执行回调函数
return (
<>
{loading ? <p>Loading...</p> : <p>{person.name}</p>}
</>
)
}
const PersonPage = () => {
// 设置初始state为空字符串
const [state, setState] = useState("");
const changeName = (name) => { // 修改name的函数
setState(name);
}
return (
<>
{/*首先将state传给name*/}
<AsyncPage name={state}/>
<button onClick={() => { // 点击按钮后将张三传给name
changeName("张三")
}}>张三
</button>
<button onClick={() => {
changeName("李四")
}}>李四
</button>
</>
)
}
useEffect和useLayoutEffect的区别
useEffect()
和useLayoutEffect()
主要的区别是调用时机不同。
useLayoutEffect()
和componentDidMount()
及componentDidUpate()
一致,再react完成DOM更新后马上同步调用代码,它会阻塞页面的渲染,而useEffect()
则会在页面渲染完后才会异步调用。
在实际使用中如果项避免页面抖动,可以把需要操作DOM的代码放在useLayoutEffect()
中,在该函数中做DOM操作,这些DOM修改会和react做出的更改一起被一次性渲染到屏幕上,只有一次回流重绘的代价。
useCallback(): 记忆函数
useCallback()
为记忆函数,它可以防止因为组件重新渲染,导致方法被重新创建,起到缓存作用。语法如下:
useCallback(() => {
// 回调函数,当array改变后,该函数才会重新声明
}, [array])
// 如果[array]为空数组,那么就是第一次创建后就被缓存,如果后期array改变了,拿到的还是老的array
// 如果不传入第二个参数,每次都会重新声明一次,拿到的就是最新的array
比如说下面一段代码中,我们可以看到有很多的函数,当我们在return中修改一个状态,就会导致整个页面重新渲染,那么这些函数(handleChange1、handleChange2…)也会被重新创建,这样会造成性能的浪费,因此可以使用useCallback将这些函数缓存下来,这样下一次页面重新渲染的时候,某些函数就不会重新被创建了。
const UseCallback = function () {
const handleChange1 = () => {
// 具体代码
}
const handleChange2 = () => {
// 具体代码
}
const handleChange3 = () => {
// 具体代码
}
const handleChange4 = () => {
// 具体代码
}
return (
<div>
{/*具体代码*/}
</div>
)
}
使用useCallback()
时,只需要将其写在整个函数外部即可,上面代码使用useCallback()
后效果如下,每当依赖项改变时,该函数才会被重新创建,如果依赖项不变,则不会重新创建。
const UseCallback = function () {
const handleChange1 = useCallback(
() => {
// 具体代码
}, [依赖项]
)
const handleChange2 = useCallback(
() => {
// 具体代码
}, [依赖项]
)
const handleChange3 = useCallback(
() => {
// 具体代码
}, [依赖项]
)
const handleChange4 = useCallback(
() => {
// 具体代码
}, [依赖项]
)
return (
<div>
{/*具体代码*/}
</div>
)
}
**useMemo():**记忆组件
useCallback()
的功能可以由useMemo()
所替代,useMemo()
也可以返回一个记忆函数,语法如下:
useMemo(() => fn, [])
// useCallback(fn, []) = useMemo(() => fn, [])
useCallback()
与useMemo()
的区别:
useCallback()
不会执行第一个参数函数,而是将其返回,useMemo()
会执行第一个函数并且将函数执行结果返回给你。useCallback()
常用记忆时间按函数,生成记忆后的时间函数传递给子组件使用,useMemo()
更适合经过函数计算得到一个确定的只,比如记忆组件。
**useRef():**保存引用值
useRef()
等价于类组件中的React.createRef()
,语法如下:
const loginRef = useRef();
使用useRef()
创建了一个值后,就可以将其绑定到DOM节点上,给DOM节点增加一个ref属性,将loginRef传入,则可以通过其current
属性获取到DOM节点的值,语法如下:
<input ref={loginRef}/>
除此之外,我们都知道useState()
可以保存一个状态,那么另一个方法就是使用useRef()
,为useRef()
传入一个初始值,它可以帮助我们记住这个状态。
useContext()
:共享状态钩子
useContext()
可以共享状态,作用是进行状态的分发(React16.x以后的版本支持),避免了使用Props进行数据的传递。语法如下:
// 第一步:创建全局的Context
const AppContext = React.createContext([初始化参数])
// 第二步:通过全局的Context进行状态值的共享
<AppContext.Provider value={{ 属性名: 值 }}>
<组件1 />
<组件2 />
</AppContext>
示例:A组件和B组件共享一个状态
const Count = () => {
const AppContext = React.createContext({});
const A = () => {
const {name} = useContext(AppContext);
return (
<div>
我是A组件,我的名字是:{name}
</div>
)
}
const B = () => {
const {name} = useContext(AppContext);
return (
<div>
我是B组件,我的名字是:{name}
</div>
)
}
return (
<AppContext.Provider value={{name: "橘猫吃不胖"}}>
<A/>
<B/>
</AppContext.Provider>
)
}
useReducer():Action钩子
在使用React
的过程中,如遇到状态管理,一般会用到Redux
,而React
本身是不提供状态管理的。而useReducer()
提供了状态管理。
useReducer()
是useState()
的替代方案。首先,关于redux
我们都知道,其原理是通过用户在页面中发起action
,从而通过reducer
方法来改变state
,从而实现页面和状态的通信。而Reducer
的形式是(state, action) => newstate
,返回当前的 state
以及与其配套的 dispatch
方法。。Hooks的useReducer()
是这样的:
const [state, dispatch] = useReducer(reducer, initialState)
它接受reducer
函数和状态的初始值作为参数,返回一个数组,其中第一项为当前的状态值,第二项为发送action
的dispatch
函数。
在某些场景下,useReducer
会比 useState
更适用,例如 state
逻辑较复杂且包含多个子值,或者下一个 state
依赖于之前的 state
等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为可以向子组件传递 dispatch
而不是回调函数。
例如:使用useReducer()实现一个计数器
const Count = () => {
const reducer = (state, action) => {
if (action.type == "add") {
return {
...state,
count: state.count + 1
}
} else {
return state
}
}
const addCount = () => {
dispatch({
type: "add"
})
}
const [state, dispatch] = useReducer(reducer, {count: 0});
return (
<>
<p>{state.count}</p>
<button onClick={addCount}>加1</button>
</>
)
}
通过代码可以看到,使用useReducer()
代替了Redux
的功能,但useReducer
无法提供中间件等功能,假如有这些需求,还是需要用到redux
。
5.说说你对受控组件和非受控组件的理解?应用场景?
受控组件:即通过setState的形式控制输入的值及更新,
非受控组件:即通过dom的形式更新值,要获取其值可以通过ref的形式去获取。
受控组件
受控组件,简单来讲,就是受我们控制的组件,组件的状态全程响应外部数据
举个简单的例子:
class TestComponent extends React.Component {
constructor (props) {
super(props);
this.state = { username: 'lindaidai' };
}
render () {
return <input name="username" value={this.state.username} />
}
}
这时候当我们在输入框输入内容的时候,会发现输入的内容并无法显示出来,也就是input
标签是一个可读的状态
这是因为value
被this.state.username
所控制住。当用户输入新的内容时,this.state.username
并不会自动更新,这样的话input
内的内容也就不会变了
如果想要解除被控制,可以为input
标签设置onChange
事件,输入的时候触发事件函数,在函数内部实现state
的更新,从而导致input
框的内容页发现改变
因此,受控组件我们一般需要初始状态和一个状态更新事件函数
非受控组件
非受控组件,简单来讲,就是不受我们控制的组件
一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态
当需要时,可以使用ref
查询 DOM 并查找其当前值,如下:
import React, { Component } from 'react';
export class UnControll extends Component {
constructor (props) {
super(props);
this.inputRef = React.createRef();
}
handleSubmit = (e) => {
console.log('我们可以获得input内的值为', this.inputRef.current.value);
e.preventDefault();
}
render () {
return (
<form onSubmit={e => this.handleSubmit(e)}>
<input defaultValue="lindaidai" ref={this.inputRef} />
<input type="submit" value="提交" />
</form>
)
}
}
应用场景
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React
组件负责处理
如果选择非受控组件的话,控制能力较弱,表单数据就由DOM本身处理,但更加方便快捷,代码量少
针对两者的区别,其应用场景如下图所示:
6.谈谈你对react事件处理的理解?
React事件机制
React基于
浏览器事件机制
实现了一套自己的事件机制,包括:事件注册
、事件合成
、事件冒泡
、事件触发
等。
React中的事件处理是什么?
React 合成事件(
SyntheticEvent
)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
为什么要使用合成事件?
- 进行浏览器兼容,实现更好的跨平台
- 避免垃圾回收
- 方便事件统一管理
合成事件与原生事件的区别?
- React 事件的命名采用小驼峰式(
camelCase
),而不是纯小写。 - 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
- 合成事件必须显式的使用
preventDefault
来组织默认行为,原生事件中可通过return false
来实现 - 使用合成事件不需要使用
addEventListener
为已创建的 DOM 元素添加监听器。只需要在该元素初始渲染的时候添加监听器即可。 - 向合成事件传参须通过箭头函数或
Function.prototype.bind
来实现
合成事件与原生事件的执行顺序
React 所有事件都挂载在
document
对象上;当真实 DOM 元素触发事件,会冒泡到document
对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行document
上挂载的事件。
所有的原生事件都有对应的合成事件吗?
不是。「几乎」所有事件都代理到了
document
,说明有例外,比如audio
、video
标签的一些媒体事件(如onplay
、onpause
等),是document
所不具有,这些事件只能够在这些标签上进行事件进行代理,但依旧用统一的入口分发函数(dispatchEvent
)进行绑定。
React事件机制总结
React
上注册的事件最终会绑定在document
这个DOM
上,而不是React
组件对应的DOM
(减少内存开销就是因为所有的事件都绑定在document
上,其他节点没有绑定事件)React
自身实现了一套事件冒泡机制,所以这也就是为什么我们event.stopPropagation()
无效的原因。React
通过队列的形式,从触发的组件向父组件回溯,然后调用他们JSX
中定义的callback
React
有一套自己的合成事件SyntheticEvent
7.谈谈你对redux的理解?
redux是什么
React
是用于构建用户界面的,帮助我们解决渲染DOM
的过程
而在整个应用中会存在很多个组件,每个组件的state
是由自身进行管理,包括组件定义自身的state
、组件之间的通信通过props
传递、使用Context
实现数据共享
如果让每个组件都存储自身相关的状态,理论上来讲不会影响应用的运行,但在开发及后续维护阶段,我们将花费大量精力去查询状态的变化过程
这种情况下,如果将所有的状态进行集中管理,当需要更新状态的时候,仅需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的
redux
就是一个实现上述集中管理的容器,遵循三大基本原则:
- 单一数据源
- state 是只读的
- 使用纯函数来执行修改
注意的是,redux
并不是只应用在react
中,还与其他界面库一起使用,如Vue
redux工作原理
redux
要求我们把数据都放在 store
公共存储空间
一个组件改变了 store
里的数据内容,其他组件就能感知到 store
的变化,再来取数据,从而间接的实现了这些数据传递的功能
工作流程图如下所示:
根据流程图,可以想象,React Components
是借书的用户, Action Creactor
是借书时说的话(借什么书), Store
是图书馆管理员,Reducer
是记录本(借什么书,还什么书,在哪儿,需要查一下), state
是书籍信息
整个流程就是借书的用户需要先存在,然后需要借书,需要一句话来描述借什么书,图书馆管理员听到后需要查一下记录本,了解图书的位置,最后图书馆管理员会把这本书给到这个借书人
转换为代码是,React Components
需要获取一些数据, 然后它就告知 Store
需要获取数据,这就是就是 Action Creactor
, Store
接收到之后去 Reducer
查一下, Reducer
会告诉 Store
应该给这个组件什么数据
redux如何使用
创建一个store
的公共数据区域
import { createStore } from 'redux' // 引入一个第三方的方法
const store = createStore() // 创建数据的公共存储区域(管理员)
还需要创建一个记录本去辅助管理数据,也就是reduecer
,本质就是一个函数,接收两个参数state
,action
,返回state
// 设置默认值
const initialState = {
counter: 0
}
const reducer = (state = initialState, action) => {
}
然后就可以将记录本传递给store
,两者建立连接。如下:
const store = createStore(reducer)
如果想要获取store里面的数据,则通过store.getState()
来获取当前state
console.log(store.getState());
下面再看看如何更改store
里面数据,是通过dispatch
来派发action
,通常action
中都会有type
属性,也可以携带其他的数据
store.dispatch({
type: "INCREMENT"
})
store.dispath({
type: "DECREMENT"
})
store.dispatch({
type: "ADD_NUMBER",
number: 5
})
下面再来看看修改reducer
中的处理逻辑:
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}
注意,reducer
是一个纯函数,不需要直接修改state
这样派发action
之后,既可以通过store.subscribe
监听store
的变化,如下:
store.subscribe(() => {
console.log(store.getState());
})
在React
项目中,会搭配react-redux
进行使用
完整代码如下:
const redux = require('redux');
const initialState = {
counter: 0
}
// 创建reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}
// 根据reducer创建store
const store = redux.createStore(reducer);
store.subscribe(() => {
console.log(store.getState());
})
// 修改store中的state
store.dispatch({
type: "INCREMENT"
})
// console.log(store.getState());
store.dispatch({
type: "DECREMENT"
})
// console.log(store.getState());
store.dispatch({
type: "ADD_NUMBER",
number: 5
})
// console.log(store.getState());
小结
createStore
可以帮助创建store
store.dispatch
帮助派发action
,action
会传递给store
store.getState
这个方法可以帮助获取store
里边所有的数据内容store.subscrible
方法订阅store
的改变,只要store
发生改变,store.subscrible
这个函数接收的这个回调函数就会被执行
8.讲讲 Redux 和 Vuex区别?
相同点
- state 共享数据
- 流程一致:定义全局state,触发,修改state
- 原理相似,通过全局注入store。
不同点
- 从实现原理上来说:
Redux
使用的是不可变数据,而Vuex
的数据是可变的。Redux
每次都是用新的state替换旧的state
,而Vuex
是直接修改Redux
在检测数据变化的时候,是通过diff
的方式比较差异的,而Vuex
其实和Vue
的原理一样,是通过getter/setter
来比较的
- 从表现层来说:
vuex
定义了state、getter、mutation、action
四个对象;redux
定义了state、reducer、action
。vuex
中state
统一存放,方便理解;reduxstate
依赖所有reducer
的初始值vuex
有getter
,目的是快捷得到state
;redux
没有这层,react-redux mapStateToProps
参数做了这个工作。vuex
中mutation
只是单纯赋值(很浅的一层);redux
中reducer
只是单纯设置新state
(很浅的一层)。他俩作用类似,但书写方式不同vuex
中action
有较为复杂的异步ajax
请求;redux
中action
中可简单可复杂,简单就直接发送数据对象({type:xxx, your-data}
),复杂需要调用异步ajax
(依赖redux-thunk
插件)。vuex
触发方式有两种commit
同步和dispatch
异步;redux
同步和异步都使用dispatch
通俗点理解就是,
vuex
弱化dispatch
,通过commit
进行store
状态的一次更变;取消了action
概念,不必传入特定的action
形式进行指定变更;弱化reducer
,基于commit
参数直接对数据进行转变,使得框架更加简易;
共同思想
- 单一的数据源
- 变化可以预测
本质上∶ redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案。
9.说说Connect组件用法及原理?
1. connect用法
作用:连接React组件与 Redux store
connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])
// 这个函数允许我们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => {
return {
count: state.count
}
}
- 这个函数的第一个参数就是
Redux
的store
,我们从中摘取了count
属性。你不必将state
中的数据原封不动地传入组件,可以根据state
中的数据,动态地输出组件需要的(最小)属性 - 函数的第二个参数
ownProps
,是组件自己的props
当
state
变化,或者ownProps
变化的时候,mapStateToProps
都会被调用,计算出一个新的stateProps
,(在与ownProps merge
后)更新给组件
mapDispatchToProps(dispatch, ownProps): dispatchProps
connect
的第二个参数是mapDispatchToProps
,它的功能是,将action
作为props
绑定到组件上,也会成为MyComp
的props
2. 原理解析
首先
connect
之所以会成功,是因为Provider
组件
- 在原应用组件上包裹一层,使原来整个应用成为Provider的子组件
- 接收
Redux
的store
作为props
,通过context
对象传递给子孙组件上的connect
connect
做了些什么
它真正连接
Redux
和React
,它包在我们的容器组件的外一层,它接收上面Provider
提供的store
里面的state
和dispatch
,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件
3. 源码
connect
是一个高阶函数,首先传入mapStateToProps
、mapDispatchToProps
,然后返回一个生产Component
的函数(wrapWithConnect
),然后再将真正的Component
作为参数传入wrapWithConnect
,这样就生产出一个经过包裹的Connect
组件,该组件具有如下特点
- 通过
props.store
获取祖先Component
的store props
包括stateProps
、dispatchProps
、parentProps
,合并在一起得到nextState
,作为props
传给真正的Component
componentDidMount
时,添加事件this.store.subscribe(this.handleChange)
,实现页面交互shouldComponentUpdate
时判断是否有避免进行渲染,提升页面性能,并得到nextState
componentWillUnmount
时移除注册的事件this.handleChange
// 主要逻辑
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
// 从祖先Component处获得store
this.store = props.store || context.store
this.stateProps = computeStateProps(this.store, props)
this.dispatchProps = computeDispatchProps(this.store, props)
this.state = { storeState: null }
// 对stateProps、dispatchProps、parentProps进行合并
this.updateState()
}
shouldComponentUpdate(nextProps, nextState) {
// 进行判断,当数据发生改变时,Component重新渲染
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
componentDidMount() {
// 改变Component的state
this.store.subscribe(() = {
this.setState({
storeState: this.store.getState()
})
})
}
render() {
// 生成包裹组件Connect
return (
<WrappedComponent {...this.nextState} />
)
}
}
Connect.contextTypes = {
store: storeShape
}
return Connect;
}
}
10.说说React Jsx转换成真实DOM过程?
什么是JSX?
JSX
即JavaScript XML
。一种在React
组件内部构建标签的类XML
语法。JSX
为react.js
开发的一套语法糖,也是react.js
的使用基础。React
在不使用JSX
的情况下一样可以工作,然而使用JSX
可以提高组件的可读性,因此推荐使用JSX
。
class MyComponent extends React.Component {
render() {
let props = this.props;
return (
<div className="my-component">
<a href={props.url}>{props.name}</a>
</div>
);
}
}
jsx的优点
- 允许使用熟悉的语法来定义
HTML
元素树; - 提供更加语义化且移动的标签;
- 程序结构更容易被直观化;
- 抽象了
React Element
的创建过程; - 可以随时掌控
HTML
标签以及生成这些标签的代码; - 是原生的
JavaScript
。
react 中jsx语法糖的本质
Jsx
是语法糖,实质是js
函数,需要babel
来解析,核心函数是React.createElement(tag,{attrbuties},children)
,参数tag
是标签名可以是html
标签和组件名,attrbuties
参数是标签的属性,children
参数是tag
的子元素。用来创建一个vnode
,最后渲染到页面上。
React Jsx转换成真实DOM是什么
react
通过将组件编写的JSX
映射到屏幕,以及组件中的状态发生了变化之后 React
会将这些「变化」更新到屏幕上
在前面文章了解中,JSX
通过babel
最终转化成React.createElement
这种形式,例如:
<div>
<img src="avatar.png" className="profile" />
<Hello />
</div>
会被bebel
转化成如下:
React.createElement(
"div",
null,
React.createElement("img", {
src: "avatar.png",
className: "profile"
}),
React.createElement(Hello, null)
);
在转化过程中,babel
在编译时会判断 JSX
中组件的首字母:
- 当首字母为小写时,其被认定为原生
DOM
标签,createElement
的第一个变量被编译为字符串 - 当首字母为大写时,其被认定为自定义组件,
createElement
的第一个变量被编译为对象
最终都会通过RenderDOM.render(...)
方法进行挂载,如下:
ReactDOM.render(<App />, document.getElementById("root"));
过程
在react
中,节点大致可以分成四个类别:
- 原生标签节点
- 文本节点
- 函数组件
- 类组件
如下所示:
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
<h3>ClassComponent</h3>
<p className={this.props.color}>{this.props.name}</p >
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
FunctionComponent
<p>{props.name}</p >
</div>
);
}
const jsx = (
<div className="border">
<p>xx</p >
< a href=" ">xxx</ a>
<FunctionComponent name="函数组件" />
<ClassComponent name="类组件" color="red" />
</div>
);
这些类别最终都会被转化成React.createElement
这种形式
React.createElement
其被调用时会传⼊标签类型type
,标签属性props
及若干子元素children
,作用是生成一个虚拟Dom
对象,如下所示:
function createElement(type, config, ...children) {
if (config) {
delete config.__self;
delete config.__source;
}
// ! 源码中做了详细处理,⽐如过滤掉key、ref等
const props = {
...config,
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
return {
type,
props
};
}
function createTextNode(text) {
return {
type: TEXT,
props: {
children: [],
nodeValue: text
}
};
}
export default {
createElement
};
createElement
会根据传入的节点信息进行一个判断:
- 如果是原生标签节点,
type
是字符串,如div
、span
- 如果是文本节点,
type
就没有,这里是TEXT
- 如果是函数组件,
type
是函数名 - 如果是类组件,
type
是类名
虚拟DOM
会通过ReactDOM.render
进行渲染成真实DOM
,使用方法如下:
ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM
元素都会被替换,后续的调用则会使用 React
的 diff
算法进行高效的更新
如果提供了可选的回调函数callback
,该回调将在组件被渲染或更新之后被执行
render
大致实现方法如下:
function render(vnode, container) {
console.log("vnode", vnode); // 虚拟DOM对象
// vnode _> node
const node = createNode(vnode, container);
container.appendChild(node);
}
// 创建真实DOM节点
function createNode(vnode, parentNode) {
let node = null;
const {type, props} = vnode;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else if (typeof type === "function") {
node = type.isReactComponent
? updateClassComponent(vnode, parentNode)
: updateFunctionComponent(vnode, parentNode);
} else {
node = document.createDocumentFragment();
}
reconcileChildren(props.children, node);
updateNode(node, props);
return node;
}
// 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中
function reconcileChildren(children, node) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (Array.isArray(child)) {
for (let j = 0; j < child.length; j++) {
render(child[j], node);
}
} else {
render(child, node);
}
}
}
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}
// 返回真实dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
const {type, props} = vnode;
let vvnode = type(props);
const node = createNode(vvnode, parentNode);
return node;
}
// 返回真实dom节点
// 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
const {type, props} = vnode;
let cmp = new type(props);
const vvnode = cmp.render();
const node = createNode(vvnode, parentNode);
return node;
}
export default {
render
};
总结
在react
源码中,虚拟Dom
转化成真实Dom
整体流程如下图所示:
其渲染流程如下所示:
- 使用
React.createElement
或JSX
编写React
组件,实际上所有的JSX
代码最后都会转换成React.createElement(...)
,Babel
帮助我们完成了这个转换的过程。 createElement
函数对key
和ref
等特殊的props
进行处理,并获取defaultProps
对默认props
进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM
对象ReactDOM.render
将生成好的虚拟DOM
渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
11.React 组件间怎么进行通信?
通信是什么
我们将组件间通信可以拆分为两个词:
- 组件
- 通信
React
的组件灵活多样,按照不同的方式可以分成很多类型的组件
而通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的,广义上,任何信息的交通都是通信
组件间通信
即指:组件通过某种方式来传递信息
以达到某个目的
如何通信
组件传递的方式有很多种,根据传送者
和接收者
可以分为如下:
- 父组件向子组件传递
- 子组件向父组件传递
- 兄弟组件之间的通信
- 父组件向后代组件传递
- 非关系组件传递
父组件向子组件传递
由于React
的数据流动为单向的,父组件向子组件传递是最常见的方式
父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过
props
属性就能接收父组件传递过来的参数
function EmailInput(props) {
return (
<label>
Email: <input value={props.email} />
</label>
);
}
const element = <EmailInput email="123124132@163.com" />;
子组件向父组件传递
子组件向父组件通信的基本思路是,父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
父组件对应代码如下:
class Parents extends Component {
constructor() {
super();
this.state = {
price: 0
};
}
getItemPrice(e) {
this.setState({
price: e
});
}
render() {
return (
<div>
<div>price: {this.state.price}</div>
{/* 向子组件中传入一个函数 */}
<Child getPrice={this.getItemPrice.bind(this)} />
</div>
);
}
}
子组件对应代码如下:
class Child extends Component {
clickGoods(e) {
// 在此函数中传入值
this.props.getPrice(e);
}
render() {
return (
<div>
<button onClick={this.clickGoods.bind(this, 100)}>goods1</button>
<button onClick={this.clickGoods.bind(this, 1000)}>goods2</button>
</div>
);
}
}
兄弟组件之间的通信
如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过使用父组件传递
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {count: 0}
}
setCount = () => {
this.setState({count: this.state.count + 1})
}
render() {
return (
<div>
<SiblingA
count={this.state.count}
/>
<SiblingB
onClick={this.setCount}
/>
</div>
);
}
}
父组件向后代组件传递
父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样
使用context
提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据
通过使用React.createContext
创建一个context
const PriceContext = React.createContext('price')
context
创建成功后,其下存在Provider
组件用于创建数据源,Consumer
组件用于接收数据,使用实例如下:
Provider
组件通过value
属性用于给后代组件传递数据:
<PriceContext.Provider value={100}>
</PriceContext.Provider>
如果想要获取Provider
传递的数据,可以通过Consumer
组件或者或者使用contextType
属性接收,对应分别如下:
class MyClass extends React.Component {
static contextType = PriceContext;
render() {
let price = this.context;
/* 基于这个值进行渲染工作 */
}
}
Consumer
组件:
<PriceContext.Consumer>
{ /*这里是一个函数*/ }
{
price => <div>price:{price}</div>
}
</PriceContext.Consumer>
非关系组件传递
如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如redux
。关于redux
的使用后续再详细介绍
总结
由于React
是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值
因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中
12.说说你对fiber架构的理解?解决了什么问题?
React 的核心流程可以分为两个部分:
reconciliation
(调度算法,也可称为render
)- 更新
state
与props
; - 调用生命周期钩子;
- 生成
virtual dom
- 这里应该称为
Fiber Tree
更为符合;
- 这里应该称为
- 通过新旧
vdom
进行diff
算法,获取vdom change
- 确定是否需要重新渲染
- 更新
commit
- 如需要,则操作
dom
节点更新
- 如需要,则操作
要了解 Fiber,我们首先来看为什么需要它
- 问题:
- 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。而关键点,便是 同步阻塞。在之前的调度算法中,
React
需要实例化每个类组件,生成一颗组件树,使用 同步递归 的方式进行遍历渲染,而这个过程最大的问题就是无法 暂停和恢复。
- 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。而关键点,便是 同步阻塞。在之前的调度算法中,
- 解决方案:
- 解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而
React Fiber
便是为了实现任务分割而诞生的
- 解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而
- 简述
- 在
React V16
将调度算法进行了重构, 将之前的stack reconciler
重构成新版的fiber reconciler
,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启 - 这里我理解为是一种 任务分割调度算法,主要是 将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制
- 在
- 核心
Fiber
这里可以具象为一个 数据结构
class Fiber {
constructor(instance) {
this.instance = instance
// 指向第一个 child 节点
this.child = child
// 指向父节点
this.return = parent
// 指向第一个兄弟节点
this.sibling = previous
}
}
- 链表树遍历算法: 通过 节点保存与映射,便能够随时地进行 停止和重启, 这样便能达到实现任务分割的基本前提
- 首先通过不断遍历子节点,到树末尾;
- 开始通过
sibling
遍历兄弟节点; return
返回父节点,继续执行2;- 直到 root
节点
后,跳出遍历;
- 任务分割,React 中的渲染更新可以分成两个阶段
- reconciliation 阶段: vdom 的数据对比,是个适合拆分的阶段,比如对比一部分树后,先暂停执行个动画调用,待完成后再回来继续比对
- Commit 阶段: 将 change list 更新到 dom 上,并不适合拆分,才能保持数据与 UI 的同步。否则可能由于阻塞 UI 更新,而导致数据更新和 UI 不一致的情况
- 分散执行: 任务分割后,就可以把小任务单元分散到浏览器的空闲期间去排队执行,而实现的关键是两个新API:
requestIdleCallback
与requestAnimationFrame
- 低优先级的任务交给
requestIdleCallback
处理,这是个浏览器提供的事件循环空闲期的回调函数,需要pollyfill
,而且拥有deadline
参数,限制执行事件,以继续切分任务; - 高优先级的任务交给
requestAnimationFrame
处理;
- 低优先级的任务交给
// 类似于这样的方式
requestIdleCallback((deadline) => {
// 当有空闲时间时,我们执行一个组件渲染;
// 把任务塞到一个个碎片时间中去;
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent);
}
});
- 优先级策略:
文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
React的Fiber工作原理,解决了什么问题
React Fiber
是一种基于浏览器的单线程调度算法。
React Fiber
用类似 requestIdleCallback
的机制来做异步 diff
。但是之前数据结构不支持这样的实现异步 diff
,于是 React
实现了一个类似链表的数据结构,将原来的 递归diff
变成了现在的 遍历diff
,这样就能做到异步可更新了
Fiber
其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber
)。- 核心思想是 任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。
- 当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路。
Fiber 是什么
Fiber
的中文翻译叫纤程,与进程、线程同为程序执行过程,Fiber
就是比线程还要纤细的一个过程。纤程意在对渲染过程实现进行更加精细的控制。
在react
中,主要做了以下的操作:
- 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
- 增加了异步任务,调用
requestIdleCallback api
,浏览器空闲的时候执行 dom diff
树变成了链表,一个dom
对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber
是对 React
核心算法(即调和过程)的重写。
从编码角度来看,Fiber
是 React
内部所定义的一种数据结构,它是 Fiber
树结构的节点单位,也就是 React 16
新架构下的"虚拟 DOM
"。
一个 fiber
就是一个 JavaScript
对象,Fiber
的数据结构如下:
type Fiber = {
// 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
tag: WorkTag,
// ReactElement里面的key
key: null | string,
// ReactElement.type,调用`createElement`的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 表示当前代表的节点类型
type: any,
// 表示当前FiberNode对应的element组件实例
stateNode: any,
// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,
ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
// 当前处理过程中的组件props对象
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,
// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的state
memoizedState: any,
// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,
mode: TypeOfMode,
// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,
// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,
// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,
// 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
expirationTime: ExpirationTime,
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// fiber的版本池,即记录fiber更新过程,便于恢复
alternate: Fiber | null,
}
Fiber 如何解决问题的
Fiber
把一个渲染任务分解为多个渲染任务,而不是一次性完成,把每一个分割得很细的任务视作一个"执行单元",React
就会检查现在还剩多少时间,如果没有时间就将控制权让出去,故任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。
即是实现了"增量渲染",实现了可中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element
对应的 Fiber
节点。
Fiber 实现原理
实现的方式是requestIdleCallback
这一 API
,但 React
团队 polyfill
了这个 API
,使其对比原生的浏览器兼容性更好且拓展了特性。
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序。
requestIdleCallback
回调的执行的前提条件是当前浏览器处于空闲状态。
即requestIdleCallback
的作用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。首先 React
中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React
未完成的任务,是一种合作式调度。
简而言之,由浏览器给我们分配执行时间片,我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。
React 16
的Reconciler
基于 Fiber
节点实现,被称为 Fiber Reconciler
。
作为静态的数据结构来说,每个 Fiber
节点对应一个 React element
,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM
节点等信息。
作为动态的工作单元来说,每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作。
每个 Fiber
节点有个对应的 React element
,多个 Fiber
节点是如何连接形成树呢?靠如下三个属性:
复制
// 指向父级Fiber节点
this.return = null
// 指向子Fiber节点
this.child = null
// 指向右边第一个兄弟Fiber节点
this.sibling = null
13.React 中的 setState 同步异步的问题?
什么时候同步什么时候异步
在 React
中,如果是由 React
引发的事件处理(比如通过 onClick
引发的事件处理),调用 setState
不会同步更新 this.state
,除此之外的 setState
调用会同步更新 this.state
所谓的除此之外,指的是绕过 React
,通过 addEventListener
直接添加的事件处理函数,还有通过 setTimeout/setInterval
产生的同步任务
原因
在 React
的 setState
函数实现中,会根据一个变量 isBatchingUpdate
判断是直接更新 this.state
还是放到队列中回头再说,而且 isBatchingUpdate
默认是 false
,也就是表示 setState
会同步更新 this.state
,但是,有一个函数 batchedUpdate
,这个函数会把 isBatchingUpdate
修改为 true
,而当 React
在调用事件处理函数之前就会调用这个 batchedUpdates
,造成的后果,就是由 React
控制的事件处理过程 setState
不会同步更新 this.state
注意
setState
的 异步 并不是说内部由异步代码实现,其实本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用在更新之前,导致在合成事件和钩子函数中没法里吗拿到更新后的值,形成了所谓的"异步"
但是可以通过 setState(partialState, callback)
中的 callback
拿到更新后的结果