1. 程式人生 > >vue的原始碼學習之五——6.資料驅動(createElement)

vue的原始碼學習之五——6.資料驅動(createElement)

1. 介紹

      版本:2.5.17。 

       我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。

       學習文件:https://ustbhuangyi.github.io/vue-analysis/data-driven/create-element.html

2. createElement

 Vue.js 利用 createElement 方法建立 VNode,它定義在 src/core/vdom/create-elemenet.js

 中:

export function createElement (
  context: Component,  // vm例項
  tag: any, //標籤
  data: any, // vnode的資料
  children: any, // vnode的子節點,進而可以構建vnode樹進而對映DOM樹
  normalizationType: any, 
  alwaysNormalize: boolean
): VNode | Array<VNode> {
 //如果傳入引數時,沒有傳data這個引數,那麼實參和形參改變對應順序
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  //去呼叫 _createElement
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法實際上是對 _createElement 方法的封裝,它允許傳入的引數更加靈活,在處理這些引數後,呼叫真正建立 VNode 的函式 _createElement

// _createElement 方法有 5 個引數,
// context 表示 VNode 的上下文環境,它是 Component 型別;
// tag 表示標籤,它可以是一個字串,也可以是一個 Component;
// data 表示 VNode 的資料,它是一個 VNodeData 型別,可以在 flow/vnode.js 中找到它的定義;
// children 表示當前 VNode 的子節點,它是任意型別的,它接下來需要被規範為標準的 VNode 陣列;
// normalizationType 表示子節點規範的型別,型別不同規範的方法也就不一樣,它主要是參考 render 函式是編譯生成的還是使用者手寫的。
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 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
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElement 方法有 5 個引數:

1.context 表示 VNode 的上下文環境,它是 Component 型別;

2.tag表示標籤,它可以是一個字串,也可以是一個 Component

3.data 表示 VNode 的資料,它是一個 VNodeData 型別,可以在 flow/vnode.js 中找到它的定義

4.children 表示當前 VNode 的子節點,它是任意型別的,它接下來需要被規範為標準的 VNode 陣列;

5.normalizationType 表示子節點規範的型別,型別不同規範的方法也就不一樣,它主要是參考 render 函式是編譯生成的還是使用者手寫的。

3.children 的規範化

由於 Virtual DOM 實際上是一個樹狀結構,每一個 VNode 可能會有若干個子節點,這些子節點應該也是 VNode 的型別。_createElement 接收的第 4 個引數 children 是任意型別的,因此我們需要把它們規範成 VNode 型別。

_createElement方法會根據normalizationType不同調用不同方法 
SIMPLE_NORMALIZE = 1,ALWAYS_NORMALIZE = 2

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

 3.1 方法

     以下方法都定義在們的定義都在 src/core/vdom/helpers/normalzie-children.js 中

    3.1.1 simpleNormalizeChildren

// 對children進行遍歷,(只會有一層深度)
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
   // 如果是二維陣列,就將其concat為一維陣列
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  // 返回一維陣列,陣列中每一個元素都是一個vnode
  return children
}

simpleNormalizeChildren 方法呼叫場景是—–render 函式當函式是編譯生成的。 
理論上編譯生成的 children 都已經是 VNode 型別的,但這裡有一個例外,就是 functional component 函式式元件返回的是一個數組而不是一個根節點,所以會通過 Array.prototype.concat 方法把整個 children 陣列打平,讓它的深度只有一層。

3.1.2 normalizeChildren

export function normalizeChildren (children: any): ?Array<VNode> {
//如果傳入的是基本資料型別,例如this.message代表的字串,那麼就建立一個文字結點
  return isPrimitive(children)
      //呼叫createTextVNode函式,其實就是將其tostring,返回一個文字結點vnode
    ? [createTextVNode(children)]
      // 如果是 isArray,就呼叫normalizeArrayChildren方法
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

normalizeChildren 方法的呼叫場景有 2 種,一個場景是 render 函式是使用者手寫的,當 children 只有一個節點的時候,Vue.js 從介面層面允許使用者把 children 寫成基礎型別用來建立單個簡單的文字節點,這種情況會呼叫 createTextVNode 建立一個文字節點的VNode;另一個場景是當編譯 slot、v-for 的時候會產生巢狀陣列的情況,會呼叫 normalizeArrayChildren 方法

3.1.3 normalizeArrayChildren

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  // 遍歷children
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  如果是陣列,遞迴children
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // 合併相連的兩個文字節點
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
      // 如果children是基礎型別
    } else if (isPrimitive(c)) {
        // 合併相鄰TextNode
      if (isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
          //放到陣列中
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildren 接收 2 個引數:

  1. children 表示要規範的子節點
  2. nestedIndex 表示巢狀的索引,因為單個 child 可能是一個數組型別。

normalizeArrayChildren 主要的邏輯:

  1. 就是遍歷 children,獲得單個節點 c,
  2. 然後對 c 的型別判斷,如果是一個數組型別,則遞迴呼叫 normalizeArrayChildren;
  3. 如果是基礎型別,則通過 createTextVNode 方法轉換成 VNode 型別;
  4. 否則就已經是 VNode 型別了,如果 children 是一個列表並且列表還存在巢狀的情況,則根據 nestedIndex 去更新它的key。

這裡需要注意一點,在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續的 text 節點,會把它們合併成一個 text 節點。

3.1.4 總結 

children 的規範化,children 變成了一個型別為 VNode 的 Array。也就是說Array中每一個元素都是VNode(虛擬DOM)。 
simpleNormalizeChildren(children): 遍歷最多二維,輸出元素都是VNode的一維array 
normalizeChildren : 可遍歷多層,合併兩個連續的 text 節點,輸出元素都是VNode的一維array 

4. VNode 的建立

回到 createElement 函式,規範化 children 後,接下來會去建立一個 VNode 的例項:

et vnode, ns
//對 tag 做判斷
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  //如果是內建的節點,例如我們常用的<div id="app"></div>
  if (config.isReservedTag(tag)) {
    // 建立 vnode,config.parsePlatformTagName(tag)為平臺的保留標籤
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // 對元件進行解析
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // 如果是不認識的標籤
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
  1.  先對 tag 做判斷,如果是 string 型別,則接著判斷如果是內建的一些節點,則直接建立一個普通 VNode,
  2. 如果是為已註冊的元件名,則通過 createComponent 建立一個元件型別的 VNode,
  3. 否則建立一個未知的標籤的 VNode。
  4. 如果是 tag 一個 Component 型別,則直接呼叫
  5. createComponent 建立一個元件型別的 VNode 節點。對於 createComponent 建立元件型別的 VNode 的過程,我們之後會去介紹,本質上它還是返回了一個 VNode。

5. 總結

那麼至此,我們大致瞭解了 createElement 建立 VNode 的過程,每個 VNode 有 childrenchildren 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree,它很好的描述了我們的 DOM Tree。

回到 mountComponent 函式的過程,我們已經知道 vm._render 是如何建立了一個 VNode,接下來就是要把這個 VNode 渲染成一個真實的 DOM 並渲染出來,這個過程是通過 vm._update 完成的,接下來分析一下這個過程。