Wang's blog Wang's blog
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Wang Mings

跟随大神,成为大神!
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Python爬虫

  • 前端教程

    • 团队规范

    • Project

    • JS

    • NodeJS

    • Vue

      • 个人理解Vue和React区别
      • Vue高级用法
      • Vue2.x源码分析 - 框架结构
      • Vue2.x源码分析 - 模版编译以及挂载
      • 虚拟dom算法库 - snabbdom
      • Vue2.x源码分析 - Virtual DOM实现
      • Vue2.x源码分析 - 事件系统
      • Vue2.x源码分析 - 组件系统
        • [#](#生命周期) 生命周期
        • [#](#注册组件) 注册组件
      • Vue2.x源码分析 - Vue.nextTick
      • Vue2.x源码分析 - inject provide
      • Vue2.x源码分析 - 解析Template模板
      • Vue2.x源码分析 - 响应式原理
      • Vue2.x源码分析 - v-model
      • Vue CLI3 插件系统原理
      • Vue Loader v15 源码解析
      • Vue3 设计思想
      • Vue3 RFCS导读
      • Vue3 响应式原理 - Ref Reactive Effect源码分析
      • Vue3 API 源码解析
      • 为何Vue3 Proxy 更快
      • Vue核心原理 - 笔记
    • React

    • 效率工具

    • 读书笔记

  • 教程
  • 前端教程
  • Vue
wangmings
2022-07-19
目录

Vue2.x源码分析 - 组件系统

# # Vue2.x源码分析 - 组件系统

组件系统是Vue整个框架中非常重要的一环,涉及的内容也比较多。这里笔者还是老样子,梳理组件系统的主流程,了解Vue当中的组件系统是如何运转的。

# # 组件编译阶段

在模版编译以及挂载中我们知道,最终的模板都会解析编译成render函数。如果你是手动写render函数时,你一定少不了使用createElement函数。createElement函数,对应render中的参数h,用来手动创建VNode(最终都是形成VNode,然后再前后两个VNode进行diff对比并更新DOM)。举个应用案例:

    import Vue from 'vue'
    import App from './App.vue'
    
    // 带有el参数会最终执行render渲染。src/core/instance/render.js
    var app = new Vue({
      el: '#app',
      // 这里的 h 是 createElement方法
      // App是对应的组件
      render: h => h(App)
    })
    
1
2
3
4
5
6
7
8
9
10
11

# # createElement做了什么

  • tag 做判断,如果是 string 类型。src/core/vdom/create-element.js
    • 如果是内置的一些节点,则直接创建一个普通 VNode
    • 如果是为已注册的组件名,找到组件定义,然后通过 createComponent 创建一个组件类型的 VNode
  • 如果是非 string 类型,createComponent同上。
    • createComponent做了什么 'src/core/vdom/create-component.js'

        1.  构造子类构造函数
      
        if (isObject(Ctor)) {
            Ctor = Vue.extend(Ctor)
        }
        
1
2
3
4
        // Vue.extend 的作用就是构造一个 Vue 的子类
        // 把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回
        Vue.extend = function (extendOptions: Object): Function {
            const Super = this // this === Vue
            const Sub = function VueComponent (options) {
                this._init(options)
            }
            Sub.prototype = Object.create(Super.prototype)
            Sub.prototype.constructor = Sub
            Sub.cid = cid++
            Sub.options = mergeOptions(
                Super.options,
                extendOptions
            )
            Sub['super'] = Super
        }
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      2.  安装组件钩子函数。在组件创建、销毁等阶段会调用到。

* data.hook.init。 创建组件Vue实例。核心代码:new SubVue\(\).\$mount\(\)
* data.hook.prepatch
* data.hook.insert
* data.hook.destroy
        // 往 data.hook中注入钩子函数,在vnode patch阶段会调用
        installComponentHooks(data)
        
1
2
3
      3.  实例化VNode\(最终都是生成VNode,VNode数据结构本质是颗树\)。注意组件的VNode是没有children,取而代之的是componentOption.Ctor
        const name = Ctor.options.name || tag
        // 注意:组件的VNode没有children字段,有的是componentOptions.Ctor
        const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children },
        asyncFactory
        )
        return vnode
        
1
2
3
4
5
6
7
8
9
10

以上可以知道,在编译阶段遇到是组件时,最终也是编译成VNode节点,只不过它会额外做一些处理,比如拿到组件子类Ctor、传递propsData、设置listeners、设置data.hook等,并把这些内容作为参数,传入进 VNode.componentOptions,等待patch阶段生成真正的DOM组件。

# # 组件patch阶段

