真正的react-powerplug的源碼解讀

這個庫能做什么?

從這個庫的API上來看,這些狀態(tài)都保存在組件中。沒有一個初始化(異步)的入口。一個關于這個庫最簡單的應用例子(在所有將render props中都會說的):
在沒有render props時,是這樣寫:

class App extends React.Component {
  state = { visible: false };

  showModal = () => {
    this.setState({
      visible: true
    });
  };

  handleOk = e => {
    this.setState({
      visible: false
    });
  };

  handleCancel = e => {
    this.setState({
      visible: false
    });
  };

  render() {
    return (
      <div>
        <Button type="primary" onClick={this.showModal}>
          Open Modal
        </Button>
        <Modal
          title="Basic Modal"
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    );
  }
}

ReactDOM.render(<App />, mountNode);

如果用render props會這樣寫

class App extends React.Component {
  render() {
    return (
      <Toggle initial={false}>
        {({ on, toggle }) => (
          <Button type="primary" onClick={toggle}>
            Open Modal
          </Button>
          <Modal
            title="Basic Modal"
            visible={on}
            onOk={toggle}
            onCancel={toggle}
          >
            <p>Some contents...</p>
            <p>Some contents...</p>
            <p>Some contents...</p>
          </Modal>
        )}
      </Toggle>
    );
  }
}

ReactDOM.render(<App />, mountNode);

這個庫的API和代碼組織我個人覺得非常棒

源碼解析

Compose組件

compose方法是這個庫的核心方法。用來組合render props的組件。我現(xiàn)在只能結(jié)合react的渲染原理,執(zhí)行這段代碼,搞清楚整個流程。反正看完之后,我覺得能作者寫出這種代碼真的很牛逼
ps: 市面上所謂的react-powerplug的compose的解讀都是shit。因為它們的compose的源碼解析只有這樣一句話

image.png

雖然你也挑不出錯來,可這說了和沒說有啥差別????

react的渲染原理

這里以react最初的版本進行原理分析。react的渲染流程兩個關鍵的點: 組件的jsx轉(zhuǎn)化以及RenderDom.render函數(shù)。

  • babel會將 JSX

class App extends Component{

    render(){
        return <Hello></Hello>
    }
}

轉(zhuǎn)化為這個樣子:

var Hello = function Hello() {
    return _react2.default.createElement(World, null);
};

這里需要補充一下createElementcloneElement的三個參數(shù)如下所示

createElement(element, props,children)//第三個即為子組件

createElement以及cloneElement會返回一個element對象,element對象一般結(jié)構(gòu)如下

{
  type : 'div',
  props: {
    className : 'cn',
    children : [
      {
        type : function Header,
        props : {
          children : 'hello'
        }
      },
      {
        type : 'div',
        props : {
          children : 'start'
        }
      },
      'Right Reserve'
    ]
  }
}
image.png

element的type這樣幾種類型: function(自定義組件), string(原生的dom)之類。

  • ReactDom.render函數(shù)
    一般對于react的應用,都會有這種代碼
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'

ReactDOM.render(<App />, document.getElementById('root'))

其中ReactDOM.render函數(shù)流程是這個樣子

 const rootID = 0
  const mainComponent = instantiateReactComponent(element)
  const containerContent = mainComponent.mountComponent(rootID)

instantiateReactComponent根據(jù)element類型,生成不同的class。這些class都會實現(xiàn)同樣的方法mountComponent,看代碼

export function instantiateReactComponent(element) {
  let instance = null
  if (element === null || element === false) {
    instance = new ReactDOMEmptyComponent()
  }

  if (typeof element === 'string' || typeof element === 'number') {
    instance = new ReactDOMTextComponent(element)
  }

  if (typeof element === 'object') {
    let type = element.type
    if (typeof type === 'string') {
      instance = new ReactDomComponent(element)
    } else if (typeof type === 'function'){
      instance = new ReactCompositeComponent(element)
    }
  }
  return instance
}

上面根據(jù)element類型生成不同的類。需要關注的是其中兩個類:ReactDomComponentReactCompositeComponent。

其中ReactDomComponent表示原生組件,即瀏覽器支持的標簽(div, p, h1, etc.)。它的代碼如下所示:

class ReactDomComponent {
  constructor(element) {
    let tag = element.type

    this._element = element
    this._tag = tag.toLowerCase()
    this._rootID = 0
  }

