render helper 渲染帮助函数

183 阅读2分钟

开篇

在生成渲染函数字符串的时候用到了几个函数,比如_c _v _s _m _f _l _u 下面就分别从源码入手分析函数的运行和作用

_c

函数在src/core/instance/render.js下面定义,如图

image-20220609172809065.png

可以看到内部是调用了createElement函数,那么继续分析这个函数

image-20220609173225590.png

createElement继续调用了_createElement函数,接下来接着分析(由于 _createElement 做了很多的兼容处理,这里只保留核心代码)

function _createElement(context, tag, data, children, normalizationType) {
  let vnode; // 新建一个vnode节点
  if (typeof tag === "string") {
    //如果标签是字符串
    if (config.isReservedTag(tag)) {
      //如果是平台保留标签
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      );
    } else if (如果是组件) {
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      //不符合上面的情况
      vnode = new VNode(tag, data, children, undefined, undefined, context);
    }
  } else {
    //如果不是字符串那就是组件了
    vnode = createComponent(tag, data, context, children);
  }
  return vnode;
}
​
function createComponent(Ctor,data,context,children,tag) (
    const baseCtor = context.$options._base // 实际上是Vue的构造函数
    
    //如果Ctor不是函数就通过Vue.extend去创建构造函数
    if(isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor)
    }
    
    /*
    这里是处理异步组件,什么是异步组件呢?举个例子
    Vue.component('async-example',function(resolve,reject){
        setTimeout(function(){
            resolve({
                template : `<div>i am async component</div>`
            })
        },1000)
    })
    <async-example></async-example>
    */
    let asyncFactory
    if (isUndef(Ctor.cid)) {    //判断是否初始化了Vue 一个组件对应一个Vue实例
        asyncFactory = Ctor 
        Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
        if (Ctor === undefined) {
          return createAsyncPlaceholder(    //创建一个占位符
            asyncFactory,
            data,
            context,
            children,
            tag
          )
        }
    }
    
    data = data || {}
    
    /*
    解析配置项,判断父类的选项和基类的选项是否不一致,如果有不一致就合并
    */
    resolveConstructorOptions(Ctor)
    
    //处理v-model的情况 
    if (isDef(data.model)) {
       transformModel(Ctor.options, data)
    }
    
    //提取props,得到res[key] = val集合
    const propsData = extractPropsFromVNodeData(data, Ctor, tag)
​
    /*
    如果是函数式组件 那么就先生成一个渲染上下文,然后调用配置项里面的render方法,绑定生成的上下文,最后生成一个vnode并返回出去
    */
    if (isTrue(Ctor.options.functional)) {  
       return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    
    //获取事件监听器对象data.on 也就是在组件上设置的v-on指令
    const listeners = data.on
    //修饰符
    data.on = data.nativeOn
    
    //如果是抽象组件就保留slot
    if (isTrue(Ctor.options.abstract)) {
        const slot = data.slot
        data = {}
        if (slot) {
          data.slot = slot
       }
    }
    
    /*
    在组件的data上面设置四个对象,分别是init,insert,prepatch,destroy属性
    这四个属性分别负责组件的创建,插入,更新,销毁,会在组件的patch阶段调用
    */
    installComponentHooks(data)
    
    //创建一个新的vnode并且返回出去
    const name = Ctor.options.name || tag
    const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children },
        asyncFactory
    )
}

_v

函数在src/core/instance/render-helpers/index.js下面定义,如图

image-20220610112739918.png

image-20220610112804683.png

可以看到最后也是返回了个文本节点的vnode

_s

如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看toString方法是什么样的

image-20220610113520090.png

可以看到toString方法主要是将值转换为字符串格式

_m

如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看renderStatic方法是什么样的

export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  //获取缓存,如果没有就设置
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  if (tree && !isInFor) {
    return tree
  }
  //调用staticRenderFns数组里面的方法生成vnode 并保存到cached里面
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

在生成渲染函数字符串的时候,就已经将静态节点保存到staticRenderFns里面了,所以_m就是调用里面的方法生成vnode

_f

如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看resolveFilter方法是什么样的

