溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue.js模版和數(shù)據(jù)是怎么被渲染成DOM的

發(fā)布時間:2021-11-02 17:33:03 來源:億速云 閱讀:158 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“Vue.js模版和數(shù)據(jù)是怎么被渲染成DOM的”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

前言

Vue.js 一個核心思想是數(shù)據(jù)驅(qū)動。也就是說視圖是由數(shù)據(jù)驅(qū)動生成的,我們對視圖的修改,不會直接操作  DOM,而是通過修改數(shù)據(jù)。當(dāng)交互復(fù)雜的時候,只關(guān)心數(shù)據(jù)的修改會讓代碼的邏輯變的非常清晰,因為 DOM  變成了數(shù)據(jù)的映射,我們所有的邏輯都是對數(shù)據(jù)的修改,而不用碰觸 DOM,這樣的代碼非常利于維護(hù)。

在 Vue.js 中我們可以采用簡潔的模板語法來聲明式的將數(shù)據(jù)渲染為 DOM:

<div id="app">   {{ msg }} </div>
var app = new Vue({   el: '#app',   data: {     msg: 'Hello world!'   } })

結(jié)果頁面上會展示出Hello  world!。這是入門vue.js的時候就知道的知識。那么現(xiàn)在要問vue.js的源碼到底做了什么,才能讓模版和數(shù)據(jù)最終被渲染成了DOM???

從 new Vue() 開始

在寫vue 項目的時候,會在項目的入口文件 main.js文件里實例化一個vue 。如下:

var app = new Vue({   el: '#app',   data: {     msg: 'Hello world!'   }, })

Vue 就是一個用 Function 實現(xiàn)的類。源碼如下:在src/core/instance/index.js中

// _init 方法所在的位置 import { initMixin } from './init'  // Vue就是一個用 Function 實現(xiàn)的類,所以才通過 new Vue 去實例化它。 function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options) }

當(dāng)我們在項目中 new Vue({})傳入一個對象的時候,其實就是執(zhí)行的上面的方法,并傳入?yún)?shù)為 options  ,然后調(diào)用了this._init(options)方法。該方法在src/core/instance/init.js文件中。代碼如下:

import { initState } from './state' Vue.prototype._init = function (options?: Object) {     const vm: Component = this     // 定義了uid     vm._uid = uid++      let startTag, endTag     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {       startTag = `vue-perf-start:${vm._uid}`       endTag = `vue-perf-end:${vm._uid}`       mark(startTag)     }      vm._isVue = true     // 合并options      if (options && options._isComponent) {       initInternalComponent(vm, options)     } else {       // 這里將傳入的options全部合并在$options上。       // 因此我們可以通過$el訪問到 vue 項目中new Vue 中的el       // 通過$options.data 訪問到 vue 項目中new Vue 中的data       vm.$options = mergeOptions(         resolveConstructorOptions(vm.constructor),         options || {},         vm       )     }     if (process.env.NODE_ENV !== 'production') {       initProxy(vm)     } else {       vm._renderProxy = vm     }     // 初始化函數(shù)     vm._self = vm     initLifecycle(vm) // 生命周期函數(shù)     initEvents(vm) // 初始化事件鏈     initRender(vm)     callHook(vm, 'beforeCreate')     initInjections(vm) // resolve injections before data/props     initState(vm)     initProvide(vm) // resolve provide after data/props     callHook(vm, 'created')      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {       vm._name = formatComponentName(vm, false)       mark(endTag)       measure(`vue ${vm._name} init`, startTag, endTag)     }     // 判斷當(dāng)前的$options.el是否有el 也就是說是否傳入掛載的DOM對象     if (vm.$options.el) {       vm.$mount(vm.$options.el)     }   }

由以上代碼可知 this._init(options)主要是合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化  data、props、computed、watcher 等等。重要的部分在代碼里做里注釋。

那么接下來依然從其中一個功能為例進(jìn)行分析:以initState(vm)為例:

為什么在鉤子函數(shù)里可以訪問到 data 里定義的數(shù)據(jù)?

vue 項目中,當(dāng)定義了 data 就可以在組件的鉤子函數(shù) 或者 在 methods 函數(shù)里都可以訪問到data 里定義的屬性。這是為什么??

var app = new Vue({   el: '#app',   data:(){       return{           msg: 'Hello world!'       }   },   mounted(){     console.log(this.msg) // logs 'Hello world!'   },

分析源碼:可以看到this._init(options)方法,在初始化函數(shù)部分有一個  initState(vm)函數(shù)。該方法實在./state.js中:具體代碼如下:

export function initState (vm: Component) {   vm._watchers = []   const opts = vm.$options   // 如果定義了 props 就初始化props;   if (opts.props) initProps(vm, opts.props)   // 如果定義了methods 就初始化methods;   if (opts.methods) initMethods(vm, opts.methods)   if (opts.data) {     // 如果定義了data,就初始化data;(要分析的內(nèi)容從這里開始)     initData(vm)   } else {     observe(vm._data = {}, true /* asRootData */)   }   if (opts.computed) initComputed(vm, opts.computed)   if (opts.watch && opts.watch !== nativeWatch) {     initWatch(vm, opts.watch)   } }

在initState方法中判斷:如果定義了data,就初始化data;繼續(xù)看初始化data 的函數(shù):initData(vm)。代碼如下:

function initData (vm: Component) {  /*    這個data 就是 我們vue 項目中定義的data。也就是上面例子中的    data(){     return {       msg: 'Hello world!'     }   }   */   let data = vm.$options.data   // 拿到data 后,做了判斷,判斷它是不是一個function   data = vm._data = typeof data === 'function'     ? getData(data, vm) // 如果是 執(zhí)行了getData()方法 ,這個方法就是返回data     : data || {}   // 如果不是一個對象則在開發(fā)環(huán)境報出一個警告   if (!isPlainObject(data)) {     data = {}     process.env.NODE_ENV !== 'production' && warn(       'data functions should return an object:\n' +       'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',       vm     )   }   // 拿到data 定義的屬性   const keys = Object.keys(data)    // 拿到props   const props = vm.$options.props   // 拿到 methods   const methods = vm.$options.methods   let i = keys.length   // 做了一個循環(huán)對比,如果在data 上定義的屬性,就不能在props與methods在定義該屬性。因為不管是data里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實例上了。見proxy(vm, `_data`, key)   while (i--) {     const key = keys[i]     if (process.env.NODE_ENV !== 'production') {       if (methods && hasOwn(methods, key)) {         warn(           `Method "${key}" has already been defined as a data property.`,           vm         )       }     }     if (props && hasOwn(props, key)) {       process.env.NODE_ENV !== 'production' && warn(         `The data property "${key}" is already declared as a prop. ` +         `Use prop default value instead.`,         vm       )     } else if (!isReserved(key)) {       proxy(vm, `_data`, key) // 代理 定義了Getter 和 Setter     }   }   // observe data   observe(data, true /* asRootData */) }
// proxy 代理 const sharedPropertyDefinition = {   enumerable: true,   configurable: true,   get: noop,   set: noop } export function proxy (target: Object, sourceKey: string, key: string) {   // 通過對象 sharedPropertyDefinition  定義了Getter 和 Setter   sharedPropertyDefinition.get = function proxyGetter () {     return this[sourceKey][key]     // 當(dāng)訪問vm.key 的時候其實訪問的是 vm[sourceKey][key]     // 以上述開始的問題,當(dāng)訪問this.msg 實際是訪問 this._data.msg   }   sharedPropertyDefinition.set = function proxySetter (val) {     this[sourceKey][key] = val   }   // 對vm 的 key 做了一次Getter 和 Setter   Object.defineProperty(target, key, sharedPropertyDefinition)    }

綜上:初始化 data 實在./state.js文件里。執(zhí)行initState() 方法,該方法判斷如果定義了data,就初始化data。

如果data 是一個function,就執(zhí)行了getData()方法return data.call(vm, vm)。然后對 vm 上的 data  里定義的屬性、vm上的 props 、vm上的methods里的屬性進(jìn)行循環(huán)比對,如果在data  上定義的屬性,就不能在props與methods在定義該屬性。因為不管是data里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實例上了。見proxy(vm,  _data, key)。

然后通過proxy 方法給vm 上的屬性做了Getter 和 Setter 方法的綁定?;氐缴鲜龅膯栴},當(dāng)訪問this.msg 實際是訪問  vm._data.msg。因此在鉤子函數(shù)里確實可以訪問到 data 里定義的數(shù)據(jù)了。

不得不在說一遍,Vue  的初始化邏輯寫的非常清楚,把不同的功能邏輯拆成一些單獨的函數(shù)執(zhí)行,讓主線邏輯一目了然,這樣的編程思想是非常值得借鑒和學(xué)習(xí)的。

其它初始化的內(nèi)容大家可以自己補充,接下來看掛載vm。在初始化的最后,檢測到如果有 el 屬性,則調(diào)用 vm.$mount 方法掛載  vm,掛載的目標(biāo)就是把模板渲染成最終的 DOM,那么接下來探究 Vue 的掛載過程吧

Vue 實例掛載的實現(xiàn)

Vue 中我們是通過 $mount 實例方法去掛載 vm 的。接下來要探究執(zhí)行$mount('#app')的時候,源碼都干了什么???

new Vue({   render: h => h(App), }).$mount('#app')

$mount 方法在多個文件中都有定義,如  src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因為  $mount 這個方法的實現(xiàn)是和平臺、構(gòu)建方式都有關(guān)系。

就選取 compiler 版本的 $mount  分析吧,文件地址在src/platform/web/entry-runtime-with-compiler.js,代碼如下:

// 獲取vue 原型上的 $mount 方法, 存在變量 mount 上。 const mount = Vue.prototype.$mount Vue.prototype.$mount = function (   el?: string | Element,   hydrating?: boolean ): Component {   // query 定義在 './util/index'文件中   // 調(diào)用原生的DOM api querySelector() 方法。最后將el轉(zhuǎn)化為一個DOM 對象。   el = el && query(el)   ...   return mount.call(this, el, hydrating) }

讀代碼可知,代碼首先獲取了 vue 原型上的 $mount 方法,將其存在變量mount中,然后重新定義了該方法。該方法對傳入的el做了處理,el  可以是個字符串,也可以是DOM 對象。然后調(diào)用了 query()方法,該方法在./util/index文件中。主要是調(diào)用原生的DOM api  querySelector() 方法。最后將el轉(zhuǎn)化為一個DOM 對象返回。上述只貼出了主要的代碼部分。

源碼了還對el進(jìn)行了判斷,判斷傳入的el 是否為body 或者 html ,如果是,就會在開發(fā)環(huán)境報一個警告。vue 不可以直接掛載到body  和html上 ,因為會被覆蓋,當(dāng)覆蓋了 html 或 body 整個文檔就會報錯。

源碼還獲取到 $options 判斷是否定義render方法。如果沒有定義 render 方法,則會把 el 或者 template  字符串最終將編譯為render()函數(shù)。

最后 return mount.call(this, el, hydrating)。此處的mount是vue 原型上的 $mount  方法。在文件./runtime/index。代碼如下:

Vue.prototype.$mount = function (   el?: string | Element,   hydrating?: boolean ): Component {   el = el && inBrowser ? query(el) : undefined   return mountComponent(this, el, hydrating) }

其中參數(shù) el 表示掛載的元素,它可以是字符串,也可以是一個DOM 對象。如果是字符串在瀏覽器環(huán)境下會調(diào)用 query() 方法轉(zhuǎn)換成 DOM  對象。第二個參數(shù)是和服務(wù)端渲染相關(guān),在瀏覽器環(huán)境下我們不需要傳第二個參數(shù)。最后return  的時候調(diào)用了mountComponent()方法。該方法定義在src/core/instance/lifecycle.js,代碼如下:

export function mountComponent (   vm: Component,   el: ?Element,   hydrating?: boolean ): Component {     vm.$el = el     ...     let updateComponent     /* istanbul ignore if */     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {       updateComponent = () => {         const name = vm._name         const id = vm._uid         const startTag = `vue-perf-start:${id}`         const endTag = `vue-perf-end:${id}`              mark(startTag)         const vnode = vm._render()         mark(endTag)         measure(`vue ${name} render`, startTag, endTag)              mark(startTag)         vm._update(vnode, hydrating)         mark(endTag)         measure(`vue ${name} patch`, startTag, endTag)       }     } else {       updateComponent = () => {         vm._update(vm._render(), hydrating)       }     }        new Watcher(vm, updateComponent, noop, {     before () {       if (vm._isMounted && !vm._isDestroyed) {         callHook(vm, 'beforeUpdate')       }     }   }, true /* isRenderWatcher */)   hydrating = false    // manually mounted instance, call mounted on self   // mounted is called for render-created child components in its inserted hook   if (vm.$vnode == null) {     vm._isMounted = true     callHook(vm, 'mounted')   }   return vm }

讀代碼可知,該方法首先實例化一個渲染W(wǎng)atcher,在它的回調(diào)函數(shù)中會調(diào)用 updateComponent 方法,在此方法中調(diào)用 vm._render()  方法先生成虛擬DOM節(jié)點,最終調(diào)用 vm._update 更新 DOM。

最后判斷為根節(jié)點的時候設(shè)置 vm._isMounted 為 true, 表示這個實例已經(jīng)掛載了,同時執(zhí)行 mounted 鉤子函數(shù)。 vm.$vnode  表示 Vue 實例的父虛擬節(jié)點,所以它為 Null 則表示當(dāng)前是根 Vue 的實例。

那么vm._render()是怎樣生成虛擬DOM節(jié)點的呢?

_render()渲染虛擬DOM 節(jié)點

在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render()。Vue 的 _render()  是實例的一個私有方法,它用來把實例渲染成一個虛擬DOM節(jié)點。它的定義在 src/core/instance/render.js 文件中,代碼如下:

Vue.prototype._render = function (): VNode {     const vm: Component = this     const { render, _parentVnode } = vm.$options          ...          let vnode     try {       currentRenderingInstance = vm       vnode = render.call(vm._renderProxy, vm.$createElement)     }   }

上述代碼 從vue實例的 $options 上獲取到 render 函數(shù)。通過call()調(diào)用了_renderProxy和  createElement()方法,先來探索createElement()方法。

createElement()

createElement()是在initRender()中。如下:

// 該函數(shù)是在 _init() 過程中執(zhí)行 initRender() // 見 './init.js' 文件中的 initRender(vm) 傳入vm。就執(zhí)行到下面的方法。 export function initRender (vm: Component) {     // 被編譯后生成的render函數(shù)     vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)      // 手寫render函數(shù) 創(chuàng)建 vnode 的方法。     vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)  }

initRender()是在 _init過程中執(zhí)行了initRender()見 ./init.js 文件中的  initRender(vm)傳入vm。

在 vue 項目實際開發(fā)中,手寫 render 函數(shù) 案例如下:

new Vue({   render(createElement){     return createElement('div',{       style:{color:'red'}     },this.msg)   },   data(){     return{       msg:"hello world"     }   } }).$mount('#app')

因為是手寫的render函數(shù)省去了將 template 編譯為 render函數(shù)的過程,因此性能更好。

接下來看_renderProxy方法:

_renderProxy

_renderProxy方法,也是在 init 過程中執(zhí)行的。見文件./init.js中,代碼如下:

import { initProxy } from './proxy'  if (process.env.NODE_ENV !== 'production') {     initProxy(vm) } else {     vm._renderProxy = vm }

如果當(dāng)前環(huán)境為生產(chǎn)環(huán)境 就將 vm 直接賦值給 vm._renderProxy;

如果當(dāng)前環(huán)境為開發(fā)環(huán)境,則執(zhí)行initProxy()。

該函數(shù)在./proxy.js文件中,代碼如下:

initProxy = function initProxy (vm) {     // 判斷瀏覽器是否支持 proxy 。     if (hasProxy) {       // determine which proxy handler to use       const options = vm.$options       const handlers = options.render && options.render._withStripped         ? getHandler         : hasHandler       vm._renderProxy = new Proxy(vm, handlers)     } else {       vm._renderProxy = vm     }   }

首先判斷瀏覽器是否支持 proxy。它是ES6  新增的,用于給目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進(jìn)行過濾和改寫。

如果瀏覽器不支持 proxy, 就將 vm 直接賦值給 vm._renderProxy;

如果瀏覽器支持 proxy,就執(zhí)行new Proxy()。

綜上所述:vm._render 是通過執(zhí)行 createElement 方法并返回虛擬的DOM 節(jié)點。那么什么是虛擬的DOM呢???

虛擬的DOM

在探究vue 的虛擬DOM 之前,先推薦一個虛擬DOM開源庫。有時間,有興趣的朋友可以去深入了解。它是用一個函數(shù)去表示一個應(yīng)用程序的視圖層。view.js  是借鑒它實現(xiàn)了虛擬DOM。從而大大的提升了程序的性能。接下來我們就來看vue.js是怎么做的。

vnode 的定義在 src/core/vdom/vnode.js文件中,如下:

export default class VNode {  tag: string | void;  data: VNodeData | void;  children: ?Array<VNode>;  text: string | void;  elm: Node | void;  ... }

虛擬DOM 是個js對象,是對真實DOM  的一種抽象描述,比如標(biāo)簽名、數(shù)據(jù)、子節(jié)點名等。因為虛擬DOM只是用來映射真實DOM的渲染,所以不包含操作DOM的方法操作DOM的方法。因此更加的輕量,更加的簡單。因為虛擬DOM  的創(chuàng)建是通過createElement方法,那這個環(huán)節(jié)又是如何實現(xiàn)的呢???

createElement

Vue.js 利用 createElement 方法創(chuàng)建 DOM節(jié)點,它定義在  src/core/vdom/create-elemenet.js文件中,代碼如下:

export function createElement (  context: Component, // vm 實例  tag: any, // 標(biāo)簽  data: any, // 數(shù)據(jù)  children: any,// 子節(jié)點 可以構(gòu)造DOM 樹  normalizationType: any,  alwaysNormalize: boolean ): VNode | Array<VNode> {  // 對參數(shù)不一致的處理  if (Array.isArray(data) || isPrimitive(data)) {    normalizationType = children    children = data    data = undefined  }  if (isTrue(alwaysNormalize)) {    normalizationType = ALWAYS_NORMALIZE  }  // 處理好參數(shù),則調(diào)用 _createElement() 去真正的創(chuàng)建節(jié)點。  return _createElement(context, tag, data, children, normalizationType) }

createElement 方法是對 _createElement 方法的封裝,它允許傳入的參數(shù)更加靈活,在處理這些參數(shù)后,調(diào)用真正創(chuàng)建 DOM  節(jié)點的函數(shù)_createElement,代碼如下:

export function _createElement (  context: Component,  tag?: string | Class<Component> | Function | Object,  data?: VNodeData,  children?: any,  normalizationType?: number ): VNode | Array<VNode> {    ...    if (normalizationType === ALWAYS_NORMALIZE) {        children = normalizeChildren(children)    } else if (normalizationType === SIMPLE_NORMALIZE) {        children = simpleNormalizeChildren(children)    }    ... }

_createElement 方法提供 5 個參數(shù)如下:

  • context 表示DOM節(jié)點的上下文環(huán)境,它是 Component 類型;

  • tag 表示標(biāo)簽,它可以是一個字符串,也可以是一個 Component;

  • data 表示 DOM節(jié)點上的數(shù)據(jù),它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義;

  • children 表示當(dāng)前DOM節(jié)點的子節(jié)點,它是任意類型的,它接下來需要被規(guī)范為標(biāo)準(zhǔn)的 VNode 數(shù)組;

  • normalizationType 表示子節(jié)點規(guī)范的類型,類型不同規(guī)范的方法也就不一樣,它主要是參考 render 函數(shù)是編譯生成的還是手寫的  render 函數(shù)。

createElement 函數(shù)的流程略微有點多,本文將重點探究 children 的規(guī)范化以及 VNode 的創(chuàng)建。

children 的規(guī)范化

虛擬DOM(Virtual DOM)實際上是一個樹狀結(jié)構(gòu),每一個DOM 節(jié)點都可能會有若干個子節(jié)點,這些子節(jié)點應(yīng)該也是 VNode 的類型。

_createElement 接收的第 4 個參數(shù) children 是任意類型的,因此我們需要把它們規(guī)范成 VNode 類型。

它是根據(jù) normalizationType 的不同,調(diào)用了 normalizeChildren(children) 和  simpleNormalizeChildren(children) 方法,它們的定義都在  src/core/vdom/helpers/normalzie-children.js文件 中,代碼如下:

// render 函數(shù)是編譯生成的時候調(diào)用 // 拍平數(shù)組為一維數(shù)組 export function simpleNormalizeChildren (children: any) {   for (let i = 0; i < children.length; i++) {     if (Array.isArray(children[i])) {       return Array.prototype.concat.apply([], children)     }   }   return children } // 返回一維數(shù)組 export function normalizeChildren (children: any): ?Array<VNode> {   return isPrimitive(children)     ? [createTextVNode(children)]     : Array.isArray(children)       ? normalizeArrayChildren(children)       : undefined }

simpleNormalizeChildren 方法調(diào)用場景是 render  函數(shù)是編譯生成的。但是當(dāng)子節(jié)點為一個組件的時候,函數(shù)式組件返回的是一個數(shù)組而不是一個根節(jié)點,所以會通過 Array.prototype.concat 方法把整個  children 數(shù)組拍平,讓它的深度只有一層。

normalizeChildren 方法的調(diào)用場景有 2 種,一個場景是手寫 render 函數(shù),當(dāng) children 只有一個節(jié)點的時候,Vue.js  從接口層面允許用戶把 children 寫成基礎(chǔ)類型用來創(chuàng)建單個簡單的文本節(jié)點,這種情況會調(diào)用 createTextVNode 創(chuàng)建一個文本節(jié)點的DOM  節(jié)點;另一個場景是當(dāng)編譯 slot、v-for 的時候會產(chǎn)生嵌套數(shù)組的情況,會調(diào)用 normalizeArrayChildren 方法,代碼如下:

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {   const res = []   let i, c, lastIndex, last   for (i = 0; i < children.length; i++) {     c = children[i]     if (isUndef(c) || typeof c === 'boolean') continue     lastIndex = res.length - 1     last = res[lastIndex]     //  nested     if (Array.isArray(c)) {       if (c.length > 0) {         c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)         // merge adjacent text nodes         if (isTextNode(c[0]) && isTextNode(last)) {           res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)           c.shift()         }         res.push.apply(res, c)       }     } else if (isPrimitive(c)) {       if (isTextNode(last)) {         res[lastIndex] = createTextVNode(last.text + c)       } else if (c !== '') {         res.push(createTextVNode(c))       }     } else {       // 如果兩個節(jié)點都為文本節(jié)點,則合并他們。       if (isTextNode(c) && isTextNode(last)) {         res[lastIndex] = createTextVNode(last.text + c.text)       } else {         if (isTrue(children._isVList) &&           isDef(c.tag) &&           isUndef(c.key) &&           isDef(nestedIndex)) {           c.key = `__vlist${nestedIndex}_${i}__`         }         res.push(c)       }     }   }   return res }

normalizeArrayChildren 接收 2 個參數(shù)。

  • children 表示要規(guī)范的子節(jié)點;

  • nestedIndex 表示嵌套的索引; 因為單個 child可能是一個數(shù)組類型。 normalizeArrayChildren 主要是遍歷  children,獲得單個節(jié)點 c,然后對 c 的類型判斷,如果是一個數(shù)組類型,則遞歸調(diào)用 normalizeArrayChildren;  如果是基礎(chǔ)類型,則通過 createTextVNode 方法轉(zhuǎn)換成 VNode 類型;否則就已經(jīng)是 VNode 類型了,如果 children  是一個列表并且列表還存在嵌套的情況,則根據(jù) nestedIndex 去更新它的 key。

在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續(xù)的 text 節(jié)點,會把它們合并成一個 text 節(jié)點。

到此,children 變成了一個類型為 VNode 的 Array。這就是children 的規(guī)范化。

虛擬的DOM節(jié)點的創(chuàng)建

回到 createElement 函數(shù),規(guī)范化 children 后,接下來就要創(chuàng)建一個DOM實例,代碼如下:

let vnode, ns if (typeof tag === 'string') {   let Ctor   ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)   if (config.isReservedTag(tag)) {     // platform built-in elements     vnode = new VNode(       config.parsePlatformTagName(tag), data, children,       undefined, undefined, context     )   } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {     // component     vnode = createComponent(Ctor, data, context, children, tag)   } else {     // 不認(rèn)識的節(jié)點的處理     vnode = new VNode(       tag, data, children,       undefined, undefined, context     )   } } else {   // direct component options / constructor   vnode = createComponent(tag, data, context, children) }

這里先對 tag 做判斷,如果是 string 類型,則接著判斷如果是內(nèi)置的一些節(jié)點,則直接創(chuàng)建一個普通 VNode,如果是為已注冊的組件名,則通過  createComponent 創(chuàng)建一個組件類型的 VNode,否則創(chuàng)建一個未知的標(biāo)簽的 VNode。 如果 tag是一個 Component 類型,則直接調(diào)用  createComponent 創(chuàng)建一個組件類型的 VNode 節(jié)點。

到這一步,createElement方法就創(chuàng)建好了一個虛擬DOM樹的實例,它用來描述了真實DOM 樹,那么如何渲染為真實的DOM 樹呢???其實它是由  vm._update 完成的。

update把虛擬DOM 渲染為真實DOM

_update 方法是如何把虛擬DOM 渲染為真實DOM 的。這部分代碼在  src/core/instance/lifecycle.js文件中,代碼如下:

_update 方法是如何把虛擬DOM 渲染為真實DOM 的。這部分代碼在 src/core/instance/lifecycle.js文件中,代碼如下: Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {     const vm: Component = this     const prevEl = vm.$el     const prevVnode = vm._vnode     const restoreActiveInstance = setActiveInstance(vm)     vm._vnode = vnode     if (!prevVnode) {       // 數(shù)據(jù)的首次渲染時候執(zhí)行       vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)     }    ...   }

讀代碼可知,當(dāng)數(shù)據(jù)首次渲染的時候,調(diào)用了vm.__patch__()的方法,他接收了四個參數(shù),結(jié)合我們實際vue項目的開發(fā)過程。vm.$el就是 id 為  app 的 DOM 對象,即:

;vnode 對應(yīng)的是調(diào)用 render 函數(shù)的返回值;hydrating 在非服務(wù)端渲染情況下為  false,removeOnly 為 false。

vm.__patch__ 方法在不同的平臺的定義是不一樣的,對 web 平臺的定義在 src/platforms/web/runtime/index.js  中,代碼如下:

// 是否在瀏覽器環(huán)境 Vue.prototype.__patch__ = inBrowser ? patch : noop

在 web 平臺上,是否是服務(wù)端渲染也會對這個方法產(chǎn)生影響。因為在服務(wù)端渲染中,沒有真實的瀏覽器 DOM 環(huán)境,所以不需要把 VNode 最終轉(zhuǎn)換成  DOM,因此是一個空函數(shù),而在瀏覽器端渲染中,它指向了 patch 方法,它的定義在  src/platforms/web/runtime/patch.js文件中,代碼如下:

import * as nodeOps from 'web/runtime/node-ops' import { createPatchFunction } from 'core/vdom/patch' import baseModules from 'core/vdom/modules/index' import platformModules from 'web/runtime/modules/index'  const modules = platformModules.concat(baseModules)  export const patch: Function = createPatchFunction({ nodeOps, modules })

讀代碼可知 createPatchFunction 方法的返回值被傳入了一個對象,其中,

  • nodeOps 封裝了一系列 DOM 操作的方法;

  • modules 定義了模塊的鉤子函數(shù)的實現(xiàn); createPatchFunction方法的定義在  src/core/vdom/patch.js文件中,代碼如下:

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']  export function createPatchFunction (backend) {   let i, j   const cbs = {}    const { modules, nodeOps } = backend    for (i = 0; i < hooks.length; ++i) {     cbs[hooks[i]] = []     for (j = 0; j < modules.length; ++j) {       if (isDef(modules[j][hooks[i]])) {         cbs[hooks[i]].push(modules[j][hooks[i]])       }     }   }        // ...   // 定義了一些輔助函數(shù)         // 當(dāng)調(diào)用 vm.__dispatch__時,其實就是調(diào)用下面的 patch 方法   // 這塊應(yīng)用了函數(shù)柯理化的技巧   return function patch (oldVnode, vnode, hydrating, removeOnly) {     // ...     return vnode.elm   } }

createPatchFunction 內(nèi)部定義了一系列的輔助方法,最終返回了一個 patch 方法,這個方法就賦值給了 vm._update函數(shù)里調(diào)用的  vm.__patch__。也就是說當(dāng)調(diào)用 vm.__dispatch__時,其實就是調(diào)用patch (oldVnode, vnode, hydrating,  removeOnly) 方法,這塊其實是應(yīng)用了函數(shù)柯理化的技巧。

patch 方法接收 4個參數(shù),如下:

  • oldVnode 表示舊的 VNode 節(jié)點,它也可以不存在或者是一個 DOM 對象;

  • vnode 表示執(zhí)行 _render 后返回的 VNode 的節(jié)點;

  • hydrating 表示是否是服務(wù)端渲染;

  • removeOnly 是給 transition-group 用的。

分析patch方法,因為傳入的oldVnode實際上是一個 DOM container,所以 isRealElement 為 true,然后調(diào)用  emptyNodeAt 方法把 oldVnode 轉(zhuǎn)換成 虛擬DOM節(jié)點(一個js對象),然后再調(diào)用 createElm 方法。代碼如下:

if (isRealElement) {     oldVnode = emptyNodeAt(oldVnode) }
function createElm (   vnode,   insertedVnodeQueue,   parentElm,   refElm,   nested,   ownerArray,   index ) {   if (isDef(vnode.elm) && isDef(ownerArray)) {     vnode = ownerArray[index] = cloneVNode(vnode)   }    vnode.isRootInsert = !nested // for transition enter check    const data = vnode.data   const children = vnode.children   const tag = vnode.tag   // 接下來判斷 vnode 是否包含 tag,   // 如果包含,先對tag的合法性在非生產(chǎn)環(huán)境下做校驗,看是否是一個合法標(biāo)簽;   // 然后再去調(diào)用平臺 DOM 的操作去創(chuàng)建一個占位符元素。   if (isDef(tag)) {     if (process.env.NODE_ENV !== 'production') {       if (data && data.pre) {         creatingElmInVPre++       }       if (isUnknownElement(vnode, creatingElmInVPre)) {         warn(           'Unknown custom element: <' + tag + '> - did you ' +           'register the component correctly? For recursive components, ' +           'make sure to provide the "name" option.',           vnode.context         )       }     }      // 調(diào)用 createChildren 方法去創(chuàng)建子元素:     vnode.elm = vnode.ns       ? nodeOps.createElementNS(vnode.ns, tag)       : nodeOps.createElement(tag, vnode)     setScope(vnode)      /* istanbul ignore if */     if (__WEEX__) {       // ...     } else {       // 調(diào)用 createChildren 方法去創(chuàng)建子元素       // 用 createChildren 方法遍歷子虛擬節(jié)點,遞歸調(diào)用 createElm       // 在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節(jié)點占位符傳入。       createChildren(vnode, children, insertedVnodeQueue)       if (isDef(data)) {         invokeCreateHooks(vnode, insertedVnodeQueue)       }       insert(parentElm, vnode.elm, refElm)     }      if (process.env.NODE_ENV !== 'production' && data && data.pre) {       creatingElmInVPre--     }   } else if (isTrue(vnode.isComment)) {     vnode.elm = nodeOps.createComment(vnode.text)     insert(parentElm, vnode.elm, refElm)   } else {     vnode.elm = nodeOps.createTextNode(vnode.text)     insert(parentElm, vnode.elm, refElm)   } }

createElm方法的作用是通過虛擬節(jié)點創(chuàng)建真實的 DOM 并插入到它的父節(jié)點中。判斷 vnode 是否包含 tag,如果包含,先對 tag  的合法性在非生產(chǎn)環(huán)境下做驗證,看是否是一個合法標(biāo)簽;然后再去調(diào)用平臺 DOM 的操作去創(chuàng)建一個占位符元素。然后調(diào)用 createChildren  方法去創(chuàng)建子元素,createChildren方法代碼如下:

createChildren(vnode, children, insertedVnodeQueue)  function createChildren (vnode, children, insertedVnodeQueue) {   if (Array.isArray(children)) {     if (process.env.NODE_ENV !== 'production') {       checkDuplicateKeys(children)     }     for (let i = 0; i < children.length; ++i) {       createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)     }   } else if (isPrimitive(vnode.text)) {     nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))   } }

createChildren方法遍歷子虛擬節(jié)點,遞歸調(diào)用 createElm,在遍歷過程中會把 vnode.elm 作為父容器的 DOM  節(jié)點占位符傳入。然后調(diào)用 invokeCreateHooks方法執(zhí)行所有的 create 的鉤子并把 vnode push 到  insertedVnodeQueue 中。最后調(diào)用 insert 方法把 DOM 插入到父節(jié)點中,因為是遞歸調(diào)用,子元素會優(yōu)先調(diào)用 insert,所以整個  vnode 樹節(jié)點的插入順序是先子后父。insert 方法定義在 src/core/vdom/patch.js 文件中,代碼如下:

insert(parentElm, vnode.elm, refElm)  function insert (parent, elm, ref) {   if (isDef(parent)) {     if (isDef(ref)) {       if (ref.parentNode === parent) {         nodeOps.insertBefore(parent, elm, ref)       }     } else {       nodeOps.appendChild(parent, elm)     }   } }

Vue.js模版和數(shù)據(jù)是怎么被渲染成DOM的

“Vue.js模版和數(shù)據(jù)是怎么被渲染成DOM的”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI