最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [咖聊] 从 render 到 VNode

    正文概述 掘金(Jouryjc)   2021-07-05   552

    [咖聊] 从 render 到 VNode

    开始之前,我们先回顾执行过程图:

    1. Vue 执行工程 简单了解了整个过程;
    2. 模板编译 详聊了生成 render 函数的过程;
    3. reactive 详聊了中间环节,数据的依赖收集和派发更新过程;

    由于 _render_update 的过程还是挺复杂的,分成两期。这一期我们就先聊聊 _render 过程。

    VNode

    如果对于虚拟 DOM 还不是,可以跳转到 认识虚拟DOM。这一节详细聊聊 render 生成 VNode 的过程。上一篇讲解 reactive 时,提到了一些小矮人:

    export function installRenderHelpers (target: any) {
      target._o = markOnce
      target._n = toNumber
      target._s = toString
      target._l = renderList
      target._t = renderSlot
      target._q = looseEqual
      target._i = looseIndexOf
      target._m = renderStatic
      target._f = resolveFilter
      target._k = checkKeyCodes
      target._b = bindObjectProps
      target._v = createTextVNode
      target._e = createEmptyVNode
      target._u = resolveScopedSlots
      target._g = bindObjectListeners
    }
    

    通过下面栗子 ? 去了解其中几个:

    <div id="app">
      <Child a="hello vue" @click="handleClick"></Child>
      <ul>
        <li v-for="item of list" :key="item.id">
          {{ item.name }}
        </li>
      </ul>
    </div>
    <script>
      let Child = Vue.extend({
        name: 'Child',
    
        props: {
          a: String
        },
    
        template: `<div id="child">
    			<span @click="$emit('click')">{{ a }}</span>
        </div>`
      })
    
      new Vue({
        el: '#app',
    
        components: {
          Child
        },
    
        data() {
          return {
            list: [{
              name: 'A',
              id: 'A'
            }, {
              name: 'B',
              id: 'B'
            }, {
              name: 'C',
              id: 'C'
            }, {
              name: 'D',
              id: 'D'
            }]
          };
        },
    
        methods: {
          handleClick () {
            console.log('click event');
          }
        }
      })
    </script>
    

    ? 生成的 render 函数如下:

    with (this) {
      return _c('div', {
        attrs: {
          "id": "app"
        }
      }, [_c('child', {
        attrs: {
          "a": "hello vue"
        },
        on: {
          "click": handleClick
        }
      }), _v(" "), _c('ul', _l((list), function(item) {
        return _c('li', {
          key: item.id
        }, [_v("\n                " + _s(item.name) + "\n            ")])
      }))], 1)
    }
    

    首先看 _l 的逻辑,对应的函数 renderList 定义在 src/core/instance/render-helpers/render-list.js:

    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)
        // 遍历数组,去生成 VNode
        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)) {
        keys = Object.keys(val)
        ret = new Array(keys.length)
        for (i = 0, l = keys.length; i < l; i++) {
          key = keys[i]
          // 第一个参数是值,第二个参数是对象key,第三个参数是数组位置
          ret[i] = render(val[key], key, i)
        }
      }
      if (isDef(ret)) {
        (ret: any)._isVList = true
      }
      return ret
    }
    

    ? 中执行 renderList 时参数中的 render 就是下面方法:

    function (item) {
       return _c('li', { key: item.id }, [
         _v("\n                " + _s(item.name) + "\n            ")
       ])
    }
    

    从里往外看,首先 _s 比较简单,就是 toString 函数:

    export function toString (val: any): string {
      return val == null
        ? ''
        : typeof val === 'object'
          ? JSON.stringify(val, null, 2)
          : String(val)
    }
    

    然后是 _v ,对应的函数是 createTextVNode,方法定义在 core/vdom/vnode

    /**
     * @file 虚拟节点定义
     */
    export default class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array<VNode>;
      text: string | void;
      elm: Node | void;
      ns: string | void;
      context: Component | void; // rendered in this component's scope
      key: string | number | void;
      componentOptions: VNodeComponentOptions | void;
      componentInstance: Component | void; // component instance
      parent: VNode | void; // component placeholder node
    
      // strictly internal
      raw: boolean; // contains raw HTML? (server only)
      isStatic: boolean; // hoisted static node
      isRootInsert: boolean; // necessary for enter transition check
      isComment: boolean; // empty comment placeholder?
      isCloned: boolean; // is a cloned node?
      isOnce: boolean; // is a v-once node?
      asyncFactory: Function | void; // async component factory function
      asyncMeta: Object | void;
      isAsyncPlaceholder: boolean;
      ssrContext: Object | void;
      fnContext: Component | void; // real context vm for functional nodes
      fnOptions: ?ComponentOptions; // for SSR caching
      fnScopeId: ?string; // functional scope id support
    
      constructor (
        tag?: string, // 标签名
        data?: VNodeData, // 数据
        children?: ?Array<VNode>, // 子节点
        text?: string,  // 文本
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) {
        // 标签名
        this.tag = tag
        // 对应的对象,含具体的数据信息
        this.data = data
        // 子节点
        this.children = children
        this.text = text
        // 对应的真实dom
        this.elm = elm
        // 命名空间
        this.ns = undefined
        this.context = context
        // 函数组件作用域
        this.fnContext = undefined
        this.fnOptions = undefined
        this.fnScopeId = undefined
        this.key = data && data.key
        this.componentOptions = componentOptions
        this.componentInstance = undefined
        this.parent = undefined
        // 标记是原生HTML还是普通文本
        this.raw = false
        // 标记静态节点
        this.isStatic = false
        this.isRootInsert = true
        this.isComment = false
        this.isCloned = false
        this.isOnce = false
        this.asyncFactory = asyncFactory
        this.asyncMeta = undefined
        this.isAsyncPlaceholder = false
      }
      
      /**
       * 获取实例
       */
      get child (): Component | void {
        return this.componentInstance
      }
    }
    
    /**
     * 创建文本VNode
     */
    export function createTextVNode (val: string | number) {
      return new VNode(undefined, undefined, undefined, String(val))
    }
    

    然后我们看到比较复杂的 _c 也就是 createElement 函数,定义在 src/core/instance/render.js

    // 写 template 调用
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
    
    // 手写 render 函数调用
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
    

    createElement 位于 src/core/vdom/create-element.js

    export function createElement (
      context: Component,
      tag: any,
      data: any,
      children: any,
      normalizationType: any,
      alwaysNormalize: boolean
    ): VNode | Array<VNode> {
    
      // 满足data是一个数组或者基础类型时,说明传的是children参数,需要后移一位
      // 参数个数的统一性处理
      if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
      }
      // 手写 render 这里传进来的是 true,下面会举个?过一遍
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE
      }
      return _createElement(context, tag, data, children, normalizationType)
    }
    

    函数先判断 data 是不是一个数组或者基本类型,是的话就将参数后移一位。自己写 render 函数的时候,如果没有属性相关的配置时,我们可以第二个参数就写 children,举个 ?:

    /**
     * @demo 手写 render 函数的 ?
     */
    new Vue({
      el: '#app',
    
      render (h) {
        return h('div', [
          h('span', 'hello vue!')
        ]);
      }
    })
    

    上面 ? 中 h('span', 'hello vue!') 第二个参数不是数组也不是基础类型,会进入到参数向后移一位的逻辑,即最后会调用 _createElement(context, 'span', undefined, 'hello vue!', 2)

    处理完参数之后,会调用 _createElement

    export function _createElement (
      context: Component,
      tag?: string | Class<Component> | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
    
      /**
        * 如果传递data参数且data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象),
        * https://cn.vuejs.org/v2/guide/render-function.html#约束
        * 那么创建一个空节点
        */
      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
      // 用在<component is="...">
      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
      }
    
      // 规范化子组件参数,手写render函数会进入第一个分支
      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
          // 从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类
          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 {
        // 如果vnode没有成功创建则创建空节点
        return createEmptyVNode()
      }
    }
    

    接着看手写 render 的 ?, 会执行到 normalizeChildren

    if (normalizationType === ALWAYS_NORMALIZE) {
      children = normalizeChildren(children)
    } else if (normalizationType === SIMPLE_NORMALIZE) {
      children = simpleNormalizeChildren(children)
    }
    
    export function normalizeChildren (children: any): ?Array<VNode> {
      // 如果是基本类型孩子,创建文本VNode,否则调用normalizeArrayChildren处理
      return isPrimitive(children)
        ? [createTextVNode(children)]
        : Array.isArray(children)
          ? normalizeArrayChildren(children)
          : undefined
    }
    
    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]
    
        // 节点c是数组,递归调用
        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)
          }
        // 节点c是基础类型,则通过 createTextVNode 方法转换成 VNode 类型
        } 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 函数主要对3种情况做处理:

    1. 孩子 c 是数组,递归调用 normalizeArrayChildren
    2. 孩子 c 是普通类型,创建文本 VNode 处理;
    3. 孩子 c 已经是 VNode 类型:这里又有两种情况,如果孩子 children 是嵌套数组,那么会自动定义 key ,否则就创建文本 VNode

    以上三种情况都有一个共同的处理,就是通过 isTextNode 判断如果前后节点都是文本 VNode,会合并两个节点。并且最终都会返回一个 VNode 数组。

    回到第一个 ? 中的 _c 函数来分析组件和保留标签 VNode 生成的情况,先看组件:

    _c('child', {
      attrs: {
        "a": "hello vue"
      },
      on: {
        "click": handleClick
      }
    })
    

    Child 组件的 VNode 会直接执行 isDef(Ctor = resolveAsset(context.$options, 'components', tag)) 这部分代码,栗子 ? 中的 context.$options 打印出来是:

    ![](/Users/apple/Documents/vue源码系列/[咖聊] patch 与 diff/context.$options.jpg)

    这个 options 是在 app 执行 _init 时赋值的,这部分逻辑位于 src/core/instance/init.js

    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
    

    然后再看看 resolveAsset,这个函数位于src/core/util/options.js

    export function resolveAsset (
      options: Object,
      type: string,
      id: string,
      warnMissing?: boolean
    ): any {
      /* istanbul ignore if */
      if (typeof id !== 'string') {
        return
      }
      
      const assets = options[type]
      // check local registration variations first
      // 先直接使用 id 拿
      if (hasOwn(assets, id)) return assets[id]
    
      //  id 变成驼峰的形式再拿
      const camelizedId = camelize(id)
      if (hasOwn(assets, camelizedId)) return assets[camelizedId]
    
      // 在驼峰的基础上把首字母再变成大写的形式再拿
      const PascalCaseId = capitalize(camelizedId)
      if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
      // fallback to prototype chain
      const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
      if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
        warn(
          'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
          options
        )
      }
      return res
    }
    

    resolveAsset 通过各种变换(驼峰写法、首字母大写驼峰写法),然后找到组件的定义。然后通过 createComponent 生成组件 VNode

    export function createComponent (
    
      // 组件构造器
      Ctor: Class<Component> | Function | Object | void,
      data: ?VNodeData,
      context: Component,
      children: ?Array<VNode>,
      tag?: string
    ): VNode | Array<VNode> | void {
      if (isUndef(Ctor)) {
        return
      }
    
      // 构造子组件构造函数,在 initGlobalAPI 时定义
      const baseCtor = context.$options._base // => Vue
    
      // plain options object: turn it into a constructor
      // 我们平时写组件都是 export default { ... },这里会包一层 Vue.extend,可见 Vue 对于一些细节的处理是多么的到位,不管是 export default Vue.extend({}) 还是 export default {} 都是可以正常运行的
      if (isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor)
      }
    
      // if at this stage it's not a constructor or an async component factory,
      // reject.
      // 如果在该阶段Ctor不是一个构造函数或者是一个异步组件工厂直接返回
      if (typeof Ctor !== 'function') {
        if (process.env.NODE_ENV !== 'production') {
          warn(`Invalid Component definition: ${String(Ctor)}`, context)
        }
        return
      }
    
      // ...省略异步组件工厂
    
      data = data || {}
    
      // resolve constructor options in case global mixins are applied after
      // component constructor creation
      resolveConstructorOptions(Ctor)
    
      // transform component v-model data into props & events
      if (isDef(data.model)) {
        transformModel(Ctor.options, data)
      }
    
      // extract props
      // 根据组件的options定义,提炼 VNodeData 中存在于组件 props 属性
      const propsData = extractPropsFromVNodeData(data, Ctor, tag)
    
      // functional component
    
      // 函数式组件,无状态,无实例
      if (isTrue(Ctor.options.functional)) {
        return createFunctionalComponent(Ctor, propsData, data, context, children)
      }
    
      // extract listeners, since these needs to be treated as
      // child component listeners instead of DOM listeners
      const listeners = data.on
      // replace with listeners with .native modifier
      // so it gets processed during parent component patch.
      data.on = data.nativeOn
    
      if (isTrue(Ctor.options.abstract)) {
        // abstract components do not keep anything
        // other than props & listeners & slot
    
        // work around flow
        const slot = data.slot
        data = {}
        if (slot) {
          data.slot = slot
        }
      }
    
      // install component management hooks onto the placeholder node
      // 安装组件钩子函数
      // 把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数,patch 过程再回到这里来讲
      installComponentHooks(data)
    
      // return a placeholder vnode
      const name = Ctor.options.name || tag
    
      // 实例化 vnode
      // 区别普通元素的VNode构造,组件的VNode是没有传children的
      const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children },
        asyncFactory
      )
    
      // 省略WEEX相关逻辑
    
      return vnode
    }
    

    对于栗子 ? 中的 Child 组件,获取并处理构造函数、处理 VNode 属性,安装组件钩子,最后生成组件 VNode

    [咖聊] 从 render 到 VNode

    了解完组件 VNode,再看看 HTMLSVG (⚠️ 这里还包含 SVG 哦!)。

    保留标签的情况,本文用 li 标签作为 ?。跟 Child 一样的过程就不陈述了,执行到 _createElement 时会执行下面分支:

    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
    }
    

    ? 中 li 生成的 VNode

    [咖聊] 从 render 到 VNode

    总结

    执行 _render 时,会拉起各种小矮人函数去生成 VNode,我们着重介绍了 _l_c 这俩小家伙。_c 分成写 template 和写 render 函数两种情况,处理完参数之后调用 _createElement,这个函数主要做了两件事:规范子元素和生成 VNode。生成 VNode 又有两种情况:内置标签和组件。组件 VNode 通过 createComponent 生成,该函数做了三件事:

    1. Ctor -> 生成构造器;
    2. installComponentHooks -> 安装组件钩子;
    3. new VNode -> 实例化组件 VNode

    代码中还有很多分支处理,比如异步工厂函数、动态组件的处理等。这些都不是主流程,可以遇到问题再回头看(偷懒 ?)。有了 VNode ,下一节细品 update 过程——VNode 生成 DOM 和面试必考题 Diff


    起源地下载网 » [咖聊] 从 render 到 VNode

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元