Vue内部渲染视图的方法


Posted in Javascript onSeptember 02, 2019

1.什么是虚拟DOM

  •  以前M的命令式操作DOM即使用jQuery操作DOM节点,随着状态的增多,DOM的操作就会越来越频繁,程序的状态也越难维护,现在主流的框架都是采用声明式操作DOM,将操作DOM的方法封装起来,我们只要更改数据的状态,框架本身会帮我们操作DOM。
  • 虚拟DOM根据状态建立一颗虚拟节点树,新的虚拟节点树会与旧的虚拟节点树进行对比,只渲染发生改变的部分,如下图:

Vue内部渲染视图的方法

2.引入虚拟DOM的目的

  •  把渲染过程抽象化,从而使得组件的抽象能力也得到提升,并且可以适配DOM以外的渲染目标;
  • 可以更好地支持SSR、同构渲染等;
  • 不再依赖HTML解析器进行模板解析,可以进行更多的AOT(预编译)工作提高运行时效率,还能将Vue运行时体积进一步压缩。

VNode的定义 Vue中定义了VNode的构造函数,这样我们可以实例化不同的vnode 实例如:文本节点、元素节点以及注释节点等。

var VNode = function VNode (
 tag,
 data,
 children,
 text,
 elm,
 context,
 componentOptions,
 asyncFactory
 ) {
 this.tag = tag;
 this.data = data;
 this.children = children;
 this.text = text;
 this.elm = elm;
 this.ns = undefined;
 this.context = context;
 this.fnContext = undefined;
 this.fnOptions = undefined;
 this.fnScopeId = undefined;
 this.key = data && data.key;
 this.componentOptions = componentOptions;
 this.componentInstance = undefined;
 this.parent = undefined;
 this.raw = false;
 this.isStatic = false;
 this.isRootInsert = true;
 this.isComment = false;
 this.isCloned = false;
 this.isOnce = false;
 this.asyncFactory = asyncFactory;
 this.asyncMeta = undefined;
 this.isAsyncPlaceholder = false;
 };

vnode其实就是一个描述节点的对象,描述如何创建真实的DOM节点;vnode的作用就是新旧vnode进行对比,只更新发生变化的节点。 VNode有注释节点、文本节点、元素节点、组件节点、函数式组件、克隆节点:

注释节点

var createEmptyVNode = function (text) {
 if ( text === void 0 ) text = '';
 var node = new VNode();
 node.text = text;
 node.isComment = true;
 return node
 };

只有isComment和text属性有效,其余的默认为false或者null

文本节点

function createTextVNode (val) {
 return new VNode(undefined, undefined, undefined, String(val))
 }

只有一个text属性

克隆节点

function cloneVNode (vnode) {
 var cloned = new VNode(
  vnode.tag,
  vnode.data,
  // #7975
  // clone children array to avoid mutating original in case of cloning
  // a child.
  vnode.children && vnode.children.slice(),
  vnode.text,
  vnode.elm,
  vnode.context,
  vnode.componentOptions,
  vnode.asyncFactory
 );
 cloned.ns = vnode.ns;
 cloned.isStatic = vnode.isStatic;
 cloned.key = vnode.key;
 cloned.isComment = vnode.isComment;
 cloned.fnContext = vnode.fnContext;
 cloned.fnOptions = vnode.fnOptions;
 cloned.fnScopeId = vnode.fnScopeId;
 cloned.asyncMeta = vnode.asyncMeta;
 cloned.isCloned = true;
 return cloned
 }

克隆节点将vnode的所有属性赋值到clone节点,并且设置isCloned = true,它的作用是优化静态节点和插槽节点。以静态节点为例,因为静态节点的内容是不会改变的,当它首次生成虚拟DOM节点后,再次更新时是不需要再次生成vnode,而是将原vnode克隆一份进行渲染,这样在一定程度上提升了性能。

元素节点 元素节点一般会存在tag、data、children、context四种有效属性,形如:

{
 children: [VNode, VNode],
 context: {...},
 tag: 'div',
 data: {attr: {id: app}}
}

组件节点 组件节点有两个特有属性 (1) componentOptions,组件节点的选项参数,包含如下内容:

{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }

(2) componentInstance: 组件的实例,也是Vue的实例 对应的vnode

new VNode(
  ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  data, undefined, undefined, undefined, context,
  { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  asyncFactory
 )

{
 componentOptions: {},
 componentInstance: {},
 tag: 'vue-component-1-child',
 data: {...},
 ...
}

函数式组件 函数组件通过createFunctionalComponent函数创建, 跟组件节点类似,暂时没看到特殊属性,有的话后续再补上。

patch

虚拟DOM最重要的功能是patch,将VNode渲染为真实的DOM。

patch简介

patch中文意思是打补丁,也就是在原有的基础上修改DOM节点,也可以说是渲染视图。DOM节点的修改有三种:

  • 创建新增节点
  • 删除废弃的节点
  • 修改需要更新的节点。

当缓存上一次的oldvnode与最新的vnode不一致的时候,渲染视图以vnode为准。

初次渲染过程

当oldvnode中不存在,而vnode中存在时,就需要使用vnode新生成真实的DOM节点并插入到视图中。首先如果vnode具有tag属性,则认为它是元素属性,再根据当前环境创建真实的元素节点,元素创建后将它插入到指定的父节点。以上节生成的VNode为例,首次执行

vm._update(vm._render(), hydrating);

vm._render()为上篇生成的VNode,_update函数具体为

Vue.prototype._update = function (vnode, hydrating) {
  var vm = this;
  var prevEl = vm.$el;
  var prevVnode = vm._vnode;
  var restoreActiveInstance = setActiveInstance(vm);
  // 缓存vnode
  vm._vnode = vnode;
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // 第一次渲染,preVnode是不存在的
  if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
  }
  restoreActiveInstance();
  // update __vue__ reference
  if (prevEl) {
  prevEl.__vue__ = null;
  }
  if (vm.$el) {
  vm.$el.__vue__ = vm;
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  vm.$parent.$el = vm.$el;
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
 };

因第一次渲染,执行 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); ,注意第一个参数是oldVnode为 vm.$el 为元素节点,__patch__函数具体过程为:

(1) 先判断oldVnode是否存在,不存在就创建vnode

if (isUndef(oldVnode)) {
 // empty mount (likely as component), create new root element
 isInitialPatch = true;
 createElm(vnode, insertedVnodeQueue);
}

(2) 存在进入else,判断oldVnode是否是元素节点,如果oldVnode是元素节点,则

if (isRealElement) {
 ...
 // either not server-rendered, or hydration failed.
 // create an empty node and replace it
 oldVnode = emptyNodeAt(oldVnode);
}

创建一个oldVnode节点,其形式为

{
 asyncFactory: undefined,
 asyncMeta: undefined,
 children: [],
 componentInstance: undefined,
 componentOptions: undefined,
 context: undefined,
 data: {},
 elm: div#app,
 fnContext: undefined,
 fnOptions: undefined,
 fnScopeId: undefined,
 isAsyncPlaceholder: false,
 isCloned: false,
 isComment: false,
 isOnce: false,
 isRootInsert: true,
 isStatic: false,
 key: undefined,
 ns: undefined,
 parent: undefined,
 raw: false,
 tag: "div",
 text: undefined,
 child: undefined
}

然后获取oldVnode的元素节点以及其父节点,并创建新的节点

// replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);

// create new node
createElm(
 vnode,
 insertedVnodeQueue,
 // extremely rare edge case: do not insert if old element is in a
 // leaving transition. Only happens when combining transition +
 // keep-alive + HOCs. (#4590)
 oldElm._leaveCb ? null : parentElm,
 nodeOps.nextSibling(oldElm)
);

创建新节点的过程

// 标记是否是根节点
 vnode.isRootInsert = !nested; // for transition enter check
 // 这个函数如果vnode有componentInstance属性,会创建子组件,后续具体介绍,否则不做处理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
 return
}

接着在对子节点处理

