Vue原始碼剖析——render、patch、updata、vnode

ANFOUNNYSOUL發表於2018-08-02

個人部落格

如有錯誤,希望各位留言指點,樂意之極。

有點亂,各種方法交錯,很難理清順序,請海涵

flow前置

Vue原始碼裡,尤大采用了Flow作為靜態型別檢查,Flowfacebook出品的靜態型別檢查工具。

為什麼要用Flow? 眾所周知,JavaScript是弱型別的語言。

所謂弱型別指的是定義變數時,不需要什麼型別,在程式執行過程中會自動判斷型別,如果一個語言可以隱式轉換它的所有型別,那麼它的變數、表示式等在參與運算時,即使型別不正確,也能通過隱式轉換來得到正確地型別,這對使用者而言,就好像所有型別都能進行所有運算一樣,所以Javascript被稱作弱型別。

可能在初期的時候,這個特點有時候用著很爽,但當你在一個較大的專案中的時候,就會發現這個特性不是一般的麻煩,同事往往不太清楚你所寫的函式到底要哪種型別的引數,而且程式碼重構的時候也很麻煩。

於是基於這個需求有了TypescriptFlow的產生,但是TypeScript學習成本較大,一般來說不會為了一些便利去學習一門語言,所以Facebook在四年前開源了Flow

Vue為什麼要用Flow而不用Typescript開發框架呢?尤雨溪知乎的回答是這樣的

具體怎麼用,可以到這學習檢視中文文件。本文主要講Vue原始碼技巧,不會過多解釋Flow。

專案架構

Vue.js 是一個典型的 MVVM框架,核心思想是資料驅動和元件化。DOM是資料的一種自然對映,在Vue中只需要修改資料即可達到DOM更新的目的。元件化是將頁面上每一個獨立的功能塊或者互動模組視作一個元件,把頁面看做是容器,從而實現搭積木式的開發方式。 把原始碼download到本地我們看下目錄結構

目錄結構

Vue原始碼剖析——render、patch、updata、vnode

Vue原始碼目錄分工明確。整個目錄大致分為

  • benchmarks:處理大量資料時測試Demo
  • dist:各環境所需的版本包
  • examples:用Vue實現的一些實用Demo
  • flow: 資料型別檢測配置
  • packages: 特定環境執行需要單獨安裝的外掛
  • src: 整個原始碼的核心。
  • script: npm指令碼配置檔案
  • test: 測試用例
  • types: 新版typescript配置

核心程式碼都在src目錄下,其中包含例項化、資料響應式處理、模板編譯、事件中心、全域性配置等等都在這個目錄下。

從入口開始

從編譯器,找到根目錄下的package.json檔案,可以看到在script裡有一個dev,這個檔案生成了rollup打包器的配置,

Vue原始碼剖析——render、patch、updata、vnode

rollup -w -c scripts/config.js --environment TARGET:web-full-dev
複製程式碼

rollup表示它使用了rollup打包器,-w表示watch監聽檔案變化,c表示config使用配置檔案來打包,如果後面沒有指定檔案就預設指定rollup.config.js,再後面表示指定scripts/config.js配置rollup,--environment表示設定環境變數,後面攜帶引數TARGET:web-full-dev表示環境變數名和值,我們再到scripts/config.js,可以看到環境變數引數已經帶過來並且觸發了genConfig()函式

Vue原始碼剖析——render、patch、updata、vnode
genConfig()做了什麼

Vue原始碼剖析——render、patch、updata、vnode
其他的隱藏暫時不看,首先const opts = builds[name]builds變數查詢到配置。定義了入口檔案和輸出配置,如果定義了執行環境,就儲存到該欄位。
Vue原始碼剖析——render、patch、updata、vnode
然後在這個檔案裡找到web-full-dev對應的配置是這樣的:它主要申明瞭入口entry和模組定義format、輸出dest、環境名稱env,rollup編譯alias,框架資訊banner,入口是web/entry-runtime-with-compiler.js, 但是在當前目錄並沒有web資料夾,那怎麼找呢?在上面我們可以看到有一個resolve()路徑代理函式

Vue原始碼剖析——render、patch、updata、vnode
利用split切割傳入的檔名匹配引入的alias配置、最終定位到src/platforms/web/entry-runtime-with-compiler.js,找到Vue在這儲存了$mount的方法並且新申明瞭一個$mount的方法,利用儲存的mount方法在底部再次進行掛載處理並將結果返回。為什麼要重新申明,查閱資料後知道原來runtime-only版本並沒有後申明的$mount這部分的處理,這樣的做就可以在保持原有函式的基礎上進行復用,這一點值得我們去學習。

不輕易修改原有邏輯,但是可以將原有的函式儲存起來,再重新宣告。

整體流程

先看大概的整體流程

  • 首次渲染,執行compileToFunctions()將模板template解析成renderFn(render函式),如果renderFn已存在就跳過此部
  • 將renderFn通過vm._render()編譯成Vnode,在讀取其中變數的同時,Watcher通過Object.defindProperty()get方法收集依賴到dep,開始監聽
  • 執行updataComponent(),首先到vdom的patch()方法會將vnode渲染成真實DOM
  • 將DOM掛載到節點上,等待data發生改變
  • data屬性發生變化,首先檢視收集的依賴中是否存在該data值的引用,不存在就不管,存在則觸發Object.defindProperty()set方法修改值並且執行_updata 進行 patch()updataComponent()進行元件更新

Vue原始碼剖析——render、patch、updata、vnode

大致分為

esm 完整構建 :包含模板編譯器,渲染過程 HTML字串 → render函式 → VNode → 真實DOM節點

runtime-only 執行時構建 :不包含模板編譯器,渲染過程 render函式 → VNode → 真實DOM節點

Vue原始碼剖析——render、patch、updata、vnode
runtime-only版本是沒有template=>render這一步的,不帶模板編譯器。

解釋一下各類詞彙

  1. template 模板 :Vue的模板基於純HTML,基於Vue的模板語法,還是可以按照以前HTML式寫結構。
  2. AST 抽象語法樹: Abstract Syntax Tree 的簡稱,主要做三步
    1. parse:Vue使用HTML的Parser將HTML模板解析為AST
    2. optimizer:對AST進行一些優化static靜態節點的標記處理,提取最大的靜態樹,當_update更新介面時,會有一個patch的過程,diff演算法會直接跳過靜態節點,從而減少了patch的過程,優化了patch的效能
    3. generateCode:根據 AST 生成 render 函式
  3. renderFn 渲染函式 :渲染函式是用來生成Virtual DOM(vdom)的。Vue推薦使用模板來構建我們的應用介面,在底層實現中Vue會將模板編譯成renderFn函式,當然我們也可以不寫模板,直接寫渲染函式,以獲得更好的控制
  4. Virtual DOM (vdom,也稱為VNode):虛擬DOM樹,Vue的Virtual DOM Patching演算法是基於 Snabbdom庫 的實現,並在些基礎上作了很多的調整和改進。只能通過RenderFn執行vm._render()生成,patch的目標都是Vnode,並且每個Vnode在全域性都是唯一的
  5. patch:在上面vdom已經說到這個,但還是要說一句,patch是整個virtaul-dom當中最為核心的方法,主要功能是對舊vnode和新vnode進行diff的過程,最後生成新的DOM節點通過updataComponent()方法重新渲染,vue對此做了相當多的效能優化
  6. Watcher (觀察者):每個Vue元件都有一個對應的 Watcher ,這個 Watcher 將會在元件 render 的時候收集元件所依賴的資料,並在依賴有更新的時候,觸發元件vm._updata呼叫patch()進行diff,重新渲染DOM。

不扯廢話,開擼

掛載

Vue原始碼剖析——render、patch、updata、vnode
新掛載$mount的這個方法。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  .....
複製程式碼

key?:value (key: value|void);

el?:string|Element是flow的語法,表示傳入的el字串可以是stringElement以及void型別——undefined型別,hydrating?: boolean同樣,必須是布林型別和undefined

key:?value (key: value|void|null);

表示該key必須為value或者undefined以及null型別。

function():value (:value) :Component表示函式返回值必須為Component型別。

function(key:value1|value2) (key:value1|value2) 表示key必須為value1或者是value2型別。

編譯RenderFn

el = el && query(el)對傳入的el元素節點做了確認,如果傳入的節點容器沒有找到的便警告並且return一個createElement('div')新的div。

