React/Vue都用到了虛擬DOM,圍繞虛擬DOM,本篇主要解決下面3個問題。vue
爲何要使用虛擬DOM? 如何定義(建立)虛擬dom呢? 虛擬DOM如何映射爲真實DOM?node
咱們的編碼目標是下面的demo可以成功渲染。web
let vm = new Vue({
el: '#app',
render (h) {
return h('h1', 'Hello Vue!')
}
})
複製代碼
將下列代碼拷貝至瀏覽器中運行:算法
let d = document.createElement('div')
for(let key in d) console.log(key)
複製代碼
咱們會發現,真實dom上有很是多的屬性,經過自定義虛擬dom可以有效節省空間。小程序
另外,真實dom的重排重繪是很是消耗性能的,應該儘可能少修改,藉助虛擬DOM的diff算法,可以有效提高性能。瀏覽器
最重要的是,當前有很是多的跨端開發需求,如原生、web、小程序等等,藉助虛擬DOM有助於跨端開發,一段代碼到處運行。bash
VNode必備屬性只有tag/data/children/text/elm,其餘屬性爲vue功能須要,如componetOptions/componentInstance只在組件節點中才被使用。app
export class VNode {
tag?: string
data?: VNodeData
children?: Array<VNode>
text?: string
elm?: Node
context?: Vue
componentOptions?: VueOptions
componentInstance?: Vue
parent?: VNode
key?: string | number
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode>,
text?: string,
elm?: Node,
context?: Vue,
componentOptions?: VueOptions
) {
this.tag = tag
this.data = data || ({} as VNodeData)
this.children = children
this.text = text
this.elm = elm
this.context = context || bindContenxt
this.componentOptions = componentOptions
}
}
複製代碼
在vue-render方法中,此處h
即爲建立虛擬節點的函數。dom
new Vue({
render (h) {
return h('h1', 'hello world')
}
})
複製代碼
咱們知道真實DOM的節點類型很是多,如Element、Attr、Comment、Document、DocumentFragment、Text等,而VNode,只作4種形式:組件節點、子節點(children屬性不爲空)、文本節點、註釋節點。異步
h爲重載函數,根據參數不一樣生成不一樣類型的vnode:
子節點類型,其tag和children屬性不爲空,其text屬性爲空。
v1 = h('h1', [h('', 'hello world')])
{
children: [
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
],
data: {},
elm: undefined,
tag: "h1",
text: undefined,
}
複製代碼
文本節點類型,其tag和children屬性爲空,其text屬性不爲空。
v2 = h('', 'hello world')
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
複製代碼
文本節點類型,其tag屬性爲!
,children屬性爲空,其text屬性不爲空。
v3 = h('!', 'hello comment')
{
children: undefined,
data: {},
elm: undefined,
tag: '!',
text: 'hello world'
}
複製代碼
組件節點類型,其componentOptions屬性不爲空。
v4 = h('button-count', [])
{
children: undefined
componentInstance: Proxy {$refs: {…}, $options: {…}}
componentOptions: {Ctor: ƒ, propsData: undefined, children: Array(1), tag: "button-counter"}
data: {on: undefined, hook: {…}}
elm: button
tag: "vue-component-1-button-counter"
text: undefined
}
複製代碼
經過屬性狀態劃分爲4種類型,在進行diff算法時,針對不一樣的類型將進行不一樣的處理,如組件節點會調用createComponentInstanceForVnode
進行初始化。
咱們建立了本身的虛擬DOM,接下來,將虛擬DOM映射爲真實DOM,將Hello Vue
渲染至瀏覽器。
映射過程有一個很是重要的方法patch
,patch接收新舊節點,執行diff算法。
sameVnode
關係,則調用patchVnode
webMethods.append
的本質是執行parentElm.appendChild(createElm(vnode))
function patch(oldVnode: VNode, vnode: VNode) {
let parentElm = webMethods.parentNode(oldVnode.elm)
if (isSameVnode(oldVnode, vnode)) {
patchNode(oldVnode, vnode)
} else {
webMethods.remove(parentElm, oldVnode.elm)
webMethods.append(parentElm, createElm(vnode))
}
return parentElm
}
複製代碼
createElm
須要根據虛擬節點的類型進行不一樣的處理,同時它會將生成好的真實DOM掛載在vnode.elm
屬性之上,方便對真實dom進行操做。
function createElm(vnode: VNode): Node {
// 組件節點
if (createComponent(vnode)) {
return vnode.elm
}
if (vnode.tag === '!') {
// 註釋節點
vnode.elm = webMethods.createComment(vnode.text!)
} else if (!vnode.tag) {
// 文本節點
vnode.elm = webMethods.createText(vnode.text!)
} else {
// 子節點
vnode.elm = webMethods.createElement(vnode.tag!)
}
return vnode.elm
}
複製代碼
接着對相同虛擬節點(sameVNode
)進行比較,根據children屬性分狀況處理,如updateChilden(比較子節點),removeChildren(刪除子節點),insertChildren(添加子節點),setTextContent(修改文本的內容)。
function patchNode(oldVnode: VNode, vnode: VNode) {
let i: any
const data = vnode.data,
oldCh = oldVnode.children,
ch = vnode.children,
elm = (vnode.elm = oldVnode.elm!)
if (oldVnode === vnode) return
if (oldCh) {
// 子節點
if (ch) {
if (ch === oldCh) return
updateChildren(elm!, oldCh, ch)
} else {
removeChildren(elm!, oldCh, 0, oldCh.length - 1)
webMethods.setTextContent(elm!, vnode.text!)
}
} else {
// 文本節點
if (ch) {
webMethods.setTextContent(elm, '')
insertChildren(elm!, null, ch, 0, ch.length - 1)
} else {
webMethods.setTextContent(elm!, vnode.text!)
}
}
}
複製代碼
最終經過不斷遞歸,比較完全部虛擬DOM。
回顧咱們的DEMO,咱們須要頁面可以渲染出<h1>Hello Vue!</h1>
let vm = new Vue({
el: '#app',
render (h) {
return h('h1', 'Hello Vue!')
}
})
複製代碼
初始化vue實例後,調用render函數會返回vnode,而el指向的根節點會被初始化爲oldVnode,即:
oldVnode = {
tag: 'DIV'
elm: //指向真實dom
}
vnode = {
tag: 'h1',
ele: undefined,
children: [
{
tag: '',
text: 'hello world'
}
]
}
複製代碼
接着執行patch(oldVnode, vnode)
,對節點進行比較,完成渲染。
咱們根據上面的流程實現下功能吧。
補充說明下方法:h
爲生成VNode的函數,createNodeAt
將真實DOM轉爲虛擬DOM,patch
是進行映射的核心函數。
class Vue {
constructor (options) {
this.$options = options
this._vnode = null
if(options.el) {
this.$mount(options.el)
}
},
_render () {
return this.$options.render.call(this, h)
},
_update (vnode) {
let oldVnode = this._vnode
this._vnode = vnode
patch(oldVnode, vnode)
}
$mount (el) {
this._vnode = createNodeAt(documeng.querySelector(options.el))
this._update(this._render())
}
}
複製代碼
ps: 還沒有驗證(運行)上述代碼,後期將進行驗證。
虛擬DOM的diff算法可能沒有表述清楚,推薦直接看snabbdom。基於虛擬DOM技術進行跨平臺開發的方案有:ReactNative、Weex、taro等,還沒有學習故不作敘述。
虛擬DOM究竟提高了多少性能?(www.zhihu.com/question/31…
虛擬DOM的起源?(juejin.im/post/5d085c…
虛擬DOM的diff算法?