Vue中渲染系統(tǒng)模塊的實現詳解
我們想要實現一個簡潔版的Mini-Vue框架,應該包含三個模塊:分別是:渲染系統(tǒng)模塊、可響應式系統(tǒng)模塊、應用程序入庫模塊。
這篇就來寫一下渲染系統(tǒng)模塊。剩下兩個模塊后面有時間再更新。
vue渲染系統(tǒng)實現,應該包含三個功能,分別是:① h函數,用于返回一個VNode對象;② mount函數,用于將VNode轉為真實dom,并掛載到DOM上;③ patch函數,類似于diff算法,用于對比兩個VNode,決定如何處理新的VNode
虛擬dom本身其實就是個javascript對象。這里會寫一些核心邏輯,大家如果看vue源碼的話,會發(fā)現里面有非常多的邊界情況的處理判斷。我們只需要知道核心邏輯就可以了,也就是下面所寫的。
下面寫的會有用到個判斷類型的方法,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樹
mount函數
mount函數負責將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) }
這里第三個參數只考慮了,string和array的情況,還有對象的情況相對少一些,就是我們平常的使用的插槽
patch函數
patch函數類似于diff算法,用于兩個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 in newProps)) { if (key.startsWith('on')) { const oldValue = oldProps[key] el.removeEventListener(key.slice(2).toLowerCase(), oldValue) } else { 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] // 前面有相同節(jié)點的元素進行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) }) } } } } }
完整代碼
render.js
/** * Vue渲染系統(tǒng)實現 * 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 in newProps)) { if (key.startsWith('on')) { const oldValue = oldProps[key] el.removeEventListener(key.slice(2).toLowerCase(), oldValue) } else { 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] // 前面有相同節(jié)點的元素進行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>
到此這篇關于Vue中渲染系統(tǒng)模塊的實現詳解的文章就介紹到這了,更多相關Vue渲染系統(tǒng)模塊內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue中的<template>標簽與react中的<></>標簽區(qū)別詳解
這篇文章主要為大家介紹了vue中的<template>標簽與react中的<></>標簽區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08如何配置vue.config.js 處理static文件夾下的靜態(tài)文件
這篇文章主要介紹了如何配置vue.config.js 處理static文件夾下的靜態(tài)文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06vue3中vite.config.js相關常用配置超詳細講解
在Vue3項目中vite.config.js文件是Vite構建工具的配置文件,它用于定義項目的構建和開發(fā)選項,這篇文章主要介紹了vue3中vite.config.js相關常用配置的相關資料,需要的朋友可以參考下2025-04-04解決Antd 里面的select 選擇框聯動觸發(fā)的問題
這篇文章主要介紹了解決Antd 里面的select 選擇框聯動觸發(fā)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10