實現一個屬於自己的React框架(一)

利維亞的傑洛特發表於2019-04-01

image

image

3月31日去頤和園轉了一圈, 拍的比較滿意的幾張照片

前言

本文主要參考了preact的原始碼

準備工作

我們首先搭建開發的環境, 我們選擇webpack4。值得注意的是, 因為我們需要解析JSX的語法, 我們需要使用**@babel/plugin-transform-react-jsx**外掛。

@babel/plugin-transform-react-jsx外掛會將JSX語法做出以下格式的轉換。@babel/plugin-transform-react-jsx預設使用React.createElement, 我們可以通過設定外掛的pragma配置項, 修改預設的函式名

// before
var profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>;

// after
var profile = React.createElement("div", null,
  React.createElement("img", { src: "avatar.png", className: "profile" }),
  React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);
複製程式碼

const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const HappyPack = require('happypack')

module.exports = {
  devtool: '#cheap-module-eval-source-map',

  mode: 'development',

  target: 'web',

  entry: {
    main: path.resolve(__dirname, './example/index.js')
  },

  devServer: {
    host: '0.0.0.0',
    port: 8080,
    hot: true
  },

  resolve: {
    extensions: ['.js']
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'happypack/loader?id=js'
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader'
          }
        ]
      }
    ]
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HappyPack({
      id: 'js',
      threads: 4,
      use: [
        {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              '@babel/plugin-syntax-dynamic-import',
              [
                "@babel/plugin-transform-react-jsx",
                {
                  pragma: 'h'
                }
              ]
            ]
          }
        }
      ]
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html')
    })
  ]
}
複製程式碼

上面是完整的打包配置(如果嚴格來說, 類庫應該單獨打包的)。同時我們將@babel/plugin-transform-react-jsx外掛, pragma引數設定為"h"。我們在使用的時候, 只需要在檔案中引入h函式即可。

建立VNode

我們在這裡將會實現h方法, h方法的作用是建立一個VNode。根據編譯結果可知, h函式的引數如下。

/**
 * type為VNode的型別
 * props為VNode的屬性
 * childrens為VNode的子節點, 可能用多組子節點, 我們使用es6的rest引數
 */
h(type, props, ...childrens)
複製程式碼

VNode本質就是Javascript中物件, 因此h函式只需要返回對應的物件即可。


export function createElement (type, props, ...children) {
  if (!props) props = {}
  
  props.children = [...children]

  let key = props.key

	if (key) {
    delete props.key
  }

  return createVNode(type, props, null, key)
}

export function createVNode (type, props, text, key) {
  const VNode = {
    type,
    props,
    text,
    key,
    _dom: null,
    _children: null,
    _component: null
  }

  return VNode
}
複製程式碼

我們來使用一下,看一下h函式返回的結果, h函式返回的結果即是虛擬DOM

import { h } from 'yy-react'

console.log(
  <div>
    <h1>Hello</h1>
    <h1>World</h1>
  </div>
)
複製程式碼

image

實現render

我們可以參考React的render函式的實現, render函式接受兩個引數, React元素(VNode)以及container(掛載的DOM)。我們將要把VNode渲染成了真實的DOM節點。

下面是render函式的實現, 我們在本期還沒有來得及實現Diff方法, 讀者可以不用關注於這些。

整體程式碼的實現,參考(抄)了preact的原始碼的實現?。(我還給preact的專案提交了pr?,不過還沒有merge?)

? 文章的最後是具體實現, 但是一大坨對閱讀不是很友好,不想看的可以略過,直接看解說。

我們首先將視角轉向render, render函式裡呼叫裡diff函式, 將返回的dom掛載到document中。_prevVNode等屬性我們會在以後用到,目前可以忽略。


export function render (vnode, root) {
  let oldVNode = root._prevVNode
  let newVNode = root._prevVNode = vnode
  let dom = oldVNode ? oldVNode._dom : null
  let mounts = []
  let newDom = diff(dom, root, newVNode, oldVNode, mounts)
  if (newDom) {
    root.appendChild(newDom)
  }
}
複製程式碼

在diff中,我們將對節點型別做出判斷, VNode型別可以是普通的節點也可以是元件型別的節點, 我們這裡先對普通型別的節點做出處理。