var data = vnode.data;
 var children = vnode.children;
 var tag = vnode.tag;
 if (isDef(tag)) {
 ...
 vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode);
 setScope(vnode);

 /* istanbul ignore if */
 {
  createChildren(vnode, children, insertedVnodeQueue);
  if (isDef(data)) {
   invokeCreateHooks(vnode, insertedVnodeQueue);
  }
  insert(parentElm, vnode.elm, refElm);
 }

 if (data && data.pre) {
  creatingElmInVPre--;
 }
 }
}

将vnode的属性设置为创建元素节点elem,创建子节点 createChildren(vnode, children, insertedVnodeQueue); 该函数遍历子节点children数组

function createChildren (vnode, children, insertedVnodeQueue) {
 if (Array.isArray(children)) {
  for (var i = 0; i < children.length; ++i) {
   createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
 } else if (isPrimitive(vnode.text)) {
  // 如果vnode是文本直接挂载
  nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
 }
}

遍历children,递归createElm方法创建子元素节点

else if (isTrue(vnode.isComment)) {
 vnode.elm = nodeOps.createComment(vnode.text);
 insert(parentElm, vnode.elm, refElm);
} else {
 vnode.elm = nodeOps.createTextNode(vnode.text);
 insert(parentElm, vnode.elm, refElm);
}

如果是评论节点,直接创建评论节点,并将其插入到父节点上,其他的创建文本节点,并将其插入到父节点parentElm(刚创建的div)上去。 触发钩子,更新节点属性,将其插入到parentElm('#app'元素节点)上

{
 createChildren(vnode, children, insertedVnodeQueue);
 if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue);
 }
 insert(parentElm, vnode.elm, refElm);
}

最后将老的节点删掉

if (isDef(parentElm)) {
 removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
 invokeDestroyHook(oldVnode);
}
function removeAndInvokeRemoveHook (vnode, rm) {
 if (isDef(rm) || isDef(vnode.data)) {
  var i;
  var listeners = cbs.remove.length + 1;
  ...
  // recursively invoke hooks on child component root node
  if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
   removeAndInvokeRemoveHook(i, rm);
  }
  for (i = 0; i < cbs.remove.length; ++i) {
   cbs.remove[i](vnode, rm);
  }
  if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
   i(vnode, rm);
  } else {
   // 删除id为app的老节点
   rm();
  }
 } else {
  removeNode(vnode.elm);
 }
}

初次渲染结束。

更新节点过程

为了更好地测试,模板选用

<div id="app">{{ message }}<button @click="update">更新</button></div>

点击按钮,会更新message,重新渲染视图,生成的VNode为

{
 asyncFactory: undefined,
 asyncMeta: undefined,
 children: [VNode, VNode],
 componentInstance: undefined,
 componentOptions: undefined,
 context: Vue实例,
 data: {attrs: {id: "app"}},
 elm: undefined,
 fnContext: undefined,
 fnOptions: undefined,
 fnScopeId: undefined,
 isAsyncPlaceholder: false,
 isCloned: false,
 isComment: false,
 isOnce: false,
 isRootInsert: true,
 isStatic: false,
 key: undefined,
 ns: undefined,
 parent: undefined,
 raw: false,
 tag: "div",
 text: undefined,
 child: undefined
}

在组件更新的时候,preVnode和vnode都是存在的,执行

vm.$el = vm.__patch__(prevVnode, vnode);

实际上是运行以下函数

patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);

该函数首先判断oldVnode和vnode是否相等,相等则立即返回

if (oldVnode === vnode) {
 return
}

如果两者均为静态节点且key值相等,且vnode是被克隆或者具有isOnce属性时,vnode的组件实例componentInstance直接赋值

if (isTrue(vnode.isStatic) &&
 isTrue(oldVnode.isStatic) &&
 vnode.key === oldVnode.key &&
 (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
 vnode.componentInstance = oldVnode.componentInstance;
 return
}

接着对两者的属性值作对比,并更新

var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
 for (i = 0; i < cbs.update.length; ++i) {  // 以vnode为准更新oldVnode的不同属性
  cbs.update[i](oldVnode, vnode); 
 }
 if (isDef(i = data.hook) && isDef(i = i.update)) { 
  i(oldVnode, vnode); 
 }
}

