render, h, mount, patch
render, h, mount, patch
为了更好的揭示其原理,后文的示例和实现都是简化版,这样避免我们因为各种和主要原理无关的细节而耗费精力。
接下来本文主要会讲述四点:
- 弄清楚
Vue
中的VNode
是什么,VDOM
又是什么; - 组件选项的
render
函数作用是什么,h
又是什么; mount(app, '#app')
是如何将VNode
渲染到界面容器中;patch
的目的究竟是什么,它大概是怎么个流程;
VNode
VNode 是什么
在Vue2
,VNode
存在一个对应的类,通过它可以实例化很多不同类型的vnode
实例,这些不同类型的vnode
实例各自可以代表不同类型的DOM
元素:
在Vue3
中,VNode
是一个interface
(ts
中的类型的一种表示方式,可以描述一个对象的结构),它同样可以描述不同类型的vnode
对象,这些vnode
对象同样可以代表不同类型的DOM
元素:
VNode
本质上就是一个JS
对象,它能够描述不同的DOM
元素,原生DOM
对象的所具备的各种属性,VNode
它基本都有;
也正是因为此,VNode
可以理解为一个描述页面真实DOM
元素的对象,它描述了Vue
该如何通过它去创建一个真实的DOM
元素。
举一个最简单的例子:
VDom 是什么
由VNode
组成的Tree
就可以看作VDom
,这里其实可以类比为Dom Tree
,VDom
和 Dom
是存在对应关系的。
VNode 是的常见作用是什么
patch
Vue
中在首次渲染视图时,会调用render
生成第一个VNode
(此时页面中没有任何节点),是直接将VNode
对应的Dom
元素插入到界面上。
当在下一次渲染时,会重新调用render
得到生成一个新的VNode
,也就是newVNode
,那么上一次调用render
函数得到的VNode
就是oldVNode
(oldVNode
对应当前页面上的DOM
元素),然后调用patch(oldVNode, newVNode)
比较两者的不同,得到最小操作代价来更新界面,使得界面上的元素能够和newVNode
的对应的Dom
结构相同。
跨平台
我们知道,Vue
是通过将VNode
渲染成真实的DOM
元素,然后插入到容器中。
这里正是因为VNode
这一个抽象层的存在,将Vue
和真实的DOM
元素实现了解耦,也就是说,实现了和平台相关的DOM API
实现了解耦。
在Vue
中它是通过nodeOps
来描述的渲染API
,也就是说,其他平台只要结合自身平台实现nodeOps
即可,这样就实现了跨平台的效果。
render,h,mount 的概念和简单实现
一个Vue
组件如何变为页面上的DOM
元素,大概流程:
其中VNode
是通过render
中的h
得到的,而VNode
到视图又是通过mount
实现的。
render,h 代码实现
首先我们知道,render
函数会返回调用h --> hyperScript
得到的VNode
,因此代码如下:
mount 代码实现
mount
的作用是将VNode
解析为真实的Dom
结构然后插入到指定容器中,代码如下:
例子
patch 的概念和简单实现
patch
本身就有补丁,修补的意思。
patch(oldVNode, newVNode, container)
的作用是比较两者的不同,以最小操作代价(DOM
操作)在现有Dom
(oldVNode.el
代表当前的界面上的元素)上进行修改,最终使得界面上的Dom
元素和newVNode
描述的Dom
相同即可。
大致思路
-
如果
oldVNode
和newVNode
完全相同,那么直接返回; -
如果
oldVNode
不存在,直接挂载newVNode
即可; -
如果
oldVNode
存在, 但是newVNode
不存在,那么直接将oldVNode
对应的界面元素卸载掉即可; -
判断
oldVNode
和newVNode
的类型是否相同:-
如果不同,直接替换
oldVNode
为newVNode
即可; -
如果相同,则进行深层次的比较:
-
比较两者
props
是否相同:newProps
上有,oldProps
上没有;newProps
上有,oldProps
上有, 但是两者不同;oldProps
上有,newProps
上没有;
-
比较两者子元素是否相同:
-
如果
newVNode.children
的类型为字符串或者数字;-
如果
oldVNode.children
的类型为字符串或者数字且不和newVNode.children
相同,那么直接覆盖,el.textContent = newVNode.children
; -
如果
oldVNode.children
的类型不为字符串或者数字,那么先清空oldVNode.children
,然后插入newVNode.children
,el.innerHtml = ''; el.textContent = newVNode.children
;
-
-
如果
newVNode.children
的类型为数组;- 如果
oldVNode.children
的类型为数字或者字符串,清空oldVNode.children
文本内容,然后遍历newVNode.children
执行mount
操作; - 如果
oldVNode.children
的类型也为数组,这里我们采用简化版diff
算法:- 取
newChildren
和oldChildren
的公共长度的元素直接遍历进行patch
; - 对于
newChildren
比oldChildren
多的元素进行删除; - 对于
newChildren
比oldChildren
少的元素进行新增;
- 取
- 如果
-
-
-
patch 代码实现
例子
结合响应式最终实现示例效果
目标示例
接下来的实现就以这个示例为目标。
运行之后:
大致分解一下示例的步骤:
-
Vue
组件选项中的render
函数会返回调用h --> hyperScript
得到的VNode
; -
当在调用
app.mount
时, 会触发render
得到VNode
,然后会直接将整个VNode
变成DOM
元素插入到页面容器中; -
data
在内部会被转化为响应式数据,同时render
也有依赖响应式数据的地方,因此会触发响应式数据的get
收集依赖。当响应式数据发生改变时,会触发这些收集的依赖(也就是副作用),然后会重新触发
render
,然后调用patch(oldVNode, newVNode)
比较两者的不同。最终得到最小的操作代价来更新界面,使得界面上的元素能够和
newVNode
的对应的Dom
结构相同;
模拟实现 createApp
这里我们实现的createApp
主要就是逐步完成上文中拆分的三个步骤。
响应式相关实现可以参考: 响应式实现。
createApp
方法的参数是一个组件选项,它的返回值是一个对象,且具有mount
方法:
例子
可以看运行效果,最终和示例相同。