国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

從template到DOM(Vue.js源碼角度看內(nèi)部運行機(jī)制)

Betta / 1102人閱讀

摘要:根據(jù)樹生成所需的內(nèi)部包含與首先會將模板進(jìn)行得到一個語法樹,再通過做一些優(yōu)化,最后通過得到以及。會用正則等方式解析模板中的指令等數(shù)據(jù),形成語法樹。是將語法樹轉(zhuǎn)化成字符串的過程,得到結(jié)果是的字符串以及字符串。

寫在前面

這篇文章算是對最近寫的一系列Vue.js源碼的文章(https://github.com/answershuto/learnVue)的總結(jié)吧,在閱讀源碼的過程中也確實受益匪淺,希望自己的這些產(chǎn)出也會對同樣想要學(xué)習(xí)Vue.js源碼的小伙伴有所幫助。之前這篇文章同樣在我司(大搜車)的技術(shù)博客中發(fā)表過,歡迎大家關(guān)注我司的技術(shù)博客,給個傳送門https://blog.souche.com/。

因為對Vue.js很感興趣,而且平時工作的技術(shù)棧也是Vue.js,這幾個月花了些時間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。

文章的原地址:https://github.com/answershuto/learnVue。

在學(xué)習(xí)過程中,為Vue加上了中文的注釋https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對其他想學(xué)習(xí)Vue源碼的小伙伴有所幫助。

可能會有理解存在偏差的地方,歡迎提issue指出,共同學(xué)習(xí),共同進(jìn)步。

從new一個Vue對象開始
let vm = new Vue({
    el: "#app",
    /*some options*/
});

很多同學(xué)好奇,在new一個Vue對象的時候,內(nèi)部究竟發(fā)生了什么?

究竟Vue.js是如何將data中的數(shù)據(jù)渲染到真實的宿主環(huán)境環(huán)境中的?

又是如何通過“響應(yīng)式”修改數(shù)據(jù)的?

template是如何被編譯成真實環(huán)境中可用的HTML的?

Vue指令又是執(zhí)行的?

帶著這些疑問,我們從Vue的構(gòu)造類開始看起。

Vue構(gòu)造類
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)
}

Vue的構(gòu)造類只做了一件事情,就是調(diào)用_init函數(shù)進(jìn)行

來看一下init的代碼

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    /*一個防止vm實例自身被觀察的標(biāo)志位*/
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    /*初始化生命周期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*調(diào)用beforeCreate鉤子函數(shù)并且觸發(fā)beforeCreate鉤子事件*/
    callHook(vm, "beforeCreate")
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed與watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*調(diào)用created鉤子函數(shù)并且觸發(fā)created鉤子事件*/
    callHook(vm, "created")

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      /*格式化組件名*/
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      /*掛載組件*/
      vm.$mount(vm.$options.el)
    }
  }

_init主要做了這兩件事:

1.初始化(包括生命周期、事件、render函數(shù)、state等)。

2.$mount組件。

在生命鉤子beforeCreate與created之間會初始化state,在此過程中,會依次初始化props、methods、data、computed與watch,這也就是Vue.js對options中的數(shù)據(jù)進(jìn)行“響應(yīng)式化”(即雙向綁定)的過程。對于Vue.js響應(yīng)式原理不了解的同學(xué)可以先看一下筆者的另一片文章《Vue.js響應(yīng)式原理》。

/*初始化props、methods、data、computed與watch*/
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props)
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods)
  /*初始化data*/
  if (opts.data) {
    initData(vm)
  } else {
    /*該組件沒有data的時候綁定一個空對象*/
    observe(vm._data = {}, true /* asRootData */)
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch) initWatch(vm, opts.watch)
}
雙向綁定

以initData為例,對option的data的數(shù)據(jù)進(jìn)行雙向綁定Oberver,其他option參數(shù)雙向綁定的核心原理是一致的。

function initData (vm: Component) {

  /*得到data數(shù)據(jù)*/
  let data = vm.$options.data
  data = vm._data = typeof data === "function"
    ? getData(data, vm)
    : data || {}

  /*判斷是否是對象*/
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== "production" && warn(
      "data functions should return an object:
" +
      "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
      vm
    )
  }

  // proxy data on instance
  /*遍歷data對象*/
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length

  //遍歷data中的數(shù)據(jù)
  while (i--) {
    /*保證data中的key不與props中的key重復(fù),props優(yōu)先,如果有沖突會產(chǎn)生warning*/
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== "production" && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
      /*判斷是否是保留字段*/

      /*這里是我們前面講過的代理,將data上面的屬性代理到了vm實例上*/
      proxy(vm, `_data`, keys[i])
    }
  }
  /*Github:https://github.com/answershuto*/
  // observe data
  /*從這里開始我們要observe了,開始對數(shù)據(jù)進(jìn)行綁定,這里有尤大大的注釋asRootData,這步作為根數(shù)據(jù),下面會進(jìn)行遞歸observe進(jìn)行對深層對象的綁定。*/
  observe(data, true /* asRootData */)
}

observe會通過defineReactive對data中的對象進(jìn)行雙向綁定,最終通過Object.defineProperty對對象設(shè)置setter以及getter的方法。getter的方法主要用來進(jìn)行依賴收集,對于依賴收集不了解的同學(xué)可以參考筆者的另一篇文章《依賴收集》。setter方法會在對象被修改的時候觸發(fā)(不存在添加屬性的情況,添加屬性請用Vue.set),這時候setter會通知閉包中的Dep,Dep中有一些訂閱了這個對象改變的Watcher觀察者對象,Dep會通知Watcher對象更新視圖。

如果是修改一個數(shù)組的成員,該成員是一個對象,那只需要遞歸對數(shù)組的成員進(jìn)行雙向綁定即可。但這時候出現(xiàn)了一個問題,?如果我們進(jìn)行pop、push等操作的時候,push進(jìn)去的對象根本沒有進(jìn)行過雙向綁定,更別說pop了,那么我們?nèi)绾伪O(jiān)聽數(shù)組的這些變化呢?
Vue.js提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數(shù)組方法。修改數(shù)組原型方法的代碼可以參考o(jì)bserver/array.js以及observer/index.js。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    //.......

    if (Array.isArray(value)) {
      /*
          如果是數(shù)組,將修改后可以截獲響應(yīng)的數(shù)組方法替換掉該數(shù)組的原型中的原生方法,達(dá)到監(jiān)聽數(shù)組數(shù)據(jù)變化響應(yīng)的效果。
          這里如果當(dāng)前瀏覽器支持__proto__屬性,則直接覆蓋當(dāng)前數(shù)組對象原型上的原生數(shù)組方法,如果不支持該屬性,則直接覆蓋數(shù)組對象的原型。
      */
      const augment = hasProto
        ? protoAugment  /*直接覆蓋原型的方法來修改目標(biāo)對象*/
        : copyAugment   /*定義(覆蓋)目標(biāo)對象或數(shù)組的某一個方法*/
      augment(value, arrayMethods, arrayKeys)

      /*如果是數(shù)組則需要遍歷數(shù)組的每一個成員進(jìn)行observe*/
      this.observeArray(value)
    } else {
      /*如果是對象則直接walk進(jìn)行綁定*/
      this.walk(value)
    }
  }
}

/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
 /*直接覆蓋原型的方法來修改目標(biāo)對象或數(shù)組*/
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
/*定義(覆蓋)目標(biāo)對象或數(shù)組的某一個方法*/
function copyAugment (target: Object, src: Object, keys: Array) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
/*
 * not type checking this file because flow doesn"t play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from "../util/index"

/*取得原生數(shù)組的原型*/
const arrayProto = Array.prototype
/*創(chuàng)建一個新的數(shù)組對象,修改該對象上的數(shù)組的七個方法,防止污染原生數(shù)組方法*/
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
 /*這里重寫了數(shù)組的這些方法,在保證不污染原生數(shù)組原型的情況下重寫數(shù)組的這些方法,截獲數(shù)組的成員發(fā)生的變化,執(zhí)行原生數(shù)組操作的同時dep通知關(guān)聯(lián)的所有觀察者進(jìn)行響應(yīng)式處理*/