//判斷傳入的標籤如果是body或者是頁面根節點
//就警告禁止掛載在頁面根節點上,因為掛載會替換該節點。最後返回該節點
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
    
  const options = this.$options;    
  if (!options.render) {    //如果接受的值已經有寫好的RenderFn,則不用進行任何操作,如果render不存在,就進入此邏輯將模板編譯成renderFn
    let template = options.template
    if (template) {
        ...   //有template就使用idToTemplate()解析,最終返回該節點的innerHTML
      } if (typeof template === 'string') {
        if (template.charAt(0) === '#') {//如果模板取到的第一個字元是#
          template = idToTemplate(template)
          if (process.env.NODE_ENV !== 'production' && !template) {//開發環境並且解析模板失敗的報錯:警告模板為空或者未找到
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      }else if (template.nodeType) {
        //如果有節點型別,判定是普通節點,也返回innerHTML
        template = template.innerHTML  
      } else {  
        //沒有template就警告該模板無效
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
        //如果是節點的話,獲取html模板片段,getOuterHTML()對傳入的el元素做了相容處理,最終目的是拿到節點的outerHTML
        //getOuterHTML()可以傳入DOM節點,CSS選擇器,HTML片段
      template = getOuterHTML(el)
    }
    if (template) {
     //編譯HTML生成renderFn,賦給options,vm.$options.render此時發生變化
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        //開始標記
        mark('compile')
      }
      /*  compileToFunctions()主要是將getOuterHTML獲取的模板編譯成RenderFn函式,該函式的具體請往後翻看
       *  具體步驟之後再說,編譯大致主要分成三步 
       *  1.parse:將 html 模板解析成抽象語法樹(AST)。
       *  2.optimizer:對 AST 做優化處理。
       *  3.generateCode:根據 AST 生成 render 函式。
       */ 
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render;  //最後將解析的renderFn 賦值給當前例項
      options.staticRenderFns = staticRenderFns //編譯的配置
      
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        //結束標記
        mark('compile end')
        //根據mark()編譯過程計算耗時差,用於到控制檯performance檢視階段渲染效能
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
    //最後返回之前儲存的mount()方法進行掛載,如果此前renderFn存在就直接進行此步驟
    return mount.call(this, el, hydrating)
}
複製程式碼

這裡最重要的就是compileToFunctions()將template編譯成RenderFn,該方法請通過目錄跳轉檢視。

Vue原始碼剖析——render、patch、updata、vnode
本段程式碼對template的多種寫法做相容處理,最終取到renderFn,過程中順帶進行效能埋點等輔助功能。 最後return mount.call(...)這個在

  import Vue from './runtime/index'
複製程式碼

編譯的過程比較複雜,之後再說。到這發現Vue的原型方法並不是在這建立的,我們需要到上一級 src/platforms/runtime/index.js,

// 配置了一些全域性的方法
Vue.config.mustUseProp = mustUseProp 
Vue.config.isReservedTag = isReservedTag 
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// 安裝平臺的指令和元件
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// 如果在瀏覽器裡,證明不是服務端渲染,新增__patch__方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 掛載$mount方法。
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    //必須在瀏覽器環境才返回該節點,runtime-only版本會直接執行到這
  el = el && inBrowser ? query(el) : undefined
  
  return mountComponent(this, el, hydrating)
}
複製程式碼

hydrating這個傳參可以全域性性的理解為,服務端渲染,預設false。 最後進行mountComponent(this, el, hydrating)其實就是對元件進行一個updatewatcher的過程。具體看下mountComponent做了什麼。找到src/core/instance/lifecycle.js,這個檔案負責為例項新增生命週期類函式.

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean 
): Component {
  vm.$el = el  //首先將vm.$el將傳入的el做快取,$el現在為真實的node
  if (!vm.$options.render) {
    //因為最後只認renderFn,如果沒有的話,就建立一個空節點Vnode
    vm.$options.render = createEmptyVNode
    
    if (process.env.NODE_ENV !== 'production') {//開發環境下
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        /*
         (如果定義了template但是template首位不是'#')或者(沒有傳入element),就會警告當前使用的是runtime-only版本,
         預設不帶編譯功能,如果需要編譯的話,則需要更換構建版本,下面類似
         */
      } else {
        warn(//掛載元件失敗:template或者renderFn未定義
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 在掛載之前為當前例項初始化beforMount生命週期
  callHook(vm, 'beforeMount');
  
  // 宣告瞭一個 updateComponent 方法,這個是將要被 Watcher例項呼叫的更新元件的方法。
  // 根據效能的對比配置不同的更新方法,
  // performance+mark可以用於分析Vue元件在不同階段中花費的時間,進而知道哪裡可以優化。
  let updateComponent 
  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();//生成一個Vnode
      mark(endTag);//標記結束節點
      
      
      //做performance命名'vue ${name} render',這樣就可以在proformance中檢視應用程式的執行狀況、渲染效能,最後刪除標記和度量
      measure(`vue ${name} render`, startTag, endTag);
     
      mark(startTag);
      vm._update(vnode, hydrating);
     
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag);
    }
  } else {
    updateComponent = () => {
    // 定義一個渲染watcher函式
    // vm._render()裡會呼叫render函式,並返回一個VNode,在生成VNode的過程中,會動態計算getter,同時推入到dep裡面進行資料監聽,每次資料更新後都出觸發當前例項的_updata進行元件更新
    // _update()方法會將新vnode和舊vnode進行diff比較,最後完成dom的更新工作,該方法請往下移步
      vm._update(vm._render(), hydrating)
    }
  }
  /* 新建一個_watcher物件,將監聽目標推入dep,vm例項上掛載的_watcher主要是為了更新DOM呼叫當前vm的_watcher 的 update 方法。用來強制更新。為什麼叫強制更新呢?
   * vue裡面有判斷,如果newValue == oldValue, 那麼就不觸發watcher更新檢視了
   * vm:當前例項
   * updateComponent:用來將vnode更新到之前的dom上
   * noop:無效函式,可以理解為空函式
   * {before(){...}}:配置,如果該例項已經掛載了,就配置beforeUpdate生命週期鉤子函式
   * true:主要是用來判斷是哪個watcher的。因為computed計算屬性和如果你要在options裡面配置watch了同樣也是使用了 new Watcher ,加上這個用以區別這三者
   */
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true )
  hydrating = false  //關閉服務端渲染,服務端渲染只有created()和beforeCreate()
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製程式碼

這個函式的具體作用就是掛載節點,並對data做響應式處理。 至於為什麼會有個判斷語句來根據條件宣告 updateComponent方法,其實從 performance 可以看出,其中一個方法是用來測試renderupdate 效能的。便於在Chrome=>performance中檢視渲染效能

process.env.NODE_ENV !== 'production' && config.performance && mark
複製程式碼

首先判斷當前的環境和是否配置支援performance,然後呼叫markmeasure方法,其中mark封裝了一個方法,具體的API可以參考MDN performance,給當前元素做一個標記,然後返回一個具體的時間點,主要功能是效能埋點

if (process.env.NODE_ENV !== 'production') {
    //判斷當前瀏覽器runtime是否支援performace
  const perf = inBrowser && window.performance
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag);//標記該節點
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag)
      //作效能埋點後,刪除所有的標記和度量
      perf.clearMarks(startTag)
      perf.clearMarks(endTag)
      perf.clearMeasures(name)
    }
  }
}
複製程式碼

