lulujianglab / blog

:bento:lulujiang blog
https://lulujianglab.com/
83 stars 4 forks source link

Virtual DOM 中那些你不知道的事 #46

Open lulujianglab opened 5 years ago

lulujianglab commented 5 years ago

虚拟DOM是啥?以及diff算法原理

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能 原理:

diff算法,DOM树对比过程

https://github.com/lulujianglab/blog/issues/46

为什么虚拟 dom 会提高性能?

虚拟DOM提高性能,不是说不操作DOM,而是减少操作DOM的次数,减少回流和重绘 虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能

使用diff算法比较新旧虚拟DOM----即比较两个js对象不怎么耗性能,而比较两个真实的DOM比较耗性能,从而虚拟DOM极大的提升了性能

虚拟DOM的目的?

实现页面中DOM元素的高效更新

虚拟DOM会比真实DOM快吗?什么情况下用虚拟DOM好

虚拟DOM并不一定比原生操作DOM快。需不需要虚拟DOM,其实与框架的DOM操作机制有关 React 的基本思维模式是每次有变动就重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。 比如,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起patch简化的dom操作省下来的时间可观的多。 可以看到,innerHTML 的总计算量是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。和 DOM 操作比起来,js 计算是极其快速的。 这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受; 2) 你依然可以用类似 innerHTML 的思路去写你的应用。

这也是 React 厉害的地方。并不是说它比 DOM 快,而是说不管你数据怎么变化,我都可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用新的数据重新生成一个虚拟 DOM 树,然后比较新旧 DOM,找出差异,再更新到 DOM 树上。这就是所谓的 diff 算法

react是怎么工作的,怎么提高性能

主要还是说了下react的生命周期,还有shouldComponentUpdate这个函数,以及diff算法 https://github.com/lulujianglab/blog/issues/47

React 的工作原理

React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 "diffing" 算法来标记虚拟 DOM 中的改变,第二步是调节(reconciliation),会用 diff 的结果来更新 DOM

react 事件绑定

由于类的方法默认不会绑定this,因此在调用的时候如果忘记绑定,this的值将会是undefined。 绑定方式有以下几种:

比较:

方式1是官方推荐的绑定方式,也是性能最好的方式。方式2和方式3会有性能影响并且当方法作为属性传递给子组件的时候会引起重渲问题。方式4目前是最好的绑定方式,需要结合bable转译 this 的本质就是:this跟作用域无关的,只跟执行上下文有关

注意:只要是需要在调用的地方传参,就必须在事件绑定的地方使用bind或者箭头函数.又回到了方式二和方式三

生命周期

函数式编程,纯函数

React创建组件的方式

React 中有三种构建组件的方式 React.createClass()、ES6 class 和无状态函数

组件性能优化

shouldComponentUpdate(react 性能优化是哪个周期函数?)

这个方法用来判断是否需要调用 render 方法重新描绘 dom。 因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

pureComponent

不可变数据

key

等等优化方法,每一点的优点和缺点

如何设计一个好组件?容器组件和展示组件

合理划分组件,分为业务组件和技术组件

调用setState之后发生了什么

refs的作用

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。 我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

class CustomForm 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 属性,该属性声明的回调函数会接收 input 对应的 DOM 元素,我们将其绑定到 this 指针以便在其他的类函数中使用。 另外值得一提的是,refs 并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:

function CustomForm ({handleSubmit}) {
  let inputElement
  return (
    <form onSubmit={() => handleSubmit(inputElement.value)}>
      <input
        type='text'
        ref={(input) => inputElement = input} />
      <button type='submit'>Submit</button>
    </form>
  )
}

如果你创建了类似于下面的 Twitter 元素,那么它相关的类定义是啥样子的?

<Twitter username='tylermcginnis33'>
  {(user) => user === null
    ? <Loading />
    : <Badge info={user} />}
</Twitter>

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
// fetchUser take in a username returns a promise
// which will resolve with that username's data.
class Twitter extends Component {
  // finish this
}

回调渲染模式:这种模式中,组件会接收某个函数作为其子组件,然后在渲染函数中以 props.children 进行调用

这种模式的优势在于将父组件与子组件解耦和,父组件可以直接访问子组件的内部状态而不需要再通过 Props 传递,这样父组件能够更为方便地控制子组件展示的 UI 界面

譬如产品经理让我们将原本展示的 Badge 替换为 Profile,我们可以轻易地修改下回调函数

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
class Twitter extends Component {
  state = {
    user: null,
  }
  static propTypes = {
    username: PropTypes.string.isRequired,
  }
  componentDidMount () {
    fetchUser(this.props.username)
      .then((user) => this.setState({user}))
  }
  render () {
    return this.props.children(this.state.user)
  }
}

何为 Children

展示组件(Presentational component)和容器组件(Container component)之间有何不同

类组件(Class component)和函数式组件(Functional component)之间有何不同

(组件的)状态(state)和属性(props)之间有何不同

何为受控组件(controlled component)

<Input placeholder="请输入" value={inputValue.trim()} onChange={this.handleChangeValue} onPressEnter={this.handleSearch} />


## 高阶组件是什么和常见的高阶组件
**高阶组件是一个以组件为参数并返回一个新组件的函数。**
HOC 运行你重用代码、逻辑和引导抽象。
最常见的可能是 Redux 的 connect 函数和antD的Form.create()组件。
除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果你发现你在不同的地方写了大量代码来做同一件事时,就应该考虑将代码重构为可重用的 HOC

## 为什么建议传递给 setState 的参数是一个 callback 而不是一个对象
因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state。而通过callback的第一个参数可以拿到上一次的prevState,此时的prevState也是合并了前面多次setState计算的结果

> setState第二个参数支持回调函数,在回调里state是最新的。并且**回调的执行时机在于state合并处理之后**

