Redis Nginx hyperlink uiviewcontroller camera jtable 后台网站模板 后台管理界面 磁盘清理会误删东西吗 kubernetes架构 python基本语法 python文件 javamysql java编程基础 java接口类 安装java环境 javastring类型 java安装步骤 java集合遍历 java如何配置环境变量 java接口的修饰符 java开发语言 php实例 java游戏开发教程 windows7loader pushstate ipad锁屏 qq免安装版 linux端口映射 摩斯密码在线翻译 手机电脑模拟器 毕业证件照 游戏linux正则表达式 小米8游戏模式 一键换肤大师 视频添加水印 工程地质手册 winhex中文版下载 文件粉碎工具 固态硬盘有什么用
当前位置: 首页 > 学习教程  > 编程语言

简单实现vue中的虚拟dom和dom-diff算法

2020/11/24 11:17:39 文章标签: 测试文章如有侵权请发送至邮箱809451989@qq.com投诉后文章立即删除

写一份vue实现虚拟dom和dom更新diff算法. 很多人在面试的时候,都会被问到虚拟dom是什么,vue的diff如何实现等问题. js中通过直接操作真实的dom会消耗很大性能. 把真实的dom转化成虚拟dom来表示.更新dom的时候直接操作虚拟dom能提高很大性能. 虚拟dom是什么? 根据dom树生成对…

写一份vue实现虚拟dom和dom更新diff算法.

很多人在面试的时候,都会被问到虚拟dom是什么,vue的diff如何实现等问题.
js中通过直接操作真实的dom会消耗很大性能. 把真实的dom转化成虚拟dom来表示.更新dom的时候直接操作虚拟dom能提高很大性能.

虚拟dom是什么?

根据dom树生成对应树形结构对象.说白了虚拟dom就是一个对象,只不过把dom树通过对象来表示.

实现虚拟dom

在vue中render函数的第一个参数createElement(创建虚拟dom);

function createElement(type, props, ...children) {
    let key
    if (props.key) {
        key = props.key;
        delete props.key
    }
    //处理文本节点
    children = children.map(vnode => {
        if (typeof vnode === "string") {
            return vNode(undefined, undefined, undefined, [], vnode)
        }
        return vnode
    })
    return vNode(type, props, key, children, text = undefined)
}
function vNode(type, props, key, children, text) {
    return { type, props, key, children, text }
}

这里创建createElement方法.并且处理一下文本转为文本节点.

注意这里不需要递归处理children,因为你在vue中用法创建会在调用createElement这个方法.所以只需要一层.

创建完虚拟对象,创建render函数把虚拟dom渲染成真实dom.

实现render

这里是直接原生怼的,传入一个虚拟dom和一个容器参数.

function render(vNode, container) {
    let ele = createDomElementFrom(vNode);
    container.appendChild(ele);
}
function createDomElementFrom(vNode) {
	let { type, props, key, children, text } = vNode;
    if (type) { // 判断是否标签
        //给当前的虚拟dom对象挂载一个真实dom;
        vNode.domElement = document.createElement(type);
        // 添加属性方法
        updateEleProperties(vNode);
        //递归调用子组件
        vNode.children.forEach(element => {
        	render(element, vNode.domElement)
        });
    } else { //文本节点
        vNode.domElement = document.createTextNode(text);
    }
	return vNode.domElement
}
function updateEleProperties(newVnode, oldPros = {}) {
    //oldPros传入这个参数是更新的时候需要做对比.后面会说到.初始渲染为空.
    let element = newVnode.domElement;
    let newProps = newVnode.props;
	
    for (let key in oldPros) { //首次渲染不会走这
        // 新节点上没有老节点属性, 直接删除
        if (!newProps[key]) {
            delete element[key];
        }
        if (key === "style") {
            let oleStyleProps = oldPros.style || {};
            let a = newProps.style || {};
            for (let key in oleStyleProps) {
                // 新样式节点上没有老样式节点属性, 直接删除
                if (!a[key]) {
                    element.style[key] = '';
                }
            }
        }
    }

    for (let key in newProps) {
        //新节点上新增属性,直接添加--缺点是全部重新遍历执行一遍,没有(复用/对比)旧的属性和节点
        
        // 事件其他特殊的属性需要自己再去做其他处理这里只是style举例一下
        if (key === 'style') {
            //style特殊属性 
            let newStyleProps = newProps.style || {};
            for (let key in newStyleProps) {
                // 新节点上新增style属性,直接添加 
                element.style[key] = newStyleProps[key];
            }
        } else {
            element[key] = newProps[key];
        }
    }
}

