vue中MVVM原理及其实现

一. 什么是mvvm

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

clipboard.png

要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:

clipboard.png

1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
2.实现Observe,监听所有的数据,并对变化数据发布通知;
3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
4.结合上述方法,向外暴露mvvm方法。

二. 实现方法

首先编辑一个html文件,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MVVM原理及其实现</title>
</head>
<body>
<div id="app">
  <input type="text" v-model="message">
  <div>{{message}}</div>
  <ul><li></li></ul>
</div>
<script src="watcher.js"></script>
<script src="observe.js"></script>
<script src="compile.js"></script>
<script src="mvvm.js"></script>
<script>
  let vm = new MVVM({
    el: '#app',
    data: {
      message: 'hello world',
      a: {
        b: 'bbb'
      }
    }
  })
</script>
</body>
</html>

1.实现一个mvvm类(入口)

新建一个mvvm.js,将参数通过options传入mvvm中,并取出el和data绑定到mvvm的私有变量$el和$data中。

// mvvm.js
class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
  }
}

2.实现compile(编译模板)

新建一个compile.js文件,在mvvm.js中调用compile。compile.js接收mvvm中传过来的el和vm实例。

// mvvm.js
class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    // 如果有要编译的模板 =>编译
    if(this.$el) {
      // 将文本+元素模板进行编译
      new Compile(this.$el, this)
    }
  }
}

(1)初始化传值

// compile.js
export default class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
  },
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
}

(2)先把真实DOM移入到内存中 fragment,因为fragment在内存中,操作比较快

// compile.js
class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
    // 如果这个元素能获取到 我们才开始编译
    if(this.el) {
      // 1. 先把真实DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el)
    }
  },
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 将el中的内容全部放到内存中
  node2fragment(el) { 
    let fragment = document.createDocumentFragment()
    let firstChild
    // 遍历取出firstChild,直到firstChild为空
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment // 内存中的节点
  }
}

(3)编译 =》 在fragment中提取想要的元素节点 v-model 和文本节点


// compile.js
class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
    // 如果这个元素能获取到 我们才开始编译
    if(this.el) {
      // 1. 先把真实DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el)
      // 2. 编译 =》 在fragment中提取想要的元素节点 v-model 和文本节点
      this.compile(fragment)
      // 3. 把编译好的fragment在放回到页面中
      this.el.appendChild(fragment)
    }
  }
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 是不是指令
  isDirective(name) {
    return name.includes('v-')
  }
  // 将el中的内容全部放到内存中
  node2fragment(el) {
    let fragment = document.createDocumentFragment()
    let firstChild
    // 遍历取出firstChild,直到firstChild为空
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment // 内存中的节点
  }
  //编译 =》 提取想要的元素节点 v-model 和文本节点
  compile(fragment) {
    // 需要递归
    let childNodes = fragment.childNodes
    Array.from(childNodes).forEach(node => {
      // 是元素节点 直接调用文本编译方法 还需要深入递归检查
      if(this.isElementNode(node)) {
        this.compileElement(node)
        // 递归深入查找子节点
        this.compile(node)
      // 是文本节点 直接调用文本编译方法
      } else {
        this.compileText(node)
      }
    })
  }
  // 编译元素方法
  compileElement(node) {
    let attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      let attrName = attr.name
      // 判断属性名是否包含 v-指令
      if(this.isDirective(attrName)) {
        // 取到v-指令属性中的值(这个就是对应data中的key)
        let expr = attr.value
        // 获取指令类型
        let [,type] = attrName.split('-')
        // node vm.$data expr
        compileUtil[type](node, this.vm, expr)
      }
    })
  }
  // 这里需要编译文本
  compileText(node) {
    //取文本节点中的文本
    let expr = node.textContent
    let reg = /\{\{([^}]+)\}\}/g
    if(reg.test(expr)) {
      // node this.vm.$data text
      compileUtil['text'](node, this.vm, expr)
    }
  }
}
// 解析不同指令或者文本编译集合
const compileUtil = {
  text(node, vm, expr) { // 文本
    let updater = this.updater['textUpdate']
    updater && updater(node, getTextValue(vm, expr))
  },
  model(node, vm, expr){ // 输入框
    let updater = this.updater['modelUpdate']
    updater && updater(node, getValue(vm, expr))
  },
  // 更新函数
  updater: {
    // 文本赋值
    textUpdate(node, value) {
      node.textContent = value
    },
    // 输入框value赋值
    modelUpdate(node, value) {
      node.value = value
    }
  }
}
// 辅助工具函数
// 绑定key上对应的值,从vm.$data中取到
const getValue = (vm, expr) => {
  expr = expr.split('.') // [message, a, b, c]
  return expr.reduce((prev, next) => {
    return prev[next]
  }, vm.$data)
}
// 获取文本编译后的对应的数据
const getTextValue = (vm, expr) => {
  return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
    return getValue(vm, arguments[1])
  })
}

