Vue源碼剖析——render、patch、updata、vnode

我的Bloghtml

若有錯誤,但願各位留言指點,樂意之極。vue

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

flow前置

Vue源碼裏,尤大采用了Flow做爲靜態類型檢查,Flowfacebook出品的靜態類型檢查工具。git

爲何要用Flow? 衆所周知,JavaScript是弱類型的語言。github

所謂弱類型指的是定義變量時,不須要什麼類型,在程序運行過程當中會自動判斷類型,若是一個語言能夠隱式轉換它的全部類型,那麼它的變量、表達式等在參與運算時,即便類型不正確,也能經過隱式轉換來獲得正確地類型,這對使用者而言,就好像全部類型都能進行全部運算同樣,因此Javascript被稱做弱類型。web

可能在初期的時候,這個特色有時候用着很爽,但當你在一個較大的項目中的時候,就會發現這個特性不是通常的麻煩,同事每每不太清楚你所寫的函數到底要哪一種類型的參數,並且代碼重構的時候也很麻煩。算法

因而基於這個需求有了TypescriptFlow的產生,可是TypeScript學習成本較大,通常來講不會爲了一些便利去學習一門語言,因此Facebook在四年前開源了Flowtypescript

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

具體怎麼用,能夠到這學習查看中文文檔。本文主要講Vue源碼技巧,不會過多解釋Flow。json

項目架構

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

目錄結構

Vue源碼目錄分工明確。整個目錄大體分爲

  • benchmarks:處理大量數據時測試Demo
  • dist:各環境所需的版本包
  • examples:用Vue實現的一些實用Demo
  • flow: 數據類型檢測配置
  • packages: 特定環境運行須要單獨安裝的插件
  • src: 整個源碼的核心。
  • script: npm腳本配置文件
  • test: 測試用例
  • types: 新版typescript配置

核心代碼都在src目錄下,其中包含實例化、數據響應式處理、模板編譯、事件中心、全局配置等等都在這個目錄下。

從入口開始

從編譯器,找到根目錄下的package.json文件,能夠看到在script裏有一個dev,這個文件生成了rollup打包器的配置,

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()函數

genConfig()作了什麼

其餘的隱藏暫時不看,首先 const opts = builds[name]builds變量查找到配置。定義了入口文件和輸出配置,若是定義了運行環境,就儲存到該字段。
而後在這個文件裏找到 web-full-dev對應的配置是這樣的:它主要申明瞭入口 entry和模塊定義 format、輸出 dest、環境名稱 env,rollup編譯 alias,框架信息 banner,入口是 web/entry-runtime-with-compiler.js, 可是在當前目錄並無 web文件夾,那怎麼找呢?在上面咱們能夠看到有一個 resolve()路徑代理函數

利用 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()進行組件更新

大體分爲

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

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

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。

不扯廢話,開擼

掛載

新掛載$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,該方法請經過目錄跳轉查看。

本段代碼對 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)
    }
  }
複製代碼

經過比對新舊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()層級執行完畢後,將拋出編譯函數

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)
  }
}
複製代碼

這裏有一點頗有意思,

const cache = Object.create(null)

爲何不直接const cache = {}呢?咱們感覺下

最直觀的感覺就是隱式原型__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語法樹,

可能你看的有點頭暈,沒事,咱們無需關心這個,抽象,能讓你看懂了還叫抽象? 咱們再看下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()
  }
}
複製代碼

流程圖看下

new Vue

找到src/core/instance/index.js

建立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構造函數。

未完待續...持續更新

相關文章
相關標籤/搜索