至於剛才的vm._update()在上面lifecyle.js已經定義了

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    //首先接收vnode
    const vm: Component = this
    const prevEl = vm.$el;//真實的dom節點
    const prevVnode = vm._vnode;//之前舊的vnode
    const prevActiveInstance = activeInstance;// null
    activeInstance = vm;//獲取當前的例項
    vm._vnode = vnode;//當前新的vnode 
    if (!prevVnode) {
      // 如果需要diff的舊vnode不存在,就無法進行__patch__
      // 因此需要用新的vnode建立一個真實的dom節點
      vm.$el = vm.__patch__(
                        vm.$el, //真實的dom節點
                        vnode,  //傳入的vnode
                        hydrating, //是否服務端渲染
                        false /* removeOnly是一個只用於 <transition-group> 的特殊標籤,確保移除元素過程中保持一個正確的相對位置。 */)
    } else {
      // 如果需要diff的prevVnode存在,那麼首先對prevVnode和vnode進行diff
      // 並將需要的更新的dom操作已patch的形式打到prevVnode上,並完成真實dom的更新工作
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    
    activeInstance = prevActiveInstance;//
    // 如果存在真實的dom節點
    if (prevEl) {
      //就將之前的__vue__清空,再掛載新的
      prevEl.__vue__ = null
    }
    // 將更新後的vm掛載到的vm__vue__上快取
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // 如果當前例項的$vnode與父元件的_vnode相同,也要更新其$el
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
複製程式碼

怎麼進行patch?

__patch__是整個整個virtaul-dom當中最為核心的方法了,主要功能是對prevVnode(舊vnode)新vnode進行diff的過程,經過patch比對,最後生成新的真實dom節點更新改變部分的檢視。 在/packages/factory.js裡,定義了patch(),程式碼過多,只摘取重要部分,目前清楚流程即可,vue2.0+是參考snabbdom建立的patch虛擬dom演算法

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    //用到的引數,oldVnode:舊的vnode、vnode:新的vnode、hydrating:服務端渲染、removeOnly:避免誤操作
    //當新的vnode不存在,並且舊的vnode存在時,直接返回舊的vnode,不做patch
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    var insertedVnodeQueue = [];

    //如果舊的vnode不存在
    if (isUndef(oldVnode)) {
      //就建立一個新的節點
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
      //獲取舊vnode的節點型別
      var isRealElement = isDef(oldVnode.nodeType);
      // 如果不是真實的dom節點並且屬性相同
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 對oldVnode和vnode進行diff,並對oldVnode打patch
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
      } 
      }
    }
    //最後返回新vnode的節點內容
    return vnode.elm
  }
複製程式碼

這是一個基本的patch,它的目標轉到/src/core/vdom/patch.jspatchVnode(), 並且通過sameVnode()可以預先比對舊vnode新vnode兩者的基礎屬性,這個方法決定了接下來是否需要對oldVnodevnode進行diff

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}
複製程式碼

只有當基本屬性相同的情況下才認為這個2個vnode只是區域性發生了更新,然後才會對這2個vnode進行diff,如果2個vnode的基本屬性存在不一致的情況,那麼就會直接跳過diff的過程,進而依據vnode新建一個真實的dom,同時刪除老的節點。 首次渲染的時候,oldVnode並不存在,所以直接進行domcreateElm(vnode, insertedVnodeQueue, parentElm, refElm);建立一個新的節點,相反,存在oldVnode,當oldVnodevnode都存在且sameVnode(oldVnode, vnode)2個節點的基本屬性相同,那麼就進入了2個節點的diff過程。