;[
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
]
.forEach(function (method) {
  // cache original method
  /*將數(shù)組的原生方法緩存起來,后面要調(diào)用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*調(diào)用原生的數(shù)組方法*/
    const result = original.apply(this, args)

    /*數(shù)組新插入的元素需要重新進(jìn)行observe才能響應(yīng)式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case "push":
        inserted = args
        break
      case "unshift":
        inserted = args
        break
      case "splice":
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
      
    // notify change
    /*dep通知所有注冊的觀察者進(jìn)行響應(yīng)式處理*/
    ob.dep.notify()
    return result
  })
})

從數(shù)組的原型新建一個Object.create(arrayProto)對象,通過修改此原型可以保證原生數(shù)組方法不被污染。如果當(dāng)前瀏覽器支持__proto__這個屬性的話就可以直接覆蓋該屬性則使數(shù)組對象具有了重寫后的數(shù)組方法。如果沒有該屬性的瀏覽器,則必須通過遍歷def所有需要重寫的數(shù)組方法,這種方法效率較低,所以優(yōu)先使用第一種。

在保證不污染不覆蓋數(shù)組原生方法添加監(jiān)聽,主要做了兩個操作,第一是通知所有注冊的觀察者進(jìn)行響應(yīng)式處理,第二是如果是添加成員的操作,需要對新成員進(jìn)行observe。

但是修改了數(shù)組的原生方法以后我們還是沒法像原生數(shù)組一樣直接通過數(shù)組的下標(biāo)或者設(shè)置length來修改數(shù)組,Vue.js提供了$set()及$remove()方法。

對于更具體的講解數(shù)據(jù)雙向綁定以及Dep、Watcher的實現(xiàn)可以參考筆者的文章《從源碼角度再看數(shù)據(jù)綁定》。

template編譯

在$mount過程中,如果是獨立構(gòu)建構(gòu)建,則會在此過程中將template編譯成render function。當(dāng)然,你也可以采用運行時構(gòu)建。具體參考運行時-編譯器-vs-只包含運行時。

template是如何被編譯成render function的呢?

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  /*parse解析得到ast樹*/
  const ast = parse(template.trim(), options)
  /*
    將AST樹進(jìn)行優(yōu)化
    優(yōu)化的目標(biāo):生成模板AST樹,檢測不需要進(jìn)行DOM改變的靜態(tài)子樹。
    一旦檢測到這些靜態(tài)樹,我們就能做以下這些事情:
    1.把它們變成常數(shù),這樣我們就再也不需要每次重新渲染時創(chuàng)建新的節(jié)點了。
    2.在patch的過程中直接跳過。
 */
  optimize(ast, options)
  /*根據(jù)ast樹生成所需的code(內(nèi)部包含render與staticRenderFns)*/
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

baseCompile首先會將模板template進(jìn)行parse得到一個AST語法樹,再通過optimize做一些優(yōu)化,最后通過generate得到render以及staticRenderFns。

parse

parse的源碼可以參見https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53。

parse會用正則等方式解析template模板中的指令、class、style等數(shù)據(jù),形成AST語法樹。

optimize

optimize的主要作用是標(biāo)記static靜態(tài)節(jié)點,這是Vue在編譯過程中的一處優(yōu)化,后面當(dāng)update更新界面時,會有一個patch的過程,diff算法會直接跳過靜態(tài)節(jié)點,從而減少了比較的過程,優(yōu)化了patch的性能。

generate

generate是將AST語法樹轉(zhuǎn)化成render funtion字符串的過程,得到結(jié)果是render的字符串以及staticRenderFns字符串。

具體的template編譯實現(xiàn)請參考《聊聊Vue.js的template編譯》。

Watcher到視圖

Watcher對象會通過調(diào)用updateComponent方法來達(dá)到更新視圖的目的。這里提一下,其實Watcher并不是實時更新視圖的,Vue.js默認(rèn)會將Watcher對象存在一個隊列中,在下一個tick時更新異步更新視圖,完成了性能優(yōu)化。關(guān)于nextTick感興趣的小伙伴可以參考《Vue.js異步更新DOM策略及nextTick》。

