理解Proxy及使用Proxy实现vue数据双向绑定操作


Posted in Javascript onJuly 18, 2020

1.什么是Proxy?它的作用是?

据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。

如果对vue2.xx了解或看过源码的人都知道,vue2.xx中使用 Object.defineProperty()方法对该对象通过 递归+遍历的方式来实现对数据的监控的,具体了解

Object.defineProperty可以看我上一篇文章(https://3water.com/article/191097.htm). 但是通过上一篇Object.defineProperty文章 我们也知道,当我们使用数组的方法或改变数组的下标是不能重新触发 Object.defineProperty中的set()方法的,因此就做不到实时响应了。所以使用 Object.defineProperty 存在如下缺点:

1. 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法)。

2. 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。

因此vue3.xx中之后就改用Proxy来更好的解决如上面的问题。在学习使用Proxy实现数据双向绑定之前,我们还是一步步来,先学习了Proxy基本知识点。

Proxy基本语法

const obj = new Proxy(target, handler);

参数说明如下:

target: 被代理对象。

handler: 是一个对象,声明了代理target的一些操作。

obj: 是被代理完成之后返回的对象。

但是当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:

1. get(target, propKey, receiver)

2. set(target, propKey, value, receiver)

3. has(target, propKey)

4. construct(target, args):

5. apply(target, object, args)

如上是Proxy中handler 对象的方法,其实它和Reflect里面的方法类似的,想要了解Reflect看这篇文章

如下代码演示:

const target = {
 name: 'kongzhi'
};

const handler = {
 get: function(target, key) {
 console.log(`${key} 被读取`);
 return target[key];
 },
 set: function(target, key, value) {
 console.log(`${key} 被设置为 ${value}`);
 target[key] = value;
 }
};

const testObj = new Proxy(target, handler);

/*
 获取testObj中name属性值
 会自动执行 get函数后 打印信息:name 被读取 及输出名字 kongzhi
*/
console.log(testObj.name);

/*
 改变target中的name属性值
 打印信息如下: name 被设置为 111 
*/
testObj.name = 111;

console.log(target.name); // 输出 111

如上代码所示:也就是说 target是被代理的对象,handler是代理target的,那么handler上面有set和get方法,当每次打印target中的name属性值的时候会自动执行handler中get函数方法,当每次设置 target.name 属性值的时候,会自动调用 handler中的set方法,因此target对象对应的属性值会发生改变,同时改变后的 testObj对象也会发生改变。同理改变返回后 testObj对象中的属性也会改变原对象target的属性的,因为对象是引用类型的,是同一个引用的。如果这样还是不好理解的话,可以简单的看如下代码应该可以理解了:

const target = {
 name: 'kongzhi'
};

const testA = target;
testA.name = 'xxx';
console.log(testA.name); // 打印 xxx
console.log(target.name); // 打印 xxx

2.get(target, propKey, receiver)

该方法的含义是:用于拦截某个属性的读取操作。它有三个参数,如下解析:

target: 目标对象。

propKey: 目标对象的属性。

receiver: (可选),该参数为上下文this对象

如下代码演示:

const obj = {
 name: 'kongzhi'
};

const handler = {
 get: function(target, propKey) {
 // 使用 Reflect来判断该目标对象是否有该属性
 if (Reflect.has(target, propKey)) {
  // 使用Reflect 来读取该对象的属性
  return Reflect.get(target, propKey);
 } else {
  throw new ReferenceError('该目标对象没有该属性');
 }
 }
};

const testObj = new Proxy(obj, handler);
/* 
 Proxy中读取某个对象的属性值的话,
 就会使用get方法进行拦截,然后返回该值。
 */
console.log(testObj.name); // kongzhi

/*
 如果对象没有该属性的话,就会进入else语句,就会报错:
 Uncaught ReferenceError: 该目标对象没有该属性
*/
// console.log(testObj.name2);

/*
 其实Proxy中拦截的操作是在原型上的,因此我们也可以使用 Object.create(obj)
 来实现对象的继承的。
 如下代码演示:
*/
const testObj2 = Object.create(testObj);
console.log(testObj2.name);

