上一篇文章我們介紹了 Vue2模版編譯原理,這一章我們的目標是弄清楚模版 template和回應式資料是如何渲染成最終的DOM,資料更新驅動視圖變化這部分后期會單獨講解
我們先看一下模版和回應式資料是如何渲染成最終DOM 的流程
Vue初始化
new Vue發生了什么
Vue入口建構式
function Vue(options) {
this._init(options) // options就是用戶的選項
...
}
initMixin(Vue) // 在Vue原型上擴展初始化相關的方法,_init、$mount 等
initLifeCycle(Vue) // 在Vue原型上擴展渲染相關的方法,_render、_c、_v、_s、_update 等
export default Vue
initMixin、initLifeCycle方法
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this
vm.$options = options // 將用戶的選項掛載到實體上
// 初始化資料
initState(vm)
if (options.el) {
vm.$mount(options.el)
}
}
Vue.prototype.$mount = function (el) {
const vm = this
el = document.querySelector(el)
let ops = vm.$options
// 這里需要對模板進行編譯
const render = compileToFunction(template)
ops.render = render
// 實體掛載
mountComponent(vm, el)
}
}
export function initLifeCycle(Vue) {
Vue.prototype._render = function () {} // 渲染方法
Vue.prototype._c = function () {} // 創建節點虛擬節點
Vue.prototype._v = function () {} // 創建文本虛擬節點
Vue.prototype._s = function () {} // 處理變數
Vue.prototype._update = function () {} // 初始化元素 和 更新元素
}
在 initMixin 方法中,我們重點關注 compileToFunction模版編譯 和 mountComponent實體掛載 2個方法,我們已經在上一篇文章詳細介紹過 compileToFunction 編譯程序,接下來我們就把重心放在 mountComponent 方法上,它會用到在 initLifeCycle 方法給Vue原型上擴展的方法,在 render 和 update章節會做詳細講解
實體掛載
mountComponent 方法主要是 實體化了一個渲染 watcher,updateComponent 作為回呼會立即執行一次,watcher 還有一個其他作用,就是當回應式資料發生變化時,也會通過內部的 update方法執行updateComponent 回呼,
現在我們先無需了解 watcher 的內部實作及其原理,后面會作詳細介紹
vm._render 方法會創建一個虛擬DOM(即以 VNode節點作為基礎的樹),vm._update 方法則是把這個虛擬DOM 渲染成一個真實的 DOM 并渲染出來
export function mountComponent(vm, el) {
// 這里的el 是通過querySelector獲取的
vm.$el = el
const updateComponent = () => {
// 1.呼叫render方法創建虛擬DOM,即以 VNode節點作為基礎的樹
const vnode = vm._render() // 內部呼叫 vm.$options.render()
// 2.根據虛擬DOM 產生真實DOM,插入到el元素中
vm._update(vnode)
}
// 實體化一個渲染watcher,true用于標識是一個渲染watche
const watcher = new Watcher(vm, updateComponent, true)
}
接下來我們會重點分析最核心的 2 個方法:vm._render 和 vm._update
render
我們需要在Vue原型上擴展 _render 方法
Vue.prototype._render = function () {
// 當渲染的時候會去實體中取值,我們就可以將屬性和視圖系結在一起
const vm = this
return vm.$options.render.call(vm) // 模版編譯后生成的render方法
}
在之前的 Vue $mount程序中,我們已通過 compileToFunction方法將模版template 編譯成 render方法,其回傳一個 虛擬DOM,template轉化成render函式的結果如下
<div id="app" style="color: red; background: yellow">
hello {{name}} world
<span></span>
</div>
? anonymous(
) {
with(this){
return _c('div',{id:"app",style:{"color":"red","background":"yellow"}},
_v("hello"+_s(name)+"world"),
_c('span',null))
}
}
render 方法內部使用了 _c、_v、_s 方法,我們也需要在Vue原型上擴展它們
- _c: 創建節點虛擬節點(VNode)
- _v: 創建文本虛擬節點(VNode)
- _s: 處理變數
// _c('div',{},...children)
// _c('div',{id:"app",style:{"color":"red"," background":"yellow"}},_v("hello"+_s(name)+"world"),_c('span',null))
Vue.prototype._c = function () {
return createElementVNode(this, ...arguments)
}
// _v(text)
Vue.prototype._v = function () {
return createTextVNode(this, ...arguments)
}
Vue.prototype._s = function (value) {
if (typeof value !== 'object') return value
return JSON.stringify(value)
}
接下來我們看一下 createElementVNode 和 createTextVNode 是如何創建 VNode 的
createElement
每個 VNode 有 children,children 每個元素也是一個 VNode,這樣就形成了一個虛擬樹結構,用于描述真實的DOM樹結構,即我們的虛擬DOM
// h() _c() 創建元素的虛擬節點 VNode
export function createElementVNode(vm, tag, data, ...children) {
if (data =https://www.cnblogs.com/burc/archive/2023/03/29/= null) {
data = {}
}
let key = data.key
if (key) {
delete data.key
}
return vnode(vm, tag, key, data, children)
}
// _v() 創建文本虛擬節點
export function createTextVNode(vm, text) {
return vnode(vm, undefined, undefined, undefined, undefined, text)
}
// 虛擬節點
function vnode(vm, tag, key, data, children, text) {
return {
vm,
tag,
key,
data,
children,
text,
// ....
}
}
VNode 和 AST一樣嗎?
我們的 VNode 描述的是 DOM元素
AST 做的是語法層面的轉化,它描述的是語法本身 ,可以描述 js css html
虛擬DOM
DOM是很慢的,其元素非常龐大,當我們頻繁的去做 DOM更新,會產生一定的性能問題,我們可以直觀感受一下div元素包含的海量屬性

在Javascript物件中,Virtual DOM 表現為一個 Object物件,并且最少包含標簽名 (tag)、屬性 (attrs) 和子元素物件 (children) 三個屬性,不同框架對這三個屬性的名命可能會有差別,
實際上它只是一層對真實DOM的抽象,以JavaScript 物件 (VNode 節點) 作為基礎的樹,用物件的屬性來描述節點,最終可以通過一系列操作使這棵樹映射到真實環境上
vue中 VNode結構如下
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*當前節點的標簽名*/
this.tag = tag
/*當前節點對應的物件,包含了具體的一些資料資訊,是一個VNodeData型別,可以參考VNodeData型別中的資料資訊*/
this.data = https://www.cnblogs.com/burc/archive/2023/03/29/data
/*當前節點的子節點,是一個陣列*/
this.children = children
/*當前節點的文本*/
this.text = text
/*當前虛擬節點對應的真實dom節點*/
this.elm = elm
/*當前節點的名字空間*/
this.ns = undefined
/*編譯作用域*/
this.context = context
/*函式化組件作用域*/
this.functionalContext = undefined
/*節點的key屬性,被當作節點的標志,用以優化*/
this.key = data && data.key
/*組件的option選項*/
this.componentOptions = componentOptions
/*當前節點對應的組件的實體*/
this.componentInstance = undefined
/*當前節點的父節點*/
this.parent = undefined
/*簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false*/
this.raw = false
/*靜態節點標志*/
this.isStatic = false
/*是否作為跟節點插入*/
this.isRootInsert = true
/*是否為注釋節點*/
this.isComment = false
/*是否為克隆節點*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}
虛擬DOM的優點??????
- 提升效率,操作 DOM的代價是昂貴的,使用 diff演算法,可以減少 JavaScript操作真實DOM 帶來的性能消耗
通過 Virtual DOM 改變真正的 DOM并不比直接操作 DOM效率更高,恰恰相反,Virtual DOM 仍需要呼叫 DOM API 去操作 DOM,并且還會額外占用記憶體,but!!!我們可以通過 diff演算法,找到需要更新的最小單位,最大限度地減少DOM操作,而且在大量頻繁資料更新后,并不會立即重流重繪,而是批量操作真實的 DOM,最大限度的減少DOM操作,從而提升性能
- 跨平臺,抽象了原本的渲染程序,提供了一個中間抽象層(runtime-dom/src/nodeOps),使我們可以在不接觸真實DOM 的情況下操作 DOM,實作了跨平臺的能力,而不僅僅局限于瀏覽器的 DOM,可以是安卓和 IOS 的原生組件,也可以是近期很火熱的小程式,
runtime-dom/src/nodeOps 這里存放常見 DOM操作API,不同運行時(瀏覽器、小程式......)提供的具體實作不一樣,最終將操作方法傳遞到 runtime-core中,所以 runtime-core不需要關心平臺相關代碼
update
vm._update 的作用就是把 VNode 渲染成真實的DOM
vm._update 被呼叫的時機有 2 個,一個是首次渲染,一個是資料更新的時候,我們暫時先不考慮資料更新部分
Vue.prototype._update = function (vnode) {
// 將vnode轉化成真實dom
const vm = this
const el = vm.$el
// patch既有初始化元素的功能 ,又有更新元素的功能
vm.$el = patch(el, vnode)
}
vm._update 核心就是呼叫 patch 方法,parentElm 就是 oldVNode 的父元素,即我們的 body 節點,通過 createElm 遞回創建一個完整的 DOM樹 并 插入到 body 節點中,然后洗掉老節點
// 利用vnode創建真實元素
function createElm(vnode) {
let { tag, data, children, text } = vnode
if (typeof tag === 'string') {
// 標簽
vnode.el = document.createElement(tag) // 這里將真實節點和虛擬節點對應起來,后續如果修改屬性了
patchProps(vnode.el, data)
children.forEach(child => {
vnode.el.appendChild(createElm(child))
})
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}
// 對比屬性打補丁
function patchProps(el, props) {
for (let key in props) {
if (key === 'style') {
// { color: 'red', "background": 'yellow' }
for (let styleName in props.style) {
console.log(styleName, props.style[styleName])
el.style[styleName] = props.style[styleName]
}
} else {
el.setAttribute(key, props[key])
}
}
}
// patch既有初始化元素的功能 ,又有更新元素的功能
function patch(oldVNode, vnode) {
// 寫的是初渲染流程
const isRealElement = oldVNode.nodeType
if (isRealElement) {
const elm = oldVNode // 獲取真實元素
const parentElm = elm.parentNode // 拿到父元素
let newElm = createElm(vnode)
parentElm.insertBefore(newElm, elm.nextSibling)
parentElm.removeChild(elm) // 洗掉老節點
return newElm
} else {
// diff演算法,暫時先不考慮
}
}
參考檔案
什么是虛擬DOM?
人間不正經生活手冊轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/548522.html
標籤:其他
下一篇:前端設計模式——享元模式
