render 方法的作用?
- render 方法会负责把虚拟 DOM 变成真实 DOM 插入到容器中
- react 元素是不可变的「是一个只读对象」,只能通过执行 render 方法,来进行对元素的更新渲染
- react 只会更新数据改变的部分
render 更新的过程
- 浏览器不识别,jsx 会被编译成 createElement 语法
- 编译阶段是在 webpack 编译的时候,也就是打包的时候执行的「编译阶段不执行代码」
- 打包后的代码在浏览器里执行的时候,会执行 React.createElement() 得到虚拟 DOM「执行时已经编译成了虚拟 DOM」,返回一个 JS 对象
- 然后在通过 render 方法,它会负责把虚拟 DOM 变成真实 DOM 元素插入到 root 容器中
createElement 的实现步骤
- webapck 编译之后,会生成 createElement 语法,通过编译可以拿到标签和属性
- 参数
@param {*} type 标签名
@param {*} config 标签属性「class、id...」
@param {*} children 子标签
- 实现步骤
-
- 判断 参数的个数是否大于 3
- 大于 3,则子标签不止一个,需要把子标签转换为数组赋值给 children
if( arguments.legnth > 3 ) { children = Array.prototype.slice.call(arguments, 2) }- 小于 3,直接赋值给 children「无需处理」:props.children = children;
-
- 返回 type 和 props 属性
return { type, props } -
key 属性的作用
-可以区分子标签,方便更新,提高效率
- key 不能重复,必须是唯一的
- 不唯一的 key 会导出重复更新或者是忽略更新
- 在源码中有一个
childrenmap = { key值:标签 }- 一个 key 值对应一个元素,如果 key 值相同,在 childrenmap 中会被覆盖
- 当更新的时候就会忽略掉重复的 key 元素,只保留最后一个元素
- 当 DOM diff 比较的时候:
- 如果没有 key ,那么就会根据索引来比较,性能非常差
- 如果有唯一 key 值,拿到新值之后会根据 key 来跟旧的做比较,有的话进行位置的移动,没有则会创建并插入
React 如何更新?
- 把旧的都删除,在插入全部的新元素「性能比较差」
- react 会把老的虚拟 DOM 和新的虚拟 DOM 进行比较,这个也就是所谓的 DOM diff
- 用最小的代码进行更新
- 比较过程:进行对元素的移动,不会进行元素的删除和创建
- 让 react 更能友好的进行比较,所以给每个元素加个唯一标签,通过 key 值确定每一个元素,从而进行比较,提高了性能
什么是 React 组件?
- 可以将 UI 分成一些独立的、可复用的部件,这样你就只需要专注以构建每一个单独的部件
- 组件从概念上类似于 JS 函数,他接受任意的参数「props」,并且返回用于描述页面展示内容的 react 元素
React 组件的分类?
函数式组件
- function 组件,称为静态组件「函数式组件」
- 函数组件接收一个单一的 props 对象并返回了一个 react 元素「虚拟 DOM 对象」
- react 元素不仅可以是 DOM 标签,还可以是用户自定义的组件
- 自定义组件的名称必须是首字母大写的
- 原生组件小写开头,自定义组件大写字母开头的
- 组件必须使用前先定义
- 组件需要返回并且只能返回一个根元素
- 根元素可以使用一个空标签
- 也可以用一个小括号(),代表里面的内容是一个整体
- 组件之间可以嵌套
- react 元素不仅可以是 DOM 标签,还可以是用户自定义的组件
函数式组件的更新
- 虚拟 DOM 类型是自定义函数组件
- 函数执行,把 props 参数传进去
- 拿到返回值,也就是虚拟 DOM
- 把虚拟 DOM 变成真实 DOM,然后处理属性...
类(定义的)组件
- class 组件,称为动态组件「类组件」
- React.Component 是 react 的一个内置组件
- 编写 class 组件的时候,必须继承 react 的内置组件;
- 每一个类组件都有一个 render 函数
- 可以在构造函数里,并且只能在构造函数中给 this.state 赋值
- 定义状态对象:this.state而不是一个方法名称
- 状态对象可以存储属性和值
- 状态更新,会引起组件的刷新
- 改变状态需要使用 setState() 方法
- 属性对象「props」:父组件给的,不能改变「只读的对象」
实现类组件渲染的过程
- 解构类的定义和类的属性对象:let { type, props } = vdom;
- 创建类组件的实例:let classInstance = new type(props)
- 调用实例的 render() 方法,返回要渲染的虚拟 DOM:let renderVdom = classInstance.render();
- 根据虚拟 DOM 对象创建真实 DOM 对象:let dom = createDOM( renderVdom )
- 为了以后类组件的更新,把真实 DOM 挂载到了类的实例上:classInstance.dom = dom;
- 返回 dom:return dom;
实现类组件的更新
setState( newState ,function(){...})
- 参数:
- newState: 只传递需要修改的值
- 回调函数:可以拿到更新后的新值
- React 中更新视图,需要调用 setState 这个方法「react 提供的一个方法」
- 不要在 render 中触发 setState ,会造成一个死循环
- setState 触发,会执行 render 函数,render 中又触发 setState ... 无限循环,形成死递归
- 在 react 中,事件的更新可能是异步的,是批量的,但不是同步的
- 调用 state 之后状态并没有立刻更新,而是先缓存起来了
- 等事件函数处理完成后,在进行批量更新,一起更新并重新渲染
-
因为 jsx 事件处理函数是 react 控制的,只要归 react 控制就是批量,只要不归 react 管了,就是非批量
-
- 实现类组件的更新过程
- 新状态和旧状态进行合并
老的状态:let state = this.state; 新老状态合并:this.state = {...state, ...newState};- 调用子类的 render 方法,获取新的 state 的虚拟 DOM:
let newVdom = this.render()- 更新:updateClassComponent(this, newVodm)
- 实现更新方法
function updateClassComponent( 类的名字,新虚拟 DOM ) {}- 类组件渲染时,把真实 DOM 挂载到了类上,所以通过类可以拿到老的真实 DOM:
let dom = classInstance.dom- 执行 createDOM 方法把新的虚拟 DOM 变成真实 DOM:
let newDOM = createDOM(newVdom);- 拿到新的真实 DOM 后,获取到旧 DOM 的父节点,把它的儿子节点替换成新的 DOM:
oldDOM.parentNode.replaceChild( newDOM, oldDOM )- 最后再把新的 DOM 挂载到类上:
classInstance.dom = newDOM;
判断是类组件还是函数组件的方法
- 类组件中有 isReactComponent 属性,通过 isReactComponent 属性来判断,是否为类组件:值为 true,那么就是类组件
如何理解 setState?
- react 数据更新页面不会渲染,需要通过 setState 来触发 render 函数来进行页面的更新渲染
- setState 接收两个参数,第一个参数是传递的新数据,第二参数是一个回调函数,函数体中可以拿到更新后的状态
- setState 大部分情况下是一个异步操作,执行 setState 并不会立即去更新数据
- 如果多次调用 setState,react 内部会把所有需要更新的状态缓存的到数组中,批量更新
- 批量更新的实现:循环数组,一次进行一个新状态替换旧状态的方式,来进行状态的更新,最后执行 render 方法,进行一个更新操作
Fragment
- react 提供的一个组件,作为容器使用,不会在页面上进行渲染;
- 一个组件返回多个子元素列表,Fragments 允许将子列表分组,不需要向 DOM 添加节点
React 中如何实现组件通信
- 父子通信:props「属性」、ref「绑定 DOM」
- 子父通信: props「方法」
- 兄弟通信:状态提升、context、全局状态管理
状态提升
将 state 放在公有的父元素上,结合父子与子父实现通信
类组件中的上下文 context
- 跨组件通信
- 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
- 父组件设置了 context 之后,所有后代组件都可以拿到数据
- 使用方式 1:React.createContext+myContext.Provider+contextProp
- 创建上下文对象 myContext:React.createContext();
- 使用 myContext.Provider 组件把父组件包裹起来,value 就是要传递给后代的属性:<myContext.Provider value={...}></myContext.Provider>
- 在子组件中接收上下文对象 myContext:static contextProp = myContext;
- 获取属性:this.context
- 使用方式 2:childContextTypes+getChildContext+contextTypes
- 引入propTypes:prop-types 第三方包
- 父创建一个静态属性:static childContextTypes = {}
- 给这个静态属性中规定后代所使用的属性类型限制「需要借助第三方包:prop-types」:static childContextTypes = {name「给子组件的属性名」:propTypes.string「规定属性类型」}
- 设置给后代的属性,及属性值:getChildContext() {return:{name: xxx}}
- 子组件中必须要接收,声明一下:static contextTypes = {name: propTypes.string}「想用什么属性在这里接收什么属性」
- 然后在子组件中就可以使用:console.log( this.context )
在嵌套的子组件里修改上下文数据
- Consumer:上下文对象的 api
- 用 Consumer 组件包裹要渲染的组件
- 函数的参数就是在父组件提供的数据
- 在函数里返回要渲染的 jsx
全局状态管理
react 的生命周期
- react 的生命周期的顺序:从外到里渲染,子组件比父组件优先挂载
- 新增的生命周期和要废弃的不能同时使用
- 初始化:第一个钩子函数
- constructor(初始化)
- 挂载
- static getDerivedStateFromProps(props, state)
- props:父组件传过来的数据
- state:子组件中的私有属性
- 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用,会重新赋值,它返回一个对象来更新 state,如果返回 null 则不更新任何内容
- componentWillMount(组件将要更新)「已废弃」
- getDerivedStateFromProps 和 componentWillMount两个钩子不共存
- componentDidMount(组件挂载完成)
- react当中的AJAX请求 一般都是在这个钩子进行的
- 只加载一次「不刷新页面」
- static getDerivedStateFromProps(props, state)
- 更新
- componentWillReceiveProps(nextProps)
- 数据变化的时候,会触发 componentWillReceiveProps 钩子,可以获取到将要更新的值
- shouldComponentUpdate(nextProps, nextState)
- 询问组件是否要更新
- 参数:nextProps: 判断地址是否相同,然后在判断内容是否一样
- 返回值:false 不更新,true 更新
- 可以用来优化的一个钩子函数
- componentWillUpdate()
- 组件将要更新
- 已废弃
- componentDidUpdate()
- 组件更新完成
- 此钩子函数中不能调用 setState ,会造成死循环
- componentWillReceiveProps(nextProps)
- 销毁
- componentWillUnmount
- 组件将要销毁
- 一般用来清除定时器,或者是移除原生事件
- componentDidCatch(error, info)
- 捕获错误信息
- componentWillUnmount
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate -------以上在 17 版本中废除
- static getDereviedStateFromProps:数据变化的时候触发
- 必须加 static,是一个静态属性
- 不能和要废弃的生命周期一起用
- 必须有返回值,没有返回结果,返回 null
- 执行顺序:初始化之后,渲染之前
- 在数据改变之后也会执行「props/state」
- 生命周期内没有 this ,不能获取当前组件对象
- 参数:props/state,两个都是更新后的值
- 返回值是 props 和 state 合并之后新的 state 对象
- 返回值如果是 null,按照参数中的数据进行更新界面
- 返回值是一个对象,按照返回值来更新界面
- getSnapshopBeforeUpdate:更新前触发
- 必须有返回值,没有返回结果,返回 null
- 不能和要废弃的生命周期一起用
- 必须和 DidUpdate 一起使用
- 生明周期内可以使用 this,this 中存储的是修改后的数据
- 参数中存储的是修改前的数据
- return 的值会作为componentDidupdate第三个参数出现
- 更新后可以拿到更新之前的数据
- 页面跳转,返回上一个页面中的历史记录 -------------以上在 17 版本中新增的
react fiber 的了解
- react 15 版本之前渲染方式是同步渲染,导致页面卡顿
- react 16 版本之后渲染核心是引入了 fiber,实现异步渲染
- fiber 就是将一个时间超长的任务分段,一段一段的执行,更新前阶段可以被其他优先级高的任务打断,更新后则不能,所以,AJAX 请求不能放在 componentWillMount 阶段,会出现重复请求的问题
fiber 优缺点?
- 弊端:会导致更新之前的生命周期可能会被其他优先级高的任务打断,打断之后生命周期会重新执行,所以,我们一般不会再更新前的生命周期中做一些网络请求等 组件的渲染分成两个阶段:
- 更新前:可以被其他优先级高的任务打断,render 之前的都可以被打断
- 所以,AJAX 请求不能放在 componentWillMount 阶段,会出现重复请求的问题
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate -------以上在 17 版本中废除
- static getDereviedStateFromProps:数据变化的时候触发
- getSnapshopBeforeUpdate:更新前触发 -------以上在 17 版本中新增的
- shouldComponentUpdate
- 更新后:不会被打断,render 之后的生命周期都是第二阶段
常用的声明周期
- constructor:初始化数据
- componentDidMount:网络请求
- componentWillUnmount:
- shouldComponentUpdate:控制 render 函数是否执行
react 优化
- shouldComponentUpdate:控制组件是否重新 render
- PureComponent
路由
- hash 路由和历史路由的区别
- 路由实现的原理
- 监听地址栏的改变
- 根据地址栏的改变渲染组件
- 在线上 hash 路由和历史路由的区别
- 刷新之后会 404 「需要服务器端修改配置」
- 重定向
全局状态管理
- redux
- react-redux
- 异步中间件
- 用途:
- 多组件共享状态
- 一个组件发生改变其他组件跟着变
- 怎么用的?过程是怎么样的?
- 全局创建状态管理对象 store
- 创建 reducer:本质是一个函数,接收修改前的数据和 actions 参数
- 在函数的内部根据 action 对修改前的数据进行修改,返回新数据
- 将 store 对象和 reducer 进行关联
- 在页面上通过 store 和 getState 获取全局状态值
- 用户在组件里触发 actionsCreator 的方法,创建 action 通过 dispatch 发送给 reducer
- 在需要更新的组件中做 subscribe 监听,控制组件重新渲染
diff 算法
数据发生改变,引起虚拟 dom 改变,对比修改前后的虚拟DOM,找出差异点,根据差异点更新真实 DOM
什么是纯函数?
输出只由输入决定
使用全局状态管理的时候,需要注意什么
- 在 reducer 中不能修改原数据,可能会导致页面不更新
- 解决方式:深拷贝「JSON.stringify/JSON.parse、递归、immutable 插件」、浅拷贝「Object.assign()」
全局状态管理的时候如何处理异步问题
异步中间件:redux-thunk
全局状态管理中将网络请求放在哪里
- 组件中
- actions:
- 便于统一管理请求,减少冗余代码
- 可以准确的检测到数据的更改时间
受控组件和非受控组件
- 受控组件:原生的输入组件它的值受状态控制
- 非受控组件:原生的输入组件的值不受状态控制
高阶组件
- 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件
- 高阶组件的作用就是为了组件之间的代码复用
- 被高阶组件处理过的组件,一般在 props 中获取数据
- 应用场景:
- 属性代理:经过高阶组件处理过的属性
- 反向继承:
- 场景:自定义组件中的文本,可以进行内容的扩展
- 在高阶组件中,一般来说,子类继承父类,先执行父类的方法,在执行子类的方法。
- 通过 super 可以调用父类的方法,从而实现反向继承
- 克隆:React.cloneElement(目标元素, {新属性}, {儿子})
- @装饰器可以用在高阶组件中,直接拿到最内部的组件执行结果
hoc 及如何做代码优化?都用到哪些地方了?
可以通过高阶组件封装公有代码,路由拦截器、路由懒加载、react-loadable、From.create、withRouter
shouldComponentUpdate
- 当一个组件的 props 或者 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM,当他们不相同时 React 会更新该 DOM
- 如果渲染的组件非常多时,可以通过覆盖生命周期方法 shouldComponentUpdate 来进行优化
- shouldComponentUpdate 方法会在重新渲染前被触发,默认返回 true,可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程,包括该组件的 render 及后续操作
PureComponent
- 默认情况下,只要改了状态,那么所有的组件,不管它的属性变没变,都要更新
- PureComponent:纯组件,当组件接收的参数没有发生任何改变的时候,当前组件不会再去执行 render 「类似于:静态组件中的 memo 函数」
- PureComponent 只会进行一个浅层的比较:如果是对象的话,只看地址有没有发生改变,不看内容有没有发生变化
React Hooks
函数组件更新数据
静态组件更新数据:useState
var [count, setCount] = useState(初始值);
- count:对应的初始值数据
- setCount:用来更新数据的函数,形参是数据的新值
- 不能再 if 或者 for 循环中使用
- 不会自动合并没用到的属性及值;
- 所以,需要我们自己手动合并「使用扩展运算符:...」:
setState({...state, count: state.count+1});
- 所以,需要我们自己手动合并「使用扩展运算符:...」:
- 如果执行的函数体内是异步操作,那么我们每次执行每次执行都会形成一个闭包「变量被保护及保存起来了」,state 对应的值就是执行时的那个 state 值,不一定就是最新的 state 值
- 通过给 setState 传一个回调函数的方式来获取新的 state 值
function minus() {
setTimeout(()=>{
setState((state)=>{
console.log(state);
return {count: state.count+1}
})
}, 5000)
// 同步操作没问题
console.log(state.count)
}
- 惰性初始化 state:初始化的操作只会在第一次执行:更新的时候就不在执行当前的初始化操作
let [state, setState] = useState(function () {
console.log('初始状态')
return {count: 600}
});
静态组件更新函数:useCallback
- useCallback( function(){}, [] );
- 参数 1:第一个参数是要缓存的函数
- 参数 2:执行函数时,需要的依赖;只有当依赖项发现变化才回去执行函数
- 第二个参数不传,每次执行的函数体都是一样的,所以不会每次都要执行
useReducer:useState 的替代方案
- useState 的替代方案。它接收一个:(state, action)=>newState 的 reducer,并返回当前的 state 以及配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,比如:state 逻辑比较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
- reducer 处理器,可以接收一个老状态,返回一个新状态
let [ state, dispatch ] = useReducer( (state, action)=>{
switch(action.type) {
case 'add':
return {...state, number: n+1}
default:
return state;
}
}, {count: 100, name: "guyal_"} );
dispatch({ type: "add", n: state.count + 1 })
- 参数:
- state:更新之前的旧数据,任意类型的状态
- action:传给 dispatch 的对象,动作,是一个对象,包含:type:类型「一般是字符串」,其他类型...
- dispatch:派发函数
- 是用来修改 state 的,通过让参数 1 执行的方式来修改 state
- react 会使用 参数 1 函数执行的返回结果,来顶替老的 state
useContext:函数组件中的上下文
- 创建上下文对象 myContext:React.createContext();
- 使用 myContext.Provider 组件把父组件包裹起来,value 就是要传递给后代的属性:<myContext.Provider value={...}></myContext.Provider>
- 在子组件中接收上下文对象 myContext:let { context } = useContext(myContext);
函数组件实现生命周期
useEffect:函数组件实现数据的加载及更新
useEffect(()=>{
//页面挂载触发:比如,添加定时器
return ()=>{
//组件销毁触发:比如,清除定时器
}
},[依赖项])
- 参数
- 参数 1:回调函数,在初次加载完成和更新完成之后触发
- 可以理解为是类组件的 componentDidMount 和 componentDidUpdate 的合成体
- 加载和更新都会执行这个回调函数
- 组件销毁的时候触发:回调函数中 return 的回调函数相当于销毁的生命周期
- 参数 2:数组,放入的是依赖,只有依赖项发生改变的时候,才会执行回调函数
- 第二个参数为空数组:只执行一次,相当于类组件的 componentDidMount
- 参数 1:回调函数,在初次加载完成和更新完成之后触发
- 副作用:相对于纯函数来说,影响当前作用域之外的作用域,那么这就是副作用
- 纯函数:
- 相同的输入会产生相同的输出
- 不能修改本函数作用域之外的变量
useLayoutEffect + useRef
- useEffect 不会阻塞浏览器渲染,而 useLayoutEffect 会阻塞浏览器渲染
- useEffect 会在浏览器渲染结束绘制完成后执行,所以绘制的时候,没有移动
- useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行
缓存 Hook
memo:缓存数据
- memo 处理过的组件 若传给组件的数据没有发生改变 那么组件就不会重新执行
- 一般用来缓存值类型
- memo 进行的是一个浅比较,对象和函数是无法使用 memo 来实现缓存的
Child = memo(Child);// 添加需要缓存的组件:Child 组件
useMemo:缓存对象类型
let data = useMemo(()=>{},[依赖项])
- useMemo 一般用来缓存对象类型
- 只有依赖项改变的时候 data 才会被给成新的地址
- useMemo 需要结合 memo 函数进行优化
let data = useMemo(()=>{
return {
age:1000,
name:name
}
},[name]) // [依赖]
useCallback:缓存函数类型
let minus = useCallback(函数体,[依赖项]);
- useMemo 一般用来缓存函数使用
- 只有依赖发生改变的时候 minus才会被赋予一个新的函数地址
- useCallback 需要结合 memo 函数进行优化
let minus = useCallback(()=>{
setCount(--count)
},[name])
ref
createRef
- 使用方式:
- 创建私有属性:
xxx=React.createRef()
- 创建私有属性:
- 获取方式:
this.xxx.current
pDom = React.createRef()
- ref 只能用类组件,函数组件中没有 this
- ref.current = 类的实例
- ref 属性用于 HTML 元素,构造函数中使用 React.createRef() 创建的 ref 来接收 DOM 元素:
<p ref='pDom'></p>- 通过 this.ref.pDom 来获取 p 标签
- ref 属性值是一个箭头函数:
ref = {(el)=>{this.xxx=el}} - 如果需要处理逻辑,那么就用函数
- el 是 react 帮我们传递的实参
- xxx 就是对应的组件
forwardRef
- 函数组件可以使用 forwardRef 转发一下,ref 就可以使用了
- forwardRef 将 ref 从父组件中转发到子组件中的 dom 元素上,子组件接收 props 和 ref 作为参数
- useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
- 获取子组件中的某个元素
function Child(props:any, ref:any) {
return (<>
<h1 ref={ref}>子组件</h1>
</>)
}
const Children1 = React.forwardRef(Child)
class App extends React.Component {
createRef = React.createRef();
render() {
console.log(this.createRef);// 子组件中的 h1
return (
<>
<Children1 ref={this.createRef} />
</>
)
}F
}
获取子组件中某个元素的步骤**:
- 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 createRef 变量。
- 我们通过指定 createRef 为 JSX 属性,将其向下传递给
<Children1 ref={createRef}>。 - React 传递 createRef 给 Children1 内函数 (props, ref) => ...,作为其第二个参数。
- 我们向下转发该 ref 参数到
<h1 ref={ref}>,将其指定为 JSX 属性。 - 当 ref 挂载完成,ref.current 将指向 h1 DOM 节点。
自定义 Hook
- 自定义 hooks,只要说一个函数以 use 开头,并且里面调用了别的 Hooks
性能优化
- 渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。
- React 优化最重要的策略就是减少组件的刷新,组件属性不变的情况下,不进行 render
- 类组件:PureComponent
- 函数组件:useCallback