2018年1月

React-Redux 工作原理

原理

  • 发布/订阅

connect 过的组件订阅 state变化,dispatch通知所有订阅者更新

  • 渲染时机

每次 dispatchreducer 产生一个新 state 对象,所有 connect 组件判断是否更新,因为是数据的浅比较,所以所需对象引用变了,则该组件重新渲染

  • 性能问题

v5.0 之前存在着一个问题:父(祖先)子(孙)组件都订阅,子(孙)组件可能受到父(祖先)组件渲染影响,而导致多次渲染。v5.0 之后 发布/订阅 模块重写,解决了之前的问题,主要特点:增加层级(嵌套)观察者,保证事件通知与组件更新的顺序

v5 源码浅析

简单分析下 v5 是如何解决性能问题的:

观察者

// 源码:src/utils/Subscription.js

// 添加层级订阅
addNestedSub(listener) {
  this.trySubscribe()
  return this.listeners.subscribe(listener)
}
// 订阅
trySubscribe() {
  if (!this.unsubscribe) {
   this.unsubscribe = this.parentSub
     ? this.parentSub.addNestedSub(this.onStateChange) // 订阅到父(向上找到最开始的祖先)组件
     : this.store.subscribe(this.onStateChange) // 订阅到 redux

   this.listeners = createListenerCollection()
  }
}
// 通知层级订阅
notifyNestedSubs() {
  this.listeners.notify()
}
// 通知
notify() {
   const listeners = current = next
   for (let i = 0; i < listeners.length; i++) {
     listeners[i]()
   }
}

订阅

通过 context 传递 subscription,组件订阅前检查,如果父(祖先)组件已经订阅,则将子组件的回调函数 stateChange 订阅到父(向上找到最开始的祖先)组件的观察者,否则订阅到 redux store(上面的 trySubscribe 方法)

// 源码:src/components/connectAdvanced.js

getChildContext() {
  const subscription = this.propsMode ? null : this.subscription
    return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}

componentDidMount() {
  ...
  this.subscription.trySubscribe() // 订阅
}

initSubscription() {
  if (!shouldHandleStateChanges) return
    const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
    this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this)) // 新建订阅对象
    this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
}

发布更新

  • 父(祖先)组件需要更新,则执行 setState 重新 render, 并在 ComponentDidUpdate 时通知子(孙)组件更新(触发回调函数)
  • 父(祖先)组件不需要更新,直接通知子(孙)组件更新
// 源码:src/components/connectAdvanced.js

onStateChange() {
  this.selector.run(this.props)

  if (!this.selector.shouldComponentUpdate) {
   this.notifyNestedSubs() // 通知
  } else {
   this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
   this.setState(dummyState) // 更新自身
  }
}

notifyNestedSubsOnComponentDidUpdate() {
  // 在 componentDidUpdate 方法通知,确保自身与子组件渲染之后再通知
  this.componentDidUpdate = undefined
  this.notifyNestedSubs()
}

疑问与总结

父(祖先)组件setState明明可能会让子(孙)组件更新,为什么还需要通知一次?

  • 必要性:因为存在这样的场景:子组件可能不更新(ShouldComponentUpdate 返回 false),则需要更新的孙子组件没有受 setState 影响,为了保证所有订阅的组件都能得到数据更新,所以需要通知

  • 无副作用:假设子组件因为父组件 render 而重新 render,因为通知是在父组件 componentDidUpdate 方法触发,而子组件因为已经渲染过,数据得到了更新,所以就算再收到了通知也不会再渲染了