/src/core/vdom/patch.js裡定義裡patchVnode函式

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /* 
    * 比較新舊vnode節點,根據不同的狀態對dom做合理的更新操作(新增,移動,刪除)整個過程還會依次呼叫prepatch,update,postpatch等鉤子函式,在編譯階段生成的一些靜態子樹
    * 在這個過程中由於不會改變而直接跳過比對,動態子樹在比較過程中比較核心的部分就是當新舊vnode同時存在children,通過updateChildren方法對子節點做更新,
    * @param oldVnode 舊vnode
    * @param vnode    新vnode
    * @param insertedVnodeQueue  空陣列,用於生命週期 inserted 階段,記錄下所有新插入的節點以備呼叫
    * @param removeOnly 是一個只用於 <transition-group> 的特殊標籤,確保移除元素過程中保持一個正確的相對位置。
    */
    if (oldVnode === vnode) {
      return
    }
    
    const elm = vnode.elm = oldVnode.elm
    // 非同步佔位
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    
    //如果新vnode和舊vnode都是靜態節點,key相同,或者新vnode是一次性渲染或者克隆節點,那麼直接替換該元件例項並返回
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    // 可以往下翻去看vnode的例子,data是節點屬性,包含class style attr和指令等
    let i
    const data = vnode.data
    // 如果元件例項存在屬性並且存在prepatch鉤子函式就更新attrs/style/class/events/directives/refs等屬性
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    //如果新的vnode帶有節點屬性,isPatchable返回是否含有元件例項的tag標籤,兩者滿足
    if (isDef(data) && isPatchable(vnode)) {
      // cbs儲存了hooks鉤子函式: 'create', 'activate', 'update', 'remove', 'destroy'
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 取出cbs儲存的update鉤子函式,依次呼叫,更新attrs/style/class/events/directives/refs等屬性
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    //如果vnode沒有文字節點
    if (isUndef(vnode.text)) {
      //如果舊vnode和新vnode的子節點都存在
      if (isDef(oldCh) && isDef(ch)) {
        // 如果子節點不同,updateChildren就對子節點進行diff
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        //如果只存在新vnode
      } else if (isDef(ch)) {
        // 先將舊節點的文字清空
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 然後將vnode的children放進去
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 如果只存在舊vnode
      } else if (isDef(oldCh)) {
        // 就刪除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        // 如果只有舊vnode的文字內容
      } else if (isDef(oldVnode.text)) {
        // 直接清空內容
        nodeOps.setTextContent(elm, '')
      }
      // 如果是兩者文字內容不同
    } else if (oldVnode.text !== vnode.text) {
      // 直接更新vnode的文字內容
      nodeOps.setTextContent(elm, vnode.text)
    }
    // 更新完畢後,執行 data.hook.postpatch 鉤子,表明 patch 完畢
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
複製程式碼

Vue原始碼剖析——render、patch、updata、vnode
通過比對新舊vnode節點屬性、子元素、節點型別和內容等多種方式進行patch,過程中使用hooks更新節點屬性。 理一下邏輯 原始碼中新增了一些註釋便於理解,來理一下邏輯。

  1. 如果兩個vnode相等,不需要 patch。
  2. 如果是非同步佔位,執行 hydrate 方法或者定義 isAsyncPlaceholder 為 true,然後退出。
  3. 如果兩個vnode都為靜態,不用更新,所以將以前的 componentInstance 例項傳給當前 vnode。 退出patch
  4. 執行 prepatch 鉤子。
  5. 依次遍歷呼叫 update 回撥,執行 update鉤子。更新attrs/style/class/events/directives/refs等屬性。
  6. 如果兩個 vnode 都有 children,且 vnode 沒有 text 文字內容、兩個 vnode 不相等,執行 updateChildren 方法。這是虛擬 DOM 的關鍵。
  7. 如果新 vnode 有 children,而老的沒有,清空文字,並新增 vnode 節點。
  8. 如果老 vnode 有 children,而新的沒有,清空文字,並移除 vnode 節點。
  9. 如果兩個 vnode 都沒有 children,老 vnode 有 text ,新 vnode 沒有 text ,則清空 DOM 文字內容。
  10. 如果老 vnode 和新 vnode 的 text 不同,更新 DOM 元素文字內容。
  11. 呼叫 postpatch 鉤子告知patch完畢。

updateChildren

這個有點繞

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    /*
     * @ parentElm 父元素
     * @ oldCh  舊子節點
     * @ newCh  新子節點
     * @ insertedVnodeQueue 記錄下所有新插入的節點以備呼叫
     * @ removeOnly 是僅由<transition-group>使用的特殊標誌,在離開過渡期間,確保刪除的元素保持正確的相對位置
     */
    let oldStartIdx = 0  //oldStartIdx => 舊頭索引
    let newStartIdx = 0   //newStartIdx => 新頭索引
    let oldEndIdx = oldCh.length - 1 //oldEndIdx => 舊尾索引
    let oldStartVnode = oldCh[0] // 舊首索引節點,第一個
    let oldEndVnode = oldCh[oldEndIdx] // 舊尾索引節點,最後一個
    let newEndIdx = newCh.length - 1 //newEndIdx => 新尾索引
    let newStartVnode = newCh[0] // 新首索引節點,第一個
    let newEndVnode = newCh[newEndIdx] // 新首索引節點,最後一個

    // 可以理解為
    // 1. 舊子節點陣列的 startIndex, endIndex, startNode, endNode
    // 2. 新子節點陣列的 startIndex, endIndex, startNode, endNode

    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    //可以進行移動
    const canMove = !removeOnly  

    if (process.env.NODE_ENV !== 'production') {
      //首先會檢測新子節點有沒有重複的key
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]

        //如果舊首索引節點和新首索引節點相同
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        //對舊頭索引節點和新頭索引節點進行diff更新, 從而達到複用節點效果
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        //舊頭索引向後
        oldStartVnode = oldCh[++oldStartIdx]
        //新頭索引向後
        newStartVnode = newCh[++newStartIdx]
                //如果舊尾索引節點和新尾索引節點相似,可以複用
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        //舊尾索引節點和新尾索引節點進行更新
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        //舊尾索引向前
        oldEndVnode = oldCh[--oldEndIdx]
        //新尾索引向前
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        /*  有一種情況,如果
          * 舊【5,1,2,3,4】
          * 新【1,2,3,4,5】,那豈不是要全刪除替換一遍 5->1,1->2...?
          * 即便有key,也會出現[5,1,2,3,4]=>[1,5,2,3,4]=>[1,2,5,3,4]...這樣太耗費效能了
          * 其實我們只需要將5插入到最後一次操作即可
        */
        // 對舊首索引和新尾索引進行patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 舊vnode開始插入到真實DOM中,舊首向右移,新尾向左移
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 同上中可能,舊尾索引和新首也存在相似可能
        // 對舊首索引和新尾索引進行patch
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 舊vnode開始插入到真實DOM中,新首向左移,舊尾向右移
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //如果上面的判斷都不通過,我們就需要key-index表來達到最大程度複用了
         //如果不存在舊節點的key-index表,則建立
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
         //找到新節點在舊節點組中對應節點的位置
        idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          //如果新節點在舊節點中不存在,就建立一個新元素,我們將它插入到舊首索引節點前(createElm第4個引數)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 如果舊節點有這個新節點
          vnodeToMove = oldCh[idxInOld]
            // 將新節點和新首索引進行比對,如果型別相同就進行patch
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 然後將舊節點組中對應節點設定為undefined,代表已經遍歷過了,不在遍歷,否則可能存在重複插入的問題
            oldCh[idxInOld] = undefined
            // 如果不存在group群體偏移,就將其插入到舊首節點前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 型別不同就建立節點,並將其插入到舊首索引前(createElm第4個引數)
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        //將新首往後移一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    //當舊首索引大於舊尾索引時,代表舊節點組已經遍歷完,將剩餘的新Vnode新增到最後一個新節點的位置後
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } //如果新節點組先遍歷完,那麼代表舊節點組中剩餘節點都不需要,所以直接刪除
      else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
複製程式碼

Vnode

