VUE的渲染函數Render

在官方文檔中對render的解釋是:

類型

(createElement: () => VNode) => VNode

詳細:

字符串模板的代替方案纸泡,允許你發(fā)揮 JavaScript 最大的編程能力锹安。該渲染函數接收一個 createElement 方法作為第一個參數用來創(chuàng)建 VNode
如果組件是一個函數組件,渲染函數還會接收一個額外的 context 參數无埃,為沒有實例的函數組件提供上下文信息赴背。

上面文檔翻譯成代碼:

<div id="app">
  {{ message }}
</div>
// 等同于
render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

在Vue源碼里可以找到_render 方法,它是實例的一個私有方法逊朽,它用來把實例渲染成一個虛擬Node罕伯。
定義在 src/core/instance/render.js 文件中:

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
vnode = render.call(vm._renderProxy, vm.$createElement)

可以看到,render函數中的createElement 方法就是 vm.$createElement 方法:


export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

vm.$createElement 方法定義是在執(zhí)行initRender方法的時候,除了 vm.$createElement 方法叽讳,還有一個 vm._c 方法追他,它是被模板編譯成的render 函數使用,而 vm.$createElement是用戶手寫render 方法使用的岛蚤, 這倆個方法支持的參數相同邑狸,并且內部都調用了createElement方法。

vm._render 最終是通過執(zhí)行createElement方法并返回的是vnode涤妒,它是一個虛擬 Node单雾。接下來分析vm.$createElement


利用 createElement 方法創(chuàng)建 VNode,它定義在 src/core/vdom/create-elemenet.js 中:

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法實際上是對 _createElement 方法的封裝,它允許傳入的參數更加靈活硅堆,在處理這些參數后屿储,調用真正創(chuàng)建 VNode 的函數 _createElement

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 ((!data || !data.pre) && 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 的上下文環(huán)境,它是 Component 類型渐逃;
  2. tag 表示標簽够掠,它可以是一個字符串,也可以是一個 Component朴乖;
  3. data 表示 VNode 的數據祖屏,它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義买羞;
  4. children 表示當前 VNode 的子節(jié)點袁勺,它是任意類型的,它接下來需要被規(guī)范為標準的 VNode 數組畜普;
  5. normalizationType 表示子節(jié)點規(guī)范的類型期丰,類型不同規(guī)范的方法也就不一樣,它主要是參考 render 函數是編譯生成的還是用戶手寫的吃挑。

createElement 函數的流程略微有點多钝荡,接下來主要分析 2 個重點的流程 —— children 的規(guī)范化以及 VNode 的創(chuàng)建。

children 的規(guī)范化

從代碼:

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

這里根據normalizationType 的不同舶衬,調用了 normalizeChildren(children) simpleNormalizeChildren(children) 方法埠通。
定義在src/core/vdom/helpers/normalize-children.js :

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

simpleNormalizeChildren 方法調用場景是 render 函數是編譯生成的。理論上編譯生成的 children 都已經是 VNode 類型的逛犹,但這里有一個例外端辱,就是 functional component 函數式組件返回的是一個數組而不是一個根節(jié)點,所以會通過 Array.prototype.concat 方法把整個 children 數組打平虽画,讓它的深度只有一層舞蔽。

normalizeChildren 方法的調用場景有 2 種,一個場景是 render 函數是用戶手寫的码撰,當 children 只有一個節(jié)點的時候渗柿,Vue.js 從接口層面允許用戶把 children 寫成基礎類型用來創(chuàng)建單個簡單的文本節(jié)點,這種情況會調用 createTextVNode 創(chuàng)建一個文本節(jié)點的 VNode脖岛;另一個場景是當編譯 slot朵栖、v-for 的時候會產生嵌套數組的情況,會調用 normalizeArrayChildren 方法柴梆,接下來看一下它的實現:

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        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 表示要規(guī)范的子節(jié)點
  2. nestedIndex 表示嵌套的索引

因為單個 child 可能是一個數組類型混槐。 normalizeArrayChildren 主要的邏輯就是遍歷 children,獲得單個節(jié)點 c轩性,然后對 c 的類型判斷,如果是一個數組類型,則遞歸調用 normalizeArrayChildren;

  1. 如果是基礎類型揣苏,則通過 createTextVNode 方法轉換成 VNode 類型悯嗓;
  2. 否則就已經是 VNode 類型了,如果 children 是一個列表并且列表還存在嵌套的情況,則根據 nestedIndex 去更新它的 key卸察。

這里需要注意一點脯厨,在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續(xù)的 text 節(jié)點坑质,會把它們合并成一個 text 節(jié)點合武。

經過對 children 的規(guī)范化,children 變成了一個類型為 VNode 的 Array涡扼。

VNode 的創(chuàng)建

回到 createElement 函數稼跳,規(guī)范化 children 后,接下來會去創(chuàng)建一個 VNode 的實例:

  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 ((!data || !data.pre) && 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)
  }

