实现一个属于自己的React框架~~~~更新中~~~~~
发布于 4 个月前 作者 BengBu-YueZhang 541 次浏览 来自 分享

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](/user/babel)/preset-env'],
            plugins: [
              '[@babel](/user/babel)/plugin-syntax-dynamic-import',
              [
                "[@babel](/user/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源码分析(五)

回到顶部