简介
__
用户编写完组件,new Vue实例后,我们是如何看到界面的呢?
Vue会根据将模板编译成render函数,调用render函数生成虚拟dom,然后将虚拟dom映射成真实dom。
当数据变化时候,Vue会触发更新视图,调用render函数返回新的虚拟dom,对比新旧虚拟dom,修改真实dom,从而更新界面。
有4个关键步骤:
- 模板编译,生成渲染函数render。
- 执行render生成虚拟DOM。
- 首次渲染时候,根据虚拟DOM生成真实DOM。
- 状态更新后,diff虚拟DOM,执行patch,生成真实DOM。
模板编译
__
在Vue.js中创建HTML并不是只有模板这一种途径。既可以手动写渲染函数来创建HTML,也可以在Vue.js中使用JSX来创建HTML。
如果使用模板,可以使用Vue框架自带的模板解析器,在代码运行时候进行解析。实际项目中更多使用单文件组件,vue-loader会进行模板解析,在代码构建阶段就会将模板编译完成,构建后的代码中不需要包含模板解析器,代码量会更小。
平时使用模板时,可以在模板中使用一些变量来填充模板,还可以在模板中使用Javascript表达式,又或者是使用一些指令等。这些功能在HTML语法中是不存在的,这多亏了模板编译赋予了模板强大的功能。
模板编译的目标是生成render函数,模板最终会通过编译转换成渲染函数,渲染函数执行后,会得到一份vnode用于虚拟DOM渲染。所以模板编译其实是配合虚拟DOM进行渲染。模板编译的主要目标就是生成渲染函数。而渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的vnode,然后使用这个vnode进行渲染。
Vue是在什么时候进行模板编译的呢?是在实例化Vue组件的时候,模板编译的逻辑在Vue官方文档中能够清楚的看到:生命周期。
Vue在实例化组件时候会生成虚拟DOM,Vue先判断是否有render函数,如果有的话调用render生成虚拟DOM;如果没有render函数,则获取template选项,template选项可以是选择器、模版字符串、dom元素,Vue根据template选项进行模板编译;如果没有template,则获取el以及其子内容作为模版,然后进行模板编译。
模板编译有三个步骤:
- 将模板解析为AST。(Abstract Syntax Tree,抽象语法树)。
- 遍历AST标记静态节点。
- 使用AST生成渲染函数。
虚拟DOM
__
什么是vdom
vdom是一个用来描述真实dom的js数据结构,vdom是树状结构,每个节点对应dom的元素,保存了dom元素的标签名、属性、子节点等信息。有几种不同类型的node,TextVNode、ElementVNode、ComponentVNode等。
为什么需要虚拟dom
- 维护视图和状态的关系,在状态改变后,Vue会生成新虚拟dom,然后对比新旧虚拟dom,得到区别,从而进行patch,更新真实dom,从而保持视图和状态一致。
- 避免手工操作DOM,提升项目可维护性(virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡)
跨平台
• 浏览器渲染DOM
• 服务端渲染 SSR (Nuxt.js/Next.js)
• 原生应用(Weex/React Native)
• 小程序(mpvue/uni-app)等
diff算法
详细的说明请参考文章【5】。
diff算法的目标是更新DOM,它对比新旧虚拟DOM,根据对比的结果将旧的DOM更新为新的DOM。
diff的过程就是调用patch函数,就像打补丁一样修改真实dom。
对比两颗树算法最低的时间复杂度是O(n3),n是树的节点个数。diff算法做了近似假设,复杂度为O(n)。
diff算法认为,从旧树到新树之间发生的变化有以下几种:
- 旧节点变成新的节点,而且整个子树都发生了变化
- 节点不变,但是属性、文本内容发生变化
- 子节点顺序发生变化
当节点跨层级移动,会被认为是销毁重新创建,在一些文章中,这被理解为“按层比较”。
diff算法过程
diff算法的大体过程是,从组件根节点开始对比两棵虚拟DOM树。
判断新旧节点是否值得比较,值得比较的话,就进行对比操作,否则用新的节点替换旧的节点。新旧节点比较完后,如果有子节点,还要比较子节点(child),在旧的child中找到和新的child匹配的节点,如果找到(说明这两个child“值得比较”)并且顺序不同则调整顺序,然后再对两个child执行对比操作。
“值得比较”是个很重要的概念,值得比较意味着Vue认为旧节点经过更新就可以得到新的节点,而不需要完全用新的节点代替旧的节点:
// 是否值得比较
function sameVnode(oldVnode, vnode){
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
新旧节点比较的过程:
- 如果新旧节点的引用一致,可以认为没有变化。
- 比较新旧节点文本,需要修改的话,则更新文本。
- 如果新节点没有子节点,老节点有子节点,直接删除老的子节点。
- 如果新节点有子节点,老节点没有子节点,则创建新的子节点。
- 新旧节点都有子节点,而且新旧节点的子节点引用不一样,会调用updateChildren函数比较子节点。
updateChildren
updateChildren操作要解决的问题是,如何将old children通过dom操作转为new children。
一个最基本的思路是二重循环,外层循环遍历new children的每个节点,内存循环中在old children中查找是否存在new children的某个节点,如果有,就移动到new children的相应位置,如果没有就创建一个新的节点放到相应位置。遍历完成后,如果old children还有节点,则将剩余节点全部删除。
vue根据经验对这种暴力算法做了优化,优化基于的经验是,应用的开发者更新组件children节点时候,大部分的操作都是在头部或者尾部插入节点。
vue的updateChildren的基本思路是,old children和new children各自有两个指向收尾的指针用于遍历
每次对比这4个指针对应的节点,如果old header或者old tail能和new的header匹配上,将old的对应节点移动到new的header的位置,如果没有匹配到的,再在old children中遍历,查找是否有能和new header匹配的,如果都没有,则新建节点。
然后移动new header和old header。
最后遍历完new children(header和tail相遇),如果old children中还有节点,则全部移除。
__
参考文章
__
Vue模板编译原理【1】
熟悉模板编译原理【2】
Vue虚拟dom实现原理【3】