react源码分析——自己实现diff算法

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值