美文网首页
vue中渲染函数Render原理解析

vue中渲染函数Render原理解析

作者: 某时橙 | 来源:发表于2021-10-20 16:38 被阅读0次
    image.png

    一句话简述:

    render函数的职责就是把模板解析成Vnode(虚拟DOM)节点

    具体是如何做的?

    可以分解成几个问题:
    1.模板编译的输出结果是什么?
    模板编译会将用户写的模板↓

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

    转换成用层级对象表达的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,
            }
          ]
        }]
      }
    

    因为本文的重点不是模板编译,所以这里的转换过程略,但我推荐你
    https://vue-js.com/learn-vue/complie/
    可以解答你的疑惑。

    render函数实际上做了两件事

    1.将ast生成render函数

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

    不用急,函数内容(with(this){....})的形成会在后面讲到。
    再说明一点,你可能会奇怪render函数为什么要先

    with(this){
    //函数执行
    }
    

    这其实是一种特殊语法,在with内部的函数执行环境会切换为this,_c明明没作为参数传进函数体内部,但为何能调用?因为this上有_c方法,而函数内部环境又是this所以能直接调用,而这个this是什么?实际上就是vue实例vm啦!

    2.运行render函数,生成vnode节点

    中间函数会多次调用_c,也就是createElemnt来生成vnode,
    _c的作用是根据用户输入的参数来生成相应的vnode节点,官方api文档中createElement的用法如下,是不是和上面render函数内的内容很像,实际上就是一回事。


    image.png

    PS:vnode长什么样子?如下,这就是render最后执行的结果

    <div id='a'><span>难凉热血</span></div>
    
    // VNode节点
    {
      tag:'div',
      data:{},
      children:[
        {
          tag:'span',
          text:'难凉热血'
        }
      ]
    }
    

    第一件事,如何生成render函数?

    前人已经讲得很完备了,直接贴出来
    https://vue-js.com/learn-vue/complie/codegen.html#_1-%E5%89%8D%E8%A8%80

    第二件事,如何执行render函数?

    //源码位置: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:用户创建vnode,将rende内的字符串整成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的实例vm定义了_c函数,
    这就为渲染函数中要调用的_c埋下了伏笔。
    但是这里实际上还不是真正定义render函数的位置,那么真正定义render函数的位置在哪里的?
    在render.js文件中,滚轮继续向下翻我们发现了这个↓

    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函数变成真正的vnode,renderProxy不知道是什么,但他可以调动提供_c,然后c会调用vm.$createEle大概
          //参数 vm.$createElement在一般情况下不需要,特殊情况暂时不知
        } 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)就是调用render函数的地方,并生成了vnode最后返回vnode节点
    在此之后,vnode利用diff算法就可以完成对页面的数据更新了。

    执行render的过程就是将_c执行,补充_c返回的vnode对象的过程

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

    那么非常感谢你看到这里,那么最后我们再解释一下_c也就是createElement相信你就能完全弄明白整个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只是个中间层,它实际上是为了调用_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()
      }
    }
    

    而对_createElement大致扫过一眼,最后return的是vnode节点,这就是_createElement的作用对比render内部来看:
    根据环境(context,一般是vm),标签(tag:div),data: attrs:{"id":"NLRX"},和 children(子节点)来生成vnode

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

    相关文章

      网友评论

          本文标题:vue中渲染函数Render原理解析

          本文链接:https://www.haomeiwen.com/subject/wpbtaltx.html