/src/core/vdom/vnode.js中有定義Vnode屬性

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag //標籤屬性
    this.data = data  //渲染成真實DOM後,節點上到class attr style 事件等...
    this.children = children //子節點,也上vnode
    this.text = text  // 文字
    this.elm = elm  //對應著真實的dom節點
    this.ns = undefined //當前節點的namespace(名稱空間)
    this.context = context //編譯的作用域
    this.fnContext = undefined // 函式化元件上下文
    this.fnOptions = undefined // 函式化元件配置項
    this.fnScopeId = undefined // 函式化元件ScopeId
    this.key = data && data.key  //只有繫結資料下存在,在diff的過程中可以提高效能
    this.componentOptions = componentOptions // 通過vue元件生成的vnode物件,若是普通dom生成的vnode,則此值為空
    this.componentInstance = undefined  //當前元件例項
    this.parent = undefined // vnode、元件的佔位節點
    this.raw = false    //是否為原生HTML或只是普通文字
    this.isStatic = false  //靜態節點標識 || keep-alive
    this.isRootInsert = true    // 是否作為根節點插入
    this.isComment = false  // 是否為註釋節點
    this.isCloned = false  //是否為克隆節點
    this.isOnce = false    //是否為v-once節點
    this.asyncFactory = asyncFactory // 非同步工廠方法
    this.asyncMeta = undefined //非同步Meta
    this.isAsyncPlaceholder = false //是否為非同步佔位

  }

  //容器例項向後相容的別名
  get child (): Component | void {
    return this.componentInstance
  }
}
複製程式碼

其他屬性不重要,最主要的上tag、data、children、key、text這幾個屬性。 VNode可以具體氛圍以下幾類

  • TextVNode 文字節點。
  • ElementVNode 普通元素節點。
  • ComponentVNode 元件節點。
  • EmptyVNode 沒有內容的註釋節點。
  • CloneVNode 克隆節點,可以是以上任意型別的節點,唯一的區別在於isCloned屬性為true 我們先定義一個vnode
 {
    tag: 'div'
    data: {
        id: 'app',
        class: 'test'
    },
    children: [
        {
            tag: 'span',
            data:{
                
            },
            text: 'this is test'
        }
    ]
}
複製程式碼

每一層物件都是一個節點。vnode

    {
        tag:'標籤1',
        attrs:{
            屬性key1:屬性value1,
            屬性key2:屬性value2,
            ...
        },
        children:[
            {
                tag:'子標籤1',
                attrs:{
                    子屬性key1:子屬性value1,
                    子屬性key2:子屬性value2,
                    ...
                },
                children:[
                    {
                        ....
                    }
                ]
            },
            {
                tag:'子標籤2',
                attrs:{
                    子屬性key1:子屬性value1,
                    子屬性key2:子屬性value2,
                    ...
                },
                children:[
                    {
                        ....
                    }
                ]
            }
        ]
    }
複製程式碼

以巢狀遞迴的方式產生最後渲染成

<div id="app" class="test">
    <span>this is test</span>
</div>
複製程式碼

Vue元件樹建立起來的整個VNode樹是唯一的。這意味著,手寫render函式不能元件化

render: function (createElement) {
  var myVnode = createElement('p', 'hi')
  return createElement('div', [
    myVnode, myVnode
  ])
}
複製程式碼

而官方的做法是可以用工廠函式進行

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
複製程式碼

為什麼要這麼做? 筆者的理解是,createElement是建立了一個子 Vnode物件,此時的Vnode已經是唯一的,你如果再重複去使用到元件中,就造成了不唯一。

看了一下,有點門路。順帶解釋下 可能你會覺得這樣寫太麻煩了,直接Array(20).map()多輕鬆。但是

new Array(20).map(function(v,i){
	console.log(v,i);//不會輸出任何東西,
})
複製程式碼

map只對有值(包括undefined)的下標項才會去依次遍歷,最後按先後順序再組成陣列,因為new Array(20)的話,陣列內的值沒有初始化,列印輸出結果為[ empty * 20]。所以map後不會列印出任何東西,因為沒有被初始化,都是空值,被略過了。

但是,第二種方式,Array.apply(null, {length: 20}),輸出為[undefined,undefined,undefined....*20],是一個已經被初始化值、包含20個undefined的陣列。再加上map(),也就是說每一次都是Array.apply(null,[undefined,undefined,.....],再熟悉一點就是Array(undefined,undefined,...*20),通過return去迴圈createElement建立20個vnode

為什麼要寫這麼複雜?ES6的Array.from能做到,但是作者應該是考慮到相容還是用了ES5就能夠辦到的事情。感嘆尤大的基礎功力...

compileToFunctions(template編譯成render)

首先在/src/platforms/web/compiler/index.js有定義compileToFunctions()方法,

// 設定編譯的選項,不設定則使用預設配置,配置項比較多
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// 通過模板匯入配置生成AST和Render
const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
複製程式碼

先看匯入的配置

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}
複製程式碼

可以看到定義了compilecompileToFunctions,前者是AST語法樹,後者是是編譯好的renderFn