vnode和oldVnode的对比以及相应的DOM操作具体如下:

// vnode不存在text属性的情况
if (isUndef(vnode.text)) {
 if (isDef(oldCh) && isDef(ch)) {
 // 子节点不相等时,更新
 if (oldCh !== ch) { 
  updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
 } else if (isDef(ch)) {
  {
  checkDuplicateKeys(ch);
  }
  // 只存在vnode的子节点,如果oldVnode存在text属性,则将元素的文本内容清空,并新增elm节点
  if (isDef(oldVnode.text)) {    nodeOps.setTextContent(elm, ''); 
  }
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
 } else if (isDef(oldCh)) {
  // 如果只存在oldVnode的子节点,则删除DOM的子节点
  removeVnodes(elm, oldCh, 0, oldCh.length - 1);
 } else if (isDef(oldVnode.text)) {
  // 只存在oldVnode有text属性,将元素的文本清空
  nodeOps.setTextContent(elm, '');
 }
} else if (oldVnode.text !== vnode.text) {
 // node和oldVnode的text属性都存在且不一致时,元素节点内容设置为vnode.text
 nodeOps.setTextContent(elm, vnode.text);
}

对于子节点的对比,先分别定义oldVnode和vnode两数组的前后两个指针索引

var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

如下图:

Vue内部渲染视图的方法

接下来是一个while循环,在这过程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 会逐渐向中间靠拢

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

当oldStartVnode或者oldEndVnode为空时,两中间移动

if (isUndef(oldStartVnode)) {
 oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
 oldEndVnode = oldCh[--oldEndIdx];
}

接下来这一块,是将 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 两两比对的过程,共四种:

else if (sameVnode(oldStartVnode, newStartVnode)) {
 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
 oldStartVnode = oldCh[++oldStartIdx];
 newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
 oldEndVnode = oldCh[--oldEndIdx];
 newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
 oldStartVnode = oldCh[++oldStartIdx];
 newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
 oldEndVnode = oldCh[--oldEndIdx];
 newStartVnode = newCh[++newStartIdx];
}

第一种: 前前相等比较

Vue内部渲染视图的方法

如果相等,则oldStartVnode.elm和newStartVnode.elm均向后移一位,继续比较。 第二种: 后后相等比较

Vue内部渲染视图的方法

如果相等,则oldEndVnode.elmnewEndVnode.elm均向前移一位,继续比较。 第三种: 前后相等比较

Vue内部渲染视图的方法

将oldStartVnode.elm节点直接移动到oldEndVnode.elm节点后面,然后将oldStartIdx向后移一位,newEndIdx向前移动一位。 第四种: 后前相等比较

Vue内部渲染视图的方法

将oldEndVnode.elm节点直接移动到oldStartVnode.elm节点后面,然后将oldEndIdx向前移一位,newStartIdx向后移动一位。 如果以上均不满足,则

else {
 if (isUndef(oldKeyToIdx)) { 
   oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); 
 }
 idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
 if (isUndef(idxInOld)) { // New element
   createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
 } else {
   vnodeToMove = oldCh[idxInOld];
   if (sameVnode(vnodeToMove, newStartVnode)) {
      patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      oldCh[idxInOld] = undefined;
      canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
   } else {
   // same key but different element. treat as new element
     createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
   }
  }
  newStartVnode = newCh[++newStartIdx];
}

createkeyToOldIdx函数的作用是建立key和index索引对应的map表,如果还是没有找到节点,则新创建节点

createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);

插入到oldStartVnode.elm节点前面,否则,如果找到了节点,并符合sameVnode,将两个节点patchVnode,并将该位置的老节点置为undefined,同时将vnodeToMove.elm移到oldStartVnode.elm的前面,以及newStartIdx往后移一位,示意图如下:

 Vue内部渲染视图的方法

如果不符合sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。 最后如果,oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的,需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可;如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多,将这些无用的老节点通过 removeVnodes 批量删除即可。到这里这个过程基本结束。

总结