(3) 将编译后的fragment放回到dom中

  let fragment = this.node2fragment(this.el)
  this.compile(fragment)
  // 3. 把编译好的fragment在放回到页面中
  this.el.appendChild(fragment)

进行到这一步,页面上初始化应该渲染完成了。如下图:

clipboard.png

3.实现observe(数据监听/劫持)

不同于发布者-订阅者模式和脏值检测,vue采用的observe + sub/pub 实现数据的劫持,通过js原生的方法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据改变时,发布消息给订阅者,然后触发相应的监听回调。
主要内容:observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。

// observe.js
class Observe {
  constructor(data) {
    this.observe(data)
  }
  // 把data数据原有的属性改成 get 和 set方法的形式
  observe(data) {
    if(!data || typeof data!== 'object') {
      return
    }
    console.log(data)
    // 将数据一一劫持
    // 先获取到data的key和value
    Object.keys(data).forEach((key) => {
      // 数据劫持
      this.defineReactive(data, key, data[key])
      this.observe(data[key]) // 深度递归劫持,保证子属性的值也会被劫持
    })
  }
  // 定义响应式
  defineReactive(obj, key, value) {
    let _this = this
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() { // 当取值时调用
        return value
      },
      set(newValue) { //当data属性中设置新值得时候 更改获取的新值
        if(newValue !== value) {
          _this.observe(newValue) // 如果是对象继续劫持
          console.log('监听到值变化了,旧值:', value, ' --> 新值:', newValue);
          value = newValue
        }
      }
    })
  }
}

完成observe.js后,修改mvvm.js文件,将属性传入observe中

// mvvm.js
class MVVM {
  constructor(options) {
    console.log(options)
    this.$el = options.el
    this.$data = options.data
    // 如果有要编译的模板 =》编译
    if(this.$el) {
      // 数据劫持 就是把对象的所有属性改成 get 和 set方法
      new Observe(this.$data)
      // 将文本+元素模板进行编译
      new Compile(this.$el, this)
    }
  }
}

可以在控制台查看到以下信息,说明劫持属性成功。

clipboard.png

实现数据劫持后,接下来的任务怎么通知订阅者了,我们需要在监听数据时实现一个消息订阅器,具体的方法是:定义一个数组,用来存放订阅者,数据变动通知(notify)订阅者,再调用订阅者的update方法。
在observe.js添加Dep类:

//observe.js

// ...
    let _this = this
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() { // 当取值时调用
        return value
      },
      set(newValue) { //当data属性中设置新值得时候 更改获取的新值
        if(newValue !== value) {
          _this.observe(newValue) // 如果是对象继续劫持
          console.log('监听到值变化了,旧值:', value, ' --> 新值:', newValue);
          value = newValue
          dep.notify() //通知所有人 数据更新了
        }
      }
    })