以上render的基本初始渲染已经完事.可以拿在浏览器上运行了.

let oldVnode = createElement("div", { className: "xxx" }, 
                             createElement("span", {}, "我是span标签")
                            );
render(oldVnode, document.querySelector("#app"))

虚拟dom的初始渲染完成,接下来就要实现dom更新的操作了.通过patch函数进行打补丁.

function patch(oldVnode, newVnode) { // patch新旧dom更新;打补丁
    if (oldVnode.type !== newVnode.type) { // 新标签类型和旧标签类型不一致,直接替换.
        return oldVnode.domElement.parentNode.replaceChild(createDomElementFrom(newVnode), oldVnode.domElement);
    }

    if (oldVnode.text !== newVnode.text) { //文本不一致
        return oldVnode.domElement.textContent = newVnode.text;
    }

    // 标签一样属性不一致
    let domElement = newVnode.domElement = oldVnode.domElement;//拿到真实dom
    updateEleProperties(newVnode, oldVnode.props);//对比新,旧的props进行更新,这里传了第二个参数就是为了新旧props对比.

    // 子节点 
    if (!newVnode.children.length) {
        //没有新的子节点
        domElement.innerHTML = '';
    } else if (oldVnode.children.length && newVnode.children.length) {
        // 新旧都有子节点 - 核心进入diff对比.
        updateChildren(domElement, oldVnode.children, newVnode.children);
    } else {
        //有新的子节点,旧的没有子节点
        newVnode.children.forEach((children) => {
            //这里的children是一个虚拟dom需要转化为真实dom直接使用函数转化.
            domElement.appendChild(createDomElementFrom(children))
        })
    }
}

dom-diff

通过双指针来标记新旧节点的开始位置,开始节点 . 结束位置和结束节点.

function isSameNode(oleVnode, newVnode) {
    return oleVnode.type === newVnode.type && oleVnode.key === newVnode.key
}//判断是否同一个节点

在进入updateChildren函数时,节点对比的时候先设想一下.几种对比情况. (五种情况)

  1. 新节点开始位置和旧节点的开始位置对比.
diff1

对比成功,新旧节点对象开头指针只需要往右移一个位置和改变对应节点信息。

function updateChildren(parent, oldChildren, newChildren) {
 // 获取相关元素,start
 let oldStartIndex = 0;
 let oldStartVnode = oldChildren[oldStartIndex];
 let oldEndIndex = oldChildren.length - 1;
 let oldEndVnode = oldChildren[oldEndIndex];

 let newStartIndex = 0;
 let newStartVnode = newChildren[newStartIndex];
 let newEndIndex = newChildren.length - 1;
 let newEndVnode = newChildren[newEndIndex];
 
 //映射表,后面会提到
 let keyIndexMap = createMapBykeyToIndex(oldChildren);
 
 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {// 那边child短,那边先出
     if (isSameNode(oldStartVnode, newStartVnode)) {
         patch(oldStartVnode, newStartVnode);//这里必须打一次补丁,有可能新老节点的props不一样
         // 这里指针只需要移动开头的   
         oldStartVnode = oldChildren[++oldStartIndex];
         newStartVnode = newChildren[++newStartIndex];
     }
 }

 //判断 新旧节点的开始和结束的索引大小, 开始索引小于结尾索引肯定还存有剩余的节点没有添加或者删除.
 if (newStartIndex <= newEndIndex) {
     // 开始节点相同,多出结尾
     for (let index = newStartIndex; index <= newEndIndex; index++) {
         let beforeElement = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].domElement : null;
         // null插入 相当于appendChild
         parent.insertBefore(createDomElementFrom(newChildren[index]), beforeElement);
     }
 }
}