import { parse } from './parser/index' // 將 HTML template解析為AST
import { optimize } from './optimizer'  // 對AST優化標記處理,提取最大的靜態樹
import { generate } from './codegen/index' // 根據 AST 生成 render 函式
import { createCompilerCreator } from './create-compiler' //允許建立使用替代編譯器,在這隻使用預設部件匯出預設編譯器

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // parseHTML 的過程,匯入配置,將template去掉空格,解析成AST ,最後返回AST元素物件
  const ast = parse(template.trim(), options)
  console.log(ast)
  
  // 預設開始優化標記處理,否則不進行優化
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 拿到最終的code。裡面包含renderFn和靜態renderFn
  const code = generate(ast, options)
  console.log(code.render)
  
  //丟擲
  return { 
    ast, 
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
複製程式碼

createCompilerCreator()接受一個函式引數,createCompiler用以建立編譯器,返回值是compile以及compileToFunctions

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,//模板
      options?: CompilerOptions // 編譯配置
    ): CompiledResult {
    
    // 將finalOptions的隱式原型__proto__指向baseOptions物件
      const finalOptions = Object.create(baseOptions) 
      const errors = []
      const tips = []
      
      finalOptions.warn = (msg, tip) => {
        (tip ? tips : errors).push(msg)
      }
      
      // 如果匯入了配置就將配置進行合併  
      if (options) {
        // 合併分支模組
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // 合併自定義指令
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // 合併其他配置
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }
      // 將傳入的函式執行,傳入模板和配置項,得到編譯結果
      const compiled = baseCompile(template, finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast))
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
複製程式碼

最後在compile()層級執行完畢後,將丟擲編譯函式

Vue原始碼剖析——render、patch、updata、vnode

compile是一個編譯器,它會將傳入的template轉換成對應的AST樹、renderFn以及staticRenderFns函式

compileToFunctions,通過執行createCompileToFunctionFn(compile)得到,createCompileToFunctionFn()是帶快取的編譯器,同時staticRenderFns以及renderFn會被轉換成Funtion物件。最終將編譯

不同平臺有一些不同的options,所以createCompiler會根據平臺區分傳入一個baseOptions,會與compile本身傳入的options進行合併得到最終的finalOptions