// ...
// 消息订阅器Dep()
class Dep {
  constructor() {
    // 订阅的数组
    this.subs = []
  }
  addSub(watcher) {
    // push到订阅数组
    this.subs.push(watcher)
  }
  notify() {
    // 通知订阅者,并执行订阅者的update回调
    this.subs.forEach(watcher => watcher.update())
  }
}

实现了消息订阅器,并且能够执行订阅者的回调,那么订阅者怎么获取,并push到订阅器数组中呢?这个要和watcher结合。

4.实现watcher(订阅中心)

Observer和Compile之间通信的桥梁是Watcher订阅中心,其主要职责是:
1、在自身实例化时往属性订阅器(Dep)里面添加自己,与Observer建立连接;
2、自身必须有一个update()方法,与Compile建立连接;
3、当属性变化时,Observer中dep.notice()通知,然后能调用自身(Watcher)的update()方法,并触发Compile中绑定的回调,实现更新。

// watcher.js
// 订阅中心(观察者): 给需要变化的那个元素 增加一个观察者, 当数据变化后,执行对应的方法
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 先获取一下老值
    this.value = this.get()
  }
  getValue(vm, expr) { // 获取实例上对应的数据
    expr = expr.split('.') // [message, a, b, c]
    return expr.reduce((prev, next) => {
      return prev[next]
    }, vm.$data)
  }
  get() { // 获取文本编译后的对应的数据
    // 获取当前订阅者
    Dep.target = this
    // 触发getter,当前订阅者添加订阅器中 在 劫持数据时,将订阅者放到订阅者数组
    let value = this.getValue(this.vm, this.expr)
    // 重置订阅者
    Dep.target = null
    return value
  }
  // 对外暴露的方法
  update() {
    let newValue = this.getValue(this.vm, this.expr)
    let oldValue = this.value
    // 更新的值 与 以前的值 进行比对, 如果发生变化就更新方法
    if(newValue !== oldValue) {
      this.cb(newValue)
    }
  }
}