// 看看他们的原型是否相等 
console.log(testObj2.__proto__ === testObj.__proto__); // 返回true

如果没有这个拦截的话,如果某个对象没有该属性的话,会输出 undefined.

3.set(target, propKey, value, receiver)

该方法是用来拦截某个属性的赋值操作,它可以接受四个参数,参数解析分别如下:

target: 目标对象。

propKey: 目标对象的属性名

value: 属性值

receiver(可选): 一般情况下是Proxy实列

如下代码演示:

const obj = {
 'name': 'kongzhi'
};

const handler = {
 set: function(obj, prop, value) {
 return Reflect.set(obj, prop, value);
 }
};

const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 输出: 我是空智
console.log(obj); // 输出: {name: '我是空智'}

当然如果设置该对象的属性是不可写的,那么set方法就不起作用了,如下代码演示:

const obj = {
 'name': 'kongzhi'
};

Object.defineProperty(obj, 'name', {
 writable: false
});

const handler = {
 set: function(obj, prop, value, receiver) {
 Reflect.set(obj, prop, value);
 }
};

const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 打印的是 kongzhi

注意:proxy对数组也是可以监听的;如下代码演示,数组中的 push方法监听:

const obj = [{
 'name': 'kongzhi'
}];

const handler = {
 set: function(obj, prop, value) {
 return Reflect.set(obj, prop, value);
 }
};

const proxy = new Proxy(obj, handler);
proxy.push({'name': 'kongzhi222'});
proxy.forEach(function(item) {
 console.log(item.name); // 打印出 kongzhi kongzhi222
});

4.has(target, propKey)

该方法是判断某个目标对象是否有该属性名。接收二个参数,分别为目标对象和属性名。返回的是一个布尔型。

如下代码演示:

const obj = {
 'name': 'kongzhi'
};

const handler = {
 has: function(target, key) {
 if (Reflect.has(target, key)) {
  return true;
 } else {
  return false;
 }
 }
};

const proxy = new Proxy(obj, handler);
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false

5.construct(target, args, newTarget):

该方法是用来拦截new命令的,它接收三个参数,分别为 目标对象,构造函数的参数对象及创造实列的对象。

第三个参数是可选的。它的作用是拦截对象属性。

如下代码演示:

function A(name) {
 this.name = name;
}

const handler = {
 construct: function(target, args, newTarget) {
 /*
  输出: function A(name) {
    this.name = name;
   }
 */
 console.log(target); 
 // 输出: ['kongzhi', {age: 30}]
 console.log(args); 
 return args
 }
};

const Test = new Proxy(A, handler);
const obj = new Test('kongzhi', {age: 30});
console.log(obj); // 输出: ['kongzhi', {age: 30}]

6.apply(target, object, args)