updateComponent = () => {
    vm._update(vm._render(), hydrating)
}

updateComponent就執(zhí)行一句話,_render函數(shù)會返回一個新的Vnode節(jié)點,傳入_update中與舊的VNode對象進(jìn)行對比,經(jīng)過一個patch的過程得到兩個VNode節(jié)點的差異,最后將這些差異渲染到真實環(huán)境形成視圖。

什么是VNode?

VNode

在刀耕火種的年代,我們需要在各個事件方法中直接操作DOM來達(dá)到修改視圖的目的。但是當(dāng)應(yīng)用一大就會變得難以維護(hù)。

那我們是不是可以把真實DOM樹抽象成一棵以JavaScript對象構(gòu)成的抽象樹,在修改抽象樹數(shù)據(jù)后將抽象樹轉(zhuǎn)化成真實DOM重繪到頁面上呢?于是虛擬DOM出現(xiàn)了,它是真實DOM的一層抽象,用屬性描述真實DOM的各個特性。當(dāng)它發(fā)生變化的時候,就會去修改視圖。

但是這樣的JavaScript操作DOM進(jìn)行重繪整個視圖層是相當(dāng)消耗性能的,我們是不是可以每次只更新它的修改呢?所以Vue.js將DOM抽象成一個以JavaScript對象為節(jié)點的虛擬DOM樹,以VNode節(jié)點模擬真實DOM,可以對這顆抽象樹進(jìn)行創(chuàng)建節(jié)點、刪除節(jié)點以及修改節(jié)點等操作,在這過程中都不需要操作真實DOM,只需要操作JavaScript對象,大大提升了性能。修改以后經(jīng)過diff算法得出一些需要修改的最小單位,再將這些小單位的視圖進(jìn)行更新。這樣做減少了很多不需要的DOM操作,大大提高了性能。

Vue就使用了這樣的抽象節(jié)點VNode,它是對真實DOM的一層抽象,而不依賴某個平臺,它可以是瀏覽器平臺,也可以是weex,甚至是node平臺也可以對這樣一棵抽象DOM樹進(jìn)行創(chuàng)建刪除修改等操作,這也為前后端同構(gòu)提供了可能。

