Vue渲染過程淺析

Vue 推薦在絕大多數情況下使用 template 來創建你的 HTML。但是模板畢竟是模板,不是真實的dom節點。從模板到真實dom節點還需要經過一些步驟

  1. 把模板編譯爲render函數
  2. 實例進行掛載, 根據根節點render函數的調用,遞歸的生成虛擬dom
  3. 對比虛擬dom,渲染到真實dom
  4. 組件內部data發生變化,組件和子組件引用data作爲props重新調用render函數,生成虛擬dom, 返回到步驟3

第一步: 模板到render

在我們使用Vue的組件化進行開發應用的時候, 如果仔細的查看我們要引入的組件, 例子如下

// App.vue 
<template>
    <div>
        hello word
    </div>
</template>

<script>

export default {
}

</script>

<style>

</style>

在我們的主入口main.js

import Vue from 'vue'
import App from './App'

console.log(App)

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

clipboard.png

我們能夠看到在我們引入的App這個模塊,裏面是一個對象,對象裏面存在一個方法叫做render。在說render函數之前,我們可以想一想,每一次加載一個組件,然後對模板進行解析,解析完後,生成Dom,掛載到頁面上。這樣會導致效率很低效。而使用Vue-cli進行組件化開發,在我們引入組件的後,其實會有一個解析器(vue-loader)對此模板進行了解析,生成了render函數。當然,如果沒有通過解析器解析爲render函數,也沒有關係,在組件第一次掛載的時候,Vue會自己進行解析。源碼請參考: https://github.com/vuejs/vue/... 
這樣,能保證組件每次調用的都是render函數,使用render函數生成VNode。

第二步:虛擬節點VNode

我們把Vue的實例掛載到#app, 會調用實例裏面的render方法,生成虛擬DOM。來看看什麼是虛擬節點,把例子修改一下。

new Vue({
  render: h => {
    let root = h(App)
    console.log('root:', root)
    return root
  }
}).$mount('#app')

clipboard.png

上面生成的VNode就是虛擬節點,虛擬節點裏面有一個屬性elm, 這個屬性指向真實的DOM節點。因爲VNode指向了真實的DOM節點,那麼虛擬節點經過對比後,生成的DOM節點就可以直接進行替換。
這樣有什麼好處呢? 
一個組件對象,如果內部的data發生變化,觸發了render函數,重新生成了VNode節點。那麼就可以直接找到所對應的節點,然後直接替換。那麼這個過程只會在本組件內發生,不會影響其他的組件。於是組件與組件是隔離的。
例子如下:

// main.js
const root = new Vue({
  data: {
    state: true
  },
  mounted() {
    setTimeout(() => {
      console.log(this)
      this.state = false
    }, 1000)
  },
  render: function(h) {
    const { state } = this // state 變化重新觸發render
    let root = h(App)
    console.log('root:', root)
    return root
  }
}).$mount('#app')
// App.vue
<script>
export default {
  render: (h) => {
    let app = h('h1', ['hello world'])
    console.log('app:', app)
    return app
  }
}
</script>

clipboard.png
我們可以看到,當main.js中重新觸發render函數的時候,render方法裏面有引用App.vue這個子組件。但是並沒有觸發App.vue組件的的render函數。

在一個組件內,什麼情況會觸發render?

如何才能觸發組件的render

數據劫持是Vue的一大特色,原理官方已經講的很多了深入響應式原理。在我們給組件的data的屬性進行的賦值的時候(set),此屬性如果在組件內部初次渲染過程被引用(data的屬性被訪問,也就是數據劫持的get), 包括生命週期方法或者render方法。於是會觸發組件的update(beforeUpdate -> render -> updated)。

注: 爲了防止data被多次set從而觸發多次update, Vue把update存放到異步隊列中。這樣就能保證多次data的set只會觸發一次update。

當props會觸發組件的重新渲染是怎麼發生的呢?

把父組件的data通過props傳遞給子組件的時候,子組件在初次渲染的時候生命週期或者render方法,有調用data相關的props的屬性, 這樣子組件也被添加到父組件的data的相關屬性依賴中,這樣父組件的data在set的時候,就相當於觸發自身和子組件的update。
例子如下:

// main.vue
import Vue from 'vue'
import App from './App'

const root = new Vue({
  data: {
    state: false
  },
  mounted() {
    setTimeout(() => {
      this.state = true
    }, 1000)
  },
  render: function(h) {
    const { state } = this // state 變化重新觸發render
    let root = h(App, { props: { status: state } })
    console.log('root:', root)
    return root
  }
}).$mount('#app')

window.root = root
// App.vue
<script>
export default {
  props: {
    status: Boolean
  },
  render: function (h){
    const { status } = this
    let app = h('h1', ['hello world'])
    console.log('app:', app)
    return app
  }
}
</script>

截圖如下:

clipboard.png
main.js中 state 狀態發生了變化,由false => true, 觸發了自身子組件的render方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章