function diff (
  dom,
  root,
  newVNode,
  oldVNode,
  mounts,
  force
) {

  let newType = newVNode.type

  if (typeof newType === 'function') {
    // render component
  } else {
    dom = diffElementNodes(
      dom,
      newVNode,
      oldVNode,
      mounts
    )
  }

  newVNode._dom = dom

  return dom
}
複製程式碼

我們接著將目光轉向diffElementNodes函式, 在diffElementNodes函式中我們會根據具體節點型別建立對應的真實的DOM節點。 例如文字型別的節點我們使用createTextNode, 而普通型別的我們使用createElement

因為整個VNode呈現的一種樹狀結構, 面對樹狀結構免不了使用遞迴去遍歷每一顆節點。我們這裡將建立後dom,作為父節點傳入diffChildren函式中(新建立的節點會append到這個父節點中)。遞迴的轉換的每一個子節點以及子節點的子節點。

由此我們也可知道,整個VNode樹的渲染的順序是由外向裡的。但是設定VNode的props的順序則是由裡向外的。


function diffElementNodes (dom, newVNode, oldVNode, mounts) {

  if (!dom) {
    dom = newVNode.type === null ? document.createTextNode(newVNode.text) : document.createElement(newVNode.type)
  }

  newVNode._dom = dom

  if (newVNode.type) {
    if (newVNode !== oldVNode) {
      let newProps = newVNode.props
      let oldProps = oldVNode.props
      if (!oldProps) {
        oldProps = {}
      }
      diffChildren(dom, newVNode, oldVNode, mounts)
      diffProps(dom, newProps, oldProps)
    }
  }

  return dom
}
複製程式碼

在diffChildren中, 我們將VNode的子VNode掛載到_children屬性上, 遍歷每一個子節點, 將子節點帶入到diff中, 完成建立的過程


function diffChildren (
  root,
  newParentVNode,
  oldParentVNode,
  mounts
) {
  let oldVNode, newVNode, newDom, i, j, index, p, oldChildrenLength

  let newChildren = newParentVNode._children || 
                    toChildVNodeArray(newParentVNode.props.children, newParentVNode._children = [])

  for (i = 0; i < newChildren.length; i++) {
    newVNode = newChildren[i]
    oldVNode = index = null

    newDom = diff(
      oldVNode ? oldVNode._dom : null,
      root,
      newVNode,
      oldVNode,
      mounts,
      null
    )

    if (newVNode && newDom) {
      root.appendChild(newDom)
    }
  }
}
複製程式碼

我們在遍歷遞迴完子節點後, 就可以使用diffProps來設定我們的root節點了。我們遍歷newProps中的每一個key, 並使用setProperty將props設定到dom上, setProperty中對一些dom屬性做了特殊的處理。比如處理了駝峰的css的key, 和數字的value自動新增px等。


function diffProps (dom, newProps, oldProps) {
  for (let key in newProps) {
    if (
      key !=='children' &&
      key!=='key' &&
      (
        !oldProps ||
        ((key === 'value' || key === 'checked') ? dom : oldProps)[key] !== newProps[key]
      )
    ) {
			setProperty(dom, key, newProps[key], oldProps[key])
		}
  }
}

function setProperty (dom, name, value, oldValue) {
  if (name === 'style') {
    let s = dom.style
    if (typeof value === 'string') {
			s.cssText = value
		} else {
			if (typeof oldValue === 'string') {
        s.cssText = ''
      } else {
				for (let i in oldValue) {
					if (value==null || !(i in value)) {
            s.setProperty(i.replace(CAMEL_REG, '-'), '')
          }
				}
			}
			for (let i in value) {
				v = value[i];
				if (oldValue==null || v!==oldValue[i]) {
					s.setProperty(i.replace(CAMEL_REG, '-'), typeof v==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (v + 'px') : v)
				}
			}
		}
  } else if (value == null) {
    dom.removeAttribute(name)
  } else if (typeof value !== 'function') {
    dom.setAttribute(name, value)
  }
}
複製程式碼

最後我們再次回到render函式,render函式最後的會將建立好的dom, append到掛載的dom中完成渲染。