先來看一下Vue.js源碼中對VNode類的定義。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component"s scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*當(dāng)前節(jié)點的標(biāo)簽名*/
    this.tag = tag
    /*當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/
    this.data = data
    /*當(dāng)前節(jié)點的子節(jié)點,是一個數(shù)組*/
    this.children = children
    /*當(dāng)前節(jié)點的文本*/
    this.text = text
    /*當(dāng)前虛擬節(jié)點對應(yīng)的真實dom節(jié)點*/
    this.elm = elm
    /*當(dāng)前節(jié)點的名字空間*/
    this.ns = undefined
    /*編譯作用域*/
    this.context = context
    /*函數(shù)化組件作用域*/
    this.functionalContext = undefined
    /*節(jié)點的key屬性,被當(dāng)作節(jié)點的標(biāo)志,用以優(yōu)化*/
    this.key = data && data.key
    /*組件的option選項*/
    this.componentOptions = componentOptions
    /*當(dāng)前節(jié)點對應(yīng)的組件的實例*/
    this.componentInstance = undefined
    /*當(dāng)前節(jié)點的父節(jié)點*/
    this.parent = undefined
    /*簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false*/
    this.raw = false
    /*靜態(tài)節(jié)點標(biāo)志*/
    this.isStatic = false
    /*是否作為跟節(jié)點插入*/
    this.isRootInsert = true
    /*是否為注釋節(jié)點*/
    this.isComment = false
    /*是否為克隆節(jié)點*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

這是一個最基礎(chǔ)的VNode節(jié)點,作為其他派生VNode類的基類,里面定義了下面這些數(shù)據(jù)。

tag: 當(dāng)前節(jié)點的標(biāo)簽名

data: 當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息

children: 當(dāng)前節(jié)點的子節(jié)點,是一個數(shù)組

text: 當(dāng)前節(jié)點的文本

elm: 當(dāng)前虛擬節(jié)點對應(yīng)的真實dom節(jié)點

ns: 當(dāng)前節(jié)點的名字空間

context: 當(dāng)前節(jié)點的編譯作用域

functionalContext: 函數(shù)化組件作用域

key: 節(jié)點的key屬性,被當(dāng)作節(jié)點的標(biāo)志,用以優(yōu)化

componentOptions: 組件的option選項

componentInstance: 當(dāng)前節(jié)點對應(yīng)的組件的實例

parent: 當(dāng)前節(jié)點的父節(jié)點

raw: 簡而言之就是是否為原生HTML或只是普通文本,innerHTML的時候為true,textContent的時候為false

isStatic: 是否為靜態(tài)節(jié)點

isRootInsert: 是否作為跟節(jié)點插入

isComment: 是否為注釋節(jié)點

isCloned: 是否為克隆節(jié)點

isOnce: 是否有v-once指令

打個比方,比如說我現(xiàn)在有這么一個VNode樹

{
    tag: "div"
    data: {
        class: "test"
    },
    children: [
        {
            tag: "span",
            data: {
                class: "demo"
            }
            text: "hello,VNode"
        }
    ]
}

渲染之后的結(jié)果就是這樣的

hello,VNode

更多操作VNode的方法,請參考《VNode節(jié)點》。

patch

最后_update會將新舊兩個VNode進(jìn)行一次patch的過程,得出兩個VNode最小的差異,然后將這些差異渲染到視圖上。

首先說一下patch的核心diff算法,diff算法是通過同層的樹節(jié)點進(jìn)行比較而非對樹進(jìn)行逐層搜索遍歷的方式,所以時間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。

這兩張圖代表舊的VNode與新VNode進(jìn)行patch的過程,他們只是在同層級的VNode之間進(jìn)行比較得到變化(第二張圖中相同顏色的方塊代表互相進(jìn)行比較的VNode節(jié)點),然后修改變化的視圖,所以十分高效。

在patch的過程中,如果兩個VNode被認(rèn)為是同一個VNode(sameVnode),則會進(jìn)行深度的比較,得出最小差異,否則直接刪除舊有DOM節(jié)點,創(chuàng)建新的DOM節(jié)點。

什么是sameVnode?

我們來看一下sameVnode的實現(xiàn)。

/*
  判斷兩個VNode節(jié)點是否是同一個節(jié)點,需要滿足以下條件
  key相同
  tag(當(dāng)前節(jié)點的標(biāo)簽名)相同
  isComment(是否為注釋節(jié)點)相同
  是否data(當(dāng)前節(jié)點對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息)都有定義
  當(dāng)標(biāo)簽是的時候,type必須相同
*/
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)
  )
}

// Some browsers do not support dynamically changing type for 
// so they need to be treated as different nodes
/*
  判斷當(dāng)標(biāo)簽是的時候,type是否相同
  某些瀏覽器不支持動態(tài)修改類型,所以他們被視為不同類型
*/
function sameInputType (a, b) {
  if (a.tag !== "input") return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB
}

當(dāng)兩個VNode的tag、key、isComment都相同,并且同時定義或未定義data的時候,且如果標(biāo)簽為input則type必須相同。這時候這兩個VNode則算sameVnode,可以直接進(jìn)行patchVnode操作。

patchVnode的規(guī)則是這樣的:

1.如果新舊VNode都是靜態(tài)的,同時它們的key相同(代表同一節(jié)點),并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換elm以及componentInstance即可。

2.新老節(jié)點均有children子節(jié)點,則對子節(jié)點進(jìn)行diff操作,調(diào)用updateChildren,這個updateChildren也是diff的核心。

3.如果老節(jié)點沒有子節(jié)點而新節(jié)點存在子節(jié)點,先清空老節(jié)點DOM的文本內(nèi)容,然后為當(dāng)前DOM節(jié)點加入子節(jié)點。

