性能优化
性能优化,永远是面试的重点,性能优化对于 React 更加重要
- 在页面中使用了
setTimout()
、addEventListener()
等,要及时在componentWillUnmount()
中销毁
- 使用异步组件
- 使用
React-loadable
动态加载组件
-
shouldComponentUpdate
(简称SCU )、React.PureComponent
、React.memo
- 不可变值 ImmutableJS
shouldComponentUpdate (nextProps, nextState) {
return true // 可以渲染,执行 render(),默认返回 true
return false // 不能渲染,不执行 render()
}
什么情况下需要使用 shouldComponentUpdate
在React中,默认情况下,如果父组件数据发生了更新,那么所有子组件都会无条件更新 !!!!!!
通过shouldComponentUpdate()
retrun fasle 来判断阻止 Header 组件做无意义的更新
shouldComponentUpdate()
并不是每次都需要使用,而是需要的时候才会优化
class App extends React.Component {
constructor () {
this.state = { list: [] }
}
render () {
return (
<div>
{/* 当list数据发生变化时,Header组件也会更新,调用 render() */}
<Header />
<List data={this.state.list}
</div>
)
}
}
在shouldComponentUpdate()
判断中,有一个有意思的问题,解释为什么 React setState()
要用不可变值
// 父组件中
changeList () {
this.state.list.push({id: 2})
this.setState({
list: this.state.list
})
}
// 子组件中
import _ from 'lodash'
shouldComponentUpdate(nextProps, nextState) {
// 数组深度比较(一次性递归到底,耗费性能,工作中慎用)
if (_.isEqual(nextProps.list, this.props.list)) {
return false // 相等,不渲染
}
return true // 不相等,渲染
}
子组件将始终不会渲染,因为在shouldComponentUpdate()
中,this.state.list.push()
已经修改了this.props.list
,而this.setState()
修改了nextProps.list
所以两个值深度比较,将始终相同。
PureComponent 和 memo
- class类组件中用
PureComponent
,无状态组件(无状态)中用memo
- PureComponent, SCU中实现了浅比较
- 浅比较已使用大部分情况(尽量不要做深度比较)
PureComponent 与普通 Component 不同的地方在于,PureComponent自带了一个shouldComponentUpdate()
,并且进行了浅比较
// memo用法
function MyComponent (props) {
/* 使用 props 渲染 */
}
// areEqual 也可不传
function areEqual(prevProps, nextProps) {
if (prevProps.seconds===nextProps.seconds) {
return true
} else {
return false
}
}
export default React.memo(MyComponent, areEqual)
immutable.js
- 彻底拥抱“不可变值”
- 基础共享数据(不是深拷贝),速度快
- 有一定学习和迁移成本
常见基础面试考题
React 组件如何通讯
- 父子组件通过 属性 和 props 通讯
- 通过 context 通讯
- 通过 Redux 通讯
this.setState()相关
import React from 'react'
class App extends React.Component {
constructor (props) {
super(props)
this.state = { count: 0 }
}
componentDidMount () {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 0
setTimeout(() => {
this.setState({count: this.state.count + 1 })
console.log(this.state.count) // 2
}, 0)
setTimeout(() => {
this.setState({count: this.state.count + 1 })
console.log(this.state.count) // 3
}, 0)
// setTimeout(function () {
// this.setState({count: this.state.count + 1 })
// console.log(this.state.count) // 报错,this 指向问题
// }, 0)
}
render () {
return <h1>{this.state.count}</h1>
}
}
export default App // 返回高阶函数
JSX本质是什么.....
前端富文本 dangerouslySetInnerHTML
const rawHtml = '<div><p>Title</p></div>'
const rawHtmlData = {
__html: rawHtml // 这里有个下划线
}
return <div dangerouslySetInnerHTML={rawHtmlData}></div>
两种绑定事件
<button onClcik={bindClcik1.bind(this)}> 使用 .bind(this) </button>
<button onClcik={bindClcik2}> 箭头函数 </button>
// 使用 class 的自带函数,需要重定向 this
bindClcik1 () { alert('bindClcik1') }
// 使用静态方法,使用箭头函数不需要使用 bind(this)
bindClick2 = () => { alert('bindClcik2') }
Event、默认事件、事件冒泡
这里打印出来的Event
对象是 React 封装过的SyntheticEvent
,可以看__proto__.constructor
。React 标准化了事件对象,因此在不同的浏览器中都会有相同的属性。
React 中事件绑定跟 Vue 中完全不同,Vue中事件绑定和触发的对象为同一元素,React中事件触发的对象为document
,绑定元素为当前元素。React的所有事件都会被挂载到document
上和DOM
事件不同。
Vue 的Event
是原生,事件被挂载到当前元素和DOM
事件一
<a href="www.baidu.com" onClick={this.getEvent} target="blank">Get Event</a>
getEvent = (event) => {
event.preventDefault() // 阻止默认事件
event.stopPropagation() // 阻止事件冒泡
console.log(event) // 非原生的 Event
console.log(event.nativeEvent) // 获取原生的 Event
console.log(event.nativeEvent.target) // 绑定事件的对象,这里为 <a></a>
console.log(event.nativeEvent.currentTarget) // 触发事件的对象,这里为 document
}
事件传参
通过.bind()
传参
<div onClick={this.getParams1.bind(this, 'id1', 'title1')}>get params 1</div>
getParams1 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event) // 最后一个参数为Event对象
}
通过箭头函数传参
<div onClick={(event) => { this.getParams2('id2', 'title2', event) }}>get params 2</div>
getParams2 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event)
}
参考 前端react面试题详细解答
表单
<div>
<label htmlFor="userName"></label>
<input value={this.state.userName} onChange={this.handleInputChange.bind(this)} />
</div>
// 实现类似双向数据绑定
handleInputChange (even t) {
const userName = event.target.value
this.setState(() => ({
userName
}))
// 下面这种写法会报错,因为 this.setState 传递一个函数时,为异步方法,等异步执行时已经没有 event
this.setState(() => ({
userName = event.target.value
}))
}
组件传参
普通参数/函数
// 父组件
<div>
<Child text={this.state.text} />
</div>
// 子组件
<div>
<p>{this.props.text}</p>
</div>
属性类型检查
import PropTypes from 'prop-types'
// 对传递的参数强校验
TodoItem.propTypes = {
content: PropTypes.string.isRequired, // 限制为字符串且必传
}
setState()
- 不可变值
- 可能是异步更新
- 可能会被合并
// 错误的写法
this.setState({
count: this.state.count + 1
})
// 正确的写法
const count = this.state.count + 1
this.setState({ count })
正确修改数组值
// 不能使用 push pop splice 等,这样违反了不可变值,会影响 shouldCompententUpdate 判断
this.setState(() => ({
list1: this.state.list1.concat(100), // 追加
list2: [...this.state.list2, 100], // 追加
list3: this.state.list3.slice(0, 3) // 截取
list4: this.state.list4.filter(item => item > 100) // 筛选
}))
正确修改对象值
this.setState(() => ({
obj1: Object.assign({}, this.state.obj1, {a: 100}),
obj2: {...this.state.obj2, a: 100}
}))
通常情况下,setState()
为异步更新数据
const count = this.state.count + 1
this.setState({
count: count
}, () => {
// 这个函数没有默认参数
// 类比 Vue 的 $nextTick
console.log(this.state.count) // 打印更新后的值
})
console.log(this.state.count) // 打印更新前的值
setState()
同步更新数据,在setTimeout()
中setState()
是同步的
setTimeout(() => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
自己定义的 DOM 事件,setState()
是同步的
componentDidMount () {
document.body.addEventListener('click', () => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
}
【重点】 传入对象,会被合并,结果只执行一次,类似于Object.assgin()
初始值 this.state.count = 0
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
输出值 this.state.count = 1
【重点】 传入函数,不会被合并,因为函数无法合并
初始值 this.state.count = 0
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
输出值 this.state.count = 3
组件生命周期
Initialization 初始化
-
constructor()
: class
的构造函数,并非React独有
Mounting 挂载
-
componentWillMount()
: 在组件即将被挂载到页面的时刻自动执行;
-
render()
: 页面挂载;
-
componentDidMount()
: 组件被挂载到页面之后自动执行;
componentWillMount()
和 componentDidMount()
,只会在页面第一次挂载的时候执行,state变化时,不会重新执行
Updation 组件更新
-
shouldComponentUpdate()
: 该生命周期要求返回一个bool
类型的结果,如果返回true
组件正常更新,如果返回false
组件将不会更新;
-
componentWillUpdate()
: 组件被更新之前执行,如果shouldComponentUpdate()
返回false
,将不会被被执行;
-
componentDidUpdate()
: 组件更新完成之后执行;
componentWillReceiveProps()
: props独有的生命周期,执行条件如下:
- 组件要从父组件接收参数;
- 只要父组件的
render()
被执行了,子组件的该生命周期就会执行;
- 如果这个组件第一次存在于父组件中,不会执行;
- 如果这个组件之前已经存在于父组件中,才会执行;
Unmounting 组件卸载
-
componentWillUnmount()
: 当组件即将被从页面中剔除的时候,会被执行;
生命周期简单使用场景
- 使用
shouldComponentUpdate()
防止页面进行不必要的渲染
# 用生命周期进行性能优化
shouldComponentUpdate () {
if (nextProps.content !== this.props.content) {
return true;
}
return false;
}
- Ajax 请求页面初始数据
componentDidMount()
不能写在render()
之中,因为会重复调用,也不能写在componentWillMount()
之中,会与RN等其它框架冲突,不然也可以写在这里面,同样是只执行一次。
同样也可以写在构造函数constructor()
之中,但是不建议这样做。
import axios from 'axios'
componentDidMount () {
axios.get('/api/todolist').then((res) => {
console.log(res.data);
this.setState(() => ({
list: [...res.data]
}));
}).catch((err) => {
console.log(err);
});
}
无状态组件(函数组件)
当一个组件只有一个render()
函数时,我们就可将这个组件定义为无状态组件,无状态组件只有一个函数。
无状态组件的性能比较高,因为它仅是一个函数,而普通组件是一个class
。
- 纯函数
- 输入props,输出JSX
- 没有实例
- 没有生命周期
- 没有state
- 不能扩展其它方法
function List (props) {
const { text } = this.props
return (
<div>{text}</div>
)
}
非受控组件
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
flag: true
}
this.nameInputRef = React.createRef() // 创建 ref
this.fileInputRef = React.createRef() // 创建 ref
}
render () {
return (
<div>
{/* 这里使用 defaultValue 而不是value,使用 ref */}
<input defaultValue={this.state.name} ref={this.nameInputRef} />
<button onClick={this.alertName.bind(this)}>alert value</button>
{/* file 类型的必须用 ref 获取 dom 来获取数据 */}
<input type="file" ref={this.fileInputRef} />
</div>
)
}
alertName () {
const ele = this.nameInputRef.current // 通过 ref 获取 dom 节点
alert(ele.value)
}
}
portals 传送门
使用 Portals 渲染到 body 上,fixed 元素要放在 body 上,有更好的浏览器兼容。
常见使用场景:
- 父组件 overflow: hidden , 但是子组件又想展示;
- 父组件的 z-index 太小;
- fixed 需要放在 body 第一层;
import ReactDOM from 'react-dom'
render () {
return ReactDOM.creatPortal(
<div>{this.props.children}</div>,
document.body
)
}
context 上下文
使用场景:公共信息(语言、主题)传递给每个组件,如果组件层级过多,用props
传递就会繁琐,用 redux
小题大做。
import React from 'react'
// 创建 Context 填入默认值(任何一个 js 变量)
export const {Provider,Consumer} = React.createContext("默认名称")
// 在最上级组件中
constructor (props) {
super(props)
this.state = { theme: 'light' }
}
render () {
// 这里使用 this.state.theme 是为了可以修改,初始化的值为默认值,不能修改
// value 中放共享的数据
return (
<Provider value={this.state.theme}>
.... <button onClick={this.changeTheme}></button>
</Provider>
)
}
// 子组件中调用
import { Consumer } from "./index";//引入父组件的Consumer容器
render () {
return (
// Consumer 容器,可以拿到上文传递下来的 theme 属性,并可以展示对应的值
<Consumer>
{ theme => <div>子组件。获取父组件的值: {theme} </div> } </Consumer>
)
}
异步组件
// 引入需要异步加载的组件
const LazyComponent = React.lazy(() => import('./lazyDemo') )
// 使用异步组件,异步组件加载中时,显示fallback中的内容
<React.Suspense fallback={<div>异步组件加载中</div>}>
<LazyComponent />
</React.Suspense>
组件公共逻辑的抽离
- Vue 中的 mixin,已被 React弃用
- 高阶组件 HOC
- Render Props
高阶组件
高阶组件不是一种功能,而是一种模式
// 高阶组件 基本用法
const HOCFactory = (Component) => {
class HOC extends React.Component {
// 在此定义多个组件的公共逻辑
render () {
return <Component {...thi.props} /> // 返回拼装的结果
}
}
return HOC
}
const MyComponent1 = HOCFactory(WrappedComponent1)
const MyComponent2 = HOCFactory(WrappedComponent2)
实际案例
import React from 'react'
// 高阶组件
const withMouse = (Component) => {
class withMouseComponent extends React.Component {
constructor(props) {
super(props)
this.state = { x: 0, y: 0 }
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
{/* 1. 透传所有 props 2. 增加 mouse 属性 */} <Component {...this.props} mouse={this.state}/>
</div>
)
}
}
return withMouseComponent
}
const App = (props) => {
const a = props.a
const { x, y } = props.mouse // 接收 mouse 属性
return (
<div style={{ height: '500px' }}>
<h1>The mouse position is ({x}, {y})</h1>
<p>{a}</p>
</div>
)
}
export default withMouse(App) // 返回高阶函数
Render Props
Render Props 核心思想:通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
class Factory extends React.Component {
constructor () {
this.state = {
/* 这里 state 即多个组件的公共逻辑的数据 */
}
}
/* 修改 state */
render () {
return <div>{this.props.render(this.state)}</div>
}
}
const App = () => {
/* render 是一个函数组件 */
<Factory render={
(props) => <p>{props.a} {props.b}...</p>
} />
}
Redux 单项数据流
- dispatch(action)
- reducer 产生 newState
- subscribe 触发通知
Redux 单项数据流图
React-router
路由模式
- hash模式(默认),如:baidu.com/#/user/10
- H5 history 模式,如:baidu.com/user/20
后者需要 server 端支持,因此无特殊需求可选择前者
常用组件
import {
HashRouter,
BrowserRouter,
Switch,
Route
} from 'react-router-dom'
function RouterComponent () {
return (
<BrowserRouter>
<Switch>
<Route path='/' exact component={Home}>
{/* 动态路由 */} <Route path='/detail/:id' exact component={Detail}></Route>
{/* 匹配404等页面 */} <Route path='*' exact component={NotFound}></Route>
</Switch>
</BrowserRouter>
)}
路由跳转
- 标签跳转,通过
<Link to="/">
这个隐性 a 标签跳转
- JS跳转,
import { useHistory } from 'react-router-dom'
&& history.push('/')
路由配置懒加载
import React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
const Home = React.lazy(() => import('./pages/Home'))
const Detail = React.lazy(() => import('./pages/Detail'))
const App = () => (
<BrowserRouter>
<React.Suspense fallback={<div>Loading...</div>}> <Switch>
<Route exact path="/" component={Home} />
<Route exact path="/detail" component={Detail} />
</Switch>
</React.Suspense>
</BrowserRouter>
)
Vue 原理
数据驱动视图(MVVM, setState),Vue MVVM ( Model + View + ViewModel )
Vue响应式
组件 data 的数据一旦变化,立刻触发视图的更新,实现数据驱动视图的第一步
核心API:Object.defineProperty,Object.defineProperty 有一些缺点,Vue3.0 开始启用 Proxy, Proxy有兼容性问题,且无法 polyfill(兼容性问题解决方案)
// Object.defineProperty 基础使用
const data = {}
const name = 'Actoress'
Object.defineProperty(data, "name", {
get: function () {
console.log('get')
return name
},
set: function (newValue) {
console.log('set')
name = newValue
}
})
// 调用
console.log(data.name) // get() 执行 => 'Actoress'
data.name = 'Wu' // set() 执行
深度监听
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性
- 数组需要重新定义数组原型
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 不能写在这里,这样会污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'Actoress',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'Wu'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
虚拟DOM(Virtual DOM)
vdom 是实现 Vue 和 React 的重要基石
为什么会有 vdom
- 有了一定复杂度,想减少计算次数比较难
- 能不能把计算,更多的转移到JS计算?因为JS执行速度很快
- vdom 用 JS 模拟 DOM 结构,计算出最小的变更,操作DOM
用JS模拟DOM结构
使用 snabbdom 操作虚拟 dom
Diff 算法
- diff 算法是 vdom 中最核心、最关键的部分
- diff 算法能在日常使用 Vue React 中体现出来(循环的 key)
优化前 树 diff 的时间复杂度 (n^3)
- 遍历Tree1,遍历Tree2
- 排序
- 假设有1000个节点,就要计算1亿次,算法不可用
优化后时间复杂度 (n^1)
- 只比较同一层级,不跨级比较
- tag 不相同,则直接删掉重建,不再深度比较
- tag 和 key,两者都相同,则认为是相同节点,不再深度比较
React 原理
数据驱动视图(MVVM, setState)
- 数据驱动视图 - React
this.setState()
- 函数式编程:函数式式编程是一种编程范式,两个最重要的概念是 纯函数、不可变值
JSX 本质
- JSX 等同于 Vue 模板
- Vue 模板不是 html
- JSX 也不是 JS
讲JSX语法,通过 React.createElement()
编译成Dom,BABEL 可以编译JSX
流程:JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
React 底层会通过 React.createElement()
这个方法,将 JSX 语法转成JS对象,React.createElement()
可以接收三个参数,第一个为标签名称,第二参数为属性,第三个参数为内容
createElement() 根据首字母大小写来区分是组件还是HTML标签,React规定组件首字母必须大写,HTML规定标签首字母必须小写
// 第一个参数为 标签(tag) 可为 'div'标签名 或 List组件
// 第二个参数为:属性(props)
// 第三个参数之后都为子节点(child),可以在第三个参数传一个数组,也可以在第三、四、五....参数中传入
React.createElement('tag', null, [child1, chlild2, child3])
或者
React.createElement('tag', { className: 'class1' }, child1, chlild2, child3)
事件合成机制
- 所有事件挂载到 document 上
- event 不是原生的,是
SyntheticEvent
合成事件对象
- 与 Vue 事件不同,和 DOM 事件也不同
为什么要合成事件机制
- 更好的兼容性和跨平台,摆脱传统DOM事件
- 挂载到document,减少内存消耗,避免频繁解绑
- 方便事件的统一管理,如:事务机制
setState 和 batchUpdate(批处理)
setState
- 有时异步(普通使用),有时同步(setTimeout, DOM事件)
- 有时合并(对象形式),有时不合并(函数形式),比较好理解(类似 Object.assign),函数无法合并
核心要点
- setState 主流程
- batchUpdate 机制
- transaction(事务)机制
看this.setState()
是否是异步,看 isBatchingUpdates 的状态,为 true 就是异步,为 false 就是同步
哪些能命中 batchUpdate 机制
- 生命周期(和它调用的函数)
- React 中注册的事件(和它调用的函数)
- React 可以“管理”的入口
哪些不能命中 batchUpdate 机制
- setTimeout setInterval等(和它调用的函数)
- 自定义的DOM时间(和它调用的函数)
- React“管不到”的入口
transaction 事务机制
常见基础面试题
1.组件之间如何通讯
- 父子组件 props
- 自定义事件
- Redux 和 Context,简单数据用 Context
2.JSX 本质
JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
3.shouldComponentUpdate 用途
4.redux单项数据流
Redux 单项数据流图
5.setState场景题
6.什么是纯函数
7.列表渲染为何要用key
- 必须用 key,且不能是 index 和 random
- diff 算法中通过 tag 和 key 判断,是否是同一个节点
- 减少渲染次数,提升渲染性能
8.函数组件 和 class 组件区别
- 纯函数,输入 props,输出JSX
- 没有实力,没有生命周期,没有state
- 不能扩展其它方法
9.如何使用异步组件
- 加载大组件
- React.lazy
- React.Suspense
10.多个组件有公共逻辑,如何抽离
11.react-router 如何配置懒加载
上文中有...
12.PureComponent 有何区别
- 实现了浅比较的 shouldComponentUpdate
- 优化性能
- 但要结合不可变值使用
13.React事件和DOM事件的区别
- 所有事件挂载到 document 上
- event 不是原生的,是 SyntheticEvent 合成事件对象
14.React性能优化
- 渲染列表时加Key
- 自定义事件、DOM事件及时销毁
- 合理使用异步组件
- 减少函数 bind this 的次数
- 合理使用 shouldComponentUpdate、PureComponent 和 memo
- 合理使用 ImmutableJS
- webpack层面优化
- 前端通用是能优化,如图片懒加载
- 使用SSR
React 和 Vue 的区别
相同点
- 都支持组件化
- 都是数据驱动视图
- 都是用 vdom 操作 DOM
不同点
- React 使用 JSX 拥抱JS,Vue使用模板拥抱 html
- React 函数式编程,Vue声明式编程
- React 更多需要自力更生,Vue把想要的都给你
JS 基础 - 变量类型和计算
typeof能判断哪些类型
- 识别所有类型
- 识别函数
- 判断是否是引用类型,返回都为 object,不能再细分
2. 何时使用===何时使用==
3. 值类型和引用类型的区别
引用类型的本质是相同的内存地址,出于性能问题考虑,所以JS对象使用引用类型,为了避免这种情况所以需要深拷贝
常见值类型:undefined、String、Bool、Symbol('s')
常见引用类型:Object、Array、null(指向空地址)
特殊引用类型:function
4.变量计算
字符串拼接
5. 手写深拷贝
funciton deepClone ( obj = {}) {
// 判断是否需要深拷贝,不是对象和数组
if (typeof obj !== 'object' || obj == null) {
return obj
}
let result
// 判断是否为一个数组
if (obj instanceof Array) {
result = []
} else {
result = {}
}
// 遍历对象
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归【重点】
result[key] = deepClone(obj[key])
}
}
return result
}
JS 基础 - 原型和原型链
JS本身是一个基于原型继承的语言,PS:class 的 extends 本质也是原型链继承
1.如何准确判断一个变量是不是数组?
2.手写一个简易的jQuery,考虑插件和扩展性
class jQuery {
constructor (selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this.[i] = result[i]
}
this.length = length
}
get (index) {
return this[index]
}
each (fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
}
on (type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// 复写,造*
class MyJQuery extends jQuery {
constructor (selector) {
super(selector)
}
......
}
3.class 的原型本质,怎么理解?
补充知识 - 定义class
// 父类
class People {
constructor (old) {
this.old = old
}
eat () {
consoloe.log('eating')
}
}
// 继承
class Student extends People {
constructor(name, number,old) {
super(old) // 变量传递给父类执行
this.name = name
this.number = number
}
sayHi () {
console.log(this.name, this.number)
}
}
const me = new Student('小明', 10, 20) // 新建对象
console.log(me.name) // => 小明
me.sayHi() // => 小明 10
// class 实际上是函数,可见是语法糖
typeof People => 'function'
typeof Student => 'function'
// 隐式原型和显式原型
me.__proto__ // 隐式原型 => People
Student.prototype // 显式原型 => People
me.__proto === Student.prototype => true 全等通过的话,就说明引用的是同一个地址
- 每个实例都有隐式原型
__proto__
- 每个 class 都有显式原型
prototype
- 实例的隐式原型指向对应class的显式原型
基于原型的执行规则
- 优先在自身属性和自身方法中查找
- 如果找不到则自动去
__proto__
隐式原型中查找
补充知识 - 类型判断 instanceof
instanceof 工作原理:是顺着__proto__
隐式原型一层层往上找
// 根据上方定义的class
me instanceof Student // true
me instanceof People // true
me instanceof Object // true,可以理解为 Object 是最上层的父类
[] instanceof Array // true
[] instanceof Object // true`
{} instanceof Object // true`
原型链
可以理解为,在 extend 继承时,对父类进行了一次实例化,所有拥有隐式原型__proto__
// 根据上方定义的class
Student.prototype.__proto__
People.prototype
console.log(People.prototype === Student.prototype.__proto__) ==> true
hasOwnProperty()
就是继承于Object
,hasOwnProperty(functionName) => false
无论继承还是自己的函数,均为false
,hasOwnProperty()
属性名只要是继承或者自己拥有的为true
JS 基础 - 作用域和闭包
1.this 的不同应用场景,如何取值?
- 作为普通函数
- 使用 call apply bind 改变 this 指向
- 作为对象方法被调用
- 在 class 方法中调用
- 箭头函数,永远是取上级作用域的 this
2.手写 bind 函数
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this (数组的第一项)
const that = args.shift() // 删除并返回数组第一项
// 获取 fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(that, args)
}
}
3.实际开发中闭包的应用场景,举例说明
- 隐藏数据,只提供API,如做一个简单的 cache 工具
补充知识 - 作用域和*变量
作用域
*变量
- 一个变量在当前作用域没有定义,但被使用
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域没找到,就会报错
xx is not defined
补充知识 - 闭包
作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值
- 函数*变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方
左右两张图都将打印 100
补充知识 - this
this 在各个场景中取什么值,是在函数执行的时候确定的,不是在定义函数定义的时候决定的
- 作为普通函数
- 使用 call apply bind 改变 this 指向
- 作为对象方法被调用
- 在 class 方法中调用
- 箭头函数,永远是取上级作用域的 this
call 是直接执行,bind是返回一个新的函数去执行
JS 基础 - 事件
手写一个通用绑定事件
function bindEvent (elem, type, fn) {
elem.addEventListener(type, fn)
}
Promise 图片懒加载
function loadImg (src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('图片加载失败')
}
img.scr = src
})
retrun promise
}
var result = loadImg('www.baidu.com')
性能优化
性能优化,永远是面试的重点,性能优化对于 React 更加重要
- 在页面中使用了
setTimout()
、addEventListener()
等,要及时在componentWillUnmount()
中销毁
- 使用异步组件
- 使用
React-loadable
动态加载组件
-
shouldComponentUpdate
(简称SCU )、React.PureComponent
、React.memo
- 不可变值 ImmutableJS
shouldComponentUpdate (nextProps, nextState) {
return true // 可以渲染,执行 render(),默认返回 true
return false // 不能渲染,不执行 render()
}
什么情况下需要使用 shouldComponentUpdate
在React中,默认情况下,如果父组件数据发生了更新,那么所有子组件都会无条件更新 !!!!!!
通过shouldComponentUpdate()
retrun fasle 来判断阻止 Header 组件做无意义的更新
shouldComponentUpdate()
并不是每次都需要使用,而是需要的时候才会优化
class App extends React.Component {
constructor () {
this.state = { list: [] }
}
render () {
return (
<div>
{/* 当list数据发生变化时,Header组件也会更新,调用 render() */}
<Header />
<List data={this.state.list}
</div>
)
}
}
在shouldComponentUpdate()
判断中,有一个有意思的问题,解释为什么 React setState()
要用不可变值
// 父组件中
changeList () {
this.state.list.push({id: 2})
this.setState({
list: this.state.list
})
}
// 子组件中
import _ from 'lodash'
shouldComponentUpdate(nextProps, nextState) {
// 数组深度比较(一次性递归到底,耗费性能,工作中慎用)
if (_.isEqual(nextProps.list, this.props.list)) {
return false // 相等,不渲染
}
return true // 不相等,渲染
}
子组件将始终不会渲染,因为在shouldComponentUpdate()
中,this.state.list.push()
已经修改了this.props.list
,而this.setState()
修改了nextProps.list
所以两个值深度比较,将始终相同。
PureComponent 和 memo
- class类组件中用
PureComponent
,无状态组件(无状态)中用memo
- PureComponent, SCU中实现了浅比较
- 浅比较已使用大部分情况(尽量不要做深度比较)
PureComponent 与普通 Component 不同的地方在于,PureComponent自带了一个shouldComponentUpdate()
,并且进行了浅比较
// memo用法
function MyComponent (props) {
/* 使用 props 渲染 */
}
// areEqual 也可不传
function areEqual(prevProps, nextProps) {
if (prevProps.seconds===nextProps.seconds) {
return true
} else {
return false
}
}
export default React.memo(MyComponent, areEqual)
immutable.js
- 彻底拥抱“不可变值”
- 基础共享数据(不是深拷贝),速度快
- 有一定学习和迁移成本
常见基础面试考题
React 组件如何通讯
- 父子组件通过 属性 和 props 通讯
- 通过 context 通讯
- 通过 Redux 通讯
this.setState()相关
import React from 'react'
class App extends React.Component {
constructor (props) {
super(props)
this.state = { count: 0 }
}
componentDidMount () {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 0
setTimeout(() => {
this.setState({count: this.state.count + 1 })
console.log(this.state.count) // 2
}, 0)
setTimeout(() => {
this.setState({count: this.state.count + 1 })
console.log(this.state.count) // 3
}, 0)
// setTimeout(function () {
// this.setState({count: this.state.count + 1 })
// console.log(this.state.count) // 报错,this 指向问题
// }, 0)
}
render () {
return <h1>{this.state.count}</h1>
}
}
export default App // 返回高阶函数
JSX本质是什么.....
前端富文本 dangerouslySetInnerHTML
const rawHtml = '<div><p>Title</p></div>'
const rawHtmlData = {
__html: rawHtml // 这里有个下划线
}
return <div dangerouslySetInnerHTML={rawHtmlData}></div>
两种绑定事件
<button onClcik={bindClcik1.bind(this)}> 使用 .bind(this) </button>
<button onClcik={bindClcik2}> 箭头函数 </button>
// 使用 class 的自带函数,需要重定向 this
bindClcik1 () { alert('bindClcik1') }
// 使用静态方法,使用箭头函数不需要使用 bind(this)
bindClick2 = () => { alert('bindClcik2') }
Event、默认事件、事件冒泡
这里打印出来的Event
对象是 React 封装过的SyntheticEvent
,可以看__proto__.constructor
。React 标准化了事件对象,因此在不同的浏览器中都会有相同的属性。
React 中事件绑定跟 Vue 中完全不同,Vue中事件绑定和触发的对象为同一元素,React中事件触发的对象为document
,绑定元素为当前元素。React的所有事件都会被挂载到document
上和DOM
事件不同。
Vue 的Event
是原生,事件被挂载到当前元素和DOM
事件一
<a href="www.baidu.com" onClick={this.getEvent} target="blank">Get Event</a>
getEvent = (event) => {
event.preventDefault() // 阻止默认事件
event.stopPropagation() // 阻止事件冒泡
console.log(event) // 非原生的 Event
console.log(event.nativeEvent) // 获取原生的 Event
console.log(event.nativeEvent.target) // 绑定事件的对象,这里为 <a></a>
console.log(event.nativeEvent.currentTarget) // 触发事件的对象,这里为 document
}
事件传参
通过.bind()
传参
<div onClick={this.getParams1.bind(this, 'id1', 'title1')}>get params 1</div>
getParams1 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event) // 最后一个参数为Event对象
}
通过箭头函数传参
<div onClick={(event) => { this.getParams2('id2', 'title2', event) }}>get params 2</div>
getParams2 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event)
}
参考 前端react面试题详细解答
表单
<div>
<label htmlFor="userName"></label>
<input value={this.state.userName} onChange={this.handleInputChange.bind(this)} />
</div>
// 实现类似双向数据绑定
handleInputChange (even t) {
const userName = event.target.value
this.setState(() => ({
userName
}))
// 下面这种写法会报错,因为 this.setState 传递一个函数时,为异步方法,等异步执行时已经没有 event
this.setState(() => ({
userName = event.target.value
}))
}
组件传参
普通参数/函数
// 父组件
<div>
<Child text={this.state.text} />
</div>
// 子组件
<div>
<p>{this.props.text}</p>
</div>
属性类型检查
import PropTypes from 'prop-types'
// 对传递的参数强校验
TodoItem.propTypes = {
content: PropTypes.string.isRequired, // 限制为字符串且必传
}
setState()
- 不可变值
- 可能是异步更新
- 可能会被合并
// 错误的写法
this.setState({
count: this.state.count + 1
})
// 正确的写法
const count = this.state.count + 1
this.setState({ count })
正确修改数组值
// 不能使用 push pop splice 等,这样违反了不可变值,会影响 shouldCompententUpdate 判断
this.setState(() => ({
list1: this.state.list1.concat(100), // 追加
list2: [...this.state.list2, 100], // 追加
list3: this.state.list3.slice(0, 3) // 截取
list4: this.state.list4.filter(item => item > 100) // 筛选
}))
正确修改对象值
this.setState(() => ({
obj1: Object.assign({}, this.state.obj1, {a: 100}),
obj2: {...this.state.obj2, a: 100}
}))
通常情况下,setState()
为异步更新数据
const count = this.state.count + 1
this.setState({
count: count
}, () => {
// 这个函数没有默认参数
// 类比 Vue 的 $nextTick
console.log(this.state.count) // 打印更新后的值
})
console.log(this.state.count) // 打印更新前的值
setState()
同步更新数据,在setTimeout()
中setState()
是同步的
setTimeout(() => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
自己定义的 DOM 事件,setState()
是同步的
componentDidMount () {
document.body.addEventListener('click', () => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
}
【重点】 传入对象,会被合并,结果只执行一次,类似于Object.assgin()
初始值 this.state.count = 0
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
输出值 this.state.count = 1
【重点】 传入函数,不会被合并,因为函数无法合并
初始值 this.state.count = 0
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
输出值 this.state.count = 3
组件生命周期
Initialization 初始化
-
constructor()
: class
的构造函数,并非React独有
Mounting 挂载
-
componentWillMount()
: 在组件即将被挂载到页面的时刻自动执行;
-
render()
: 页面挂载;
-
componentDidMount()
: 组件被挂载到页面之后自动执行;
componentWillMount()
和 componentDidMount()
,只会在页面第一次挂载的时候执行,state变化时,不会重新执行
Updation 组件更新
-
shouldComponentUpdate()
: 该生命周期要求返回一个bool
类型的结果,如果返回true
组件正常更新,如果返回false
组件将不会更新;
-
componentWillUpdate()
: 组件被更新之前执行,如果shouldComponentUpdate()
返回false
,将不会被被执行;
-
componentDidUpdate()
: 组件更新完成之后执行;
componentWillReceiveProps()
: props独有的生命周期,执行条件如下:
- 组件要从父组件接收参数;
- 只要父组件的
render()
被执行了,子组件的该生命周期就会执行;
- 如果这个组件第一次存在于父组件中,不会执行;
- 如果这个组件之前已经存在于父组件中,才会执行;
Unmounting 组件卸载
-
componentWillUnmount()
: 当组件即将被从页面中剔除的时候,会被执行;
生命周期简单使用场景
- 使用
shouldComponentUpdate()
防止页面进行不必要的渲染
# 用生命周期进行性能优化
shouldComponentUpdate () {
if (nextProps.content !== this.props.content) {
return true;
}
return false;
}
- Ajax 请求页面初始数据
componentDidMount()
不能写在render()
之中,因为会重复调用,也不能写在componentWillMount()
之中,会与RN等其它框架冲突,不然也可以写在这里面,同样是只执行一次。
同样也可以写在构造函数constructor()
之中,但是不建议这样做。
import axios from 'axios'
componentDidMount () {
axios.get('/api/todolist').then((res) => {
console.log(res.data);
this.setState(() => ({
list: [...res.data]
}));
}).catch((err) => {
console.log(err);
});
}
无状态组件(函数组件)
当一个组件只有一个render()
函数时,我们就可将这个组件定义为无状态组件,无状态组件只有一个函数。
无状态组件的性能比较高,因为它仅是一个函数,而普通组件是一个class
。
- 纯函数
- 输入props,输出JSX
- 没有实例
- 没有生命周期
- 没有state
- 不能扩展其它方法
function List (props) {
const { text } = this.props
return (
<div>{text}</div>
)
}
非受控组件
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
flag: true
}
this.nameInputRef = React.createRef() // 创建 ref
this.fileInputRef = React.createRef() // 创建 ref
}
render () {
return (
<div>
{/* 这里使用 defaultValue 而不是value,使用 ref */}
<input defaultValue={this.state.name} ref={this.nameInputRef} />
<button onClick={this.alertName.bind(this)}>alert value</button>
{/* file 类型的必须用 ref 获取 dom 来获取数据 */}
<input type="file" ref={this.fileInputRef} />
</div>
)
}
alertName () {
const ele = this.nameInputRef.current // 通过 ref 获取 dom 节点
alert(ele.value)
}
}
portals 传送门
使用 Portals 渲染到 body 上,fixed 元素要放在 body 上,有更好的浏览器兼容。
常见使用场景:
- 父组件 overflow: hidden , 但是子组件又想展示;
- 父组件的 z-index 太小;
- fixed 需要放在 body 第一层;
import ReactDOM from 'react-dom'
render () {
return ReactDOM.creatPortal(
<div>{this.props.children}</div>,
document.body
)
}
context 上下文
使用场景:公共信息(语言、主题)传递给每个组件,如果组件层级过多,用props
传递就会繁琐,用 redux
小题大做。
import React from 'react'
// 创建 Context 填入默认值(任何一个 js 变量)
export const {Provider,Consumer} = React.createContext("默认名称")
// 在最上级组件中
constructor (props) {
super(props)
this.state = { theme: 'light' }
}
render () {
// 这里使用 this.state.theme 是为了可以修改,初始化的值为默认值,不能修改
// value 中放共享的数据
return (
<Provider value={this.state.theme}>
.... <button onClick={this.changeTheme}></button>
</Provider>
)
}
// 子组件中调用
import { Consumer } from "./index";//引入父组件的Consumer容器
render () {
return (
// Consumer 容器,可以拿到上文传递下来的 theme 属性,并可以展示对应的值
<Consumer>
{ theme => <div>子组件。获取父组件的值: {theme} </div> } </Consumer>
)
}
异步组件
// 引入需要异步加载的组件
const LazyComponent = React.lazy(() => import('./lazyDemo') )
// 使用异步组件,异步组件加载中时,显示fallback中的内容
<React.Suspense fallback={<div>异步组件加载中</div>}>
<LazyComponent />
</React.Suspense>
组件公共逻辑的抽离
- Vue 中的 mixin,已被 React弃用
- 高阶组件 HOC
- Render Props
高阶组件
高阶组件不是一种功能,而是一种模式
// 高阶组件 基本用法
const HOCFactory = (Component) => {
class HOC extends React.Component {
// 在此定义多个组件的公共逻辑
render () {
return <Component {...thi.props} /> // 返回拼装的结果
}
}
return HOC
}
const MyComponent1 = HOCFactory(WrappedComponent1)
const MyComponent2 = HOCFactory(WrappedComponent2)
实际案例
import React from 'react'
// 高阶组件
const withMouse = (Component) => {
class withMouseComponent extends React.Component {
constructor(props) {
super(props)
this.state = { x: 0, y: 0 }
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
{/* 1. 透传所有 props 2. 增加 mouse 属性 */} <Component {...this.props} mouse={this.state}/>
</div>
)
}
}
return withMouseComponent
}
const App = (props) => {
const a = props.a
const { x, y } = props.mouse // 接收 mouse 属性
return (
<div style={{ height: '500px' }}>
<h1>The mouse position is ({x}, {y})</h1>
<p>{a}</p>
</div>
)
}
export default withMouse(App) // 返回高阶函数
Render Props
Render Props 核心思想:通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
class Factory extends React.Component {
constructor () {
this.state = {
/* 这里 state 即多个组件的公共逻辑的数据 */
}
}
/* 修改 state */
render () {
return <div>{this.props.render(this.state)}</div>
}
}
const App = () => {
/* render 是一个函数组件 */
<Factory render={
(props) => <p>{props.a} {props.b}...</p>
} />
}
Redux 单项数据流
- dispatch(action)
- reducer 产生 newState
- subscribe 触发通知
Redux 单项数据流图
React-router
路由模式
- hash模式(默认),如:baidu.com/#/user/10
- H5 history 模式,如:baidu.com/user/20
后者需要 server 端支持,因此无特殊需求可选择前者
常用组件
import {
HashRouter,
BrowserRouter,
Switch,
Route
} from 'react-router-dom'
function RouterComponent () {
return (
<BrowserRouter>
<Switch>
<Route path='/' exact component={Home}>
{/* 动态路由 */} <Route path='/detail/:id' exact component={Detail}></Route>
{/* 匹配404等页面 */} <Route path='*' exact component={NotFound}></Route>
</Switch>
</BrowserRouter>
)}
路由跳转
- 标签跳转,通过
<Link to="/">
这个隐性 a 标签跳转
- JS跳转,
import { useHistory } from 'react-router-dom'
&& history.push('/')
路由配置懒加载
import React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
const Home = React.lazy(() => import('./pages/Home'))
const Detail = React.lazy(() => import('./pages/Detail'))
const App = () => (
<BrowserRouter>
<React.Suspense fallback={<div>Loading...</div>}> <Switch>
<Route exact path="/" component={Home} />
<Route exact path="/detail" component={Detail} />
</Switch>
</React.Suspense>
</BrowserRouter>
)
Vue 原理
数据驱动视图(MVVM, setState),Vue MVVM ( Model + View + ViewModel )
Vue响应式
组件 data 的数据一旦变化,立刻触发视图的更新,实现数据驱动视图的第一步
核心API:Object.defineProperty,Object.defineProperty 有一些缺点,Vue3.0 开始启用 Proxy, Proxy有兼容性问题,且无法 polyfill(兼容性问题解决方案)
// Object.defineProperty 基础使用
const data = {}
const name = 'Actoress'
Object.defineProperty(data, "name", {
get: function () {
console.log('get')
return name
},
set: function (newValue) {
console.log('set')
name = newValue
}
})
// 调用
console.log(data.name) // get() 执行 => 'Actoress'
data.name = 'Wu' // set() 执行
深度监听
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性
- 数组需要重新定义数组原型
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 不能写在这里,这样会污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'Actoress',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'Wu'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
虚拟DOM(Virtual DOM)
vdom 是实现 Vue 和 React 的重要基石
为什么会有 vdom
- 有了一定复杂度,想减少计算次数比较难
- 能不能把计算,更多的转移到JS计算?因为JS执行速度很快
- vdom 用 JS 模拟 DOM 结构,计算出最小的变更,操作DOM
用JS模拟DOM结构
使用 snabbdom 操作虚拟 dom
Diff 算法
- diff 算法是 vdom 中最核心、最关键的部分
- diff 算法能在日常使用 Vue React 中体现出来(循环的 key)
优化前 树 diff 的时间复杂度 (n^3)
- 遍历Tree1,遍历Tree2
- 排序
- 假设有1000个节点,就要计算1亿次,算法不可用
优化后时间复杂度 (n^1)
- 只比较同一层级,不跨级比较
- tag 不相同,则直接删掉重建,不再深度比较
- tag 和 key,两者都相同,则认为是相同节点,不再深度比较
React 原理
数据驱动视图(MVVM, setState)
- 数据驱动视图 - React
this.setState()
- 函数式编程:函数式式编程是一种编程范式,两个最重要的概念是 纯函数、不可变值
JSX 本质
- JSX 等同于 Vue 模板
- Vue 模板不是 html
- JSX 也不是 JS
讲JSX语法,通过 React.createElement()
编译成Dom,BABEL 可以编译JSX
流程:JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
React 底层会通过 React.createElement()
这个方法,将 JSX 语法转成JS对象,React.createElement()
可以接收三个参数,第一个为标签名称,第二参数为属性,第三个参数为内容
createElement() 根据首字母大小写来区分是组件还是HTML标签,React规定组件首字母必须大写,HTML规定标签首字母必须小写
// 第一个参数为 标签(tag) 可为 'div'标签名 或 List组件
// 第二个参数为:属性(props)
// 第三个参数之后都为子节点(child),可以在第三个参数传一个数组,也可以在第三、四、五....参数中传入
React.createElement('tag', null, [child1, chlild2, child3])
或者
React.createElement('tag', { className: 'class1' }, child1, chlild2, child3)
事件合成机制
- 所有事件挂载到 document 上
- event 不是原生的,是
SyntheticEvent
合成事件对象
- 与 Vue 事件不同,和 DOM 事件也不同
为什么要合成事件机制
- 更好的兼容性和跨平台,摆脱传统DOM事件
- 挂载到document,减少内存消耗,避免频繁解绑
- 方便事件的统一管理,如:事务机制
setState 和 batchUpdate(批处理)
setState
- 有时异步(普通使用),有时同步(setTimeout, DOM事件)
- 有时合并(对象形式),有时不合并(函数形式),比较好理解(类似 Object.assign),函数无法合并
核心要点
- setState 主流程
- batchUpdate 机制
- transaction(事务)机制
看this.setState()
是否是异步,看 isBatchingUpdates 的状态,为 true 就是异步,为 false 就是同步
哪些能命中 batchUpdate 机制
- 生命周期(和它调用的函数)
- React 中注册的事件(和它调用的函数)
- React 可以“管理”的入口
哪些不能命中 batchUpdate 机制
- setTimeout setInterval等(和它调用的函数)
- 自定义的DOM时间(和它调用的函数)
- React“管不到”的入口
transaction 事务机制
常见基础面试题
1.组件之间如何通讯
- 父子组件 props
- 自定义事件
- Redux 和 Context,简单数据用 Context
2.JSX 本质
JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
3.shouldComponentUpdate 用途
4.redux单项数据流
Redux 单项数据流图
5.setState场景题
6.什么是纯函数
7.列表渲染为何要用key
- 必须用 key,且不能是 index 和 random
- diff 算法中通过 tag 和 key 判断,是否是同一个节点
- 减少渲染次数,提升渲染性能
8.函数组件 和 class 组件区别
- 纯函数,输入 props,输出JSX
- 没有实力,没有生命周期,没有state
- 不能扩展其它方法
9.如何使用异步组件
- 加载大组件
- React.lazy
- React.Suspense
10.多个组件有公共逻辑,如何抽离
11.react-router 如何配置懒加载
上文中有...
12.PureComponent 有何区别
- 实现了浅比较的 shouldComponentUpdate
- 优化性能
- 但要结合不可变值使用
13.React事件和DOM事件的区别
- 所有事件挂载到 document 上
- event 不是原生的,是 SyntheticEvent 合成事件对象
14.React性能优化
- 渲染列表时加Key
- 自定义事件、DOM事件及时销毁
- 合理使用异步组件
- 减少函数 bind this 的次数
- 合理使用 shouldComponentUpdate、PureComponent 和 memo
- 合理使用 ImmutableJS
- webpack层面优化
- 前端通用是能优化,如图片懒加载
- 使用SSR
React 和 Vue 的区别
相同点
- 都支持组件化
- 都是数据驱动视图
- 都是用 vdom 操作 DOM
不同点
- React 使用 JSX 拥抱JS,Vue使用模板拥抱 html
- React 函数式编程,Vue声明式编程
- React 更多需要自力更生,Vue把想要的都给你
JS 基础 - 变量类型和计算
typeof能判断哪些类型
- 识别所有类型
- 识别函数
- 判断是否是引用类型,返回都为 object,不能再细分
2. 何时使用===何时使用==
3. 值类型和引用类型的区别
引用类型的本质是相同的内存地址,出于性能问题考虑,所以JS对象使用引用类型,为了避免这种情况所以需要深拷贝
常见值类型:undefined、String、Bool、Symbol('s')
常见引用类型:Object、Array、null(指向空地址)
特殊引用类型:function
4.变量计算
字符串拼接
5. 手写深拷贝
funciton deepClone ( obj = {}) {
// 判断是否需要深拷贝,不是对象和数组
if (typeof obj !== 'object' || obj == null) {
return obj
}
let result
// 判断是否为一个数组
if (obj instanceof Array) {
result = []
} else {
result = {}
}
// 遍历对象
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归【重点】
result[key] = deepClone(obj[key])
}
}
return result
}
JS 基础 - 原型和原型链
JS本身是一个基于原型继承的语言,PS:class 的 extends 本质也是原型链继承
1.如何准确判断一个变量是不是数组?
2.手写一个简易的jQuery,考虑插件和扩展性
class jQuery {
constructor (selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this.[i] = result[i]
}
this.length = length
}
get (index) {
return this[index]
}
each (fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
}
on (type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// 复写,造*
class MyJQuery extends jQuery {
constructor (selector) {
super(selector)
}
......
}
3.class 的原型本质,怎么理解?
补充知识 - 定义class
// 父类
class People {
constructor (old) {
this.old = old
}
eat () {
consoloe.log('eating')
}
}
// 继承
class Student extends People {
constructor(name, number,old) {
super(old) // 变量传递给父类执行
this.name = name
this.number = number
}
sayHi () {
console.log(this.name, this.number)
}
}
const me = new Student('小明', 10, 20) // 新建对象
console.log(me.name) // => 小明
me.sayHi() // => 小明 10
// class 实际上是函数,可见是语法糖
typeof People => 'function'
typeof Student => 'function'
// 隐式原型和显式原型
me.__proto__ // 隐式原型 => People
Student.prototype // 显式原型 => People
me.__proto === Student.prototype => true 全等通过的话,就说明引用的是同一个地址
- 每个实例都有隐式原型
__proto__
- 每个 class 都有显式原型
prototype
- 实例的隐式原型指向对应class的显式原型
基于原型的执行规则
- 优先在自身属性和自身方法中查找
- 如果找不到则自动去
__proto__
隐式原型中查找
补充知识 - 类型判断 instanceof
instanceof 工作原理:是顺着__proto__
隐式原型一层层往上找
// 根据上方定义的class
me instanceof Student // true
me instanceof People // true
me instanceof Object // true,可以理解为 Object 是最上层的父类
[] instanceof Array // true
[] instanceof Object // true`
{} instanceof Object // true`
原型链
可以理解为,在 extend 继承时,对父类进行了一次实例化,所有拥有隐式原型__proto__
// 根据上方定义的class
Student.prototype.__proto__
People.prototype
console.log(People.prototype === Student.prototype.__proto__) ==> true
hasOwnProperty()
就是继承于Object
,hasOwnProperty(functionName) => false
无论继承还是自己的函数,均为false
,hasOwnProperty()
属性名只要是继承或者自己拥有的为true
JS 基础 - 作用域和闭包
1.this 的不同应用场景,如何取值?
- 作为普通函数
- 使用 call apply bind 改变 this 指向
- 作为对象方法被调用
- 在 class 方法中调用
- 箭头函数,永远是取上级作用域的 this
2.手写 bind 函数
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this (数组的第一项)
const that = args.shift() // 删除并返回数组第一项
// 获取 fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(that, args)
}
}
3.实际开发中闭包的应用场景,举例说明
- 隐藏数据,只提供API,如做一个简单的 cache 工具
补充知识 - 作用域和*变量
作用域
*变量
- 一个变量在当前作用域没有定义,但被使用
- 向上级作用域,一层一层依次寻找,直至找到为止
- 如果到全局作用域没找到,就会报错
xx is not defined
补充知识 - 闭包
作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值
- 函数*变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方
左右两张图都将打印 100
补充知识 - this
this 在各个场景中取什么值,是在函数执行的时候确定的,不是在定义函数定义的时候决定的
- 作为普通函数
- 使用 call apply bind 改变 this 指向
- 作为对象方法被调用
- 在 class 方法中调用
- 箭头函数,永远是取上级作用域的 this
call 是直接执行,bind是返回一个新的函数去执行
JS 基础 - 事件
手写一个通用绑定事件
function bindEvent (elem, type, fn) {
elem.addEventListener(type, fn)
}
Promise 图片懒加载
function loadImg (src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('图片加载失败')
}
img.scr = src
})
retrun promise
}
var result = loadImg('www.baidu.com')