测试数据. 以下自行修改图中对应子节点信息.

let oldVnode = createElement("div", { className: "xxx" },
                             createElement("li", { key: "A" }, "A"),
                             createElement("li", { key: "B" }, "B"),
                             createElement("li", { key: "C" }, "C"),
                             createElement("li", { key: "D" }, "D")
                            );
render(oldVnode, document.querySelector("#app"))
let newVnode = createElement("div", {},
                             createElement("li", { key: "A" }, "A"),
                             createElement("li", { key: "B" }, "B"),
                             createElement("li", { key: "C" }, "C"),
                             createElement("li", { key: "D", id: "ID" }, "D"),
                             createElement("li", { key: "E" }, "E"),
                            );
setTimeout(() => {
 patch(oldVnode, newVnode)
}, 2000);
  1. 新节点结束位置和旧节点的结束位置对比.

    2和1的相反, 就是改变新旧节点对象尾部指针(左移)和修改对应节点信息

    diff2
    if (isSameNode(oldStartVnode, newStartVnode)) {
        patch(oldStartVnode, newStartVnode);//这里必须打一次补丁,有可能新老节点的props不一样  
        oldStartVnode = oldChildren[++oldStartIndex];
        newStartVnode = newChildren[++newStartIndex];
    }  else if (isSameNode(oldEndVnode, newEndVnode)) { //在1的条件下添加这个判断条件
        patch(oldEndVnode, newEndVnode);
        oldEndVnode = oldChildren[--oldEndIndex];
        newEndVnode = newChildren[--newEndIndex];
    }
    
    (无需修改,以便思考)
    //这里取到循环结束的索引 通过insertBefore插入到父节点,就是dom相关的操作点.
    if (newStartIndex <= newEndIndex) {
        // 开始节点相同,多出结尾
        for (let index = newStartIndex; index <= newEndIndex; index++) {
            let beforeElement = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].domElement : null;
            // null插入 相当于appendChild
            parent.insertBefore(createDomElementFrom(newChildren[index]), beforeElement);
        }
    }
    
  2. 新节点开始位置和旧节点的结束位置对比.

    diff3

    对比成功把旧的节点添加到尾部,旧节点对象开始指针往右移一位,新节点对象尾部指针往左移一位以及修改相关节点信息。

    if (isSameNode(oldStartVnode, newStartVnode)) {
        patch(oldStartVnode, newStartVnode);//这里必须打一次补丁,有可能新老节点的props不一样  
        oldStartVnode = oldChildren[++oldStartIndex];
        newStartVnode = newChildren[++newStartIndex];
    }  else if (isSameNode(oldEndVnode, newEndVnode)) { //在1的条件下添加这个判断条件
        patch(oldEndVnode, newEndVnode);
        oldEndVnode = oldChildren[--oldEndIndex];
        newEndVnode = newChildren[--newEndIndex];
    }  else if (isSameNode(oldStartVnode, newEndVnode)) {
        patch(oldStartVnode, newEndVnode);
     	// 这里取到最后节点兄弟节点之前插入节点.
        parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSiblings);
        oldStartVnode = oldChildren[++oldStartIndex];
        newEndVnode = newChildren[--newEndIndex];
    } 
    
  3. 新节点结束位置和旧节点的开始位置对比.
    diff4
    对比成功把旧的节点插入到开头,旧节点对象尾部指针往左移一位,新节点对象开始指针往右移一位以及修改相关节点信息。跟3的相反。

    if (isSameNode(oldStartVnode, newStartVnode)) {
        patch(oldStartVnode, newStartVnode);//这里必须打一次补丁,有可能新老节点的props不一样  
        oldStartVnode = oldChildren[++oldStartIndex];
        newStartVnode = newChildren[++newStartIndex];
    }  else if (isSameNode(oldEndVnode, newEndVnode)) { //在1的条件下添加这个判断条件
        patch(oldEndVnode, newEndVnode);
        oldEndVnode = oldChildren[--oldEndIndex];
        newEndVnode = newChildren[--newEndIndex];
    }  else if (isSameNode(oldStartVnode, newEndVnode)) {
        patch(oldStartVnode, newEndVnode);
     	// 取到最后节点兄弟节点之前插入节点.
        parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSiblings);
        oldStartVnode = oldChildren[++oldStartIndex];
        newEndVnode = newChildren[--newEndIndex];
    }  else if (isSameNode(oldEndVnode, newStartVnode)) {
        patch(oldEndVnode, newStartVnode);
        // 取得旧的尾部节点插入到旧的开始节点.
        parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement);
        oldEndVnode = oldChildren[--oldEndIndex];
        newStartVnode = newChildren[++newStartIndex];
    } 
    
  4. 上面条件都不符合时,进行暴力循环创建旧子节点的key和index关联的映射表, 看是否有复用的旧节点.
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cwFswMpx-1606187596889)(E:\桌面\diff5.png)]

    // 创建映射表
    function createMapBykeyToIndex(oldChildren) {
        let map = {};
        for (let index = 0; index < oldChildren.length; index++) {
            let element = oldChildren[index];
            if (element.key) {
                map[element.key] = index
            }
        }
        return map;
    }
    
    //在updateChildren下,建立好映射表。一开始已经创建好
    let keyIndexMap = createMapBykeyToIndex(oldChildren);
    
    if (isSameNode(oldStartVnode, newStartVnode)) {
        patch(oldStartVnode, newStartVnode);//这里必须打一次补丁,有可能新老节点的props不一样  
        oldStartVnode = oldChildren[++oldStartIndex];
        newStartVnode = newChildren[++newStartIndex];
    }  else if (isSameNode(oldEndVnode, newEndVnode)) { //在1的条件下添加这个判断条件
        patch(oldEndVnode, newEndVnode);
        oldEndVnode = oldChildren[--oldEndIndex];
        newEndVnode = newChildren[--newEndIndex];
    }  else if (isSameNode(oldStartVnode, newEndVnode)) {
        patch(oldStartVnode, newEndVnode);
        // 取到最后节点兄弟节点之前插入节点.
        parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSiblings);
        oldStartVnode = oldChildren[++oldStartIndex];
        newEndVnode = newChildren[--newEndIndex];
    }  else if (isSameNode(oldEndVnode, newStartVnode)) {
        patch(oldEndVnode, newStartVnode);
        // 取得旧的尾部节点插入到旧的开始节点.
        parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement);
        oldEndVnode = oldChildren[--oldEndIndex];
        newStartVnode = newChildren[++newStartIndex];
    } else {  //无规则对比  需要和老节点的key对比复用 -> 需要做一个映射表;  key->index
        let index = keyIndexMap[newStartVnode.key];
        if (index == null) {//没有复用的 -- 直接插入到旧的开头节点。新的节点对象指针往右移。
    		//新节点对象还是虚拟dom
            parent.insertBefore(createDomElementFrom(newStartVnode), oldStartVnode.domElement);
            newStartVnode = newChildren[++newStartIndex];
        } else {//有复用的 -- 拿到旧节点。插入到旧的开头节点。新的节点对象指针往右移。旧的节点对象用undefined填充.
            patch(oldChildren[index], newStartVnode);
            parent.insertBefore(oldChildren[index].domElement, oldStartVnode.domElement);
            newStartVnode = newChildren[++newStartIndex];
            oldChildren[index] = undefined;
        }
     	//这里会出现旧节点没有删除. 需要在while结束后判断旧节点是否还有存留也就是索引对比.
    }
    
    if (newStartIndex <= newEndIndex) {
        // 开始节点相同,多出结尾
        for (let index = newStartIndex; index <= newEndIndex; index++) {
            let beforeElement = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].domElement : null;
            // null插入 相当于appendChild
            parent.insertBefore(createDomElementFrom(newChildren[index]), beforeElement);
        }
    } else if (oldStartIndex <= oldEndIndex) {
        // 老节点比较长,保留了以前的节点-需要删除
        for (let index = oldStartIndex; index <= oldEndIndex; index++) {
            let element = oldChildren[index]
            if (element) { //过滤掉undefined
                parent.removeChild(element.domElement);
            }
        }
    }
    

    这里基本的虚拟 dom和dom-diff算法已经完成.以下是完整的代码

    function createElement(type, props, ...children) {
        let key
        if (props.key) {
            key = props.key;
            delete props.key
        }
        //处理文本节点
        children = children.map(vnode => {
            if (typeof vnode === "string") {
                return vNode(undefined, undefined, undefined, [], vnode)
            }
            return vnode
        })
        return vNode(type, props, key, children, text = undefined)
    }
    
    function vNode(type, props, key, children, text) {
        return { type, props, key, children, text }
    }
    
    /**
             * @descriptions 渲染真实到容器上
             * @params {*} vNode 虚拟节点
             * @params {*} container 容器
             */
    function render(vNode, container) {
        let ele = createDomElementFrom(vNode);
        container.appendChild(ele);
    }
    function createDomElementFrom(vNode) {
        let { type, props, key, children, text } = vNode;
        if (type) { // 标签
            vNode.domElement = document.createElement(type);
            // 添加属性 
            updateEleProperties(vNode);
            //递归调用子组件
            vNode.children.forEach(element => {
                render(element, vNode.domElement)
            });
        } else { //文本节点
            vNode.domElement = document.createTextNode(text);
        }
        return vNode.domElement
    }
    function updateEleProperties(newVnode, oldPros = {}) {
        let element = newVnode.domElement;
        let newProps = newVnode.props;// 这里需要设置一下attr
    
        for (let key in oldPros) {
            // 新节点上没有老节点属性, 直接删除
            if (!newProps[key]) {
                delete element[key];
            }
            if (key === "style") {
                let oleStyleProps = oldPros.style || {};
                let a = newProps.style || {};
                for (let key in oleStyleProps) {
                    // 新样式节点上没有老样式节点属性, 直接删除
                    if (!a[key]) {
                        element.style[key] = '';
                    }
                }
            }
        }
    
        for (let key in newProps) {
            // 新节点上新增属性,直接添加  -- 缺点是全部重新遍历执行一遍,没有(复用/对比)旧的属性和节点
            if (key === 'style') {
                let newStyleProps = newProps.style || {};
                for (let key in newStyleProps) {
                    // 新节点上新增属性,直接添加  -- 需要做对比
                    element.style[key] = newStyleProps[key];
                }
            } else {
                element[key] = newProps[key];
            }
        }
    }
    
    function patch(oldVnode, newVnode) { // patch新旧dom更新;打补丁
        if (oldVnode.type !== newVnode.type) { // 新节点和旧节点不一致
            return oldVnode.domElement.parentNode.replaceChild(createDomElementFrom(newVnode), oldVnode.domElement);
        }
    
        if (oldVnode.text !== newVnode.text) { //文本不一致
            return oldVnode.domElement.textContent = newVnode.text;
        }
    
        // 标签一样属性不一致
        let domElement = newVnode.domElement = oldVnode.domElement;
        updateEleProperties(newVnode, oldVnode.props);
    
        // 子节点 
        if (!newVnode.children.length) {
            //没有新的子节点
            domElement.innerHTML = '';
        } else if (oldVnode.children.length && newVnode.children.length) {
            // 都有
            updateChildren(domElement, oldVnode.children, newVnode.children);
        } else {
            //有新的子节点,没有旧节点
            newVnode.children.forEach((children) => {
                domElement.appendChild(createDomElementFrom(children))
            })
        }
    }
    
    /**
             * 核心diff算法
             * 双指针 新老 子节点对比
            */
    function isSameNode(oleVnode, newVnode) {
        return oleVnode.type === newVnode.type && oleVnode.key === newVnode.key
    }
    
    // 创建映射表
    function createMapBykeyToIndex(oldChildren) {
        let map = {};
        for (let index = 0; index < oldChildren.length; index++) {
            let element = oldChildren[index];
            if (element.key) {
                map[element.key] = index
            }
        }
        return map;
    }
    
    function updateChildren(parent, oldChildren, newChildren) {
        // 获取相关元素,start
        let oldStartIndex = 0;
        let oldStartVnode = oldChildren[oldStartIndex];
        let oldEndIndex = oldChildren.length - 1;
        let oldEndVnode = oldChildren[oldEndIndex];
    
        let newStartIndex = 0;
        let newStartVnode = newChildren[newStartIndex];
        let newEndIndex = newChildren.length - 1;
        let newEndVnode = newChildren[newEndIndex];
    
        let keyIndexMap = createMapBykeyToIndex(oldChildren);
    
        while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {// 那边child短,那边先出
            /**
                      * 1.判断头 是否相同 
                      * 2.判断尾 是否相同
                      * 3.判断头尾 是否相同
                      * 4.判断尾头 是否相同
                      * 5.判断 是否相同
                    */
            if (isSameNode(oldStartVnode, newStartVnode)) {
                patch(oldStartVnode, newStartVnode);//打补丁
                oldStartVnode = oldChildren[++oldStartIndex];
                newStartVnode = newChildren[++newStartIndex];
            } else if (isSameNode(oldEndVnode, newEndVnode)) {
                patch(oldEndVnode, newEndVnode);//打补丁
                oldEndVnode = oldChildren[--oldEndIndex];
                newEndVnode = newChildren[--newEndIndex];
            } else if (isSameNode(oldStartVnode, newEndVnode)) {
                patch(oldStartVnode, newEndVnode);
                parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSiblings);
                oldStartVnode = oldChildren[++oldStartIndex];
                newEndVnode = newChildren[--newEndIndex];
            } else if (isSameNode(oldEndVnode, newStartVnode)) {
                patch(oldEndVnode, newStartVnode);
                parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement);
                oldEndVnode = oldChildren[--oldEndIndex];
                newStartVnode = newChildren[++newStartIndex];
            } else {  //无规则对比  需要和老节点的key对比复用 -> 需要做一个映射表;  key->index
                let index = keyIndexMap[newStartVnode.key];
                if (index == null) {//没有复用的
                    parent.insertBefore(createDomElementFrom(newStartVnode), oldStartVnode.domElement);
                    newStartVnode = newChildren[++newStartIndex];
                } else {//有复用的
                    patch(oldChildren[index], newStartVnode);
                    parent.insertBefore(oldChildren[index].domElement, oldStartVnode.domElement);
                    newStartVnode = newChildren[++newStartIndex];
                    oldChildren[index] = undefined;
                }
            }
        }
    
        if (newStartIndex <= newEndIndex) {
            // 开始节点相同,多出结尾
            for (let index = newStartIndex; index <= newEndIndex; index++) {
                let beforeElement = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].domElement : null;
                // null插入 相当于appendChild
                parent.insertBefore(createDomElementFrom(newChildren[index]), beforeElement);
            }
        } else if (oldStartIndex <= oldEndIndex) {
            // 老节点比较长,保留了以前的节点-需要删除
            for (let index = oldStartIndex; index <= oldEndIndex; index++) {
                let element = oldChildren[index]
                if (element) {
                    parent.removeChild(element.domElement);
                }
            }
        }
    }
    
    
    let oldVnode = createElement("div", { className: "xxx" },
                                 createElement("li", { key: "A" }, "A"),
                                 createElement("li", { key: "B" }, "B"),
                                 createElement("li", { key: "C" }, "C"),
                                 createElement("li", { key: "D" }, "D")
                                );
    render(oldVnode, document.querySelector("#app"))
    let newVnode = createElement("div", {},
                                 createElement("li", { key: "A" }, "A"),
                                 createElement("li", { key: "B" }, "B"),
                                 createElement("li", { key: "C" }, "C"),
                                 createElement("li", { key: "D", id: "ID" }, "D"),
                                 createElement("li", { key: "E" }, "E"),
                                );
    setTimeout(() => {
        patch(oldVnode, newVnode)
    }, 2000);
    

代码实现完,总结实现的步数就4步;

  1. 创建虚拟dom.
  2. 虚拟dom挂载到真实dom.
  3. 对比虚拟dom的差异(diff).
  4. 通过patch对dom的更新打补丁.

本文链接: http://www.dtmao.cc/news_show_400444.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?