4.當(dāng)新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候,則移除該DOM節(jié)點的所有子節(jié)點。

5.當(dāng)新老節(jié)點都無子節(jié)點的時候,只是文本的替換。

updateChildren
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, elmToMove, refElm

    // removeOnly is a special flag used only by 
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    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)) {
        /*前四種情況其實是指定key的時候,判定為同一個VNode,則直接patchVnode即可,分別比較oldCh以及newCh的兩頭節(jié)點2*2=4種情況*/
        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
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          生成一個key與舊VNode的key對應(yīng)的哈希表(只有第一次進(jìn)來undefined的時候會生成,也為后面檢測重復(fù)的key值做鋪墊)
          比如childre是這樣的 [{xx: xx, key: "key0"}, {xx: xx, key: "key1"}, {xx: xx, key: "key2"}]  beginIdx = 0   endIdx = 2  
          結(jié)果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode節(jié)點存在key并且這個key在oldVnode中能找到則返回這個節(jié)點的idxInOld(即第幾個節(jié)點,下標(biāo))*/
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode沒有key或者是該key沒有在老節(jié)點中找到則創(chuàng)建一個新的節(jié)點*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*獲取同key的老節(jié)點*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !elmToMove) {
            /*如果elmToMove不存在說明之前已經(jīng)有新節(jié)點放入過這個key的DOM中,提示可能存在重復(fù)的key,確保v-for的時候item有唯一的key值*/
            warn(
              "It seems there are duplicate keys that is causing an update error. " +
              "Make sure each v-for item has a unique key."
            )
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            /*Github:https://github.com/answershuto*/
            /*如果新VNode與得到的有相同key的節(jié)點是同一個VNode則進(jìn)行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因為已經(jīng)patchVnode進(jìn)去了,所以將這個老節(jié)點賦值undefined,之后如果還有新節(jié)點與該節(jié)點key相同可以檢測出來提示已有重復(fù)的key*/
            oldCh[idxInOld] = undefined
            /*當(dāng)有標(biāo)識位canMove實可以直接插入oldStartVnode對應(yīng)的真實DOM節(jié)點前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時候(比如說tag不一樣或者是有不一樣type的input標(biāo)簽),創(chuàng)建一個新的節(jié)點*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /*全部比較完成以后,發(fā)現(xiàn)oldStartIdx > oldEndIdx的話,說明老節(jié)點已經(jīng)遍歷完了,新節(jié)點比老節(jié)點多,所以這時候多出來的新節(jié)點需要一個一個創(chuàng)建出來加入到真實DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比較完成以后發(fā)現(xiàn)newStartIdx > newEndIdx,則說明新節(jié)點已經(jīng)遍歷完了,老節(jié)點多余新節(jié)點,這個時候需要將多余的老節(jié)點從真實DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

直接看源碼可能比較難以捋清其中的關(guān)系,我們通過圖來看一下。

首先,在新老兩個VNode節(jié)點的左右頭尾兩側(cè)都有一個變量標(biāo)記,在遍歷過程中這幾個變量都會向中間靠攏。當(dāng)oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時結(jié)束循環(huán)。

索引與VNode節(jié)點的對應(yīng)關(guān)系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode

在遍歷中,如果存在key,并且滿足sameVnode,會將該DOM節(jié)點進(jìn)行復(fù)用,否則則會創(chuàng)建一個新的DOM節(jié)點。

首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2*2=4種比較方法。

當(dāng)新老VNode節(jié)點的start或者end滿足sameVnode時,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節(jié)點進(jìn)行patchVnode即可。

如果oldStartVnode與newEndVnode滿足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。

這時候說明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,進(jìn)行patchVnode的同時還需要將真實DOM節(jié)點移動到oldEndVnode的后面。

如果oldEndVnode與newStartVnode滿足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。

這說明oldEndVnode跑到了oldStartVnode的前面,進(jìn)行patchVnode的同時真實的DOM節(jié)點移動到了oldStartVnode的前面。

如果以上情況均不符合,則通過createKeyToOldIdx會得到一個oldKeyToIdx,里面存放了一個key為舊的VNode,value為對應(yīng)index序列的哈希表。從這個哈希表中可以找到是否有與newStartVnode一致key的舊的VNode節(jié)點,如果同時滿足sameVnode,patchVnode的同時會將這個真實DOM(elmToMove)移動到oldStartVnode對應(yīng)的真實DOM的前面。

當(dāng)然也有可能newStartVnode在舊的VNode節(jié)點找不到一致的key,或者是即便key相同卻不是sameVnode,這個時候會調(diào)用createElm創(chuàng)建一個新的DOM節(jié)點。

到這里循環(huán)已經(jīng)結(jié)束了,那么剩下我們還需要處理多余或者不夠的真實DOM節(jié)點。

1.當(dāng)結(jié)束時oldStartIdx > oldEndIdx,這個時候老的VNode節(jié)點已經(jīng)遍歷完了,但是新的節(jié)點還沒有。說明了新的VNode節(jié)點實際上比老的VNode節(jié)點多,也就是比真實DOM多,需要將剩下的(也就是新增的)VNode節(jié)點插入到真實DOM節(jié)點中去,此時調(diào)用addVnodes(批量調(diào)用createElm的接口將這些節(jié)點加入到真實DOM中去)。

2。同理,當(dāng)newStartIdx > newEndIdx時,新的VNode節(jié)點已經(jīng)遍歷完了,但是老的節(jié)點還有剩余,說明真實DOM節(jié)點多余了,需要從文檔中刪除,這時候調(diào)用removeVnodes將這些多余的真實DOM刪除。

更詳細(xì)的diff實現(xiàn)參考筆者的文章VirtualDOM與diff(Vue.js實現(xiàn)).MarkDown)。

映射到真實DOM

由于Vue使用了虛擬DOM,所以虛擬DOM可以在任何支持JavaScript語言的平臺上操作,譬如說目前Vue支持的瀏覽器平臺或是weex,在虛擬DOM的實現(xiàn)上是一致的。那么最后虛擬DOM如何映射到真實的DOM節(jié)點上呢?

Vue為平臺做了一層適配層,瀏覽器平臺見/platforms/web/runtime/node-ops.js以及weex平臺見/platforms/weex/runtime/node-ops.js。不同平臺之間通過適配層對外提供相同的接口,虛擬DOM進(jìn)行操作真實DOM節(jié)點的時候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實現(xiàn)則不需要關(guān)心,它會根據(jù)平臺的改變而改變。

現(xiàn)在又出現(xiàn)了一個問題,我們只是將虛擬DOM映射成了真實的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?

這要依賴于虛擬DOM的生命鉤子。虛擬DOM提供了如下的鉤子函數(shù),分別在不同的時期會進(jìn)行調(diào)用。

const hooks = ["create", "activate", "update", "remove", "destroy"]

/*構(gòu)建cbs回調(diào)函數(shù),web平臺上見/platforms/web/runtime/modules*/
  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]])
      }
    }
  }