  mountComponent(rootID) {
    this._rootID = rootID
    if (typeof this._element.type !== 'string') {
      throw new Error('DOMComponent\'s Element.type must be string')
    }

    let ret = `<${this._tag} `
    let props = this._element.props
    for (var propsName in props) {
      if (propsName === 'children') {
        continue
      }
      let propsValue = props[propsName]
      ret += `${propsName}=${propsValue}`
    }
    ret += '>'

    let tagContent = ''
    if (props.children) {
      tagContent = this._mountChildren(props.children) //重點是這個,如何解析子組件
    }
    ret += tagContent
    ret += `</${this._tag}>`
    return ret
  }
}

其中_mountChildren的代碼如下所示

let result = ''
  for (let index in children) {
    const child = children[index]
    const childrenComponent = instantiateReactComponent(child) //又來重復整個流程
    result += childrenComponent.mountComponent(index)
  }
  return result

重頭戲來了, ReactCompositeComponent類的代碼呢?


class ReactCompositeComponent {
  constructor(element) {
    this._element = element
    this._rootId = 0
  }

  mountComponent(rootID) {
    this._rootId = rootID
    if (typeof this._element.type !== 'function') {
      throw new Error('CompositeComponent\'s Element.type must be function')
    }

    const Component = this._element.type
    const props = this._element.props
    const instance = new Component(props)

    const renderedElement = instance.render() //如果是函數(shù)組件應該是直接調(diào)用函數(shù)方法
    const renderedComponent = instantiateReactComponent(renderedElement)
    const renderedResult = renderedComponent.mountComponent(rootID)
    return renderedResult
  }
}

ReactCompositeComponent類。首先初始化類,獲取實例。調(diào)用render方法,返回element對象,然后根據(jù)element對象,轉(zhuǎn)化對應的component類。調(diào)用該類的mountComponent函數(shù)。

總結(jié)下來,react的渲染流程圖如下所示:

image.png

基本上我將[React 初始化渲染](https://www.ahonn.me/2017/06/08/write-a-react-from-scratch-init-render/
https://zhuanlan.zhihu.com/p/45091185)這篇文章摘了重點拿出來說。關于react的渲染流程,這篇文章寫得非常詳細了。

搞清楚react的渲染原理,此時可以來結(jié)合compose的源碼,來搞清楚compose代碼執(zhí)行的流程。

compose代碼執(zhí)行流程

先看compose的使用

import { compose, Counter, Toggle } from 'react-powerplug' // note lowercased (c)ompose

// the order matters
const ToggleCounter = compose(
  <Counter initial={5} />, // accept a element
  Toggle,                  // or just a component
)

<ToggleCounter>
  {(counter, toggle) => {
    <div>hello</div>
  }}
</ToggleCounter>

再看compose的代碼

const compose = (...elements) => {
  const reversedElements = elements.reverse()

  return composedProps => {
    // Stack children arguments recursively and pass
    // it down until the last component that render children
    // with these stacked arguments
    function stackProps(i, elements, propsList = []) {
      const element = elements[i]
      const isTheLast = i === 0

      // Check if is latest component.
      // If is latest then render children,
      // Otherwise continue stacking arguments
      const renderFn = props =>
        isTheLast
          ? renderProps(composedProps, ...propsList.concat(props))
          : stackProps(i - 1, elements, propsList.concat(props))


      // Clone a element if it's passed created as <Element initial={} />
      // Or create it if passed as just Element
      const elementFn = isElement(element)
        ? React.cloneElement
        : React.createElement

      return elementFn(element, {}, renderFn)
    }

    return stackProps(elements.length - 1, reversedElements)
  }
}

結(jié)合react的渲染原理來看。

1.從ReactDOM.render(ToggleCounter,)開始
2.因為ToggleCounter是函數(shù)式組件,經(jīng)過instantiateReactComponent函數(shù)后,調(diào)用的是ReactCompositeComponent的mountComponent方法,根據(jù)我上面畫的流程圖
3.初始化ToggleCounter實例并調(diào)用render函數(shù)(函數(shù)式組件,應該直接調(diào)用函數(shù)即可)

//1.調(diào)用ToggleCounter函數(shù)
const ToggleCounter =  (composedProps)=> {
 return stackProps(elements.length - 1, reversedElements)
}
ToggleCounter((a,b)=> {<div>hello</div>})
//既:
 stackProps(Counter,[Toggle,Counter], [])

4.而調(diào)用stackProps函數(shù)結(jié)果是,生成元素B

//元素B
cloneElement(Counter, {}, (props)=> {stackProps(0,[Toggle,Counter], propsList.concat(props))})

4.元素B又是函數(shù)式組件。重復上訴流程。初始化Counter實例并調(diào)用render函數(shù)(函數(shù)式組件調(diào)用函數(shù)本身)。而Counter的函數(shù)本質(zhì)邏輯,是將其內(nèi)部state作為參數(shù)給它的children。既這段代碼

 const fn = isFn(children) ? children : render

  return fn ? fn(...props) : null

所以執(zhí)行了這段代碼

//1.此時元素B的children如下所示:
const children1 = (props)=> {stackProps(0,[Toggle,Counter], propsList.concat(props))})
//2.執(zhí)行couter函數(shù)
children1(propsCounter) = stackProps(0,[Toggle,Counter], [propsCounter])