export function resolveFilter (id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}
​
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  const assets = options[type]
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  return res
}

可以看到也是调用了用户自己定义的filter方法,将值返回出去

_l

如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看renderList方法是什么样的

/*
renderList里面接受两个参数,第一个是循环生成vnode的次数,第二个是render方法,这里放一个v-for的generate函数
_l((3), function (item, index) {
    return _c('span', {
        key: index
    }, [_v(_s(item))])
}), 0)
不断调用render方法,将生成的vnode节点放到ret里面,然后返回ret
*/
export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode> {
  let ret: ?Array<VNode>, i, l, keys, key
  if (Array.isArray(val) || typeof val === 'string') {
    ret = new Array(val.length)
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
  } else if (typeof val === 'number') {
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    if (hasSymbol && val[Symbol.iterator]) {
      ret = []
      const iterator: Iterator<any> = val[Symbol.iterator]()
      let result = iterator.next()
      while (!result.done) {
        ret.push(render(result.value, ret.length))
        result = iterator.next()
      }
    } else {
      keys = Object.keys(val)
      ret = new Array(keys.length)
      for (i = 0, l = keys.length; i < l; i++) {
        key = keys[i]
        ret[i] = render(val[key], key, i)
      }
    }
  }
  if (!isDef(ret)) {
    ret = []
  }
  (ret: any)._isVList = true
  return ret
}

_u

如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看resolveScopedSlots方法是什么样的

/*
格式化generate生成的slot值,如下
_u([{
    key: "header",
    fn: function(user) {
    return [_v(_s(user))]
    }
}])
最终会生成
{
    $stable: true,
    header: function(user) {
        return [_v(_s(user))]
    }
}
*/
export function resolveScopedSlots (
  fns: ScopedSlotsData, // see flow/vnode
  res?: Object,
  // the following are added in 2.6
  hasDynamicKeys?: boolean,
  contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {
  res = res || { $stable: !hasDynamicKeys }
  for (let i = 0; i < fns.length; i++) {
    const slot = fns[i]
    if (Array.isArray(slot)) {
      resolveScopedSlots(slot, res, hasDynamicKeys)
    } else if (slot) {
      // marker for reverse proxying v-slot without scope on this.$slots
      if (slot.proxy) {
        slot.fn.proxy = true
      }
      res[slot.key] = slot.fn
    }
  }
  if (contentHashKey) {
    (res: any).$key = contentHashKey
  }
  return res
}

总结

template是如何转换为vnode并渲染到页面上的呢?

1.先进行Vue的初始化,最后执行vm.$mount(vm.$options.el)方法

2.vm.$mount方法先获取template字符串,调用compileToFunctions方法,获取render函数(渲染函数字符串)和staticRenderFns(静态节点渲染函数数组),然后放到vm.$options上。最后调用mount方法,绑定当前this环境。

3.compileToFunctions先调用compile方法,将template字符串和options传进去,得到一个对象,里面包括ast和render和staticRenderFns属性,然后调用对象的staticRenderFns,创建静态节点vnode放进一个数组里,最后返回该对象和静态节点数组

4.compile调用baseCompile方法,将template字符串和finalOptions(包含警告函数,用于捕捉错误信息)传进去,然后得到ast,render,staticRenderFns属性,返回出去

5.baseCompile做了三件事情,第一件事情是将template字符串转换为ast对象,第二件事情是对ast对象标记是否是静态节点和静态根节点(需要满足三个条件,节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点),第三件事情是将ast对象转换为render渲染函数

6.mount方法先获取el对象,也就是root(根节点),之后调用mountComponent方法,将实例传进去

7.mountComponent方法先执行beforeMount方法(生命周期),之后定义一个updateComponent方法,如下

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

之后实例化一个watcher,将当前实例,updateComponent,noop,before(beforeUpdate 生命周期)传进去,之后会执行一遍updateComponent方法

8.vm.render方法会生成一次vnode,将值传给vm.update方法

9.vm._update方法会调用patch方法,如果旧的vnode不存在,那么就根据vnode重新创造节点,插入到dom中。如果旧的vnode存在,那么就通过diff算法,对比两者之间的差距,更新页面。