同理,也會根據(jù)不同平臺有自己不同的實現(xiàn),我們這里以Web平臺為例。Web平臺的鉤子函數(shù)見/platforms/web/runtime/modules。里面有對attr、class、props、events、style以及transition(過渡狀態(tài))的DOM屬性進(jìn)行操作。

以attr為例,代碼很簡單。

/* @flow */

import { isIE9 } from "core/util/env"

import {
  extend,
  isDef,
  isUndef
} from "shared/util"

import {
  isXlink,
  xlinkNS,
  getXlinkProp,
  isBooleanAttr,
  isEnumeratedAttr,
  isFalsyAttrValue
} from "web/util/index"

/*更新attr*/
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  /*如果舊的以及新的VNode節(jié)點均沒有attr屬性,則直接返回*/
  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
    return
  }
  let key, cur, old
  /*VNode節(jié)點對應(yīng)的Dom實例*/
  const elm = vnode.elm
  /*舊VNode節(jié)點的attr*/
  const oldAttrs = oldVnode.data.attrs || {}
  /*新VNode節(jié)點的attr*/
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  /*如果新的VNode的attr已經(jīng)有__ob__(代表已經(jīng)被Observe處理過了), 進(jìn)行深拷貝*/
  if (isDef(attrs.__ob__)) {
    attrs = vnode.data.attrs = extend({}, attrs)
  }

  /*遍歷attr,不一致則替換*/
  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if (old !== cur) {
      setAttr(elm, key, cur)
    }
  }
  // #4391: in IE9, setting type can reset value for input[type=radio]
  /* istanbul ignore if */
  if (isIE9 && attrs.value !== oldAttrs.value) {
    setAttr(elm, "value", attrs.value)
  }
  for (key in oldAttrs) {
    if (isUndef(attrs[key])) {
      if (isXlink(key)) {
        elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
      } else if (!isEnumeratedAttr(key)) {
        elm.removeAttribute(key)
      }
    }
  }
}

