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