在Vue2.x源码分析 - 模版编译以及挂载讲到,patch阶段会把VNode转换成真正的 DOM 节点。patch(oldVNode, vnode)的过程(如果只考虑组件初始化渲染的话),会调用 createElm(vnode),根据vnode类型创建真实DOM元素。'src/core/vdom/patch.js'

    function patch (oldVnode, vnode, hydrating, removeOnly) {
        // 1. 新vnode为空,销毁老vnode
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        if (isUndef(oldVnode)) {
          // 2. 新vnode,没有老vnode,直接根据新的VNode创建真实DOM(初始化渲染都走这流程)
          createElm(vnode, insertedVnodeQueue)
        } else {
          // 新老vnode都有
          if (sameVnode(oldVnode, vnode)) {
            // 3. 有相同的type和key,patch对比更新
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
          } else {
            // 4. vnode数据结构不同,直接创建新vnode,销毁老vnode
            // 根据新vnode,创建真实dom
            createElm(vnode)
    
            // 根据老vnode,销毁dom
            if (isDef(parentElm)) {
              removeVnodes(parentElm, [oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {
              invokeDestroyHook(oldVnode)
            }
          }
        }
    
        return vnode.elm
      }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    function createElm (
        vnode,
        insertedVnodeQueue,
        parentElm,
        refElm,
        nested,
        ownerArray,
        index
      ) {
        // 1. 如果是组件vnode,初始化组件new SubVue(options)
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
          return
        }
    
        // 以下都是非组件,包括:dom vnode/文本 vnode/注释 vnode
        const data = vnode.data
        const children = vnode.children
        const tag = vnode.tag
        // 2. 如果有tag标签,表明是dom vnode
        if (isDef(tag)) {
          vnode.elm = nodeOps.createElement(tag, vnode)
    
          // 处理children,childVNode执行createElm,递归。
          createChildren(vnode, children, insertedVnodeQueue)
          insert(parentElm, vnode.elm, refElm)
        } else if (isTrue(vnode.isComment)) {
          // 3. 注释类型
          vnode.elm = nodeOps.createComment(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        } else {
          // 4. 文本类型
          vnode.elm = nodeOps.createTextNode(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        }
      }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

后面还有createChildren/updateChildren等流程,核心还是依据前后两个VNode进行diff算法,再更新真实DOM。以下贴出笔者vue patch流程的思维导图:

image

# # 生命周期

    // 调用钩子,即找到vm.$options[hook]执行
    export function callHook (vm: Component, hook: string) {
      const handlers = vm.$options[hook]
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
            handlers[i].call(vm)
        }
      }
      // 程序化的事件侦听器:https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%A8%8B%E5%BA%8F%E5%8C%96%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%BE%A6%E5%90%AC%E5%99%A8
      if (vm._hasHookEvent) { // Vue.prototype.$on:vm._hasHookEvent = /^hook:/.test(event)
        vm.$emit('hook:' + hook)
      }
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • beforeCreate
    • beforeCreate 的钩子函数中不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。
    • beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中
  • created
    • 能获得props、data值,但不能操作dom
    // new Vue({options})时执行this._init()
    Vue.prototype._init = function (options?: Object) {
      // ...
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
      // ...
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • beforeMount
    • DOM 挂载之前,它的调用时机是在 mountComponent 函数中,定义在 src/core/instance/lifecycle.js 中
  • mounted
    • 把 VNode patch 到真实 DOM 后
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      // ...
      callHook(vm, 'beforeMount')
    
      let updateComponent = () => {
        // vm._render得到VNode
        // vm._update真正去更新DOM
        vm._update(vm._render(), hydrating)
      }
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    
      // vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是通过外部 new Vue 初始化过程。
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • beforeUpdate
    • beforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中
  • update
    • update 的执行时机是在flushSchedulerQueue 函数调用的时候, 它的定义在 src/core/observer/scheduler.js 中
  • beforeDestroy & destroyed
    • beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,调用 $destroy 方法,它的定义在 src/core/instance/lifecycle.js 中。
  • activated & deactivated
    • activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子

# # 注册组件

  • 全局注册API
    • Vue.component
    • Vue.directive
    • Vue.filter
  • 过程
    • component:使用Vue.extend拿到Vue子类,挂载到 Vue.options.components上。当有组件注册后,即可根据注册的名字,找到组件定义(组件可理解为一个Object对象)。
    • directive/filter,挂载到Vue.options.directives/filters上
  • 总结
    • 注册组件,重点是把 components 合并到 vm.$options.components 上,这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,方便后续创建。
    • 局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。
    // src/core/global-api/assets.js
    import { ASSET_TYPES } from 'shared/constants'
    import { isPlainObject, validateComponentName } from '../util/index'
    
    export function initAssetRegisters (Vue: GlobalAPI) {
      /**
       * Create asset registration methods.
       */
      ASSET_TYPES.forEach(type => {
        Vue[type] = function (
          id: string,
          definition: Function | Object
        ): Function | Object | void {
          if (!definition) {
            return this.options[type + 's'][id]
          } else {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && type === 'component') {
              validateComponentName(id)
            }
            // component处理,使用Vue.extend扩展,使得继承Vue类
            if (type === 'component' && isPlainObject(definition)) {
              definition.name = definition.name || id
              definition = this.options._base.extend(definition)
            }
    
            // directive处理,如果是function,绑定为对象
            // 官方API:https://cn.vuejs.org/v2/guide/custom-directive.html
            if (type === 'directive' && typeof definition === 'function') {
              definition = { bind: definition, update: definition }
            }
            // 资源型的componet、directive、filter都是挂载到vm.options上
            this.options[type + 's'][id] = definition
            return definition
          }
        }
      })
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
编辑 (opens new window)
Vue2.x源码分析 - 事件系统
Vue2.x源码分析 - Vue.nextTick

← Vue2.x源码分析 - 事件系统 Vue2.x源码分析 - Vue.nextTick→

最近更新
01
theme-vdoing-blog博客静态编译问题
09-16
02
搜索引擎
07-19
03
友情链接
07-19
更多文章>
Theme by Vdoing | Copyright © 2019-2022 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式