這里先對 tag 做判斷

  1. 如果是 string 類型吃沪,則接著判斷如果是內置的一些節(jié)點汤善,則直接創(chuàng)建一個普通 VNode
  2. 如果是為已注冊的組件名,則通過 createComponent 創(chuàng)建一個組件類型的 VNode票彪,否則創(chuàng)建一個未知的標簽的 VNode
  3. 如果是 tag 一個 Component 類型红淡,則直接調用 createComponent 創(chuàng)建一個組件類型的 VNode 節(jié)點。

對于 createComponent 創(chuàng)建組件類型的 VNode 的過程降铸,略過需要補充在旱,本質上它還是返回了一個 VNode。

總結

那么至此推掸,我們大致了解了 createElement 創(chuàng)建 VNode 的過程桶蝎,每個 VNode 有 children,children 每個元素也是一個 VNode终佛,這樣就形成了一個 VNode Tree俊嗽,它很好的描述了我們的 DOM Tree。

回到 mountComponent 函數的過程铃彰,我們已經知道 vm._render 是如何創(chuàng)建了一個 VNode绍豁,接下來就是要把這個 VNode 渲染成一個真實的 DOM 并渲染出來,這個過程是通過 vm._update 完成的牙捉,后續(xù)接著講竹揍。

參考文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市邪铲,隨后出現的幾起案子芬位,更是在濱河造成了極大的恐慌,老刑警劉巖带到,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昧碉,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機被饿,發(fā)現死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門四康,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狭握,你說我怎么就攤上這事闪金。” “怎么了论颅?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵哎垦,是天一觀的道長。 經常有香客問我恃疯,道長漏设,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任澡谭,我火速辦了婚禮愿题,結果婚禮上,老公的妹妹穿的比我還像新娘蛙奖。我一直安慰自己潘酗,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布雁仲。 她就那樣靜靜地躺著仔夺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪攒砖。 梳的紋絲不亂的頭發(fā)上缸兔,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音吹艇,去河邊找鬼惰蜜。 笑死,一個胖子當著我的面吹牛受神,可吹牛的內容都是我干的抛猖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼鼻听,長吁一口氣:“原來是場噩夢啊……” “哼财著!你這毒婦竟也來了?” 一聲冷哼從身側響起撑碴,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撑教,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后醉拓,有當地人在樹林里發(fā)現了一具尸體伟姐,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡收苏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了愤兵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倒戏。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖恐似,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情傍念,我是刑警寧澤矫夷,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站憋槐,受9級特大地震影響双藕,放射性物質發(fā)生泄漏。R本人自食惡果不足惜阳仔,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一忧陪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧近范,春花似錦嘶摊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至斥杜,卻和暖如春虱颗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔗喂。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工忘渔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缰儿。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓畦粮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親返弹。 傳聞我的和親對象是個殘疾皇子锈玉,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359