以上所述是小编给大家介绍的Vue内部渲染视图的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
用js做一个小游戏平台 (一)
Dec 29 Javascript
javascript控制frame,iframe的src属性代码
Dec 31 Javascript
jQuery EasyUI 中文API Layout(Tabs)
Apr 27 Javascript
学习并汇集javascript匿名函数
Nov 25 Javascript
javascript基础知识大全 便于大家学习,也便于我自己查看
Aug 17 Javascript
Javascript页面添加到收藏夹的简单方法
Aug 07 Javascript
jQuery对html元素的取值与赋值实例详解
Dec 18 Javascript
js流动式效果显示当前系统时间
May 16 Javascript
JavaScript版经典游戏之扫雷游戏完整示例【附demo源码下载】
Dec 12 Javascript
在mpvue框架中使用Vant WeappUI组件库的注意事项【推进】
Jun 09 Javascript
Element MessageBox弹框的具体使用
Jul 27 Javascript
vue props default Array或是Object的正确写法说明
Jul 30 Javascript
一步一步实现Vue的响应式(对象观测)
Sep 02 #Javascript
Layui多选只有最后一个值的解决方法
Sep 02 #Javascript
解决layui checkbox 提交多个值的问题
Sep 02 #Javascript
LayUI动态设置checkbox不显示的解决方法
Sep 02 #Javascript
layui checkbox默认选中,获取选中值,清空所有选中项的例子
Sep 02 #Javascript
layui 选择列表,打勾,点击确定返回数据的例子
Sep 02 #Javascript
利用JS响应式修改vue实现页面的input值
Sep 02 #Javascript
You might like
Yii PHP Framework实用入门教程(详细介绍)
2013/06/18 PHP
百度地图经纬度转换到腾讯地图/Google 对应的经纬度
2015/08/28 PHP
Symfony核心类概述
2016/03/17 PHP
PHP插件PHPMailer发送邮件功能
2017/02/28 PHP
PHP基于Closure类创建匿名函数的方法详解
2017/08/17 PHP
浅谈Javascript如何实现匀速运动
2014/12/19 Javascript
React.js入门实例教程之创建hello world 的5种方式
2016/05/11 Javascript
jQuery前端开发35个小技巧
2016/05/24 Javascript
webpack中引用jquery的简单实现
2016/06/08 Javascript
jQuery实现简洁的轮播图效果实例
2016/09/07 Javascript
jquery日历插件e-calendar升级版
2016/11/10 Javascript
JavaScript中从setTimeout与setInterval到AJAX异步
2017/02/13 Javascript
解读ES6中class关键字
2017/11/20 Javascript
vue.js整合vux中的上拉加载下拉刷新实例教程
2018/01/09 Javascript
Vue3.0数据响应式原理详解
2019/10/09 Javascript
python模拟新浪微博登陆功能(新浪微博爬虫)
2013/12/24 Python
Python实现的飞速中文网小说下载脚本
2015/04/23 Python
Python实现的IP端口扫描工具类示例
2019/02/15 Python
PyQt5 在label显示的图片中绘制矩形的方法
2019/06/17 Python
Django处理Ajax发送的Get请求代码详解
2019/07/29 Python
python常用排序算法的实现代码
2019/11/08 Python
Python如何计算语句执行时间
2019/11/22 Python
python 通过文件夹导入包的操作
2020/06/01 Python
详解Django中异步任务之django-celery
2020/11/05 Python
Cpython解释器中的GIL全局解释器锁
2020/11/09 Python
css3 边框、背景、文本效果的实现代码
2018/03/21 HTML / CSS
联想印度官方网上商店:Lenovo India
2019/08/24 全球购物
写自荐信有哪些不宜?
2013/10/17 职场文书
网络公司美工设计工作个人的自我评价
2013/11/03 职场文书
写给保洁员表扬信
2014/01/08 职场文书
党员岗位承诺书
2014/03/25 职场文书
银行柜员与客户起冲突检讨书
2014/09/27 职场文书
7个你应该知道的JS原生错误类型
2021/04/29 Javascript
Python函数中的不定长参数相关知识总结
2021/06/24 Python
php双向队列实例讲解
2021/11/17 PHP
Element-ui Layout布局(Row和Col组件)的实现
2021/12/06 Vue.js