react

  • 一次学习,多端受用
    react中抽象了一层虚拟dom,所以我们可以频繁的修改状态,但是更改的都是虚拟dom。当虚拟dom发生变化后,会集中更新到真实的dom,因为虚拟dom的存在,只要替换掉底层的渲染引擎,就可以突破浏览器。

  • pureComponent

class PureComponent extends Component {
  shouldComponentUpdate (nextProps, nextState) {
    const { state, props } = this
    function shdowCompare(a, b) {
      return a === b || Object.keys(a).every(k => a[key] === b[k])
    }
    return shdowCompare(state, nextState) && shdowCompare(props, nextProps)
  }
}
  • react中使用createElement和JSX来实例化组件
    componentWillReceiveProps 一般用来将新的props同步到state。
    为了提升性能,react会把多次setState合并成一次。

  • 组件和事件
    react事件是天生的事件代理,看起来散落在元素上,其实react仅仅在根元素绑定事件,所有事件通过事件代理响应。
    事件中的事件对象并不是原生事件对象,而是封装过的,屏蔽了浏览器的差异。react也提供了访问原生事件的方式。

class EventEmitter {
  constructor () {
    this.eventMap = {}
  }
  sub (name, cb) {
    const eventList = this.eventMap[name] = this.eventMap[name] || []
    eventList.push(cb)
  }
  pub(name, ...data) {}
}
  • 高阶组件
function HOC1(innerComponent) {
  return class WrapComponent extends Components {
    render () { 
        return ({{ this.props.children }})
    }
  }
}

function HOC2(innerComponent) {
  return class WrapComponent extends innerComponent{
    
  }
}
function SetTimeoutHOC (InnerComponent) {
  return class wrapComponent extends InnerComponent {
    componentwill
  }
}

通过ref调用react组件以及组件中的方法,同时也可以调用原生dom
react会对输出的内容进行xss过滤,但有时不需要,比如这个接口返回html片段的情况下,通过dangerousSetInnerHTML可以将HTML片段直接设置到DOM上。

  • 初识store
let store = {
  dispatch,
  getState,
  subscribe,
  replaceReducer
}

dispatch: 派发action
subscribe(listener): 订阅页面数据状态,即store中state的变化
getState: 获取当前页面状态树
replaceReducer(nextReducer): 社区一些热更新或者代码分离技术可能会使用到。

  • 编写reducer函数更新数据
const updateReducer (preState, action) {
  switch (action.type) {
    case  'case1':
      return newState1
    default:     
      return preState
  }
}

当无法匹配action时,默认返回preState
当页面数据状态更新之后,如何促使页面发生UI更新?实际上是store.subscribe(cb)订阅数据更新,并由cb完成UI更新。

  • 合理拆分reducer函数
    普通变量存在于栈内存中,但是对象其实存在于堆内存。当我们获取对象时,首先获取栈内存的引用地址,然后根据引用地址从堆内存中获取所需要的值。

  • deepClone

const deepClone = data => {
  let t = type(data), o, i, length;
  // 创建新的数组和对象
  if (t === 'array) {
    o = []
  } else if (t === 'object') {
    o = {}
  } else { return data }
  
  if (t == 'array')  {
    data.forEach(v => o.push(deepClone(v)))
    return o
  }
  
}

实际开发中,如果数据有多层。深拷贝对于开发性能并不友好。

Redux 中间件和异步

中间件就是在派发action和执行reducer之间,添加自定义扩展功能。
中间件可以在action到达reducer之前进行日志记录,中断action触发,甚至修改action。

  • redux-thunk中间件

假如有一个异步需求,比如需要派发一个网络请求action,在网络请求返回之后再派发一个action返回数据渲染页面。
设想一下:如果dispatch可以接收一个函数作为参数,在函数体内进行异步操作,并在异步完成后再派发action。

store.dispatch(fetchNewBook('learnRedux'))
function fetchNewBook (book) {
  return (dispatch) => {
    dispatch({})
    ajax({}).then(v => { dispatch({}) })
  }
}