root.appendChild(newDom)
複製程式碼

完整示例

github的倉庫地址將在完成後放出


// create-element.js
export function render (vnode, root) {
  let oldVNode = root._prevVNode
  let newVNode = root._prevVNode = vnode
  let dom = oldVNode ? oldVNode._dom : null
  let mounts = []
  let newDom = diff(dom, root, newVNode, oldVNode, mounts)
  if (newDom) {
    root.appendChild(newDom)
  }
  runDidMount(mounts, vnode)
}

// diff.js
function diff (
  dom,
  root,
  newVNode,
  oldVNode,
  mounts,
  force
) {
  if (oldVNode == null || newVNode == null || newVNode.type !== oldVNode.type) {
    if (!newVNode) return null
    dom = null
    oldVNode = {}
  }

  let newType = newVNode.type

  if (typeof newType === 'function') {
    // render component
  } else {
    dom = diffElementNodes(
      dom,
      newVNode,
      oldVNode,
      mounts
    )
  }

  newVNode._dom = dom

  return dom
}

function diffElementNodes (dom, newVNode, oldVNode, mounts) {

  if (!dom) {
    dom = newVNode.type === null ? document.createTextNode(newVNode.text) : document.createElement(newVNode.type)
  }

  newVNode._dom = dom

  if (newVNode.type) {
    if (newVNode !== oldVNode) {
      let newProps = newVNode.props
      let oldProps = oldVNode.props
      if (!oldProps) {
        oldProps = {}
      }
      diffChildren(dom, newVNode, oldVNode, mounts)
      diffProps(dom, newProps, oldProps)
    }
  }

  return dom
}

// diff-children.js
function diffChildren (
  root,
  newParentVNode,
  oldParentVNode,
  mounts
) {
  let oldVNode, newVNode, newDom, i, j, index, p, oldChildrenLength

  let newChildren = newParentVNode._children || 
                    toChildVNodeArray(newParentVNode.props.children, newParentVNode._children = [])

  for (i = 0; i < newChildren.length; i++) {
    newVNode = newChildren[i]
    oldVNode = index = null

    newDom = diff(
      oldVNode ? oldVNode._dom : null,
      root,
      newVNode,
      oldVNode,
      mounts,
      null
    )

    if (newVNode && newDom) {
      root.appendChild(newDom)
    }
  }
}

// diffProps.js
function diffProps (dom, newProps, oldProps) {
  for (let key in newProps) {
    if (
      key !=='children' &&
      key!=='key' &&
      (
        !oldProps ||
        ((key === 'value' || key === 'checked') ? dom : oldProps)[key] !== newProps[key]
      )
    ) {
			setProperty(dom, key, newProps[key], oldProps[key])
		}
  }
  for (let key in oldProps) {
  }
}

// diff-props
function diffProps (dom, newProps, oldProps) {
  for (let key in newProps) {
    if (
      key !=='children' &&
      key!=='key' &&
      (
        !oldProps ||
        ((key === 'value' || key === 'checked') ? dom : oldProps)[key] !== newProps[key]
      )
    ) {
			setProperty(dom, key, newProps[key], oldProps[key])
		}
  }
  for (let key in oldProps) {
  }
}

function setProperty (dom, name, value, oldValue) {
  if (name === 'style') {
    let s = dom.style
    if (typeof value === 'string') {
			s.cssText = value
		} else {
			if (typeof oldValue === 'string') {
        s.cssText = ''
      } else {
				for (let i in oldValue) {
					if (value==null || !(i in value)) {
            s.setProperty(i.replace(CAMEL_REG, '-'), '')
          }
				}
			}
			for (let i in value) {
				v = value[i];
				if (oldValue==null || v!==oldValue[i]) {
					s.setProperty(i.replace(CAMEL_REG, '-'), typeof v==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (v + 'px') : v)
				}
			}
		}
  } else if (value == null) {
    dom.removeAttribute(name)
  } else if (typeof value !== 'function') {
    dom.setAttribute(name, value)
  }
}
複製程式碼

其他

preact原始碼分析(一)

preact原始碼分析(二)

preact原始碼分析(三)

preact原始碼分析(四)

preact原始碼分析(五)

相關文章