该方法是拦截函数的调用的。该方法接收三个参数,分别是目标对象。目标对象上下文this对象 和 目标对象的数组;它和 Reflect.apply参数是一样的,了解 Reflect.apply(https://3water.com/article/191099.htm).

使用demo如下演示:

function testA(p1, p2) {
 return p1 + p2;
}
const handler = {
 apply: function(target, ctx, args) {
 /*
  这里的 ...arguments 其实就是上面的三个参数 target, ctx, args 对应的值。
  分别为:
  target: function testA(p1, p2) {
  return p1 + p2;
  }
  ctx: undefined
  args: [1, 2]
  使用 Reflect.apply(...arguments) 调用testA函数,因此返回 (1+2) * 2 = 6
 */
 console.log(...arguments);
 return Reflect.apply(...arguments) * 2;
 }
}
const proxy = new Proxy(testA, handler);
console.log(proxy(1, 2)); // 6

// 也可以如下调用
console.log(proxy.apply(null, [1, 3])); // 8
// 我们也可以使用 Reflect.apply 调用
console.log(Reflect.apply(proxy, null, [3, 5])); // 16

7.使用Proxy实现简单的vue双向绑定

vue3.x使用了Proxy来对数据进行监听了,因此我们来简单的来学习下使用Proxy来实现一个简单的vue双向绑定。

我们都知道实现数据双向绑定,需要实现如下几点:

1. 需要实现一个数据监听器 Observer, 能够对所有数据进行监听,如果有数据变动的话,拿到最新的值并通知订阅者Watcher.

2. 需要实现一个指令解析器Compile,它能够对每个元素的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的函数。

3. 需要实现一个Watcher, 它是链接Observer和Compile的桥梁,它能够订阅并收到每个属性变动的通知,然后会执行指令绑定的相对应

的回调函数,从而更新视图。

下面是一个简单的demo源码如下(我们可以参考下,理解下原理):

<!DOCTYPE html>
 <html>
 <head>
  <meta charset="utf-8">
  <title>标题</title>
 </head>
 <body>
  <div id="app">
  <input type="text" v-model='count' />
  <input type="button" value="增加" @click="add" />
  <input type="button" value="减少" @click="reduce" />
  <div v-bind="count"></div>
  </div>
  <script type="text/javascript"> 
  class Vue {
   constructor(options) {
   this.$el = document.querySelector(options.el);
   this.$methods = options.methods;
   this._binding = {};
   this._observer(options.data);
   this._compile(this.$el);
   }
   _pushWatcher(watcher) {
   if (!this._binding[watcher.key]) {
    this._binding[watcher.key] = [];
   }
   this._binding[watcher.key].push(watcher);
   }
   /*
   observer的作用是能够对所有的数据进行监听操作,通过使用Proxy对象
   中的set方法来监听,如有发生变动就会拿到最新值通知订阅者。
   */
   _observer(datas) {
   const me = this;
   const handler = {
    set(target, key, value) {
    const rets = Reflect.set(target, key, value);
    me._binding[key].map(item => {
     item.update();
    });
    return rets;
    }
   };
   this.$data = new Proxy(datas, handler);
   }
   /*
   指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的更新函数
   */
   _compile(root) {
   const nodes = Array.prototype.slice.call(root.children);
   const data = this.$data;
   nodes.map(node => {
    if (node.children && node.children.length) {
    this._compile(node.children);
    }
    const $input = node.tagName.toLocaleUpperCase() === "INPUT";
    const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
    const $vmodel = node.hasAttribute('v-model');
    // 如果是input框 或 textarea 的话,并且带有 v-model 属性的
    if (($vmodel && $input) || ($vmodel && $textarea)) {
    const key = node.getAttribute('v-model');
    this._pushWatcher(new Watcher(node, 'value', data, key));
    node.addEventListener('input', () => {
     data[key] = node.value;
    });
    }
    if (node.hasAttribute('v-bind')) {
    const key = node.getAttribute('v-bind');
    this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
    }
    if (node.hasAttribute('@click')) {
    const methodName = node.getAttribute('@click');
    const method = this.$methods[methodName].bind(data);
    node.addEventListener('click', method);
    }
   });
   }
  }
  /*
   watcher的作用是 链接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,
   执行指令绑定的响应的回调函数,从而更新视图。
  */
  class Watcher {
   constructor(node, attr, data, key) {
   this.node = node;
   this.attr = attr;
   this.data = data;
   this.key = key;
   }
   update() {
   this.node[this.attr] = this.data[this.key];
   }
  }
  </script>
  <script type="text/javascript">
  new Vue({
   el: '#app',
   data: {
   count: 0
   },
   methods: {
   add() {
    this.count++;
   },
   reduce() {
    this.count--;
   }
   }
  });
  </script>
 </body>
</html>

如上代码我们来分析下原理如下:

首先他是使用ES6编写的语法来实现的。首先我们想实现类似vue那要的初始化代码,如下这样设想:

new Vue({
 el: '#app',
 data: {
 count: 0
 },
 methods: {
 add() {
  this.count++;
 },
 reduce() {
  this.count--;
 }
 }
});

因此使用ES6 基本语法如下:

class Vue {
 constructor(options) {
 this.$el = document.querySelector(options.el);
 this.$methods = options.methods;
 this._binding = {};
 this._observer(options.data);
 this._compile(this.$el);
 }
}

Vue类使用new创建一个实例化的时候,就会执行 constructor方法代码,因此options是vue传入的一个对象,它有 el,data, methods等属性。 如上代码先执行 this._observer(options.data); 该 observer 函数就是监听所有数据的变动函数。基本代码如下:

1. 实现Observer对所有的数据进行监听。

_observer(datas) {
 const me = this;
 const handler = {
 set(target, key, value) {
  const rets = Reflect.set(target, key, value);
  me._binding[key].map(item => {
  item.update();
  });
  return rets;
 }
 };
 this.$data = new Proxy(datas, handler);
}

使用了我们上面介绍的Proxy中的set方法对所有的数据进行监听,只要我们Vue实列属性data中有任何数据发生改变的话,都会自动调用Proxy中的set方法,我们上面的代码使用了 const rets = Reflect.set(target, key, value); return rets; 这样的代码,就是对我们的data中的任何数据发生改变后,使用该方法重新设置新值,然后返回给 this.$data保存到这个全局里面。

me._binding[key].map(item => {
 item.update();
});

如上this._binding 是一个对象,对象里面保存了所有的指令及对应函数,如果发生改变,拿到最新值通知订阅者,因此通知Watcher类中的update方法,如下Watcher类代码如下:

/*
 watcher的作用是 链接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,
 执行指令绑定的响应的回调函数,从而更新视图。
*/
class Watcher {
 constructor(node, attr, data, key) {
 this.node = node;
 this.attr = attr;
 this.data = data;
 this.key = key;
 }
 update() {
 this.node[this.attr] = this.data[this.key];
 }
}

2. 实现Compile

如下代码初始化

class Vue {
 constructor(options) {
 this.$el = document.querySelector(options.el);
 this._compile(this.$el);
 }
}

_compile 函数的作用就是对页面中每个元素节点的指令进行解析和扫描的,根据指令模板替换数据,以及绑定相应的更新函数。

代码如下:

_compile(root) {
 const nodes = Array.prototype.slice.call(root.children);
 const data = this.$data;
 nodes.map(node => {
  if (node.children && node.children.length) {
  this._compile(node.children);
  }
  const $input = node.tagName.toLocaleUpperCase() === "INPUT";
  const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
  const $vmodel = node.hasAttribute('v-model');
  // 如果是input框 或 textarea 的话,并且带有 v-model 属性的
  if (($vmodel && $input) || ($vmodel && $textarea)) {
  const key = node.getAttribute('v-model');
  this._pushWatcher(new Watcher(node, 'value', data, key));
  node.addEventListener('input', () => {
   data[key] = node.value;
  });
  }
  if (node.hasAttribute('v-bind')) {
  const key = node.getAttribute('v-bind');
  this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
  }
  if (node.hasAttribute('@click')) {
  const methodName = node.getAttribute('@click');
  const method = this.$methods[methodName].bind(data);
  node.addEventListener('click', method);
  }
 });
 }
}