export function createCompileToFunctionFn (compile: Function): Function {
  // 宣告快取器
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 合併配置
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    //開發環境下嘗試檢測CSP,類似於使用者瀏覽器設定,需要放寬限制否則無法進行編譯,一般情況下可以忽略
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }
    //有快取的時候優先讀取快取的結果,並且返回 ,
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // 沒有快取結果則直接編譯 
    const compiled = compile(template, options)

    // 檢查編譯錯誤/提示 
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:\n\n${template}\n\n` +
          compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // 將程式碼轉換成功能 
    const res = {}
    const fnGenErrors = []
    // 將render轉換成Funtion物件
    res.render = createFunction(compiled.render, fnGenErrors)
    // 將staticRenderFns全部轉化成Funtion物件
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    //檢查函式生成錯誤。只在編譯器本身存在錯誤時才會發生,作者主要用於codegen開發使用
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }
    //最後存放在快取中,下一次用可以進行讀取
    return (cache[key] = res)
  }
}
複製程式碼

Vue原始碼剖析——render、patch、updata、vnode
這裡有一點很有意思,

const cache = Object.create(null)

為什麼不直接const cache = {}呢?我們感受下

Vue原始碼剖析——render、patch、updata、vnode
最直觀的感受就是隱式原型__proto__在null上面沒有,首先const cache = {}會繼承Object.prototype上所有的原型方法。而null不會,另一個使用Object.create(null)的理由是,使用for..in迴圈的時候會遍歷物件原型鏈上的屬性,使用Object.create(null)就不必再對屬性進行檢查了,當然,我們也可以直接使用Object.keys[]。除非你想你需要一個非常乾淨且高度可定製的物件當作資料字典或者想節省hasOwnProperty的一些效能損失。

HTML轉RenderFn

我們先寫點程式碼

<div id="app"></div>
  <script>
      var vm = new Vue({
        el:'#app',
        template:`
          <div @click="changeName()">
            <span>{{name}}</span>
            <ul>
              <li v-for="(item,index) in like" :key="index">{{item}}</li>
            </ul>
          </div>`,
        data:{
          name:'Seven',
          like:['旅遊','電影','滑雪']
        },methods:{
          changeName(){
            this.name = 'Floyd'
          }
        }
      })
    </script>
複製程式碼

我們先看下他的AST語法樹,

Vue原始碼剖析——render、patch、updata、vnode

可能你看的有點頭暈,沒事,我們無需關心這個,抽象,能讓你看懂了還叫抽象? 我們再看下render函式

with(this){return _c('div',{on:{"click":function($event){changeName()}}},[_c('span',[_v(_s(name))]),_v(" "),_c('ul',_l((like),function(item,index){return _c('li',{key:index},[_v(_s(item))])}))])}
複製程式碼

為了方便大家看清楚結構,費會勁手動格式化以下

with(this) {
      return _c('div', 
                {
                  on: {
                    "click": function ($event) {
                      changeName()
                    }
                  }
                }, 
                [
                  _c('span', [ _v(_s(name)) ]), 
                  _v(" "), 
                  _c('ul', 
                      _l( (like), function (item, index) {
                      return _c('li', 
                                  {
                                    key: index
                                  }, 
                                  [
                                    _v( _s(item) )
                                  ]
                                )
                    })
                  )
                ]
              )
    }
複製程式碼

可能有些人想著更看不懂,沒事,這個邏輯可以看懂的。

_c(
    '標籤名',
    {
        on:{//繫結
            屬性1:值,
            屬性2:值,
            ...
        }
    },
    [//子節點
       _c(
            '標籤名',
            {
                on:{//繫結
                    子屬性1:值,
                    子屬性2:值,
                    ...
                }
            },
            [
                //子標籤...
            ]
        }
    ]
)

複製程式碼

將renderFn編譯Vnode

由於使用的with(this)語法,函式內有所變數都依賴於this變數,_c等同與this._c等同與vm._c,我們列印下vm._c

JavaScript語言精粹一書中提到,儘量不要在你的函式內使用with()語法,它可能會讓你的應用程式無法除錯。但是尤雨溪這麼使用,使用閉包將其封裝在了函式內,無需擔心外洩。

ƒ (a, b, c, d) { return createElement(vm, a, b, c, d, false); }

/src/core/instance/render.js定義該方法
// 將 createElement 函式繫結到這個例項上以便在其中獲得renderFn上下文。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
複製程式碼

指向createElement()函式,它又指向_createElement(),該函式定義在/src/core/vdom/create-element.js。最終返回的是一個Vnode。該函式定義可以在本文目錄裡跳轉檢視 其他的函式我們可以在 /rc/core/instance/render-helper/index.js裡找到相關定義

export function installRenderHelpers (target: any) {
  target._o = markOnce   // v-once靜態元件
  target._n = toNumber   // 判斷是否數字,先parse再isNAN
  target._s = toString   // 需解析的文字,之前在parser階段已經有所修飾
  target._l = renderList //  v-for節點
  target._t = renderSlot // slot節點
  target._q = looseEqual //  檢測兩個變數是否相等
  target._i = looseIndexOf // 檢測陣列中是否包含與目標變數相等的項
  target._m = renderStatic // 渲染靜態內容
  target._f = resolveFilter // filters處理
  target._k = checkKeyCodes // 從config配置中檢查eventKeyCode是否存在
  target._b = bindObjectProps // 合併v-bind指令到VNode中
  target._v = createTextVNode  // 建立文字節點
  target._e = createEmptyVNode // 註釋節點
  target._u = resolveScopedSlots // 處理ScopedSlots
  target._g = bindObjectListeners // 處理事件繫結
}
複製程式碼

createElement

var SIMPLE_NORMALIZE = 1;
var ALWAYS_NORMALIZE = 2;

function createElement (
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  // 相容不傳data的情況
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
  }
  // 如果alwaysNormalize是true
  // 那麼normalizationType應該設定為常量ALWAYS_NORMALIZE的值
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
   // 呼叫_createElement建立虛擬節點
  return _createElement(context, tag, data, children, normalizationType)
}

function _createElement (
  context,
  tag,
  data,
  children,
  normalizationType
) {
   /*
    * 如果存在data.__ob__,說明data是被Observer觀察的資料
    * 不能用作虛擬節點的data
    * 需要丟擲警告,並返回一個空節點
    * 
    * 被監控的data不能被用作vnode渲染的資料的原因是:data在vnode渲染過程中可能會被改變,這樣會觸發監控,導致不符合預期的操作
    * 
    */
  if (isDef(data) && isDef((data).__ob__)) {
    "development" !== '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()
  }
  // 當元件的is屬性被設定為一個false的值
  if (isDef(data) && isDef(data.is)) {
    tag = data.is;
  }
  // Vue將不會知道要把這個元件渲染成什麼,所以渲染一個空節點
  if (!tag) {
    return createEmptyVNode()
  }
  // 如果key是原始值,就警告key不能是原始值,必須string或者是number型別的值
  if ("development" !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      );
    }
  }
    // 作用域插槽
    // 如果子元素是陣列並且第一個是renderFn,就將其轉移到scopedSlots
  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);
  }
  var vnode, ns;
  //如果標籤名是string型別
  if (typeof tag === 'string') {
    var Ctor;
    // 取到如果當前有自己的vnode和名稱空間 或者 獲取標籤名的名稱空間
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    // 判斷是否為保留標籤
    if (config.isReservedTag(tag)) {
       // 如果是保留標籤,就建立一個這樣的vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );// 如果不是保留標籤,那麼我們將嘗試從vm例項的components上查詢是否有這個標籤的定義,自定義元件
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        //  如果找到了這個標籤的定義,就以此建立虛擬元件節點
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
       // 保底方案,正常建立一個vnode
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      );
    }
  } else {
    // direct component options / constructor
    // 當tag不是字串的時候,就是元件的構造類,直接建立
    vnode = createComponent(tag, data, context, children);
  }
  // 如果vnode是陣列,直接返回。
  if (Array.isArray(vnode)) {
    return vnode
              //如果有vnode
  } else if (isDef(vnode)) {
     // 如果有namespace,就應用下namespace,然後返回vnode
    if (isDef(ns)) { applyNS(vnode, ns); }
    // 如果定義了資料,就將其深度遍歷,針對於class或者是style
    if (isDef(data)) { registerDeepBindings(data); }
    return vnode
  } else {
    //保底建立空VNode
    return createEmptyVNode()
  }
}
複製程式碼

流程圖看下

Vue原始碼剖析——render、patch、updata、vnode

new Vue

找到src/core/instance/index.js

Vue原始碼剖析——render、patch、updata、vnode
建立Vue函式,並且檢測當前是不是開發環境,如果Vue不是通過new例項化的將警告。然後初始化this._init(options)。為什麼(this instanceof Vue)這一句可以判斷是否使用了new操作符?

已new來呼叫建構函式會經歷4個步驟:

  • 建立一個新物件;
  • 將建構函式的作用域賦給新物件(因此this 就指向了這個新物件);
  • 執行建構函式中的程式碼(為這個新物件新增屬性);
  • 返回新物件。 而instanceof用來檢測Vue建構函式的prototype是否存在於this的原型鏈上,換句話說,如果使用new例項化的時候,this就指向了這個新建立的物件,這時this instanceof Vue這句話的意思就是判斷新建立的物件是否是Vue型別的,也就相當於判斷新例項物件的constructor是否是Vue建構函式。

未完待續...持續更新

相關文章