怎么立即获取到修改后的state呢?可以通过 setState 中传递函数的方式及回调去实现
```js
state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 2
  })
}

如果只是通过回调去实现,只能立即获取上一步修改后的结果

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 1
  })
}

demo-01:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 四:2
  })
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 五:2
  })
  console.log("console-end: " + this.state.count) // 二:0
}

demo-02:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 四:1
  })
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 五:1
  })
  console.log("console-end: " + this.state.count) // 二:0
}

React 其实会维护着一个 state 的更新队列,每次调用 setState 都会先把当前修改的 state 推进这个队列,在最后,React 会对这个队列进行合并处理,然后去执行回调。根据最终的合并结果再去走下面的流程(更新虚拟dom,触发渲染)

setState为什么要设计成异步的?react的setState同步还是异步?

setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  setTimeout(() => { // setTimeout中调用
    console.log("setTimeout-first: " + this.state.count); // 1
    this.setState({ count: this.state.count + 1 });
    console.log("setTimeout-end: " + this.state.count); // 2
  }, 0)
}

(在构造函数中)调用 super(props) 的目的是什么

在 super() 被调用之前,子类是不能使用 this 的,在 ES6 中,子类必须在 constructor 中调用 super()。 传递 props 给 super() 的原因则是便于(在子类中)能在 constructor 访问 this.props

如何实现异步网络请求的?应该在 React 组件的何处发起 Ajax 请求,为什么

在 React 组件中,应该在 componentDidMount 中发起网络请求。 这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。 如果在之前发起请求,可能在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,将不起作用。在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了。

react16新特性

尤其理解time slice和suspense

在 React 当中 Element 和 Component 有何区别

简单地说,一个 React element 描述了你想在屏幕上看到什么。换个说法就是,一个 React element 是一些 UI 的对象表示。

一个 React Component 是一个函数或一个类,它可以接受输入并返回一个 React element t(通常是通过 JSX ,它被转化成一个 createElement 调用)

路由实现原理

react-router等一众路由插件实现的功能是更新页面的视图,但是却不重新请求页面,也就是说,其实,他们并没有实际进行了跳转,而是修改了页面的DOM并通过修改页面的URL来模拟跳转 在HTML5之前,页面路由只有hash模式,而HTML5中History对象的新增方法,带来了另一种模式:history模式

hash模式: 在HTML5之前,vue-router是通过修改URL的hash值来达到修改页面URL并生成历史记录,虽然不会重新请求页面,但会发现路径前总会有一个#,比如http://localhost:8080/#/b 因为没开启history模式的情况下,vue-router是通过hashchange事件来监听URL中hash的改变并通过修改hash来模拟路径的变化 hash模式最大的优点是兼容性强,可以兼容一众老式浏览器。而它最大的缺点是,页面URL中一直挂着一个难看的#

history模式: HTML5发布后,又有了history模式 vue-router的history模式就是通过HTML5中History对象的pushState方法进行模拟的 HTML5还提供了一个popstate事件,当用户点击前进、后退按钮,或者调用back、forward、go方法时触发,可以监听URL的改变 不仅如此,还可以使用pushState的第一个参数进行传值,使用History的state属性进行取值。 但是目前只有兼容了HTML5的浏览器(IE10+)才能使用history模式

简述 flux 思想

Flux 的最大特点,就是数据的"单向流动"。

React 项目用过什么脚手架

redux 有什么缺点

组件间通信

React key是干嘛的?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识

说一下 redux ,redux、react-redux等原理

https://github.com/lulujianglab/blog/issues/34

基本思想

整个应用的 state 保持在一个单一的 store 中 改变应用 state 的唯一方式是在应用中触发 actions,然后为这些 actions 编写 reducers 来修改 state

store

Store 是一个 javascript 对象,它保存了整个应用的 state。与此同时,Store 也承担以下职责:

action

Actions 是一个纯 javascript 对象,它们必须有一个 type 属性表明正在执行的 action 的类型。实质上,action 是将数据从应用程序发送到 store 的有效载荷

reducer

一个 reducer 是一个纯函数,该函数以先前的 state 和一个 action 作为参数,并返回下一个 state

Redux Thunk 的作用是什么

Redux thunk 是一个允许你编写返回一个函数而不是一个 action 的 actions creators 的中间件。如果满足某个条件,thunk 则可以用来延迟 action 的派发(dispatch),这可以处理异步 action 的派发(dispatch)

何为纯函数(pure function)

一个纯函数是一个不依赖于且不改变其作用域之外的变量状态的函数,这也意味着一个纯函数对于同样的参数总是返回同样的结果

combineReducers

combineReducers 函数主要用来接收一个对象,将参数过滤后返回一个函数。该函数里有一个过滤参数后的对象 finalReducers,遍历该对象,然后执行对象中的每一个 reducer 函数,最后将新的 state 返回

介绍Redux数据流的流程

view 到 redux 的过程中会派发一个 action , action 通过 Store 的 dispatch 方法,会派发给 store , store接收到 action ,再连同之前的 state 一起传给 reducer , reducer 返回一个新的数据给 store , store 就可以去改变自己的 state ,组件接收到新的 state 就可以重新渲染页面了

其中,redux有三个基本属性,Action,Reducer,Store

Redux如何实现多个组件之间的通信,多个组件使用相同状态如何进行管理

react-redux(Provider 传入到最外层组件store 在需要用到的地方 用 connect 获取 (mapStateToProps, mapDispatchToProps) 在页面中引用) 类似发布订阅模式, 一个地方修改了这个值, 其他所有使用了这个相同状态的地方也会更改

const App = (
  <Provider store={store}>
    <TodoList />
  </Provider>
)
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

多个组件之间如何拆分各自的state,每块小的组件有自己的状态,它们之间还有一些公共的状态需要维护,如何思考这块

在实际项目开发中,如果 reducer 存放过多的数据,可能会造成代码的不可维护,我们需要把总的 reducer 拆分为多个子的 reducer,然后再做一个整合。 一个全局的 reducer , 页面级别的 reducer , 然后redux 里有个 combineReducers 把所有的 reducer 合在一起,小组件的使用与全局的使用是分开的互不影响

import { combineReducers } from 'redux'
import headerReducer from '../common/header/store/reducer'

export default combineReducers({
  header: headerReducer
})

然后在页面中通过state.header.focused访问

使用过的Redux中间件

react-redux React-Redux是 redux官方提供的 React 绑定库,可以在 react 中非常方便是使用 redux React-redux有两个核心方法。一个是Provider,另一个是connect

redux-thunk(把action 返回的对象 换成一个异步函数) :

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
) 
export default store

因为reducer 必须是纯函数,是不能进行ajax这种异步操作的,而在组件中直接进行ajax异步操作或者复杂的业务逻辑处理,组件会显得过于臃肿。 有了redux-thunk,就可以将ajax异步请求或者是复杂的逻辑放到 action 去处理,原则上 action 返回的是一个对象,但当我们使用 redux-thunk 中间件后, action 就可以返回一个函数了,继而可以在函数里边进行异步操作,也就可以把组件中获取数据的请求放入这个函数中了

export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/list.json').then((res) => {
      const data = res.data
      const action = initListAction(data)
      dispatch(action)
    })
  }
}

componentDidMount() {
  const action = getTodoList()
  store.dispatch(action) // 调用 store.dispatch()这个函数时,action这个函数就会被执行
}

redux-saga 虽然redux-thunk可以接受函数作为action,但函数的内部如果有多个异步操作,就需要为每一个异步操作都定义一个action,异步操作太为分散而不易维护 这个时候就可以用到redux-saga。 redux-saga也是做异步代码拆分的,可以完全替代 redux-thunk 。 要通过redux-saga的createSagaMiddleware()方法创建saga中间件,还需要有一个sagajs文件,这样通过sagaMiddleware.run(mySaga)来运行这个sagas文件,这样当组件dispatch一个action的时候,不仅reducer可以接收到,sagas文件中也可以接收到了

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'
import mySaga from './sagas'

const sagaMiddleware = createSagaMiddleware() // 创建saga中间件
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
) 
sagaMiddleware.run(mySaga)
export default store

sagajs导出的是个生成器函数,通过在生成器函数中使用takeEvery方法,捕捉派发出来的所有 action。takeEvery有两个参数,第一个参数时action type,第二个参数是函数,一旦接收到符合条件的 action type,就执行第二个参数方法,所以就可以把异步逻辑写到这个方法里集中处理了,这个方法中有一个put方法也可以派发action操作。类似于 redux 原始的 dispatch,都可以发出 action,且发出的 action 都会被 reducer 监听到

import { takeEvery, put  } from 'redux-saga/effects'
import axios from 'axios'

function* getInitList() {
  const res = yield axios.get('/list.json')
  const action = initListAction(res.data)
  yield put(action)
}
function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList)
}
export default mySaga

redux-saga 远比 redux-thunk 复杂的多, redux-saga 里边有非常多的api,我们只用了 takeEvery 、 put ,官方文档中还有很多我们经常用到的 call 、 takeLatest 等,在处理大型项目时, redux-saga 是要优于 redux-thunk 的;但是从另一角度来说, redux-thunk 几乎没有任何 api ,特点就是在 action 里面返回的内容不仅仅是个对象,还可以是个函数

只要使用了中间件,就需要使用applyMiddleware()函数,顾名思义,是运行中间件的函数。它是Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。 中间件就是指的 dispatch 方法的一个封装或者升级。(redux-thunk中间件:对 dispatch 方法做了封装之后,既可以接收对象,又可以接收函数了)所以中间件本质上只是拓展了store.dispatch方法