给dispatch函数传一个异步函数fetchNewBook,这就是redux-thunk中间件对dispatch功能的增强,注意:中间件参数顺序有讲究。

  • react和redux的衔接点
    root组件需要获取页面的状态数据,并向下进行派发。这样,store.getState()的返回值就需要传递给root组件,作为props的存在。同时又需要在组件中调用dispatch方法。再通过store.subscribe()订阅state的改变。

  • 使用react-redux库
    容器组件:指的是数据状态和逻辑的容器。它并不负责展示,只维护内部状态,进行数据分发和派发action。
    展示组件:只负责接收数据并展示。

那么react-redux怎么生成容器组件?

Connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)

connect用来连接容器组件和展示组件。connect的核心是将开发者定义的组件包装转换成容器组件。所生成的容器组件能使用store中的哪些数据,全由connect参数决定。
connect是典型的柯里化函数,第一次设置参数,第二次接收一个组件,并在该组件的基础上返回一个容器组件。第一个参数是一个函数,它的返回值将设置给组件的props,默认情况下dispatch会注入到组件的props上。
那么connect是如何获取store中的内容呢?
一般做法是将provider作为整个应用的根组件,并获取store作为它的prop。

深入理解redux

  • redux源码探索-store的实现
store = {
  dispatch,
  getState,
  subscribe,
  replaceReducer
}
const render = () => {
  document.body.innerText = store.getState()
}
store.subscribe(render)
render()

接下来,我们思考如何实现store。

const createStore = (reducer) => {
  let state
  let listeners = []
  const getState = () => state
  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listener => listener())
  }
  const subscribe = (listener) => { 
    listeners.push(listener) 
    return () => {
      listeners = listeners.filter (item => item !== listener)
    }
  }
  return {
    getState,
    dispatch,
    subscribe
  }
}

subscribe返回一个函数用于取消订阅,其实现方式是数组的filter方法。
createStore被调用后,Redux就会设置一个初始的空状态,我们只需要在createStore方法中加入如下一行触发一个空action即可。

  • combineReducers的实现
const combineReducers = (reducers) => {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce((nextState, key) => {
        // 进入不存在的action会返回原来的state
        nextState[key] = reducers[key] (state[key], action)
        return nextState
    }, {})
  }
}

combineReducers返回一个rootReducers, rootReducers返回经过各个reducer处理后的全新数据状态,既更新后的state。
为了获取所有reducer的计算结果,我们使用Object.keys对Object进行遍历,这样我们就可以根据数组的每一项进行state的计算,因为这个数组的每一项都是自定义的reducer函数。

  • dispatch的改造 - 实现记录日志
    dispatch实质就是一个函数,它负责调用reducer方法,依靠reducer的执行进行状态变更,接着依次执行各监听函数,目的是实现视图的更新。
    这样我们的入手点就在dispatch函数的入手前后。
    创建一个addLogToDispatch函数,用来生成取代原始的dispatch方法。这个函数对store中的dispatch进行拦截,并记录原始的dispatch为rawDispatch,addLogToDispatch在行为应予原始dispatch保持一致。
const addLogToDispatch = (store) => {
  const rawDispatch = store.dispatch
  return (action) => {
     console.log('pre state:', store.getState())
     const returnvalue = rawDispatch(action)
     console.log('next state:', store.getState())
     return returnvalue
  }
}
  • dispatch的改造 - 识别Promise
    异步场景原理是令dispatch接收一个函数,在这个函数中进行异步操作,既然dispatch可以接收一个函数,那么也可以接收一个promise
    思路是dispatch接收一个promise对象,在这个promise对象resolve后,我们使用原始的dispatch进行触发。
const addLogToDispatch = (store) => {
  const rawDispatch = store.dispatch
  return (action) => {
    if (typeof action.then == 'function') {
      return action.then(rawDispatch)
    }
    rawDispatch(action)
  }
}

这里使用一种比较投机的方式,检查action参数是否有then方法,且这个方法是函数类型,既判断action是否是一个thenable对象,如果是就会等待promise resolve,生成一个js对象,这个对象就是标准的action。代码执行时,每一个中间件获得的dispatch都已经被改造,声明一个中间件·数组是一个更好的方式,redux的思想就是先将dispatch增强改造函数保存起来,然后提供给redux,每一个中间件都对dispatch进行改造,并将改造后的dispatch,既next向下传递。
中间件的执行过程依赖中间件数据和dispatch

const wrapDispatchM = (store, middleware) => {
  middleware.forEach(m =>store.dispatch = m(store)(store.dispatch))
}

