在前面我们实现了_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。