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)
}
}
複製程式碼