最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue源码解析-组件化&虚拟DOM

    正文概述 掘金(老刘大话前端)   2021-03-30   522

    上一篇,我们分析了compiler过程,其核心是将template转化为render函数。

    那么,问题来了:

    • render函数执行后:得到的是什么?
    • 虚拟DOM又是什么?
    • vue多层组件嵌套,其组件化又是如何实现的?

    vue源码解析-组件化&虚拟DOM

    我们带着这些问题,来一探究竟。

    一. Render函数

    我们知道,compiler结果是个render函数。(不熟悉的小伙伴,可以看我的上一篇文章:vue源码解析-compiler)。

    先来看一个 ?:

    <html>
      <head>
        <meta charset="utf-8"/>
      </head>
    
      <body>
        <div id='root'>
        
        </div>
        <script src="../vue/dist/vue.js"></script>
        <script>
    
          Vue.component("test", {
            template: "<div>{{ testName }}</div>",
            data() {
              return {
                testName: '这是测试名称啊'
              }
            }
          })
    
          let vm = new Vue({
            el: '#root',
            data() {
              return {
                a: "这是根节点"
              }
            },
            template: "<div data-test='这是测试属性'> <test/> </div>",
          })
          
        </script>
      </body>
    </html>
    

    执行结果如下:

    vue源码解析-组件化&虚拟DOM

    vue在mount的时候,会调用如下代码:

    mountComponent 主干:

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

    updateComponent是在Watcher实例化时调用,这里vm会执行update方法更新视图,而参数是render函数的返回结果。下面,我们重点分析vm._render背后发生了什么

    _render函数,在vue初始化renderMixin的时候有定义。其核心代码如下:

    renderMixin

    Vue.prototype._render = function (): VNode {
      const vm: Component = this
      const { render, _parentVnode } = vm.$options
      
      // ...
      try {
        vnode = render.call(vm._renderProxy, vm.$createElement)
      }catch(e) {
        // ...
      }
      
      // ...
      return vnode
    }
    

    我们可以看到,执行_render方法,实际上就是执行 render.call(vm._renderProxy, vm.$createElement)

    而render的定义在vm.$options上, 这个其实就是compiler出来的render函数。render函数结果如下:(这一步不清楚的小伙伴可以参考我的上一篇分享:vue源码解析-compiler)

    (
      function anonymous() {
        with(this){
          return _c(
            'div',
            {
              attrs:{
                 "data-test":"这是测试属性"
              }
            },
            [
             _c('test')
            ],
            1
          )
        }
      }
    )
    

    render.call实际上执行就是这个函数。this的指向,就是当前组件实例化的对象。首次执行是Vue。

    那么_c又是什么呢?这个定义,实际上在vue初始化initRender定义的。

     vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    

    下面,我们重点分析createElement

    二. createElement

    render函数执行后,最终是调用的createElement函数,其参数就是function anonymous对应的入参。

    我们先来看下createElement函数的定义:

    export function createElement (
      context: Component,
      tag: any,
      data: any,
      children: any,
      normalizationType: any,
      alwaysNormalize: boolean
    ): VNode | Array<VNode> {
      // ...
      return _createElement(context, tag, data, children, normalizationType)
    }
    
    export function _createElement (
      context: Component,
      tag?: string | Class<Component> | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
      // 做了一些判断,避免使用可观察的data对象做虚拟节点data,否则返回 空节点
      
      // ...
      
      // tag不存在,返回空节点
      
      // ...
      
      if(typeof tag == 'string') {
        let Ctor
        
        if (config.isReservedTag(tag)) {
          // 如果是平台保留标签,将返回一个虚拟DOM
           vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
          )
        }else if( 
          (!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))
        ) {
          // 这里就是我们写个 组件 components
          vnode = createComponent(Ctor, data, context, children, tag)
        } else {
          // 未知标签,返回一个虚拟dom
          vnode = new VNode(tag, data, children, undefined, undefined, context)
        }
      } else {
        // 直接组件 options / constructor
        vnode = createComponent(tag, data, context, children)
      }
      
      // ...
      
      return vnode
    }
    

    根据render()执行,首先会执行 _c("test"),实际上调用的是 createElement(vm, "test", undefined, undefined, undefined, false) 这个时候,会进入 createComponent 逻辑。

    需要说明的是,从这里开始,需要聊到vue组件化了。这里,我们重点关注 4个函数:

      1. Vue.component
      1. extend
      1. resolveAsset
      1. createComponent

    下面,我们来逐个分析

    三. 组件化-Vue.component

    在我们的demo中,先注册了test组件,然后在div中使用。

    我们先待了解Vue.component背后发生了什么。

    在vue初始化时,会调用一个 initGlobalAPI 方法,这个函数里面,就是声明的Vue下面的全局Api,比如我们经常使用的:Vue.component, Vue.extend, Vue.use, Vue.minxin等等。

    其中initAssetRegisters这一步很重要。我们先来看主干代码:

    ASSET_TYPES.forEach(type => {
        Vue[type] = function(id: string, definition: Function | Object): Function | Object | void {
          // ...
          
          if (type === 'component' && isPlainObject(definition)) {
              definition.name = definition.name || id
              definition = this.options._base.extend(definition)
          }
          // ...
          this.options[type + 's'][id] = definition
          return definition
        }
    })
    

    ASSET_TYPES如下:

    export const ASSET_TYPES = [
      'component',
      'directive',
      'filter'
    ]
    

    这里我们可以看到Vue.component第一个参数就是组件的name,第二个是组件的options。

    this.options._base指向的就是Vue的构造函数。下面我们看Vue.extend方法

    四. 组件化-extend

    主干代码如下:

    export function initExtend (Vue: GlobalAPI) {
      const Super = this
      const SuperId = Super.cid
      
      // 省略cid cache
      // ...
      
      // 校验组件名称...
      
      const Sub = function VueComponent (options) {
        this._init(options)
      }
      Sub.prototype = Object.create(Super.prototype)
      Sub.prototype.constructor = Sub
      
      Sub.options = mergeOptions(
        Super.options,
        extendOptions
      )
      Sub['super'] = Super
      
      // 初始化props...
      
      // 初始化computed...
      
      // 初始化VueComponent构造函数的方法,实际上使用的是Vue构造函数方法
      Sub.extend = Super.extend
      Sub.mixin = Super.mixin
      Sub.use = Super.use
      
      // 添加asset types, 其实就是3个值 component, directive, filter
      ASSET_TYPES.forEach(function (type) {
        Sub[type] = Super[type]
      })
      
      // ...其他options
      
      // cache superId, 下次执行extend时,在cache中存在,直接返回,不必再走继承流程
      
      return Sub;
    }
    

    vue源码解析-组件化&虚拟DOM

    可以看到,在我们使用Vue.component注册组件的时候:被注册的组件,他也是个构造函数,只是他不是Vue,他是VueComponent。

    而VueComponent继承了Vue,他拥有了Vue的一切。

    好了,到这里,我们终于知道,注册组件,实际上就是以下结构:

    Vue.$options.components.__proto__ = {
      test: VueComponent // 子组件的构造函数
    }
    

    而这个VueComponent通过原型继承至Vue,所以如果子组件里面,还有孙组件。那么子组件中的结构就变成如下:

    VueComponent.$options.components.__proto__ = {
      "孙组件name": VueComponent // 孙组件的构造函数
    }
    

    如此递归下去,在每个组件的$options.components属性上,都存放着对应的子组件的构造函数。这一点很重要,这才是实现组件化的开始。

    五. 组件化-resolveAsset

    在createElement中,我们看到y有这么两行代码:

    let Ctor
    // ...
    Ctor = resolveAsset(context.$options, 'components', tag)
    // ...
    

    这一步也很重要,我们继续看resolveAsset 主干:

    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;
    }
    

    这里已经很清晰了,assets变量指的就是Vue.$options.components对象。id就是 我们demo中的子组件name = "test"。

    这里,我们看到camelize和capitalize这2个函数。

    • 其中camelize就是将子组件名使用中划线连接起来,比如,我们的组件name = "helloWorld", 最终转化为 hello-world使用。 这也是为什么,我们使用中划线的方式可以引用组件的原因。
    • 其中capitalize函数,是将首字母进行大写,比如:name = "test",组件名称将转化为Test,这也是为什么,我们在vue中,首字母大写组件名也能正常引用的原因。

    到这里,我们可以看到,最后返回的是Vue.$options.components.test,其实就是子组件的构造函数。即Ctor就是子组件test的构造函数。

    六. 组件化-createComponent

    主干代码如下:

    export function createComponent (
      Ctor: Class<Component> | Function | Object | void,
      data: ?VNodeData,
      context: Component,
      children: ?Array<VNode>,
      tag?: string
    ): VNode | Array<VNode> | void {
      
      // ...
      
      // 异步组件暂不在讨论范围之内,暂时忽略,后面关注我,分析异步组件实现
      
      // 调用子组件options merge 
      
      // ...
      
      // 函数式组件,暂时忽略,不在本次主流程中讨论,后面关注我,分析函数式组件实现
      
      // ...
      
      installComponentHooks(data)
      
      
      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
      )
      
      // weex 平台兼容
      
      return vnode
    }
    

    installComponentHooks

    该函数也是非常重要的一个环节,在render函数执行,最终返回虚拟DOM时,组件内部的转化处理,也有个生命周期。

    其核心hooks如下:

    const componentVNodeHooks = {
      init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
        // keep-alive逻辑,暂省略
        // ...
        const child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        )
        child.$mount(hydrating ? vnode.elm : undefined, hydrating)
      },
      
      prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
        const options = vnode.componentOptions
        const child = vnode.componentInstance = oldVnode.componentInstance
        updateChildComponent(
          child,
          options.propsData, // updated props
          options.listeners, // updated listeners
          vnode, // new parent vnode
          options.children // new children
        )
      },
      
      insert(vnode: MountedComponentVNode) {
        // 调用子 组件 mounted hooks
        
        // ...
       
        // keep-alive 逻辑处理
      },
      
      destroy(vnode: MountedComponentVNode) {
        // 调用子组件 $destroy 方法
        // ...
        // 删除当前组件active 实例
      }
    }
    

    到这里,我们已经看到,data对象上绑定了对应的 组件 实例化的hooks。

    注意,这里子组件还没有实例化,相关的Dep依赖收集还未开始。

    这个时候,data: VNodeData 数据结构大致如下:

    {
      on: 'xx',
      hooks: {
        init: () => { 
           // ...
        },
        prepatch: () => {
          // ...
        },
        insert: () => {
          // ...
        },
        destroy: () => {
          // ...
        }
      }
    }
    

    下面,我们进入组件化实现的另一个环节-虚拟DOM

    七. 组件化-VNode

    真实DOM

    在html中,我们任意打印一个真实的div dom,会看到如下效果:

    vue源码解析-组件化&虚拟DOM

    可以看到,一个真实dom的基础属性就有这么多,总计296个属性。

    虚拟DOM

    vnode 中文意为 虚拟DOM。 什么是虚拟DOM,其实他就是对 真实DOM 的一种描述。

    世间万物的本质,就是 数据结构 + 算法。同样,真实的dom,我们也可以使用js对象进行描述他。

    通过上图,我们可以发现,真实的dom有许多属性,而虚拟dom根本不需要那么多,只需要知道,tag名称,数据对象,是否有childrens,parent是谁等基本属性。

    频繁操作真实的dom代价是昂贵的,而操作虚拟dom,代价是很小的,我们可以通过虚拟的dom,即js对象,在内存中变更对比,再一把crud。

    实际上,虚拟dom存在的意义,不外乎两种:

    • 提升性能
    • 跨平台

    虚拟dom主干代码:

    class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array<VNode>;
      text: string | void;
      elm: Node | void;
      ns: string | void;
      context: Component | void; 
      key: string | number | void;
      componentOptions: VNodeComponentOptions | void;
      componentInstance: Component | void; 
      parent: VNode | void; 
      
      // 其他属性 ...
      
      isStatic: boolean;
      isComment: boolean;
      ssrContext: Object | void;
      
      // ...
      
      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
        this.elm = elm
        this.ns = undefined
        this.context = context
        // ...
        this.key = data && data.key
        this.componentOptions = componentOptions
        this.componentInstance = undefined
        this.parent = undefined
        // ...
      }
      
      get child (): Component | void {
        return this.componentInstance
      }
    }
    
    // 其他方法,创建空节点
    
    // clone vnode...
    
    // 创建文本节点...
    
    

    我们可以看到,VNode是一个class类,其中常用的属性: tag, data, children, text, elm, context, componentOptions等,这些参数很重要。

    接着第六步,createComponent:

    需要指出的是:

    • data: VNodeData 不一定有值。如果是组件,该值为空。 componentOptions和componentInstance是组件的特有属性。
    • 组件的tag和普通的不一样,组件的tag统一为: "vue-component-${cid}-${name}"

    createComponent方法,最后返回虚拟dom,子组件Test: 其核心结构大致如下:

    {
      tag: "vue-component-1-test",
      text: '',
      isStatic: false, // 是否是静态节点,后面做diff算法时,是很关键的一步
      isComment: false,
      ele: div, // div为真实的dom div 对象, 每个子组件,都会先生成一个空div占位节点
      data: {
        hooks: {
          init: () => {
            // ...
          },
          prepatch: () => {
            // ...
          },
          insert: () => {
            // ...
          },
          destroy: () => {
            // ...
          }
        }
      }, // data数据很重要,组件实例化时需要使用到
      context: Vue,
      componentOptions: {
        // 此项很重要,是组件特有属性
        Ctor: VueComponent,   // 子组件构造函数
        children: undefined,  // 子组件中是否还有嵌套,这里我们demo中没有
        tag: "test",
        // ...
      },
      componentInstance: undefined, // 子组件的实例对象, 为什么这里是undefined,因为还没实例化
      children: undefined,
      // ...
    }
    

    好了,到这里,虚拟dom神秘的面纱被揭开了,他就是个js对象。

    demo中,整个div的虚拟dom结构如下:

    {
      tag: "div",
      isStatic: false,
      isRootInsert: true,
      isComment: false,
      // ...
      data: {
        attrs: {
          "data-test": "这是测试属性"
        }
      },
      context: Vue,
      componentOptions: undefined,
      componentInstance: undefined,
      children: [
        // 上述子组件VNode
      ],
      // ...
    }
    

    我们再回归到 菜单一 中的 mountComponent函数。

    我们可以看到vm._render() 函数执行结果,就是返回的一个虚拟DOM。 这个虚拟dom,实际上就是个js对象,如上述代码所述。

    而组件化工作并未结束,高潮即将来临:

    下面,我们来分析:update patch工作。

    vue源码解析-组件化&虚拟DOM

    八. 组件化-Patch

    patch过程,是vue将虚拟dom转化为真实dom,展示在页面上的最后一个环节了。 但在此处,我们只看组件化实现相关部分。 其他部分:更新队列,diff算法,映射真实dom等,我会在下个章节里面,重点来分析。

    废话少说,开干。

    vue源码解析-组件化&虚拟DOM

    patch环节,核心的入口是 createPatchFunction。我先开看 与 组件化相关的 核心代码

    function createPatchFunction(backend) {
      const { modules, nodeOps } = backend
      
      function createElm(
        vnode,
        insertedVnodeQueue,
        parentElm,
        refElm,
        nested,
        ownerArray,
        index
      ) {
        if( createComponent(vnode, insertedVnodeQueue, parentElm, refElm) ) return
        
        // ...
        
        if( isDef(tag) ) {
          // ...
          
          if(__WEEX__) {
            // weex 平台相关处理
          }else {
            createChildren(vnode, children, insertedVnodeQueue)
          }
        }
      }
      
      
      function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        // ...
        let i = vnode.data
        if (isDef(i)) {
    
          const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
          
          if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */)
          }
          
          if (isDef(vnode.componentInstance)) {
            // 初始化component, insert队列
            
            // 暂时忽略...
          }
        }
      }
      
      function createChildren (vnode, children, insertedVnodeQueue) {
        // ...
        for (let i = 0; i < children.length; ++i) {
          createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
        }
        // ...
      }
      
      return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // ...
        
        createElm(
          vnode, 
          insertedVnodeQueue, 
          oldElm._leaveCb ? null : parentElm, 
          nodeOps.nextSibling(oldElm)
        )
        // ...
      }
    }
    

    抽丝剥茧,我们直接看组件化相关逻辑,vnode就是 render函数执行之后的虚拟dom js对象。

    不难看出,

    • 第一层的vdom对象,没有hook属性,最后整个函数返回undefined,不会进行相关hooks
    • 往下执行到createChildren 方法,递归执行所有childrens
    • 我们demo只,只有一个子组件test,这里将再次调用 createElm 方法。而此时传入的vnode就是子组件vnode数据

    重点来了,子组件vnode进入createComponent方法,此时vnode.componentInstance 是存在的。(前面说过,这是组件的特有属性)

    核心:

     let i = vnode.data
     if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false)
     }
    

    子组件vnode.data 实际上就是 之前提到的 组件实例化中的生命周期hooks,包括: init, prepatch, insert, destroy

    显示子组件的Init方法是存在的, 那么执行子组件的init方法。

    init核心如下:

     init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
       // keep-alive处理省略...
       
       const child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        )
        child.$mount(hydrating ? vnode.elm : undefined, hydrating)
     }
    

    createComponentInstanceForVnode核心如下:

    function createComponentInstanceForVnode (vnode: any, parent: any) {
      // ...
      
      return new vnode.componentOptions.Ctor(options)
    }
    

    vue源码解析-组件化&虚拟DOM

    终于,我们看到了子组件处理的本质。

    前面我们提到,vnode.componentOptions.Ctor 就是子组件的构造函数。即VueComponent构造函数。

    而此构造函数通过原型继承至Vue构造函数。 那么当子组件的VueComponent构造函数被实例化时。我们再回顾下,VueComponent相关代码:

    // ...
    const Sub = function VueComponent (options) {
       this._init(options)
     }
     Sub.prototype = Object.create(Super.prototype)
     Sub.prototype.constructor = Sub
     
     // ...
    

    所以,当实例化子组件的时候,就会执行_init方法,而_init方法继承至Vue构造函数。那么子组件实例化时,实际上就是走了一遍 new Vue的过程,只是当前的this指向是VueComponent,而不是Vue。

    具体实例化发了什么,不清楚的小伙伴,可以看我的《vue源码解析-开始》。

    实例化子组件后,其实_watcher 还是 undefined。

    紧接着执行:

    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    

    子组件被挂载了。 不清楚$mount背后发生了什么的小伙伴,可以看我的《vue源码解析-$mount》

    而子组件挂载时,又将执行子组件的compiler,返回对应的子组件render函数。(demo中,第一次执行时,子组件是个_c(test))。这里,子组件render函数如下:

    function anonymous(
    ) {
    with(this){return _c('div',[_v(_s(testName)+" "+_s(a))])}
    }
    

    那么当子组件执行render函数,返回虚拟dom时, 那么将触发子组件的依赖收集。(而不是Watcher, Dep, Observe实例化时,真正的执行dep收集在render函数返回虚拟dom阶段)。

    嗯,综上,如果是n层嵌套,那么将递归执行上面的流程,每个组件其实都是单独的一个 VueComponent构造函数。每个子组件被实例化时,都是新的vm实例。

    组件化的设计很巧妙,这里只分析了核心流程,而细枝末节还有许多,比如:异步组件,函数式组件,keep-alive等。这些我会在后面的章节,详细探讨。

    下一章,我们将分析,patch详细过程

    码字不易,多多关注~?


    起源地下载网 » vue源码解析-组件化&虚拟DOM

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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