vue中渲染函數(shù)Render原理解析

image.png

一句話簡(jiǎn)述:

render函數(shù)的職責(zé)就是把模板解析成Vnode(虛擬DOM)節(jié)點(diǎn)

具體是如何做的?

可以分解成幾個(gè)問題:
1.模板編譯的輸出結(jié)果是什么?
模板編譯會(huì)將用戶寫的模板↓

<div id="NLRX"><p>Hello {{name}}</p></div>

轉(zhuǎn)換成用層級(jí)對(duì)象表達(dá)的ast

ast = {
    'type': 1,
    'tag': 'div',
    'attrsList': [
        {
            'name':'id',
            'value':'NLRX',
        }
    ],
    'attrsMap': {
      'id': 'NLRX',
    },
    'static':false,
    'parent': undefined,
    'plain': false,
    'children': [{
      'type': 1,
      'tag': 'p',
      'plain': false,
      'static':false,
      'children': [
        {
            'type': 2,
            'expression': '"Hello "+_s(name)',
            'text': 'Hello {{name}}',
            'static':false,
        }
      ]
    }]
  }

因?yàn)楸疚牡闹攸c(diǎn)不是模板編譯,所以這里的轉(zhuǎn)換過程略,但我推薦你
https://vue-js.com/learn-vue/complie/
可以解答你的疑惑。

render函數(shù)實(shí)際上做了兩件事

1.將ast生成render函數(shù)

with(this){
    reurn _c(
        'div',
        {
            attrs:{"id":"NLRX"},
        }
        [
            _c('p'),
            [
                _v("Hello "+_s(name))
            ]
        ])
}

不用急,函數(shù)內(nèi)容(with(this){....})的形成會(huì)在后面講到。
再說明一點(diǎn),你可能會(huì)奇怪render函數(shù)為什么要先

with(this){
//函數(shù)執(zhí)行
}

這其實(shí)是一種特殊語(yǔ)法,在with內(nèi)部的函數(shù)執(zhí)行環(huán)境會(huì)切換為this,_c明明沒作為參數(shù)傳進(jìn)函數(shù)體內(nèi)部,但為何能調(diào)用?因?yàn)閠his上有_c方法,而函數(shù)內(nèi)部環(huán)境又是this所以能直接調(diào)用,而這個(gè)this是什么?實(shí)際上就是vue實(shí)例vm啦!

2.運(yùn)行render函數(shù),生成vnode節(jié)點(diǎn)

中間函數(shù)會(huì)多次調(diào)用_c,也就是createElemnt來生成vnode,
_c的作用是根據(jù)用戶輸入的參數(shù)來生成相應(yīng)的vnode節(jié)點(diǎn),官方api文檔中createElement的用法如下,是不是和上面render函數(shù)內(nèi)的內(nèi)容很像,實(shí)際上就是一回事。


image.png

PS:vnode長(zhǎng)什么樣子?如下,這就是render最后執(zhí)行的結(jié)果

<div id='a'><span>難涼熱血</span></div>

// VNode節(jié)點(diǎn)
{
  tag:'div',
  data:{},
  children:[
    {
      tag:'span',
      text:'難涼熱血'
    }
  ]
}

第一件事,如何生成render函數(shù)?

前人已經(jīng)講得很完備了,直接貼出來
https://vue-js.com/learn-vue/complie/codegen.html#_1-%E5%89%8D%E8%A8%80

第二件事,如何執(zhí)行render函數(shù)?

//源碼位置:src/code/instance/render.js


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 i  t.
  // 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) //mark2:用戶創(chuàng)建vnode,將rende內(nèi)的字符串整成vnode模板
  // 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') {
    ... 這里不重要
  }
}

在initRender中vue為vue的實(shí)例vm定義了_c函數(shù),
這就為渲染函數(shù)中要調(diào)用的_c埋下了伏筆。
但是這里實(shí)際上還不是真正定義render函數(shù)的位置,那么真正定義render函數(shù)的位置在哪里的?
在render.js文件中,滾輪繼續(xù)向下翻我們發(fā)現(xiàn)了這個(gè)↓

function renderMixin (Vue: Class<Component>) {
 ......省去不重要邏輯

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


    // render self
    let vnode
    try {
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement) //mark:從with函數(shù)變成真正的vnode,renderProxy不知道是什么,但他可以調(diào)動(dòng)提供_c,然后c會(huì)調(diào)用vm.$createEle大概
      //參數(shù) vm.$createElement在一般情況下不需要,特殊情況暫時(shí)不知
    } catch (e) {
      ...
    } finally {
     ...
    }
    // 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)) {
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}

觀察 vnode = render.call(vm._renderProxy, vm.$createElement)就是調(diào)用render函數(shù)的地方,并生成了vnode最后返回vnode節(jié)點(diǎn)
在此之后,vnode利用diff算法就可以完成對(duì)頁(yè)面的數(shù)據(jù)更新了。

執(zhí)行render的過程就是將_c執(zhí)行,補(bǔ)充_c返回的vnode對(duì)象的過程

with(this){
    reurn _c(
        'div',
        {
            attrs:{"id":"NLRX"},
        }
        [
            _c('p'),
            [
                _v("Hello "+_s(name))
            ]
        ])
}

那么非常感謝你看到這里,那么最后我們?cè)俳忉屢幌耞c也就是createElement相信你就能完全弄明白整個(gè)render的過程了!
以下是createElement的源碼:

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只是個(gè)中間層,它實(shí)際上是為了調(diào)用_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
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      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()
  }
}

而對(duì)_createElement大致掃過一眼,最后return的是vnode節(jié)點(diǎn),這就是_createElement的作用對(duì)比render內(nèi)部來看:
根據(jù)環(huán)境(context,一般是vm),標(biāo)簽(tag:div),data: attrs:{"id":"NLRX"},和 children(子節(jié)點(diǎn))來生成vnode

with(this){
    reurn _c(
        'div',
        {
            attrs:{"id":"NLRX"},
        }
        [
            _c('p'),
            [
                _v("Hello "+_s(name))
            ]
        ])
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在官方文檔中對(duì)render的解釋是: 類型 詳細(xì): 字符串模板的代替方案,允許你發(fā)揮 JavaScript 最大的...
    QLing09閱讀 930評(píng)論 0 3
  • 一、 什么是render渲染函數(shù)? 一句話概括:在JS代碼中,可以用render函數(shù)來渲染大量重復(fù)繁瑣的DOM。 ...
    西瓜魚仔閱讀 1,602評(píng)論 0 5
  • update Vue 的 _update 是實(shí)例的一個(gè)私有方法,它被調(diào)用的時(shí)機(jī)有 2 個(gè),一個(gè)是首次渲染,一個(gè)是數(shù)...
    阿暢_閱讀 4,683評(píng)論 0 1
  • 模板渲染過程在實(shí)際使用vue的過程可能并不需要太深理解,但就vue來說,這些底層思想可以更好地讓我們理解這個(gè)框架,...
    指尖跳動(dòng)閱讀 14,896評(píng)論 1 15
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,898評(píng)論 28 54

友情鏈接更多精彩內(nèi)容