從 0 到 1 實現 React 系列 —— 組件和 state|props

閱讀源碼一個痛處是會陷進理不順主幹的困局中,本系列文章在實現一個 (x)react 的同時理順 React 框架的主幹內容(JSX/虛擬DOM/組件/...)html

組件即函數

在上一篇 JSX 和 Virtual DOM 中,解釋了 JSX 渲染到界面的過程並實現了相應代碼,代碼調用以下所示:node

import React from 'react'
import ReactDOM from 'react-dom'

const element = (
  <div className="title">
    hello<span className="content">world!</span>
  </div>
)

ReactDOM.render(
  element,
  document.getElementById('root')
)

本小節,咱們接着探究組件渲染到界面的過程。在此咱們引入組件的概念,組件本質上就是一個函數,以下就是一段標準組件代碼:react

import React from 'react'

// 寫法 1:
class A {
  render() {
    return <div>I'm componentA</div>
  }
}

// 寫法 2:無狀態組件
const A = () => <div>I'm componentA</div>

ReactDOM.render(<A />, document.body)

<A name="componentA" /> 是 JSX 的寫法,和上一篇同理,babel 將其轉化爲 React.createElement() 的形式,轉化結果以下所示:git

React.createElement(A, null)

能夠看到當 JSX 中是自定義組件的時候,createElement 後接的第一個參數變爲了函數,在 repl 打印 <A name="componentA" />,結果以下:github

{
  attributes: undefined,
  children: [],
  key: undefined,
  nodeName: ƒ A()
}

注意這時返回的 Virtual DOM 中的 nodeName 也變爲了函數。根據這些線索,咱們對以前的 render 函數進行改造。api

function render(vdom, container) {
  if (_.isFunction(vdom.nodeName)) { // 若是 JSX 中是自定義組件
    let component, returnVdom
    if (vdom.nodeName.prototype.render) {
      component = new vdom.nodeName()
      returnVdom = component.render()
    } else {
      returnVdom = vdom.nodeName() // 針對無狀態組件:const A = () => <div>I'm componentsA</div>
    }
    render(returnVdom, container)
    return
  }
}

至此,咱們完成了對組件的處理邏輯。babel

props 和 state 的實現

在上個小節組件 A 中,是沒有引入任何屬性和狀態的,咱們但願組件間能進行屬性的傳遞(props)以及組件內能進行狀態的記錄(state)。app

import React, { Component } from 'react'

class A extends Component {
  render() {
    return <div>I'm {this.props.name}</div>
  }
}

ReactDOM.render(<A name="componentA" />, document.body)

在上面這段代碼中,看到 A 函數繼承自 Component。咱們來構造這個父類 Component,並在其添加 state、props、setState 等屬性方法,從而讓子類繼承到它們。框架

function Component(props) {
  this.props = props
  this.state = this.state || {}
}

首先,咱們將組件外的 props 傳進組件內,修改 render 函數中如下代碼:dom

function render(vdom, container) {
  if (_.isFunction(vdom.nodeName)) {
    let component, returnVdom
    if (vdom.nodeName.prototype.render) {
      component = new vdom.nodeName(vdom.attributes) // 將組件外的 props 傳進組件內
      returnVdom = component.render()
    } else {
      returnVdom = vdom.nodeName(vdom.attributes)     // 處理無狀態組件:const A = (props) => <div>I'm {props.name}</div>
    }
    ...
  }
  ...
}

實現完組件間 props 的傳遞後,再來聊聊 state,在 react 中是經過 setState 來完成組件狀態的改變的,後續章節會對這個 api(異步)深刻探究,這裏簡單實現以下:

function Component(props) {
  this.props = props
  this.state = this.state || {}
}

Component.prototype.setState = function() {
  this.state = Object.assign({}, this.state, updateObj) // 這裏簡單實現,後續篇章會深刻探究
  const returnVdom = this.render() // 從新渲染
  document.getElementById('root').innerHTML = null
  render(returnVdom, document.getElementById('root'))
}

此時雖然已經實現了 setState 的功能,可是 document.getElementById('root') 節點寫死在 setState 中顯然不是咱們但願的,咱們將 dom 節點相關轉移到 _render 函數中:

Component.prototype.setState = function(updateObj) {
  this.state = Object.assign({}, this.state, updateObj)
  _render(this) // 從新渲染
}

天然地,重構與之相關的 render 函數:

function render(vdom, container) {
  let component
  if (_.isFunction(vdom.nodeName)) {
    if (vdom.nodeName.prototype.render) {
      component = new vdom.nodeName(vdom.attributes)
    } else {
      component = vdom.nodeName(vdom.attributes) // 處理無狀態組件:const A = (props) => <div>I'm {props.name}</div>
    }
  }
  component ? _render(component, container) : _render(vdom, container)
}

在 render 函數中分離出 _render 函數的目的是爲了讓 setState 函數中也能調用 _render 邏輯。完整 _render 函數以下:

function _render(component, container) {
  const vdom = component.render ? component.render() : component
  if (_.isString(vdom) || _.isNumber(vdom)) {
    container.innerText = container.innerText + vdom
    return
  }
  const dom = document.createElement(vdom.nodeName)
  for (let attr in vdom.attributes) {
    setAttribute(dom, attr, vdom.attributes[attr])
  }
  vdom.children.forEach(vdomChild => render(vdomChild, dom))
  if (component.container) {  // 注意:調用 setState 方法時是進入這段邏輯,從而實現咱們將 dom 的邏輯與 setState 函數分離的目標;知識點: new 出來的同一個實例
    component.container.innerHTML = null
    component.container.appendChild(dom)
    return
  }
  component.container = container
  container.appendChild(dom)
}

讓咱們用下面這個用例跑下寫好的 react 吧!

class A extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 1
    }
  }

  click() {
    this.setState({
      count: ++this.state.count
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.click.bind(this)}>Click Me!</button>
        <div>{this.props.name}:{this.state.count}</div>
      </div>
    )
  }
}

ReactDOM.render(
  <A name="count" />,
  document.getElementById('root')
)

效果圖以下:

至此,咱們實現了 props 和 state 部分的邏輯。

小結

組件即函數;當 JSX 中是自定義組件時,通過 babel 轉化後的 React.createElement(fn, ..) 後中的第一個參數變爲了函數,除此以外其它邏輯與 JSX 中爲 html 元素的時候相同;

此外咱們將 state/props/setState 等 api 封裝進了父類 React.Component 中,從而在子類中能調用這些屬性和方法。

在下篇,咱們會繼續實現生命週期機制,若有疏漏,歡迎斧正。

項目地址

相關文章
相關標籤/搜索