React 簡單實現(一)

風辰月發表於2018-07-27

React 是一款用於構建使用者介面的 JavaScript 庫。它以宣告式編寫 UI,建立擁有各自狀態的元件,再由元件構成更加複雜的介面。

一、前言

React 中我們使用 JSX 語法來替代常規的 JavaScript,並通過 Babel 編譯,比如例子1:

// JSX語法
let str = <h1 className="title">hello</h1>

// Babel編譯後
var str = React.createElement(
  "h1",
  { className: "title" },
  "hello"
)
複製程式碼

然後我們控制檯輸出 str,得到結果如下:

React 簡單實現(一)
例子2:

// JSX語法
let App = function MyComponent (props) {
  return <h1>hello{props.value}</h1>
}

// Babel編譯後
var App = function MyComponent(props) {
  return React.createElement(
    "h1",
    null,
    "hello",
    props.value
  )
}
複製程式碼

控制檯輸出 App,得到結果如下:

React 簡單實現(一)

這個物件就是虛擬 DOM 物件,最後通過 ReactDOM.render 方法將虛擬 DOM 解析渲染到頁面上。下面我們就分別來實現 createElementrender 方法。

二、createElement 實現

此方法就是實現一個資料結構,把 JSX 編譯後的結構以巢狀形式儲存在資料結構物件中,實現程式碼如下:

/**
 * Create and return a new ReactElement of the given type.
 */
function createElement (type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.length <= 1 ? children[0] : children
    }
  }
}
複製程式碼

三、render 實現

render 函式就是解析虛擬 DOM,然後通過 appendChild 渲染到頁面上,實現程式碼如下:

/**
 * 將真實dom渲染到頁面
 * @param {*虛擬dom} vnode 
 * @param {*頁面容器} container 
 * @param {*渲染後的回撥} callback 
 */
function render (vnode, container, callback) {
  container.appendChild(_render(vnode))
  callback && callback()
}
/**
 * 將虛擬dom轉成真實節點
 * @param {*虛擬dom} vnode 
 */
function _render (vnode) {}
複製程式碼

但是虛擬 DOM 的有三種型別,不同型別有不同的渲染方式,下面我們分別來分析一下。

1、字串型別

字串型別即是文字,比如上面的 hello,直接建立文字節點返回即可,實現程式碼如下:

function _render (vnode) {
  // 如果是普通字元
  if (util.isString(vnode) || util.isNumber(vnode)) {
    return document.createTextNode(vnode)
  }
}
複製程式碼

2、標籤型別

常見標籤如 div/h1/span 等,建立元素標籤後需要遍歷設定屬性,實現程式碼如下:

function _render (vnode) {
  let { type, props } = vnode
  let { children, ...attrs } = props
  // 如果是標籤元素,如 div span
  let dom = document.createElement(type)
  if (children) {
    if (util.isArray(children)) {
      children.forEach(child => {
        render(child, dom)
      })
    } else {
      dom.appendChild(document.createTextNode(children))
    }
  }
  // 設定屬性
  setAttributes(attrs, dom)
  return dom
}
複製程式碼

3、函式型別

函式型別也分二種情況,一種是無狀態普通函式即函式元件,另一種則是繼承了 React.Component 類的有狀態函式,即類元件。開始之前需要定義一個 Component 父類以便繼承,實現程式碼如下:

/**
 * Create React.Component class
 */
class Component {
  constructor (props) {
    this.props = props
    this.state = {}
  }
  componentWillMount () {
    console.log('Component: componentWillMount')
  }
  componentDidMount () {
    console.log('Component: componentDidMount')
  }
  setState () {
    console.log('Component: setState')
  }
}
複製程式碼

不管哪種元件,為了保證行為統一性,我們可以分別經過元件建立,元件渲染,然後返回真實 DOM 節點,最後渲染到頁面上的步驟。實現程式碼如下:

function _render (vnode) {
  let { type, props } = vnode
  let { children, ...attrs } = props
  // 如果是函式,說明是元件(包括函式元件和類元件)
  if (util.isFunction(type)) {
    // 建立元件
    let comp = createComponent(type, props)
    comp.props = props
    // 渲染元件
    let dom = renderComponent(comp)
    comp.dom = dom
    // 設定屬性
    setAttributes(attrs, dom)
    return dom
  }
}
// 建立元件
function createComponent (comp, props) {
  // 如果是類元件,需要new一個例項,可以呼叫render方法
  if (comp.prototype.render) {
    comp = new comp(props)
  } else {
    comp.render = function () {
      return comp(props)
    }
  }
  return comp
}

// 渲染元件:執行元件的render方法,然後通過_render將虛擬dom轉成真實dom,最後返回真實節點
function renderComponent (comp) {
  let dom
  // 如果未渲染過元件
  if (!comp.dom) {
    if (comp.componentWillMount) {
      comp.componentWillMount()
    }
  }
  dom = _render(comp.render())
  if (comp.componentDidMount) {
    comp.componentDidMount()
  }
  return dom
}
複製程式碼

到這裡解析和渲染功能大體完成,下面介紹一下如何修改元件狀態。

四、setState 實現

在上面我們通過 setAttribute 已經將事件繫結到元素上,事件中很有可能會修改元件狀態,比如this.setState({a: this.state.a + 1}) 語句,以此更新檢視。不過通過上面虛擬 DOM 解析成真實 DOM 的學習,在這裡先忽略 DOM Diff 功能,重新渲染還是很好實現的,程式碼如下:

class Component {
  setState (newState) {
    console.log('Component: setState')
    Object.assign(this.state, newState)
    let old = this.dom
    // renderComponent:整體重新渲染,然後返回真實節點
    let newDom = renderComponent(this)
    old.parentNode.replaceChild(newDom, old)
    this.dom = newDom
  }
}
複製程式碼

原始碼

好了,今天的學習就先到這裡吧???~

相關文章