如上代码,

1. 拿到根元素的子节点,然后让子元素变成数组的形式,如代码:

const nodes = Array.prototype.slice.call(root.children);

2. 保存变动后的 this.$data, 如下代码:

const data = this.$data;

3. nodes子节点进行遍历,如果改子节点还有子节点的话,就会递归调用 _compile方法,如下代码:

nodes.map(node => {
 if (node.children && node.children.length) {
 this._compile(node.children);
 }
});

4. 对子节点进行判断,如果子节点是input元素或textarea元素的话,并且有 v-model这样的指令的话,如下代码:

nodes.map(node => {
 const $input = node.tagName.toLocaleUpperCase() === "INPUT";
 const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
 const $vmodel = node.hasAttribute('v-model');
 // 如果是input框 或 textarea 的话,并且带有 v-model 属性的
 if (($vmodel && $input) || ($vmodel && $textarea)) {
 const key = node.getAttribute('v-model');
 this._pushWatcher(new Watcher(node, 'value', data, key));
 node.addEventListener('input', () => {
  data[key] = node.value;
 });
 }
});

如上代码,如果有 v-model,就获取v-model该属性值,如代码:

const key = node.getAttribute('v-model');

然后把该指令通知订阅者 Watcher; 如下代码:

this._pushWatcher(new Watcher(node, 'value', data, key));