/*設(shè)置attr*/
function setAttr (el: Element, key: string, value: any) {
  if (isBooleanAttr(key)) {
    // set attribute for blank value
    // e.g. 
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, key)
    }
  } else if (isEnumeratedAttr(key)) {
    el.setAttribute(key, isFalsyAttrValue(value) || value === "false" ? "false" : "true")
  } else if (isXlink(key)) {
    if (isFalsyAttrValue(value)) {
      el.removeAttributeNS(xlinkNS, getXlinkProp(key))
    } else {
      el.setAttributeNS(xlinkNS, key, value)
    }
  } else {
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, value)
    }
  }
}

export default {
  create: updateAttrs,
  update: updateAttrs
}

attr只需要在create以及update鉤子被調(diào)用時更新DOM的attr屬性即可。

最后

至此,我們已經(jīng)從template到真實DOM的整個過程梳理完了。現(xiàn)在再去看這張圖,是不是更清晰了呢?

關(guān)于

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: https://github.com/answershuto

Blog:http://answershuto.github.io/

知乎主頁:https://www.zhihu.com/people/cao-yang-49/activities

知乎專欄:https://zhuanlan.zhihu.com/ranmo

掘金: https://juejin.im/user/58f87ae844d9040069ca7507

osChina:https://my.oschina.net/u/3161824/blog

轉(zhuǎn)載請注明出處,謝謝。

歡迎關(guān)注我的公眾號

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/89080.html

相關(guān)文章

  • Vue.js 源碼學(xué)習(xí)筆記

    摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個樣子了而且狀態(tài)機(jī)標(biāo)識由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時執(zhí)行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結(jié)構(gòu)梳...

    darkbaby123 評論0 收藏0
  • Vue.js 源碼學(xué)習(xí)筆記

    摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個樣子了而且狀態(tài)機(jī)標(biāo)識由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時執(zhí)行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結(jié)構(gòu)梳...

    jsdt 評論0 收藏0
  • vue源碼閱讀之?dāng)?shù)據(jù)渲染過程

    摘要:圖在中應(yīng)用三數(shù)據(jù)渲染過程數(shù)據(jù)綁定實現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁面的過程,在中定義了一個的構(gòu)造函數(shù)。一、概述 vue已是目前國內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進(jìn)行總結(jié)。本文旨在梳理初始化頁面時data中的數(shù)據(jù)是如何渲染到頁面上的。本文將帶著這個疑問一點點追究vue的思路。總體來說vue模版渲染大致流程如圖1所...

    AlphaGooo 評論0 收藏0
  • JavaScript 進(jìn)階之深入理解數(shù)據(jù)雙向綁定

    摘要:當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時候,我們希望能夠通知對方也更新,這就是所謂的數(shù)據(jù)雙向綁定。返回值返回傳入函數(shù)的對象,即第一個參數(shù)該方法重點是描述,對象里目前存在的屬性描述符有兩種主要形式數(shù)據(jù)描述符和存取描述符。 前言 談起當(dāng)前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對于大多數(shù)人來說,我們更多的是在使用框架,對于框架解決痛點背后使用的基本原理往往關(guān)注...

    sarva 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<