实现一个属于自己的React框架(一)

35次阅读

共计 7836 个字符,预计需要花费 20 分钟才能阅读完成。

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>
)

实现 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 源码分析(五)

正文完
 0