就会调用 Watcher类的constructor的方法,如下代码:

class Watcher {
 constructor(node, attr, data, key) {
 this.node = node;
 this.attr = attr;
 this.data = data;
 this.key = key;
 }
}

把 node节点,attr属性,data数据,v-model指令key保存到this对象中了。然后调用 this._pushWatcher(watcher); 这样方法。

_pushWatcher代码如下:

if (!this._binding[watcher.key]) {
 this._binding[watcher.key] = [];
}
this._binding[watcher.key].push(watcher);

如上代码,先判断 this._binding 有没有 v-model指令中的key, 如果没有的话,就把该 this._binding[key] = []; 设置成空数组。然后就把它存入 this._binding[key] 数组里面去。

5. 对于 input 或 textarea 这样的 v-model 会绑定相对应的函数,如下代码:

node.addEventListener('input', () => {
 data[key] = node.value;
});

当input或textarea有值发生改变的话,那么就把最新的值存入 Vue类中的data对象里面去,因此data中的数据会发生改变,因此会自动触发执行 _observer 函数中的Proxy中的set方法函数,还是一样,首先更新最新值,使用代码:

const rets = Reflect.set(target, key, value);

然后遍历 保存到 this._binding 对象中对应的键;如下代码:

me._binding[key].map(item => {
 console.log(item);
 item.update();
});

如上,我们在input输入框输入1的时候,打印item值如下所示:

理解Proxy及使用Proxy实现vue数据双向绑定操作

然后执行 item.update()方法,update方法如下:

class Watcher {
 update() {
 this.node[this.attr] = this.data[this.key];
 }
}

就会更新值到视图里面去,比如input或textarea, 那么 attr = 'value', node 是该元素的节点,key 就是 v-model中的属性值,因此 this.node['value'] = this.data[key];

然后同时代码中如果有 v-bind这样的指令的话,也会和上面的逻辑一样判断和执行;如下 v-bind指令代码如下:

if (node.hasAttribute('v-bind')) {
 const key = node.getAttribute('v-bind');
 this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
}

然后也会更新到视图里面去,那么 attr = 'innerHTML', node 是该元素的节点,key 也是 v-model中的属性值了,因此 this.node.innerHTML = thid.data['key'];

比如页面中html代码如下:

<div id="app"> <input type="text" v-model='count' /> <input type="button" value="增加" @click="add" /> <input type="button" value="减少" @click="reduce" /> <div v-bind="count"></div> </div>

实列化代码如下:

new Vue({
 el: '#app',
 data: {
 count: 0
 },
 methods: {
 add() {
  this.count++;
 },
 reduce() {
  this.count--;
 }
 }
});

因此上面的 node 是 <input type="text" v-model='count' /> input中的node节点了,因此 node.value = this.data['count']; 因此 input框的值就更新了,同时 <div v-bind="count"></div> 该节点通过 node.innerHTML = this.data['count'] 这样的话,值也得到了更新了。

6. 对于页面中元素节点带有 @click这样的方法,也有判断,如下代码:

if (node.hasAttribute('@click')) {
 const methodName = node.getAttribute('@click');
 const method = this.$methods[methodName].bind(data);
 node.addEventListener('click', method);
}

如上代码先判断该node是否有该属性,然后获取该属性的值,比如html页面中有 @click="add" 和 @click="reduce" 这样的,当点击的时候,也会调用 this.methods[methodName].bind(data)中对应vue实列中对应的函数的。因此也会执行函数的,其中data就是this.data,监听该对象的值发生改变的话,同样会调用 Proxy中的set函数,最后也是一样执行函数去更新视图的。如上就是使用proxy实现数据双向绑定的基本原理的。