//而stackProps返回了新的元素C
React.createElement(Toggle, {},(props)=> {renderProps((a,b)=> {<div>hello</div>}, ...[propsCounter].concat(props))})

5.元素C又是函數(shù)式組件。重復上訴流程。同Counter一樣,執(zhí)行這段代碼

//1.Toggle的子組件children2如下所示
const children2 = (props)=> {renderProps((a,b)=> {React.createElement(div, {}, 'demo')} ...[propsCounter].concat(props)}
//2.Toggle組件調(diào)用render函數(shù)(函數(shù)式組件,直接執(zhí)行本函數(shù))
children2(propsToggle) = renderProps((a,b)=> {<div>hello</div>}, ...[propsCounter,propsToggle ]}
                     

renderProps的函數(shù)的定義

const renderProps = ({ children, render }, ...props) => {
  if (process.env.NODE_ENV !== 'production') {
    warn(
      isFn(children) && isFn(render),
      'You are using the children and render props together.\n' +
        'This is impossible, therefore, only the children will be used.'
    )
  }

  const fn = isFn(children) ? children : render

  return fn ? fn(...props) : null
}

所以,相當于執(zhí)行這個步驟

fn = (a,b)=> {<div>hello</div>}
fn([propsCounter,propsToggle])

獲得瀏覽器支持的標簽div,初始化成ReactDOMComponent進行渲染。結(jié)束。
在看這段代碼的時候,結(jié)合渲染原理。搞清楚了compose的整個流程。

Value組件

Value組件是基礎組件。非常簡單的兩個功能:設置state和重置state。也是使用的render Props的方式

class Value extends Component {
  state = {
    value: this.props.initial,
  }

  _set = (updater, cb = noop) => {
    const { onChange = noop } = this.props

    this.setState(
      typeof updater === 'function'
        ? state => ({ value: updater(state.value) })
        : { value: updater },
      () => {
        onChange(this.state.value)
        cb()
      }
    )
  }
  _reset = (cb = noop) => {
    this._set(this.props.initial, cb)
  }

  render() {
    return renderProps(this.props, {
      value: this.state.value,
      set: this._set,
      reset: this._reset,
    })
  }
}

export default Value

這個代碼太簡單了。不需要怎么說。renderProps是一個基本方法:

const renderProps = ({ children, render }, ...props) => {
  if (process.env.NODE_ENV !== 'production') {
    warn(
      isFn(children) && isFn(render),
      'You are using the children and render props together.\n' +
        'This is impossible, therefore, only the children will be used.'
    )
  }

  const fn = isFn(children) ? children : render

  return fn ? fn(...props) : null
}

通過判斷children是否

List組件

const complement = fn => (...args) => !fn(...args)

const List = ({ initial = [], onChange, ...props }) => (
  <Value initial={initial} onChange={onChange}>
    {({ value, set, reset }) =>
      renderProps(props, {
        list: value,
        first: () => value[0],
        last: () => value[Math.max(0, value.length - 1)],
        set: list => set(list),
        push: (...values) => set(list => [...list, ...values]),
        pull: predicate => set(list => list.filter(complement(predicate))),
        sort: compareFn => set(list => [...list].sort(compareFn)),
        reset,
      })
    }
  </Value>
)

只是對Value組件再來了一層包裝,包裝成更符合List組件的API.

hover組件

hover這類狀態(tài)組件,更簡單了。加了一些綁定事件設置值。

const Hover = ({ onChange, ...props }) => (
  <Value initial={false} onChange={onChange}>
    {({ value, set }) =>
      renderProps(props, {
        hovered: value,
        bind: {
          onMouseEnter: () => set(true),
          onMouseLeave: () => set(false),
        },
      })
    }
  </Value>
)

參考:
react 的源碼解析:
https://juejin.im/post/5b9a45fc5188255c402af11f

react的渲染原理
https://www.ahonn.me/2017/06/08/write-a-react-from-scratch-init-render/
https://github.com/creeperyang/blog/issues/30

https://github.com/dt-fe/weekly/blob/master/75.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md

https://juejin.im/post/5b9a45fc5188255c402af11f

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容