什么是虛擬DOM
react 中的 virtual DOM (虛擬DOM),其實(shí)就是JS對(duì)象。
眾所周知,瀏覽器的DOM元素的渲染效率極低,對(duì)DOM的優(yōu)化是前端開發(fā)人員一直以來很頭疼的問題,而虛擬DOM就是針對(duì)真實(shí)的DOM元素渲染效率低下而問世的。虛擬DOM在內(nèi)存中以JS對(duì)象的形式存在,模擬了真實(shí)DOM的所有結(jié)構(gòu),在將虛擬DOM渲染到頁面上之前,我們的所有操作都在虛擬DOM上進(jìn)行,你要知道:對(duì)JS對(duì)象的操作要比對(duì)DOM的操作快得多,所以虛擬DOM的出現(xiàn)使前端性能得到了極大的優(yōu)化。
虛擬DOM模 真實(shí)DOM的操作
假設(shè)我們有一個(gè)創(chuàng)建虛擬DOM的方法:createElement( tag, props, children )
createElement方法可以幫我們創(chuàng)建一個(gè)虛擬DOM,來模仿你想要的真實(shí)DOM的結(jié)構(gòu)
參數(shù):
- tag:DOM元素的標(biāo)簽( 'a' , 'p ', 'div' ...)
- props:DOM元素的屬性( {className:'box' , id:'container' , style:{fontSize:'20px'} , key:1 , ...})
- children:DOM元素的內(nèi)容( 字符串或其他虛擬DOM元素組成的數(shù)組 )
// 用 createElement 方法創(chuàng)建一個(gè)虛擬DOM的結(jié)構(gòu)
let virtualDOM = createElement('div',{id:'container'},[
createElement('p',{className:'msg',key:1},'這是一條消息'),
createElement('p',{className:'msg',key:2},'這是另一條消息'),
createElement(null,null,'這是一條沒有標(biāo)簽包裹的文本'),
createElement('button',{className:'btn',key:3},'按鈕')
]);
現(xiàn)在,很簡(jiǎn)單的一個(gè)虛擬DOM結(jié)構(gòu)已經(jīng)創(chuàng)建完成了,顯然,雖然它擁有自己的屬性和結(jié)構(gòu),但是目前為止這個(gè)虛擬DOM只是一個(gè)JS對(duì)象而已,我們對(duì)它進(jìn)行的任何操作都是在內(nèi)存中完成的(虛擬DOM性能好的原因)。但是我們最終需要的是一個(gè)真實(shí)的DOM,所以我們還需要一個(gè)render方法:render( virtualDOM, DOM )
render 方法可以將你的虛擬DOM解析成真實(shí)的DOM并渲染到頁面上
參數(shù):
- virtualDOM:需要解析的虛擬DOM
- DOM:需要渲染在哪個(gè)DOM里
render(virtualDOM,document.getElementById('root'));
至此,一個(gè)簡(jiǎn)單的 virtualDOM 模擬 真實(shí)DOM 的流程就結(jié)束了。
虛擬DOM原理
初始化:定義類型。
// 定義一個(gè)tag類型集合
const tagTypes = {
HTML:"HTML",
TEXT:"TEXT"
}
// 定義children類型集合
const childrenTypes = {
// 子元素只有一個(gè) 說明是字符串
single:"single",
// 子元素是一個(gè)數(shù)組 數(shù)組里是多個(gè)元素
many:"many",
// 子元素是一個(gè)空 沒有子元素
empty:"empty"
}
createElement()
實(shí)現(xiàn)原理:根據(jù)你傳入?yún)?shù)的類型,返回一個(gè)對(duì)應(yīng)出來你想要的結(jié)構(gòu)的整合后的JS對(duì)象。
// 創(chuàng)建虛擬dom的方法
function createElment(tag,props,children){
// 定義tag類型
let type;
// 如果tag存在,那么該元素就是HTML元素,否則是字符串
if(typeof tag === 'string'){
type = tagTypes.HTML;
}else{
type = tagTypes.TEXT;
}
// 定義children類型
let childrenType;
// 如果children是文本的時(shí)候就創(chuàng)建一個(gè)文本虛擬dom
// 如果children是數(shù)組的時(shí)候就創(chuàng)建一個(gè)有子節(jié)點(diǎn)的虛擬dom
// 如果children是空的時(shí)候就創(chuàng)建一個(gè)空虛擬dom
if(typeof children === 'string'){
childrenType = childrenTypes.single;
// createTextNode:創(chuàng)建文本DOM方法
children = createTextNode(children)
}else if(Array.isArray(children)){
childrenType = childrenTypes.many;
}else{
childrenType = childrenTypes.empty;
}
// 返回虛擬dom對(duì)象
return {
el:null,
type,
tag,
props,
children,
childrenType
}
}
//創(chuàng)建文本虛擬dom,直接返回一個(gè)對(duì)應(yīng)的文本虛擬DOM
function createTextNode(text){
return {
type:'text',
tag:null,
props:null,
children:text,
childrenType:childrenTypes.empty
}
}
render()
// 渲染方法
function render(vnode, container){
if(container.vnode){
// 如果虛擬DOM已經(jīng)存在,那么執(zhí)行更新
// 這一步是相當(dāng)復(fù)雜的diff算法,單獨(dú)開辟章節(jié)來講,此處暫時(shí)只考慮首次渲染
}else{
// 如果虛擬DOM沒有存在,那么執(zhí)行掛載(首次渲染)
mounted(vnode, container);
}
// 判斷是初次渲染還是更新渲染
container.vnode = vnode;
}
// 首次渲染函數(shù)
function mounted(vnode,container){
let {type} = vnode;
if(type === 'HTML'){
// 渲染HTML元素方法
mountedElement(vnode,container)
}else{
// 渲染文本元素方法
mountedText(vnode,container)
}
}
// 渲染HTML元素的方法
function mountedElement( vnode, container ){
let { type, tag, props, children, childrenType } = vnode;
// el是真是的DOM元素,此處創(chuàng)建tag對(duì)應(yīng)的DOM元素并賦給el
var el = document.createElement(tag);
vnode.el = el;
// 遍歷設(shè)置props屬性
if(props){
for(var key in props){
/* 設(shè)置DOM的屬性(方法在代碼最后)
語法:patchProps( 設(shè)置屬性的元素, 屬性的key值, 舊的value值, 新的value ) */
patchProps(el,key,null,props[key])
}
}
// 判斷該元素的子元素的類型
if(childrenType === childrenTypes.single){
// 如果 childrenType 屬性為 single 那么肯定是文本,用渲染文本方法將子元素渲染
mountedText(children, el)
}else if(childrenType === childrenTypes.many){
// 如果 childrenType 屬性為 many 則肯定是嵌套子元素,遍歷后用首次渲染方法將子元素渲染(遞歸)
children.forEach((item)=>{
mounted( item, el )
})
}
// 渲染完成后,最終要插入到父級(jí)里面(最高父級(jí)就是root)
container.appendChild(el);
}
// 渲染文本虛擬dom的方法
function mountedText(vnode,container){
// 創(chuàng)建一個(gè)對(duì)應(yīng)的文本節(jié)點(diǎn),直接插入父元素
var textNode = document.createTextNode(vnode.children);
vnode.el = textNode;
container.appendChild(textNode);
}
// 掛載屬性的方法(部分情況)
// patchProps( 設(shè)置屬性的元素, 屬性的key值, 舊的value值, 新的value )
function patchProps(el, key, oldVal, newVal) {
switch (key) {
case 'className':
el.className = newVal;
break;
case 'id':
el.id = newVal;
break;
case 'onClick':
el.addEventListener("click", newVal);
break;
case 'style': {
for (var sKey in newVal) {
el.style[sKey] = newVal[sKey];
}
break;
}
default:
if (key != 'key') {
el.setAttribute(key, newVal);
}
}
}
至此,一個(gè)簡(jiǎn)單版的react中虛擬DOM的底層原理就實(shí)現(xiàn)啦
??ヽ(°▽°)ノ?