【vue源碼解析】render到底作了什麼?

render的做用

       render函數能夠做爲一道分割線,render函數的左邊能夠稱之爲編譯期,將Vue的模板轉換爲渲染函數。render函數的右邊是Vue的運行時,主要是基於渲染函數生成Virtual DOM樹,Diff和Patch。vue

  • render渲染函數將結合數據生成Virtual DOM的。
  • 有了虛擬的DOM樹後,再交給Patch函數,負責把這些虛擬DOM真正施加到真實的DOM上。在這個過程當中,Vue有自身的響應式系統來偵測在渲染過程當中所依賴到的數據來源。在渲染過程當中,偵測到數據來源以後就能夠精確感知數據源的變更。
  • 根據須要從新進行渲染。當從新進行渲染以後,會生成一個新的樹,將新的樹與舊的樹進行diff對比,就能夠最終落實到真實DOM上的改動。

一個簡單的實例,看render:


vue的渲染機制能夠總結以下圖:

                         

render前置操做

  • 爲什麼要用with(this){}包裹?
  • 什麼時候將render函數的字符串轉成函數?

第1個問題?:node

with一般被當作重複引用同一個對象中的多個屬性的快捷方式,能夠不須要重複引用對象自己。引自《你不知道的JavaScript》上2.2.2節with

with(this)會造成塊級做用域this,render函數裏面的變量都會指向Vue實例(this)數組

第2個問題?:緩存

通過parse生成AST和optimize對AST樹的優化,會生成render函數的字符串。在下圖框出的createCompilerCreator(在src\compiler\create-compiler.js),調用createCompileToFunctionFn將字符串轉成函數。bash



createCompileToFunctionFn(在src\compiler\to-function.js)中,會優先讀緩存信息,若沒有才會執行編譯方法,同時將render字符串經過createFunction(在src\compiler\to-function.js中)調用New Function()的方法,創造render函數,而且緩存信息。
app


function createFunction (code, errors) { 
 try {    
    return new Function(code)  
 } catch (err) {    
    errors.push({ err, code })    
    return noop 
 }
}複製代碼

render詳解

render初始化入口:src\core\instance\index.js


renderMixin(在src\core\instance\render.js)主要作了三件事情

  1. 執行installRenderHelpers(Vue.prototype),在原型上擴展以下,生成vnode節點的幾個函數解析,在執行render函數時,會調用,這裏先簡單的看一個。

    export function renderSlot (  
        name: string,  
        fallback: ?Array<VNode>,  
        props: ?Object,  
        bindObject: ?Object): ?Array<VNode>     { 
           const scopedSlotFn = this.$scopedSlots[name]  
           let nodes
         if (scopedSlotFn) {         
             props = props || {}   
             if (bindObject) {     
                props = extend(extend({}, bindObject), props)    
             }    
             nodes = scopedSlotFn(props) || fallback 
         } else {    
             nodes = this.$slots[name] || fallback  
         }  
         const target = props && props.slot  
         if (target) {    
        //調用createElemnet          return this.$createElement('template', { slot: target }, nodes) 
         } else {    
             return nodes 
         }
    }
    //有做用域插槽,會合並props和須要綁定的對象,否則直接去$solt數組裏取,最後會調用creatElement()複製代碼
  2. 在原型上擴展Vue.prototype.$nextTick方法,在watch監聽數據變化時,不會立馬更新視圖,會推到一個隊列裏,nextTick會觸發視圖的更新
  3.  在原型上擴展Vue.prototype._render。

//核心代碼
vnode = render.call(vm._renderProxy, vm.$createElement)複製代碼

       以上是render函數執行的核心代碼,render歸根結底,是調用createElement建立vnode節點。下面會詳細分析createElement到底作了哪些事情,首先咱們先經過一個例子,來看下,createElement方法的入參,主要爲3個入參,可經過一個例子呈現:dom

  1.  第一個參數是HTML標籤字符 「必選」
  2. 第一個參數是HTML標籤字符 「必選」
  3. 第三個參數是傳涵蓋子元素的一個數組 「可選」

<div id="app">
    <render-element></render-element>
</div>

Vue.component('render-element', {
    render: function (createElement) {
        var self = this
        return createElement(
            'div', // 第一個參數是HTML標籤字符 「必選」
            {
                class: {
                    title: true
                },
                style: {
                    border: '1px solid',
                    padding: '10px'
                }
            }, // 第二個參數是包含模板相關屬性的數據對象 「可選」
            [
                createElement('h1', 'Hello Vue!'),
                createElement('p', '開始學習Vue!')
            ] // 第三個參數是傳涵蓋子元素的一個數組 「可選」
        )
    }
})

let app = new Vue({
    el: '#app'
})複製代碼

createElement的詳細解析過程

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {

    // 兼容不傳data的狀況
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
    }
    if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
        // 調用_createElement建立虛擬節點
        return _createElement(context, tag, data, children, normalizationType)
    }

    function _createElement (context, tag, data, children, normalizationType) {
        // 若是存在data.__ob__,說明data是被Observer觀察的數據,不能用做虛擬節點的data,返回一個空節點
        if (data && data.__ob__) {
            process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context
            )
            return createEmptyVNode()
        }

        // 當組件的tag未空,渲染一個空節點
        if (!tag) {
            return createEmptyVNode()
        }

        // 做用域插槽
        if (Array.isArray(children) && typeof children[0] === 'function') {
            data = data || {}
            data.scopedSlots = { default: children[0] }
            children.length = 0
        }

        // 根據normalizationType的值,選擇不一樣的處理方法
        if (normalizationType === ALWAYS_NORMALIZE) {
            children = normalizeChildren(children)
        } else if (normalizationType === SIMPLE_NORMALIZE) {
            children = simpleNormalizeChildren(children)
        }
        let vnode, ns

        // 若是標籤名是字符串類型
        if (typeof tag === 'string') {
            let Ctor
            // 獲取標籤命名空間
            ns = config.getTagNamespace(tag)

            // 判斷是否爲保留標籤
            if (config.isReservedTag(tag)) {
                // 若是是保留標籤,就建立一個這樣的vnode
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                )

                // vm的components上查找是否有這個標籤的定義
            } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
                // 若是找到了這個標籤的定義,就以此建立虛擬組件節點
                vnode = createComponent(Ctor, data, context, children, tag)
            } else {
                // 兜底方案,正常建立一個vnode
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                )
            }

        // 當tag不是字符串的時候,咱們認爲tag是組件的構造類,直接建立
        } else {
            vnode = createComponent(tag, data, context, children)
        }

        // 若是有vnode
        if (vnode) {
            // 應用namespace,綁定data,而後返回vnode
            if (ns) applyNS(vnode, ns)
            if (isDef(data)) registerDeepBindings(data)
            return vnode
        } else {
            return createEmptyVNode()
        }
    }
}複製代碼

梳理成流程圖以下:函數

特別注意當經過tag判斷爲組件時,會執行createcompontent()oop

總結

最後總結下render函數的編譯的主要幾個步驟:
學習

  1. 將template字符串解析成ast
  2. 優化:將那些不會被改變的節點(statics)打上標記
  3. 生成render函數字符串,並用with包裹(最新版本有改成buble)
  4. 經過new Function的方式生成render函數並緩存

另外本文的重點是createElement如何生成一個vnode,接下來vnode如何映射到正式的dom上,是經過數據變化,通知vm.watcher,最終調用vm.update,最後調用patch方法映射到真實的dom節點中,這裏涉及到數據雙向綁定相關,請詳見[vue]雙向數據綁定。

相關文章
相關標籤/搜索