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>
)
实现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)
}
}