const promise = (store) => (next) => (action) => {
  if (typeof action.then === 'function') {
    return action.then(next)
  }
  return next(action)
}

m数组的执行顺序与我们预期执行的顺序相反

const wrapDispatchM = (store, middleware) => {
  middleware.slice().reverse().forEach(m =>store.dispatch = m(store)(store.dispatch))
}

中间件实际上就是action在到达reducer之前,增加的一个中间环节。

function applyMiddleware (...middlewares) {
  return (next) => 
                (reducer, initialState) => {
                    let store = next(reducer, initialState)
                    let dispatch = store.dispatch
                    let chain = []
                    let middlewareAPI = { 
                          getState: store.getState, 
                          dispatch: (action) => dispatch(action) 
                    }
                    chain = middlewares.map(m => m(middlewareAPI))
                    dispatch = compose(...chain, store.dispatch)
                    return { ...store, dispatch }
                }
}

function compose (...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return func[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
 A.B.C.store.dispatch => A(B(C(store.dispatch)))
 A.B.C.D => A(B(C(D)))
[A.B.C.D].reduce((a. b) => a(b)) = A(B(C(D)))
let listeners = []
let state = ;
function dispatch (action) {
  state = reducer(state, action)
  listeners.slice().forEach(i => i())
  return action
}

function subscribe (listener) {
  listeners.push(listener)
  return function (i) {
    listeners = listeners.filter (i => i !== listener)
  }
}
  • react-redux 究竟是什么
    provider组件

connect组件

  • 通过context获取provider的store,因此它具有了访问store.state

揭秘react同构应用

一个完整的应用除了纯粹的静态内容,还包括各种响应事件,用户交互等。这就意味浏览器还要执行js脚本,以完成绑定事件、处理异步交互等工作。浏览器进一步渲染的过程中,判断已有的dom结构和即将渲染的结构是否相同,若相同,则不重新渲染dom结构,只进行事件绑定即可。
renderToString生成的HTMl字符串的每个DOM节点都有一个data-react-id属性,根节点会有一个data-checksum. 如果两个组件有相同的props和DOM结构,那么data-checksum属性是一样的。浏览器通过data-checksum判断react组件是否只渲染一次。
使用ReactDOM.hydrate 会最大限度的保留服务器端渲染的结构。

react16还提供了renderToNodeStream 方法实现服务器端渲染。该方法将持续产生字节流,最终通过流的形式返回HTML字符串,这样有利于页面初始加载和首屏时间。

  1. 因为在服务器端不支持组件挂载的浏览器环境,所以react组件只有componentDidMount之前的生命周期方法,所以我们最好将依赖浏览器的特性放到componentDidMount中处理。
    因为服务器不存在ajax的概念,所以可以试用isomorphic-fetch实现一致性的封装。
    可以通过window是浏览器特有的对象这一特点,进行环境和逻辑区分。

浏览器端可以通过服务器端注入的全局变量得到初始状态。

function throttle (fn, interval) {
  let doing = false;
  return () => {
    if (doing) return;
    fn()
    setTimeout(() => { doing = false }, interval || 500)
  }
}
  • Function as Child Component
class ScrollPos extends Component {
  state = { position: null }
  componentDidMount  = () => {
    window.addEventListener ('scroll', this.handleScroll)
  }
  componentWillUnMount  = () => {
    window.removeEventListener ('scroll', this.handleScroll)
  }
  render() {
    return (
      
{this.props.children(this.state.position)}
) } }

this.props.children会有多种类型,其中包括undefined, array,object

  • setState
    setState 存在延迟批处理情况,但有一些更新又是同步,setState除了更改this.state的值之外,还要负责触发重新渲染逻辑,这里面要经过核心的diff算法,最后才决定是否渲染,以及如何渲染。
    深入源码,setState中会根据isBatchingUpdates变量判断是直接更新state,还是放到队列中稍后更新。
    总结:在react控制的事件处理中,setState不会同步更新状态,而在react控制之外则会同步更新。在交互过程中,使用的都是在React库中封装的事件,例如select,input,button等,这种情况下setState就会以异步的方式执行。
    实际上绕过react,直接通过js原生直接添加事件处理函数,就会出现同步更新的状态。
    在编写react组件时,我们使用JSX来描述虚拟DOM,事实上,JSX总被编译成createElement,一般babel为我们做了这件事情。
const helloWorld = React.createElement('div', null, 'hello')
ReactDOM.render(helloWorld, document.getElementById('root'))

我们需要使helloWorld创建的节点渲染出来并插入到root节点中

function anElement(ele, child) {
  if (typeof ele === 'function') {
    return ele()
  } else {
    const anele = document.createElement(ele)
    anele,innerHTML = child.join('')
    return anele
  }
}
function createElement (el, props, ...child) {
  return anElement(el, child)
}
window.React = {
  createElement
}
window.ReactDOM = {
  render: (el, root) => { root.appendChild(el) }
}

再深入考虑,children也不仅仅是简单的文本节点,它还可能有其他子组件。
加入我们需要创建一个Component父类,在此类中进行props的初始化和赋值。

class Component {
  constructor (props) {
    this.props = props
  }
}
  • 优化
  1. 对已经实例化的class进行缓存
  2. 当处理一个已经在缓存池中的class时,直接返回实例
  3. 使用标志位对class进行标识
  4. 对于新的渲染诉求,生成新的dom树
  5. 计算两颗dom树的diff
  6. 将diff更新到真实的dom中
  7. 使用render方法对v-dom进行操作,而不是直接应用在真实的dom中
  8. 收集diff,并计算出对真实的dom所做的最小更新
  9. 将最小更新应用在真实的dom中

关于diff的细节,将两棵树比较时间复杂度降为了o(n),具体表现为

  • 前端页面中,跨层级的操作特别少,可以忽略不计
  • 拥有相同类型的两个组件,拥有相似的树形结构,拥有不同类型的两个组件,拥有不同的树形结构
  • 对于同一层级的一组节点,可以通过唯一id区分

基于第一点,react对树的比较算法,实际上只对树进行分层比较,两棵树只会对同一层的节点比较。这样只需要遍历一次树。基于第二点,react在diff时,同一类型的组件,按照原策略进行比较,如果类型不同,则直接进行替换。基于第三点,列表节点组件通过开发者设置的唯一key,来协助实现添加、删除和排序操作。
当然react允许开发者通过shuoldComponentUpdate方法直接决定组件是否需要diff
redux数据扁平化策略

体积过大的性能优化,基于公共资源的分割,给公共的资源包添加缓存。
基于业务的代码分割:
有了以上认知,我们进一步思考,是否可以将app也进行拆分
合理选择分割维度

  • 按照业务逻辑和依赖库划分
  • 按照路由划分
  • 按照组件划分
import Loadable from 'react-loadable'

const MyLoadingSpinner = () => {
  
}
  • 按需加载实现原理
    使用syntax-dynamic-import 这样一个babel插件,通过动态导入实现按需加载,默认情况下,导入的模块是静态的,接下来我们看一看,react如何与动态导入相结合。
    对按需加载的关注点在控制加载上,比如如何加载脚本,脚本是否加载。
class Async extends Component {
  componentWillMount = () => {
    this.cancelUpdate = false
    this.props.load.then(c => { 
      this.C = c 
      if (!this.cancelUpdate) {
        this.forceUpdate()
      }
    })
  }
  componentWillUnmount = () => { this.cancelUpdate = true }
  render  = () => {
    const { componentProps } = this.props
    return this.C ? this.C.default ?  
    :   :  null
  }
}
  • 正确理解虚拟dom带来的优化
    浏览器解析HTML之后,渲染引擎负责展现和渲染页面样式。还需要配合解析css,构建渲染树。
    react通过以下几种方式保证虚拟dom diff算法和更新的高效性能。
  1. 高效的diff算法
  2. Batch操作

当任何一个组件使用setState方法时,会触发组件本身重新渲染。同时因其维护两套虚拟dom,一套更新后的,一套更新前的。通过对这两套虚拟运用diff,找到需要变化的最小单元集,然后把这个最小单元集运用在真实的DOM中。
当同层组件比较时,如果state或props发生变化,则直接重新渲染组件本身。同一层节点比较时,开发者可以使用key属性来‘声明’同一层级节点的更新方式。
找到最小单元集后,更新不一定同步进行。react会进行setState的Batch操作。
浅浅比较,复杂类型只判断引用是否相同。
在使用purecomponent的时候,在更新props和state的时候,返回一个新的对象和数组。

你可能感兴趣的:(react)