以上这篇理解Proxy及使用Proxy实现vue数据双向绑定操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 容错处理代码, 屏蔽错误信息
Mar 09 Javascript
发布一个高效的JavaScript分析、压缩工具 JavaScript Analyser
Nov 30 Javascript
JavaScript脚本性能优化注意事项
Nov 18 Javascript
jquery blockUI 遮罩不能消失与不能提交的解决方法
Sep 17 Javascript
js获取下拉列表的值和元素个数示例
May 07 Javascript
移动手机APP手指滑动切换图片特效附源码下载
Nov 30 Javascript
使用jQuery+EasyUI实现CheckBoxTree的级联选中特效
Dec 06 Javascript
JavaScript 是什么意思
Sep 22 Javascript
微信小程序实现工作时间段选择
Feb 15 Javascript
Vue Object 的变化侦测实现代码
Apr 15 Javascript
Vue项目接入Paypal实现示例详解
Jun 04 Javascript
vue 判断两个时间插件结束时间必选大于开始时间的代码
Nov 04 Javascript
深入理解 ES6中的 Reflect用法
Jul 18 #Javascript
详谈Object.defineProperty 及实现数据双向绑定
Jul 18 #Javascript
webpack+vue-cil 中proxyTable配置接口地址代理操作
Jul 18 #Javascript
Vue项目前后端联调(使用proxyTable实现跨域方式)
Jul 18 #Javascript
完美解决通过IP地址访问VUE项目的问题
Jul 18 #Javascript
Vue移动端项目实现使用手机预览调试操作
Jul 18 #Javascript
vue中移动端调取本地的复制的文本方式
Jul 18 #Javascript
You might like
Symfony学习十分钟入门经典教程
2016/02/03 PHP
Gird组件 Part-3:范例RSSFeed Viewer
2007/03/10 Javascript
用js实现trim()的解决办法
2013/04/16 Javascript
Node.js开发指南中的简单实例(mysql版)
2013/09/17 Javascript
ExtJS自定义主题(theme)样式详解
2013/11/18 Javascript
jQuery实现统计复选框选中数量
2014/11/24 Javascript
javascript二维数组转置实例
2015/01/22 Javascript
JavaScript中isPrototypeOf函数作用和使用实例
2015/06/01 Javascript
jQuery EasyUI Pagination实现分页的常用方法
2016/05/21 Javascript
浅析JS中对函数function的理解(基础篇)
2016/10/14 Javascript
react.js 翻页插件实例代码
2017/01/19 Javascript
详解Javascript几种跨域方式总结
2017/02/27 Javascript
Bootstrap导航简单实现代码
2017/03/06 Javascript
vue微信分享的实现(在当前页面分享其他页面)
2019/04/16 Javascript
11个教程中不常被提及的JavaScript小技巧(推荐)
2019/04/17 Javascript
详解Vue调用手机相机和相册以及上传
2019/05/05 Javascript
微信小程序实现消息框弹出动画
2020/04/18 Javascript
node.js通过Sequelize 连接MySQL的方法
2020/12/28 Javascript
[01:00:04]DOTA2上海特级锦标赛B组小组赛#1 Alliance VS Spirit第二局
2016/02/26 DOTA
Python实现将n个点均匀地分布在球面上的方法
2015/03/12 Python
在Python中实现贪婪排名算法的教程
2015/04/17 Python
python中异常报错处理方法汇总
2016/11/20 Python
Python之Scrapy爬虫框架安装及简单使用详解
2017/12/22 Python
Apache部署Django项目图文详解
2019/07/30 Python
python 读取更新中的log 或其它文本方式
2019/12/24 Python
检测浏览器对HTML5和CSS3支持度的方法
2015/06/25 HTML / CSS
h5网页水印SDK的实现代码示例
2019/02/19 HTML / CSS
介绍一下木马病毒的种类
2015/07/26 面试题
你们项目是如何进行变更控制的
2015/08/26 面试题
汽车维修与检测专业应届生求职信
2013/11/12 职场文书
研修第一天随笔感言
2014/02/15 职场文书
支部书记四风问题对照检查材料
2014/10/04 职场文书
党委书记群众路线对照检查材料思想汇报
2014/10/04 职场文书
钱塘江大潮导游词
2015/02/03 职场文书
2016年党员学习廉政准则心得体会
2016/01/20 职场文书
Java时间工具类Date的常用处理方法
2022/05/25 Java/Android