欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue内部渲染视图的方法

程序员文章站 2022-09-07 08:58:23
1.什么是虚拟dom  以前m的命令式操作dom即使用jquery操作dom节点,随着状态的增多,dom的操作就会越来越频繁,程序的状态也越难维护,现在...

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内部渲染视图的方法,希望对大家有所帮助