Mini-Vue フレームワークの簡潔なバージョンを実装したいと考えています。これには、レンダリング システム モジュール、応答システム モジュール、アプリケーション ストレージ モジュールの 3 つのモジュールが含まれている必要があります。
この記事ではレンダリングシステムモジュールについて書きます。残りの 2 つのモジュールを後で更新する時間があります。
vue レンダリング システムの実装には、次の 3 つの関数が含まれている必要があります: ① h 関数、VNode オブジェクトを返すために使用されます、② mount 関数、VNode を実際の dom に変換し、DOM にマウントするために使用されます、③ patch 関数、同様のものdiff アルゴリズムに基づいて、2 つの VNode を比較し、新しい VNode を処理する方法を決定するために使用されます。
仮想 dom 自体は実際には JavaScript オブジェクトです。ここにはいくつかのコアロジックを書きますが、Vue のソースコードを見ると、境界条件の判定がたくさんあることがわかります。理解する必要があるのは、以下に書かれているコアロジックだけです。
以下は型を判定するメソッドを使用します。getObjType は次のとおりです。
データ型を判断する最も正確な方法でもあります。
function getObjType(obj) {
let toString = Object.prototype.toString
let map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
}
if (obj instanceof Element) {
return 'element'
}
return map[toString.call(obj)]
}
h関数
h 関数は実際には非常に単純で、js オブジェクトを返します。
/**
* 虚拟dom本身就是个javascript对象
* @param {String} tag 标签名称
* @param {Object} props 属性配置
* @param {String | Array} children 子元素
* @returns
*/
const h = (tag, props, children) => ({ tag, props, children })
ご覧のとおり、これは仮想 dom ツリーです
マウント関数
マウント関数は、vnode を実際の dom に変換し、指定されたコンテナーの下にマウントします。
/**
* 将虚拟dom转为真实dom并挂载到指定容器
* @param {Object} vnode 虚拟dom
* @param {Element} container 容器
*/
const mount = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.tag)
if(vnode.props) {
for(let key in vnode.props) {
const value = vnode.props[key]
if(key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
if(vnode.children) {
if(getObjType(vnode.children) === 'string') {
el.textContent = vnode.children
} else if(getObjType(vnode.children) === 'array') {
vnode.children.forEach(item => {
mount(item, el)
})
}
}
container.appendChild(el)
}
ここでの 3 番目のパラメーターは、文字列と配列の場合のみを考慮し、比較的少数のオブジェクト (通常使用するスロット) の場合のみを考慮します。
パッチ機能
patch 関数は diff アルゴリズムに似ており、2 つの VNode を比較し、新しい VNode の処理方法を決定するために使用されます。
/**
* 类似diff对比两个vnode
* @param {Object} n1 旧vnode
* @param {Object} n2 新vnode
*/
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1parentEl = n1.el.parentElement
n1parentEl.removeChild(n1.el)
mount(n2, n1parentEl)
} else {
const el = n2.el = n1.el
// 处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
for (let key in oldProps) {
if (key.startsWith('on')) {
const oldValue = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (getObjType(newChildren) === 'string') {
if (getObjType(oldChildren) === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else if (getObjType(newChildren) === 'array') {
if (getObjType(oldChildren) === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else if (getObjType(oldChildren) === 'array') {
// oldChildren -> [vnode1, vnode2, vnode3]
// newChildren -> [vnode1, vnode5, vnode6, vnode7, vnode8]
// 前面有相同节点的元素进行patch操作
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 2. newChildren.length > oldChildren.length 多余的做挂载操作
if (newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach(item => {
mount(item, el)
})
}
// 3. newChildren.length < oldChildren.length 多余的做移除操作
if (newChildren.length < oldChildren.length) {
oldChildren.slice(commonLength).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
完全なコード
レンダリング.js
/**
* Vue渲染系统实现
* 1. h函数,用于返回一个VNode对象;
* 2. mount函数,用于将VNode挂载到DOM上;
* 3. patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
*/
/**
* 虚拟dom本身就是个javascript对象
* @param {String} tag 标签名称
* @param {Object} props 属性配置
* @param {String | Array} children 子元素
* @returns
*/
const h = (tag, props, children) => ({ tag, props, children })
/**
* 将虚拟dom转为真实dom并挂载到指定容器
* @param {Object} vnode 虚拟dom
* @param {Element} container 容器
*/
const mount = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.tag)
if (vnode.props) {
for (let key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
if (vnode.children) {
if (getObjType(vnode.children) === 'string') {
el.textContent = vnode.children
} else if (getObjType(vnode.children) === 'array') {
vnode.children.forEach(item => {
mount(item, el)
})
}
}
container.appendChild(el)
}
/**
* 类似diff对比两个vnode
* @param {Object} n1 旧vnode
* @param {Object} n2 新vnode
*/
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1parentEl = n1.el.parentElement
n1parentEl.removeChild(n1.el)
mount(n2, n1parentEl)
} else {
const el = n2.el = n1.el
// 处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
for (let key in oldProps) {
if (key.startsWith('on')) {
const oldValue = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (getObjType(newChildren) === 'string') {
if (getObjType(oldChildren) === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else if (getObjType(newChildren) === 'array') {
if (getObjType(oldChildren) === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else if (getObjType(oldChildren) === 'array') {
// oldChildren -> [vnode1, vnode2, vnode3]
// newChildren -> [vnode1, vnode5, vnode6, vnode7, vnode8]
// 前面有相同节点的元素进行patch操作
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 2. newChildren.length > oldChildren.length 多余的做挂载操作
if (newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach(item => {
mount(item, el)
})
}
// 3. newChildren.length < oldChildren.length 多余的做移除操作
if (newChildren.length < oldChildren.length) {
oldChildren.slice(commonLength).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
function getObjType (obj) {
let toString = Object.prototype.toString
let map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
}
if (obj instanceof Element) {
return 'element'
}
return map[toString.call(obj)]
}
テスト:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./render.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const vnode = h('div', { class: 'wft' }, [
h('h1', null, '标题'),
h('span', null, '哈哈哈')
])
setTimeout(() => {
mount(vnode, document.getElementById('app'))
}, 1500)
const newVNode = h('div', { class: 'new-wft', id: 'wft' }, [
h('h3', null, '我是h3')
])
setTimeout(() => {
patch(vnode, newVNode)
}, 4000)
</script>
</body>
</html>