vue源码学习与分析(一):vm实例如何渲染
前言
接着上一篇文章,关于
new Vue({})
脚本程序执行的时候发生了什么?为什么执行了这个方法之后,就可以对应在界面上展示相应的信息(如下图所示)
🌟 猜想:一个html需要在界面上展示对应的渲染结果,那么需要对应的添加相应的html标签
,才可能使对应的节点元素能够正常展示!
👉 那么,问题就演变为Vue是如何生成对应的html出来的?
要想了解这个渲染的过程,需要先了解一下相关的概念,方便后续直接深入了解vue的渲染过程!!!
理解相关的元素
- 虚拟节点: VNode
- 创建虚拟节点: createElement
虚拟节点: VNode
*vnode是一个虚拟节点对象,用于描述组件树上的一个节点,包含了节点的名称(tag)、节点属性信息(data)、子节点(children)*,主要将原本在html中可视化的节点信息,抽象为虚拟的节点(带一定的数据结构在其中),在
vue
环境中,我们仅需要操作这个VNode
即可,而无需直接去操作dom!!
😕 为什么要将普通的html的dom操作,转换为VNode的操作??
👉 因为直接操作dom是一个耗性能的过程,对一个dom节点的操作,将有可能导致布局的调整,而致使浏览器产生回流以及重绘,这个回流以及重绘的详细介绍,可以看之前我这边写过的另外一篇文章:回流与重绘
🌠 最终实现这样子的一个目的:将所有的操作dom变换,都调整为VNode的计算逻辑,最终只需要将执行结果(html结果字符串
),插入到目标位置上
创建虚拟节点:createElement
对内部的
_createElement函数
进行了一个包装,主要用于兼容单个以及数组级别的Element的创建!
😕 那么,这个_createElement函数
又是怎么一回事呢??
1 | // 主要根据传递进来的数据以及上下文信息,用来创建一个VNode虚拟节点 |
🌠 一般最终创建出来的vnode
格式如下(当然远不止这么简单):
1 | { |
附带上这个虚拟dom的一个完整过程流程图
渲染过程分析
回到原点,从以下 👇 代码出发
1 | <div id="app"> |
1 | var vm = new Vue({ |
🌠 按照之前文章的分析,最终执行到了vm.$mount(vm.$options.el)
$mount方法的定义
1 | Vue.prototype.$mount = function ( |
🌠 这里是找寻到对应的el对应的id属性,然后转成为一个普通的htmlnode节点,并进入mountComponent
方法
mountComponent方法的定义
1 | // src/core/instance/lifecycle.js 141行 |
也就是说当我们执行的$mount(el)
的时候,也就是创建了一个Watcher
对象,并在对应的回调方法中进行了 👇 2⃣ 个动作:
- _render: 根据HtmlNode节点以及vm实例,渲染出对应的虚拟dom的过程;
- _update: 根据虚拟dom,生成对应的结果html,并进行html的替换动作!
⚠ 关于这里Watcher
的工作原理,将在下一章中关于数据的双向绑定进行具体分析一波!
而这里的_render
方法内容如下:
1 | Vue.prototype._render = function(): VNode{ |
😖 这里_render
又反过来去调用这个options.render
,那么这里的options.render方法
,它是从何而来的呢?它是什么时候被定义的呢??
🌠 这里我们通过debugger,发现这个函数体的内容是运行时生成的,其结构如下所示:
:face_with_monocle: 那么这个函数体,它又是在什么时候,怎么被生成的呢??
动态生成的运行时函数
再次回过头来从入口文件进行分析,发现在对应的入口处, 🈶 对应的关于该方法api的一个定义:
1 | // 主要是通过继承并重载对应的$mount,来追加自定义的逻辑,这里仅根据实际情况进行最简单的分析! |
🌠 这里的render即是我们所想要找寻的方法内容,那么这里的compileToFunctions()
方法的目的是什么呢?
👻 关于这里的compileToFunctions()
其中底层的逻辑较为复杂,这边简单概括一下:compileToFunctions函数是vue.js的模版编译器,主要用于将模版字符串转换为可执行的Javascript代码,其返回结果是一个对象,包含有两个属性:编译后的render函数,以及staticRenderFns函数,这两者都可以用来渲染视图!
👉 🈶 去大致地走了一遍其中的逻辑,无非就是根据模版,解析出对应的ast
树,然后解析该树,最终生成对应的代码,然后再采用一个Function
来将对应的字符串代码给包裹起来,使其能够成为一个可执行的代码!
🌠 再回到之前的vnode = render.call(vm._renderProxy, vm.$createElement)
,这一行代码,这里也就是将生成的可执行代码进行call
动作执行,也就是执行生成的运行时函数,该函数的结果如下所示:
😖 但是生成的运行时函数,其中的函数都是具有对应的代号,这里借助于installRenderHelpers()函数
,可在执行时关联对照查询:
🌠 也就是通过执行_c()
函数,来创建对应的vnode虚拟节点对象!
生成并替换/修改结果html
😕 👇 这里生成的虚拟dom对象之后,它又是如何生成目标html内容的呢?
👉 一切从进入_update()
方法开始!!
1 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { |
😖 那么这里的__patch__()
方法的目的是什么呢?
👉 通过代码跟踪执行,发现关于patch()
方法的定义如下:
1 | //src/core/vdom/patch.js |
上述代码对应的一个流程图如下所示:
👉 套上最开始的new Vue({})
的一个过程结合分析一波:
☝ 就是这么的简单,旧的dom不存在,直接根据新的虚拟dom来创建对应的结果html,并替换添加到旧的html位置,然后将旧的html给移除掉!
🌠 patch()
方法是重要的虚拟dom更新函数,主要用于将虚拟dom转换为真实的dom并应用到页面上!他主要作用是对比新旧节点,并更新视图。
一般来说,patch()
🈶 2⃣ 步骤:
- 通过
createElm()
函数创建一个真实的HtmlDom元素; - 通过
patchVnode()
函数对比新旧节点并更新视图 - 通过
updateChildren()
最终更新孩子节点视图的入口
⚠ 这里需要注意的是,patch()
方法从头到尾并没有操作到数据(即组件的状态和数据)!!
创建真实的Dom元素-createElm
createElm
函数是用于创建dom
元素并添加到文档中的方法,主要将虚拟的dom转换为真实的dom!
🌠 一个vnode中的elm属性如果为非空的话,那么这个vnode则之前肯定有被使用过,因为在vue的领域中,vnode.elm
属性只在此方法中创建过,直接去覆盖这个已使用过的elm
属性,可能会存在一些错误覆盖的问题,因此createElm
一般采用克隆的方式!
参数说明:
- vonde: 代表即将被渲染的虚拟dom;
- insertedVnodeQueue: 一个数组,用于存储已经挂载的 vnode,确保它们的钩子函数正确地执行;
- parentElm: 虚拟dom对应的父节点;
- relElm: 参照物节点,用以标识vnode即将是在参照物节点之前还是之后插入;
- nested: 一个布尔值,表示当前节点是否为嵌套节点;
- ownerArray: 当前节点所属的数组,用来维护节点的位置信息;
- index: 当前节点在其父级节点中的索引位置
执行流程:
🌠 从上面我们可以看出createElm
无非是根据tag标签类型,调用document
的相关API动作来创建对应的真实Dom节点元素 👉 这里有一个疑问 😕 就是如果待创建的节点是嵌套的孩子节点元素的话,那么它的创建顺序应该是怎样的呢?根据代码分析:应该是从做往右一颗子树的创建完毕,才进入下一个节点的创建
比如有 👇 的一个代码:
1 | <div id="app"> |
👇 是对应的输出结果:
从这里的输出结果,我们可以得出 👇 对应的一个节点创建的顺序:
差分并更新新旧节点元素-patchVnode
patchVnode
函数是用于对比更新单个节点的,其差分的流程如下图所示:
👽 在vnode中存在着一个属性isStatic
,该属性主要用来标识当前节点是否为静态节点,而所谓的静态节点,是指在编译阶段就已确定,不会发生变化的节点,通常包括纯文本节点、静态子节点等,这些节点在后续的更新过程中,不需要重新渲染和比对,就可以直接复用之前的渲染结果,从而提高渲染性能!!
👽 在vnode中存在着一个属性isAsyncPlaceholder
,该属性用来标记当前节点是否为异步组件的占位符,而所谓的异步组件,就是指在Vue.js中,可以通过Vue.component
方法来定义的组件,该函数的第二个参数是一个函数式组件,该组件会异步加载并渲染, 👉 当渲染异步组件时,会先渲染一个占位符,也就是isAsyncPlaceholder
属性所在的节点,然后等待异步组件加载完成后,再将异步组件渲染到该节点位置上!!
👽 在vnode中存在着一个属性isComment
,该属性用来标记当前节点是否为注释节点,就是<!-- 这是注释 -->
,在虚拟DOM中,注释节点也可以被表示为一个虚拟节点对象,当渲染到注释节点时,会直接忽略该节点并继续往下渲染,而且需要注意 ⚠ 的是注释节点虽然不参与渲染,但是存在于DOM树中,因此可以通过DOM API 访问到注释节点,而且还可以在模版中使用注释节点来进行一些特殊处理,例如在模版中注释掉一些内容等
👽 在vnode存在着一个属性data.hook.prepatch
,该属性为一个函数回调,主要接收两个参数(旧的vnode,新的vnode),用来在更新过程中标记是否需要执行一些预处理操作的标识属性,会在每次的update
阶段被调用,并且会在执行真正的patch
操作之前被调用,用来执行一些预处理操作!!
1 | new Vue({ |
🌠 hook
属性可用来注册一些生命周期函数,当一个节点需要被更新时,会依次执行prepatch
、update
、postpatch
方法,分别代表节点更新前的预处理、节点更新时的处理以及节点更新后的处理动作!
😕 思考这样子的一个问题,如果想要在普通的template模版中使用这个hook的话,应该如何使用呢?
1 | <template> |
👽 在vnode中存在着一个属性text
,该属性用来标识节点内包含的文本内容,如果该节点是一个纯文本节点,则text
属性就是该节点的全部内容,如果该节点包含了其他的子节点,则text
属性会被忽略,通常在更新的过程中判断节点是否需要重新渲染,如果一个节点的text
属性没有发生变化,则表示该节点的内容没有被修改过,不需要重新渲染,否则渲染之!!
更新真实的Dom元素-updateChildren
updateChildren
函数是用于对比新旧节点的孩子节点的关键函数。它是由patch
函数内部调用的,用于处理同一层级下的多个子节点的更新!
👇 是对应的更新孩子节点的差分对比逻辑
🌠 将上述的流程进行一个简化流程的分析,如下图所示: