Vue内部渲染视图的方法

1.什么是虚拟DOM

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

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;

如下图:

接下来是一个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];
}

第一种: 前前相等比较

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

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

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

将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往后移一位,示意图如下:

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

总结

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

(0)

相关推荐

  • 详解vue 数组和对象渲染问题

    最近项目有点忙碌,遇到好多问题都没有总结(╥﹏╥),在开发过程中,取vuex中的数组渲染完成之后,再次修改数组的值,数据更新了,但是视图并没有更新.以为是数组更新的问题,后来又以为是因为vuex导致的问题. 最后强制刷新组件解决了问题,但是还没有找到根本问题的所在... 数组更新检测 在 vue 中使用数组的push().pop().shift().unshift().splice().sort().reverse() .filter().concat() 方法时,改变数组的同时可以触发视图的变

  • 解决Vue 通过下表修改数组,页面不渲染的问题

    需要注意的是,Vue之所以能够监听Model状态的变化,是因为JavaScript语言本身提供了Proxy或者Object.observe()机制来监听对象状态的变化.但是,对于数组元素的赋值,却没有办法直接监听,因此,如果我们直接对数组元素赋值: vm.todos[0] = { name: 'New name', description: 'New description' }; 会导致Vue无法更新View. 正确的方法是不要对数组元素赋值,而是更新: vm.todos[0].name =

  • Vue+ElementUI实现表单动态渲染、可视化配置的方法

    动态渲染就是有一个异步的数据,大概长这样: { "inline": true, "labelPosition": "right", "labelWidth": "", "size": "small", "statusIcon": true, "formItemList": [ { "type": "

  • Vue渲染过程浅析

    Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML.但是模板毕竟是模板,不是真实的dom节点.从模板到真实dom节点还需要经过一些步骤 把模板编译为render函数 实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom 对比虚拟dom,渲染到真实dom 组件内部data发生变化,组件和子组件引用data作为props重新调用render函数,生成虚拟dom, 返回到步骤3 第一步: 模板到render 在我们使用Vue的组件化进行开发应用的时候, 如果仔细

  • vue2.0的虚拟DOM渲染思路分析

    1.为什么需要虚拟DOM 前面我们从零开始写了一个简单的类Vue框架(文章链接),其中的模板解析和渲染是通过Compile函数来完成的,采用了文档碎片代替了直接对页面中DOM元素的操作,在完成数据的更改后通过appendChild函数将真实的DOM插入到页面. 虽然采用的是文档碎片,但是操作的还是真实的DOM. 而我们知道操作DOM的代价是昂贵的,所以vue2.0采用了虚拟DOM来代替对真实DOM的操作,最后通过某种机制来完成对真实DOM的更新,渲染视图. 所谓的虚拟DOM,其实就是 用JS来模

  • vue异步axios获取的数据渲染到页面的方法

    我们在vue,数据很多事异步获取来的,如果在template直接使用,会报错,undefined. 因为先渲染后得到的数据,那如何才能不报错呢? computed!!! 举个例子 index.vue 忽略坑人的传参方式... created(){ this.init() this.axios.post('/wanwei/appserver/eqInfo/eqBaseInfo?reqjson={"eq_code":"BJTE1W03011SF001SBQDGPXTGYKG001

  • 解决vue.js 数据渲染成功仍报错的问题

    最近在做一个vue项目,用的是官方推荐的axios请求数据,数据结构是一级对象嵌套二级对象,发现一级对象数据渲染不报错,二级数据渲染报错.很是郁闷!data函数如下 export default { name: 'hello', data() { return { card:{} } } } 返回的数据如下: { "object":{ "subObject":"123", ... } } 报错的原因是在data函数return的card里没有二级

  • VUE2.0+ElementUI2.0表格el-table循环动态列渲染的写法详解

    先看看ElementUI里关于el-table的template数据结构: <template> <el-table :data="tableData" style="width: 100%"> <el-table-column prop="date" label="日期" width="180"> </el-table-column> <el-tabl

  • Vue在页面数据渲染完成之后的调用方法

    在使用Vue框架的时候,有时候需要在Vue在页面数据渲染完成之后调用方法,不然获取不到准确的数据,特别是在获取列表的高度的时候,由于数据没有加载完,获取不到准确的高度.之前在使用jquery的时候,有ready可以帮助我们实现,但是Vue不存在此类方法,我们需要结合watch和this.$nextTick()来实现. nextTick:在下次 DOM 更新循环结束之后执行延迟回调. watch:用于观察Vue实例上的数据变动.对应一个对象,键是观察表达式,值是对应回调. 之前我是这样子使用nex

  • vue中实现先请求数据再渲染dom分享

    在项目中遇到了一个问题,下面是vue template中的代码: 我之前的写法是 这样做的结果是下面取dom的操作,取到的dom都是undefined,也就是没有取到. 原因是并没有按照 请求数据->渲染dom->获取dom的顺序执行,实际的执行顺序是 先获取dom,而此时数组option中还是空的,上面的v-for循环也就没有渲染出dom,所以根本取不到(不理解是为什么) 后来我又把请求数据写在了created函数中,把取dom的操作写在mounted函数中,竟然还是先执行取dom的操作(是

  • vue element动态渲染、移除表单并添加验证的实现

    又接到新需求了吧~~ 背景 在一个大表单里,有可能会出现这种需求,用户可以自己操作动态添加.移除表单,更加个性化的效果. 常见于填写个人信息.附加内容的表单 例如: "工作经历"可以用户自己点击继续添加按钮,在原有的表单后面 append 多一个表单,不需要就点击右上方 X 按钮移除 问题 在实现之前,提出几个问题 vue 怎么动态渲染或移除表单上去 v-model 怎么绑定动态添加表单的 value 值 动态新增的表单如何验证 动态表单怎么填写对应的 prop ... 好吧,我当时也

随机推荐