// observe.js 
// ... 省略
Object.defineProperty(data, key, {
    get: function() {
        // 在取值时将订阅者push入订阅者数组
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
// ... 省略

上面步骤搭建了watcher与observe之间的连接,还需要搭建watcher与之间的连接。
我们需要在compile中解析不同指令或者文本编译集合的时候绑定watcher.

// compile.js
// ...省略
 model(node, vm, expr){ // 输入框
    let updater = this.updater['modelUpdate']
    // 这里加一个监控 数据变化了 应该调用这个watcher的callback
    new Watcher(vm, expr, (newValue) => {
      // 当值变化后 会调用cb ,将新值传递过来
      updater && updater(node, this.getValue(vm, expr))
    })
    node.addEventListener('input', (e) => {
      let newValue = e.target.value
      this.setVal(vm, expr, newValue)
    })
    updater && updater(node, this.getValue(vm, expr))
 },
// ...省略

此时,在浏览器控制台执行下图操作,手动改变 message 属性的值,发现输入框的值也随之变化,v-model 绑定完成。

clipboard.png

weixin_33979363
关注 关注
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
mvvm:剖析vue实现原理,简单实现mvvm
01-31
剖析Vue实现原理-如何实现双向绑定mvvm 将项目改造成基于webpack的实现 使用webpack打包 # npm install webpack webpack-dev-server webpack-merge --save-dev # npm run build ♡babel装载机 安装 # npm install babel-core babel-loader babel-preset-env --save-dev 配置 { " presets " : [ [ " env " , { " modules " : false , " targets " : { " browsers " : [ " > 1% " , " last 2 versions " , " not ie <= 8 " ] } }] ] } 用法 # git clone https://github.com/liyanlong/mvvm # cd mvvm # npm install # npm run dev 当前对象 观察 深度
虚拟DOM & DIff算法
Wmt3344的博客
08-26 495
一、真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting 第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。 第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。 第三步,将DOM树和样式表,关...
vue mvvm 原理
weixin_45677987的博客
02-19 795
1: M ; Modle: 模型, view: 视图。 VM: 代表的是,相当于MVC控制器。 数据的变化影响了视图。 视图的变化影响数据。 2: MVVM: 原理配合数据劫持: objectDefiniproperty: 通过set 方法和get 方法劫持。 当我们数据发生变化的时候。 通知我们的视图进行更新。 实现了那些东西: objectserver() watcher() 方法: React: 框架实现了典型数据单向流, vue 框架实现了数据双向流。 双...
Vue学习】Vue原理MVVM模式
zx1041561837的博客
02-27 1504
本文介绍了Vue框架的原理。具体包括:1. 介绍了MVVM模式,2. Vue响应式机制,3. Vue监听对象的过程,4. Vue监听数组的实现,5. 使用Object.defineProperty监听数据存在的缺点。
详解VueMVVM原理实现方法
01-19
下面由我阿巴阿巴的详细走一遍VueMVVM原理实现,这篇文章大家可以学习到: 1.Vue数据双向绑定核心代码模块以及实现原理 2.订阅者-发布者模式是如何做到让数据驱动视图、视图驱动数据再驱动视图 3.如何对元素...
浅析vueMVVM实现原理
10-17
主要介绍了浅析vueMVVM实现原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
手写实现vuemvvm原理
08-14
手写实现vuemvvm原理
VueMVVM实现原理
z2428478096的博客
11-21 1232
vue是采用数据劫持配合发布者-订阅者模式的方式,通过object.definerProperty()来劫持各个属性的setter和gettter,在数据变动时,发布消息给依赖收集器,去通知观察者,做出对应的回调函数,去更新视图。 MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化表,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通信桥梁,达到...
VueMVVM原理解析和实现
leeta的博客
08-12 1082
下面由我阿巴阿巴的详细走一遍VueMVVM原理实现,这篇文章大家可以学习到: 1.Vue数据双向绑定核心代码模块以及实现原理 2.订阅者-发布者模式是如何做到让数据驱动视图、视图驱动数据再驱动视图 3.如何对元素节点上的指令进行解析并且关联订阅者实现视图更新 1、思路整理 实现的流程图: 我们要实现一个类MVVM简单版本的Vue框架,就需要实现以下几点: 1、实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者。 2、.
VueMVVM原理及其实现
Static_HackSun的博客
01-03 2210
一. 什么是mvvm MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。 MVVM分为三个部分:分别是M(Model,模型层 ),V(View,视图层),VM(ViewModel,V与M连接的桥梁,也可以看作为控制器) 1、 M:模型层,主要负责业务数据相关; 2、 V:视图层,顾名思义,负责视图相关,细分下来就是html+css层; 3、 VM:V与M沟通的...
什么是MVVMvueMVVM原理
weixin_42901443的博客
05-03 422
Mvvm定义MVVM是Model-View-ViewModel的简写。即模型-视图-视图模型。【模型】指的是后端传递的数据。【视图】指的是所看到的页面。【视图模型】mvvm模式的核心,它是连接view和model的桥梁。
VueMVVM设计模式(第一个Vue程序)
liudachu的博客
09-21 734
二、第一个Vue程序 1. 什么是MVVM MVVM (Model-View-ViewModel) 是一种软件架构设计模式,由微软WPF (用于替代WinForm,以前就是用这个技术开发桌面应用程序的)和Silverlight (类似于Java Applet,简单点说就是在浏览器上运行的WPF)的架构师Ken Cooper和Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由John Gossman (同样也是WPF和Silverlight的架构师)于2005年在他的博客上发表。 MVV
VueMVVM模型
weixin_45331887的博客
04-29 2905
MVVM从字面意思来理解就是划分为 View model VIewModel 三个部分,分别表达了View(视图)、Model(数据)、ViewModel(负责两者之间的数据处理操作)。这就类似于Mvc框架的Model层、View层、congtroller层,MVVM的本质就是MVC的升级版,更好的应用于前端开发。
vue 简介 (MVVM介绍,超详细)
qq_61950936的博客
08-12 7835
vue 简介 (详细)。vue 是一套用于构建用户界面的前端框架。本文详细介绍,vue数据源,MVVM等等。
关于对VueMVVM的理解
weixin_45687036的博客
02-28 4326
关于对VueMVVM的理解
VueMVVM原理
qq_39540493的博客
02-09 668
MVC:传统的MVC是指用户的操作会调用服务器接口,服务器将处理结果返回给前端Model,然后页面进行重新渲染。 MVVMMVVM模型则是指ViewModel通过Data Bindings绑定数据来监听数据的变化,从而更新页面的DOM,ViewModel还通过DOM Listeners来监听DOM的变化从而去改变Model。 ...
Vuemvvm框架
m0_67736146的博客
05-08 1541
Vue框架MVVM是指“模型-视图-视图模型”
vuemvvm原理
最新发布
07-28
Vue,使用Vue实例作为ViewModel,通过数据绑定和指令等方式实现MVVM原理。具体流程如下: 1. 创建Vue实例,传入一个配置对象。配置对象包含了模板(template)、数据(data)、方法(methods)等属性。 2. ...

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
149
原创
195
点赞
1326
收藏
6335
粉丝
关注
私信
写文章

热门文章

  • Excel类似split功能 19507
  • 关于图片或者文件在数据库的存储方式归纳 16978
  • 开机出现USB Device over current status Detected ,15秒关闭 15472
  • javascript之面试题精讲 11299
  • stm32 HAL库 HAL_UART_Transmit_DMA 发送函数分析 10102

最新评论

  • 打开fiddler v5 chrome https链接不能打开问题

    林 在: 亲测无效,后者慎用

  • 解决双击excel文件打开多个excel.exe进程的问题

    weixin_44749037: 显示无法编辑

  • 使用VMware将Linux装在物理硬盘上,开机即可进入Linux

    铅笔日记: 现在连二维码都挂了

  • 关于图片或者文件在数据库的存储方式归纳

    MoLuLiRen: 帮助很大,感谢楼主。楼主大大加油

  • 关于图片或者文件在数据库的存储方式归纳

    weixin_45520366: 讲的真好,清晰明了通俗易懂

您愿意向朋友推荐“博客详情页”吗?

  • 强烈不推荐
  • 不推荐
  • 一般般
  • 推荐
  • 强烈推荐
提交

最新文章

  • matlab练习程序(图像球面化)
  • 带你手写基于 Spring 的可插拔式 RPC 框架(五)注册中心
  • Go语言开发环境配置
2019年383篇
2018年739篇
2017年897篇
2016年541篇
2015年391篇
2014年319篇
2013年292篇
2012年276篇
2011年204篇
2010年133篇
2009年108篇
2008年86篇
2007年76篇
2006年44篇
2005年23篇
2004年7篇

目录

目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值

深圳SEO优化公司清远SEO按效果付费南联品牌网站设计推荐南山网站搭建报价汕头关键词按天收费公司酒泉设计公司网站滨州网站优化塘坑网页制作公司汉中百搜标王价格深圳网站制作设计报价大连关键词排名推荐兴安盟网站搭建公司邢台网站制作推荐怒江百度网站优化排名公司石家庄设计网站报价上饶设计网站多少钱兴安盟网站建设设计蚌埠seo网站推广报价焦作网站搜索优化锦州百度标王公司濮阳企业网站设计价格廊坊百姓网标王推广价格茂名网站seo优化公司铜仁网页制作漳州网站关键词优化推荐玉林百度竞价临夏设计公司网站公司延边关键词按天计费多少钱聊城推广网站哪家好平凉企业网站改版价格乌海网络营销歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化