在前面我們實現了_render方法,就是把虛擬dom轉換爲真實的dom,下面我們需要優化一下這個方法,不要讓它傻乎乎的渲染整個dom樹,對比需要變化的地方,只渲染需要變化的地方,這樣的過程,就是diff算法。
對比當前真實的dom跟虛擬的dom,一邊對比一邊更新。只對比同一級的dom。
主要是拿真實的dom跟虛擬的dom對比。我們之前的_render方法,就沒有那麼好用了,現在我們來實現一下diff方法
react-dom/diff.js
/**
*
* @param {*} dom 真實的dom
* @param {*} vnode 虛擬dom
* @param {*} container
*/
export function diff(dom, vnode, container) {
// 對比節點的變化
const ret = diffNode(dom, vnode)
if(container) container.appendChild(ret);
return ret;
}
跟render方法很像,都是最後一步才把生成的dom樹,添加到html中,ret應該是我們需要dom元素,與_render方法不同的是,這裏的dom元素是我們對比更新過的,找個最小的需要更新的單元,更新後生成的dom元素。這裏的核心就在diffNode方法的實現。
第一個參數就是真實的dom,第二個參數是虛擬的dom
就是類似上面的結構。
jsx就是下面的這種數據結構
(<div className='active'>
1
<h1>react</h1>
<button key='9' onClick={this.handlerClick.bind(this)}>改變狀態 </button>
</div>)
可以看到,tag:div的這個虛擬dom還有一個children是一個數組,分別點開發現,結構類似最外層的虛擬dom,實時上,它們也是虛擬的dom對象,可以看到,虛擬的dom對象可能是數值,也可能是對象,也可能是字符串。
如果說是數值,就是一個文本節點,但是這裏需要轉換爲字符串;
如果說字符串,也是一個文本節點;
如果說是對象,就是一個標籤,帶有屬性,或者還帶有children,子節點;
還有一種是函數,也就是我們的函數組件,留到下面再說。
先來看下前2種
export function diffNode(dom, vnode) {
let out = dom;
if(vnode === undefined || vnode === null || typeof vnode === 'boolean') return document.createTextNode('');
// 如果是數值
if(typeof vnode === 'number') vnode = String(vnode)
// 如果是字符串
if(typeof vnode === 'string') {
// 是文本節點
if(dom && dom.nodeType === 3) {
// 如果真是的文本跟虛擬的文本不相等,需要更新
if(dom.textContent !== vnode) {
// 更新文本內容
dom.textContent = vnode
}
} else {
out = document.createTextNode(vnode);
if(dom && dom.parentNode) {
dom.parentNode.replaceNode(out, dom)
}
}
return out;
}
}
- 首先需要判斷虛擬dom的類型,如果說vnode不存在,就可以返回一個空的文本節點,注意,這裏要是一個文本節點,不然後面在插入dom 的時候會報錯。
- 然後如果是數值直接轉換爲字符串
- 如果是字符串,我們需要看下有有真實的dom
- 有,說明不是第一次渲染,是第n次的更新。需要確認一個dom的節點類型,3說明是問文本節點,這個時候需要對比一個真實dom的文本節點,其實也就是文本內容,是否等於傳遞進來的虛擬dom的文本內容,這時的vnode是一個字符串,如果不等,直接替換就行
- 如果不是文本節點,dom不存在,這時需要把傳遞進來的字符串變成文本節點,然後返回,或者dom存在的話,把這個位置的節點替換掉。
- 如果是對象。那麼必然會有一個dom元素,看下面的兩種情況對比:
如果是串jsx
const ele = (
<div className='active'>
<h1>nihao</h1>
<p>內容</p>
</div>
)
console.log(ele)
如果是一個組件
import React from './react'
import ReactDOM from './react-dom'
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 1
}
}
handlerClick() {
this.setDate({
num: this.state.num + 1,
})
}
render() {
return (
<div className='active'>
1
<h1>react</h1>
<button key='9' onClick={this.handlerClick.bind(this)}>改變狀態</button>
</div>
)
}
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));
由上面的對比可以看出,tag是不一樣的,tag是函數的時候,渲染的是一個組件。所以代碼需要做一下區分:
export function diffNode(dom, vnode) {
let out = dom;
if(vnode === undefined || vnode === null || typeof vnode === 'boolean') return document.createTextNode('');
// 如果是數值
if(typeof vnode === 'number') vnode = String(vnode)
// 如果是字符串
if(typeof vnode === 'string') {
if(dom && dom.nodeType === 3) {
if(dom.textContent !== vnode) {
// 更新文本內容
dom.textContent = vnode
}
} else {
out = document.createTextNode(vnode);
if(dom && dom.parentNode) {
dom.parentNode.replaceNode(out, dom)
}
}
return out;
}
if(typeof vnode.tag === 'function') {
// tag是函數
}
// 非文本dom節點
if(!dom) {
out = document.createElement(vnode.tag)
}
// 對比並更改屬性
diffAttribute(out, vnode)
return out;
}
先不看是函數的情況,如果說是一串jsx我們需要對比tag標籤屬性,如果dom不存在,我們創建一個dom(第一次渲染的時候),如果dom存在,我們需要對比這兩個dom之間的屬性是否一致,這裏就交給diffAttribute方法,就是拿到dom的屬性跟vnode中的attrs做下對比,一致就不做改變,不一致就改變,有則改,無則移除。
怎麼拿到一個真是dom的屬性,看下下面打印:
<body>
<div id='app' data-uri= '892' style="color: black"></div>
</body>
<script>
var app = document.getElementById('app');
console.log(app.attributes);
</script>
得到一個類數組,屬性值是dom元素上的屬性。
var app = document.getElementById('app');
var domAttrs = app.attributes;
console.log([...domAttrs].forEach(item => {console.log(item.name, item.value)}));
可以拿到屬性和屬性值,這樣就可以拿到真是dom上所有的屬性了。
function diffAttribute(dom, vnode) {
// dom原來的節點,vnode虛擬的節點
const oldAttris = {};
const newAttris = vnode.attrs;
const domAttrs = dom.attributes;
[...domAttrs].forEach(item => {
oldAttris[item.name] = item.value
})
// 對比屬性,老屬性不在新屬性中,移除老的屬性
for(let key in oldAttris) {
if(!(key in newAttris)) {
setArribute(dom, key, undefined)
}
}
// 屬性不一致,重置屬性
for(let key in newAttris) {
if(oldAttris[key] !== newAttris[key]) {
setArribute(dom, key, newAttris[key])
}
}
}
setArribute方法不需要做調整,還是之前寫好的。
export function setArribute(dom, key, value) {
// class
if(key === 'className') {
key = 'class';
}
// 事件
if(/on\w+/.test(key)) {
key = key.toLowerCase()
dom[key] = value || ''
} else if(key === 'style') { // 樣式
if(!value || typeof value === 'string') {
dom.style.cssText = value || ''
} else if(value && typeof value === 'object') {
for(let k in value) {
if(typeof value[k] === 'number') {
dom.style[k] = value[k] + 'px'
} else {
dom.style[k] = value[k]
}
}
}
} else {
if(key in dom) {
dom[key] = value;
}
if(value) {
dom.setAttribute(key,value)
} else {
dom.removeAttribute(key)
}
}
}
這裏我們處理完了,tag和attrs,可以看到vnode中可能還會有childrem,是一個數組,是tag元素的子節點形成的虛擬dom,這裏的dom也是需要我們做對比生成的。diffNode方法做下面調整
if(!dom) {
out = document.createElement(vnode.tag)
}
if(vnode.children && vnode.children.length>0 || (out.childNodes && out.childNodes.length>0)) {
diffChildren(out, vnode.children)
}
// 對比並更改屬性
diffAttribute(out, vnode)
return out;
子節點的比較交給diffChildren方法來處理,就是拿tag標籤所包含的子節點,跟新生成vnode中的children做對比。
react中有一個很關鍵的提升性能的點,就是key,因爲react是以key來區分組件和元素的,如果有key會提高效率。
在我們不知一個元素下面有多少子節點的情況下,會通過原生方法獲取所有的子節點,然後再循環所有的子節點,如果說有key 的話,我們只需要找到相應的key,然後拿有相同key的元素取比較就可以了。
這裏在創建虛擬dom的時候,需要加上key
import Component from './component'
const React = {
createElement,
Component
}
function createElement(tag, attrs, ...children) {
attrs = attrs || {}
return {
tag,
attrs,
children,
key: attrs.key || null
}
}
export default React
然後拿到dom首先把有key 跟 沒有 key 的區分開。
function diffChildren(dom, vChildren) {
const domChildren = dom.childNodes;
const children = [];
const keyed = {};
// 把有key 的dom 跟沒有key 的dom 區分開
if(domChildren && domChildren.length > 0) {
domChildren.forEach((domChild) => {
const key = domChild.getAttribute && domChild.getAttribute('key');
if(key) keyed[key] = domChild;
else children.push(domChild);
})
}
}
然後再遍歷vnode中的children,如果虛擬dom中也存在key,就去取相應的真實com,然後diffNode對比。如果沒有key,需要取真實的dom,從第一個開始取,每取到一個跳出循環,diffNode對比。對比完成後,再來進行增刪改的操作。
function diffChildren(dom, vChildren) {
const domChildren = dom.childNodes;
const children = [];
const keyed = {};
// 把有key 的dom 跟沒有key 的dom 區分開
if(domChildren && domChildren.length > 0) {
domChildren.forEach((domChild) => {
const key = domChild.getAttribute && domChild.getAttribute('key');
if(key) keyed[key] = domChild;
else children.push(domChild);
})
}
if(vChildren && vChildren.length > 0) {
let min = 0;
let childrenLen = children.length;
[...vChildren].forEach((vchild,i) => {
// 拿到虛擬dom中的key
const key = vchild.key;
let child;
if(key) {
if(keyed[key]) {
child = keyed[key];
keyed[key] = undefined;
}
} else if(childrenLen > min) {
for(let j = min; j < childrenLen; j++) {
let c = children[j];
if(c) {
child = c;
children[j] = undefined;
if(j === childrenLen -1) childrenLen --;
if(j === min) min++;
break;
}
}
}
child = diffNode(child, vchild);
const f = domChildren[i]
if(child && child !== dom && child !== f) {
if(!f){
dom.appendChild(child);
} else if (child === f.nextSibling) {
removeNode()
} else {
dom.insertBefore(child, f)
}
}
})
}
}
到這裏我們已經把tag是dom元素的情況分析完成了。
還有一中情況,就是tag是函數的情況:我們可以先思考一下,如果說更新的是組件,如果組件改變了,react會卸載組件,然後加載新的組件,所以這一步就是組件的對比,卸載,加載的過程。
if(typeof vnode.tag === 'function') {
return diffComponet(out, vnode)
}
對比組件有沒有發生變化,主要是對比構造函數有沒有變;
function diffComponet(dom, vnode) {
// 如果組件沒有變化
let comp = dom;
if(comp && comp.constructor === vnode.tag) {
// 重新設置屬性
setComeponentProps(comp, vnode.attrs);
dom = comp.base
} else {
// 組件發生了變化
if(comp) {
// 先移除舊的組件
unmountComponnet(comp)
comp = null
}
// 1.創建新的組件
comp = createComponent(vnode.tag, vnode.attrs)
// 2.設置組件屬性
setComeponentProps(comp, vnode.attrs)
// 3.給當前組件掛base
dom = comp.base
}
return dom;
}
function unmountComponnet(comp) {
removeNode(comp.base)
}
function removeNode(dom) {
if(dom && dom.parentNode) {
dom.parentNode.removeNode(dom)
}
}
我們的react/index.js中,render函數不再調用_render了,而是
export function render(v, container, dom) {
diff(dom, v, container)
}
渲染組件的過程,換成了diffNode來執行。
export function renderComponent(comp) {
// v虛擬的dom對象
const v = comp.render();
const base = diffNode(comp.base,v);
if(comp.base && comp.componentWillUpdate) comp.componentWillUpdate();
if(comp.base && comp.componentDidUpdate) comp.componentDidUpdate();
else if (comp.componentDidMount) comp.componentDidMount();
// if(comp.base && comp.base.parentNode) {
// comp.base.parentNode.replaceChild(base, comp.base)
// }
// 生成真實的dom
comp.base = base;
}
這樣就完成了react的diff算法更新dom。