vue源码阅读解析(超详细、双向数据绑定原理解析、监听数组变化、computed实现、nextTick原理)

321 阅读3分钟

前言:使用 vue 也有一段时间了,前段时间看了看 vue 的源码,很多小伙伴看到vue几万行的源码都是望而止步了,当时看源码看得也很费劲,下面分享一下我对vue源码的一些理解和感触。

源码目录

在这里插入图片描述 下面会把部分不是很重要的代码进行简化,方便大家观看

1. new Vue初始化流程

新建一个 html 文件引入vue。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./vue-2.6.10/dist/vue.js"></script>
    </script>
    <script>
         new Vue({
            el:'#app',
         })
    </script>
</body>
</html>

vue 初始化就从这里开始了。 我们现在打开vue源码 src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // new Vue的时候就执行到了这个_init函数
  this._init(options)
}
initMixin(Vue) // 其他的不看,先看这个函数
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

vue 的整个构造函数就调用了 _init 方法 , 那么这个 _init 方法哪儿来的呢?

我们看看 _init 函数做了些什么。

initMixin(Vue)

我们现在打开源码 src/core/instance/init.js

   Vue.prototype._init = function (options) {
        let vm = this;
        // 每个vue都会有一个uid
        vm._uid = uid++
        // 把一些全局的api方法混入到当前实例的$options上面
        vm.$options = mergeOptions(vm.constructor.options, options)
        //生命周期钩子beforeCreate
        callHook(vm, 'beforeCreate')
        //初始化状态,props,methods,data,computed,watch
        initState(vm)
        //初始化成功后调用created钩子
        callHook(vm, 'created')
        if (vm.$options.el) {
            //开始挂载
            vm.$mount(vm.$options.el)
        }
    }

下面我们找到打开 $mount 看看它做了什么

点开源码 src/platforms/web/entry-runtime-with-compiler.js

把一些多余的 代码简化一下。

Vue.prototype.$mount = function (el) {
		//根据用户传入的 el 属性获取节点
        el = el && document.querySelector(el);
        let vm = this;
        //把节点放在 vm.$el 上方便后面使用
        vm.$el = el;
        let options = vm.$options;
        let template
        /**
         * 编译权重:
         * 优先看有没有render函数,如果有直接用
         * 如果没有render函数就看有没有template模板
         * 如果都没有就直接获取el的outerHTML作为渲染模板
         */
        if (!options.render) {
            if (!options.template) {
                template = el.outerHTML
            } else {
                template = vm.$options.template
            }
        }
        if (template) {
            //用 template 生成 render 函数
            let render = compileToFunctions(template)
            options.render = render
        }
        //调用 mount 方法开始渲染页面。
        return mount(this, el)
    }

上面代码主要实现了 vue 渲染过程中很重要的一步,得到 render 函数。

如果我们使用的 template 进行编写HTML代码,vue 内部会把模板编译成 vue 可识别的 render 函数,如果有写 render 则可以省去编译过程。( 直接写 render 函数对 vue 编译效率会更好 )

下面打开源码 src/core/instance/lifecycle.js 找到 mountComponent 方法

export function mountComponent(vm, el) {
	//渲染之前调用 beforeMount 生命周期
    callHook(vm, 'beforeMount')
    //创建一个更新渲染函数 ( 用来得到 Vnode 渲染真实 dom )
    let updateComponent = () => {
        vm.update(vm._render())
    }
    //生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染
    new Watcher(vm, updateComponent, () => {},{
        before () {
            callHook(vm, 'beforeUpdate')
        }
      },true)
    //渲染真实 dom 结束后调用 mounted 生命周期
    callHook(vm, 'mounted')
}

这里就是开始准备挂载真实 dom 了,创建了渲染 watcher ,渲染 watcher 内部调用了 updateComponent 方法。

继续往下看打开 Watcher 所在位置 src/core/observer/watcher.js

export class Watcher {
    constructor(vm,expOrFn,cb,options) {
        if (typeof expOrFn === 'function') {
        	// 保留 updateComponent 方法
            this.getters = expOrFn
        }
        this.get();
    }
    get() {
        pushTarget(this)
        let value
        // 这里调用了 updateComponent 方法
        value = this.getters.call(this.vm, this.vm);
        popTarget()
        return value
    }
}

vue 初次渲染时 watcher 内部调用了 updateComponent 方法 ( 数据添加依赖我们后面说 )

updateComponent 整个渲染周期最关键的几行。

let updateComponent = () => {
     //获取到虚拟 dom 调用 update 进行渲染 
     vm.update(vm._render())
}

我们进入 vm._render 函数

打开 src/core/instance/render.js

Vue.prototype._render = function () {
     let vm = this
     // 拿到 render 函数
     let render = vm.$options.render;
     // 调用 render 函数得到 Vnode
     return render.call(vm)
}

调用 render 函数得到 Vnode

接着我们进入 vm.update 函数

找到 vm.update 函数位置 src/core/instance/lifecycle.js

Vue.prototype.update = function (vnode) {
     let vm = this
     // 获取到上一次的 Vnode 用于 diff 对比
     const prevVnode = vm._vnode
     if (!prevVnode) {
     	 //首次渲染走这里
         vm.$el = patch(vm.$el, vnode)
     } else {
     	 //数据更新驱动视图更新走这里
         vm.$el = patch(prevVnode, vnode)
     }
     //保留 Vnode
     vm._vnode = vnode
}

进入 patch 方法 src/core/vdom/patch.js

 return function patch(el, vnode, hydrating, removeOnly) {
  	//首次渲染使用 Vnode 创建真实 dom
    createElm(vnode, false, el)
    return vnode.elm
  }
 function createElm (
    vnode, //虚拟dom
    insertedVnodeQueue,
    parentElm, //父节点
  ) {
     // 查看元素 tag 是不是组件,如果是组件就创建组件
    if (createComponent(vnode, insertedVnodeQueue, parentElm)) {
      return
    }
    const data = vnode.data //得到 data 数据
    const children = vnode.children //得到子元素
    const tag = vnode.tag //获取标签名
    vnode.elm = document.createElement(tag)
    if (isDef(tag)) {
   	  //如果有子节点递归渲染子节点
      createChildren(vnode, children, insertedVnodeQueue)
   	  //给父元素插入子元素
      parentElm.appendChild(elm)
    } else if (isTrue(vnode.isComment)) {
      //创建注释节点
      vnode.elm = document.createComment(vnode.text)
      //给父元素插入注释节点
      parentElm.appendChild(elm)
    } else {
      //创建文本节点
      vnode.elm = document.createTextNode(vnode.text)
      //给父元素插入文本节点
      parentElm.appendChild(elm)
    }
  }
function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      for (let i = 0; i < children.length; ++i) {
      	//渲染子节点
        createElm(children[i], insertedVnodeQueue, vnode.elm)
      }
    }
  }

以上就是 new Vue 的整个流程。

2. 双向数据绑定原理解析

大家可能都知道,关于Vue的双向绑定,很多人都知道,核心是 Object.defineProperty() 方法,那接下来我们就简单介绍一下! 语法: Object.defineProperty(obj, prop, descriptor) 其中: obj 要在其上定义属性的对象。 prop 要定义或修改的属性的名称。 descriptor 将被定义或修改的属性描述符。

其实,简单点来说,就是通过此方法来定义一个值。 调用,使用到了 get 方法, 赋值,使用到了 set 方法。

vue 双向绑定内部核心就是利用了两个类, Dep 类和 watcher 类

每个在页面上使用了的属性、数组、对象都会有一个 Dep 类,访问属性的时候 get 方法会收集对应的 watcher

同样渲染 watcher 也会收集对应的 Dep

vue 内部实现双向绑定过程:简单来说就是初始化 data 的时候会调用 observe 方法给,data 里的属性重写 get 方法和 set 方法,到渲染真实 dom 的时,渲染 watcher 会去访问页面上使用的属性变量,给属性的 Dep 都加上渲染函数,每次修改数据时通知渲染 watcher 更新视图

打开 src/core/observer/index.js

eexport function observe(data) {
	// 不是对象或者数组直接 return
    if (typeof data !== 'object' || data == null) {
        return
    }
    // 这里的 data 就是创建 vue 传入的 data 属性
    return new Observe(data)
}
class Observe{
    constructor(value) {
    	//添加 Dep
        this.dep = new Dep()
        //用于数组改变了可以获取到 Dep 进行更新视图
        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,
        })
        //vue 对数组做了特别的处理 数组重写了一些方法
        if (Array.isArray(value)) {
            //如果是数组重写数组方法,再进行尝试监听
            value.__proto__ = arrayMethods;
            //尝试监听数组内部的属性。
            this.observeArray(value)
        } else {
        	//递归处理对象
            this.walk(value)
        }
    }
    walk(data) {
        Object.keys(data).forEach((key, index) => {
            defineProperty(data,key,data[key])
        })
    }
    observeArray(value) {
        value.forEach(item => {
            observe(item)
        })
    }
}

Dep 类的实现 src/core/observer/dep.js

let id = 0
export class Dep{
    constructor() {
        this.subs = [];
        this.id = id++
    }
    //给 watcher 添加 Dep
    depend() {
        Dep.target.addDep(this)
    }
    //给 dep 添加对应的 watch
    addSub(watch) {
        this.subs.push(watch)
    }
    //调用 watcher 里的渲染函数
    notify() {
        this.subs.forEach((watcher) => {
            if (watcher) {
                watcher.upDate()
            }
        })
    }
}
const targetStack = []
// 渲染阶段,访问页面上的属性变量时,给对应的 Dep 添加 watcher
export function pushTarget(watcher) {
    targetStack.push(watcher)
    Dep.target = watcher
}
// 访问结束后删除
export function popTarget() {
    targetStack.pop()
    Dep.target = targetStack[targetStack.length - 1]
}

每个属性、对象、数组上都有一个 Dep 类型,Dep 类主要就是收集用于渲染的 watcher

watchersrc/core/observer/watcher.js

export class Watcher {
    constructor(vm,expOrFn,cb,options) {
        this.vm = vm
        this.expOrFn = expOrFn
        this.deps = [];
        this.set = {}
        this.id = id++
        if (typeof expOrFn === 'function') {
            this.getters = expOrFn
        } 
        this.value = this.get();
    }
    addDep(dep) {
        let id = dep.id
        //去重防止 dep 添加 watch 多次
        if (!this.set[id]) {
            // watcher 添加 dep
            this.deps.push(dep)
            //重点!给 dep 添加 watch
            dep.addSub(this)
            this.set[id] = true;
        }
    }
    get() {
        //标记 target
        pushTarget(this)
        // 重点,这里会去访问我们给属性重写的 get 方法,添加 watcher 依赖
         this.getters.call(this.vm, this.vm);
        //弹出target防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
        popTarget()
        return value
    }
   	upDate() {
        this.get(this)
   }
}

接着看看 defineProperty 方法

function defineProperty(data, key, value) {
    //childDep 这个 dep 只会存在 {} 或 [] 里面
    let childDep = observe(value)
    //这个 dep 会存在每个属性里面
    let dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
            //渲染的期间给每个放在页面上的变量添加 watcher
            //只有渲染阶段才会 Dep.target ,有正常访问 target 是没有的
            if (Dep.target) {
                //给属性 dep 添加 watcher
                dep.depend()
                if (childDep) {
                    //给属性是数组或者是对象的添加 watcher
                    childDep.dep.depend()
                    if (Array.isArray(value)) {
                        //如果是数组递归数组给数组里面的数组添加 watcher
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set(newValue) {
            if(newValue == value)return
            //更新数组或者对象的时候也要创建一个新的 dep 给 childDep
            childDep = observe(newValue)
            value = newValue
            //更新视图
            dep.notify()
        }
    })
}

初始化 data 属性时,递归给 data 的属性,重写 get set,同时会给它们身上都添加一个 Dep 类, 渲染阶段 Dep 类会收集 watcher 。每次修改数据会调用 dep.notify() 更新视图

3. 监听数组变化

点开 src/core/observer/array.js

//获取数组的原型Array.prototype,拿到数组原有的方法
const arrayProto = Array.prototype
//创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
//列出需要重写的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
//遍历列出的方法
methodsToPatch.forEach((key) => {
    //重写数组方法
    arrayMethods[key] = function (...args) {
    	// 数组原有的方法
        let result = arrayProto[key].apply(this, args);
        let inserted
        //该数组是响应式的时候,拿到数组上的 __ob__ 属性
        let ob = this.__ob__
        //处理如果是数组添加的对象或者是数组
        switch (key) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
            default:
            break;
        }
        if (inserted) ob.observeArray(inserted)
        //重点!每个响应式数组上都会有一个 __ob__ 利用我们保留的 __ob__ 属性获取 notify 方法更新视图
        ob.dep.notify()
        return result
    }
})

4. Watch监听的实现

打开 src/core/instance/state.js 找到 initWatch

function initWatch(vm) {
	//获取到传入的 watch
    let watch = vm.$options.watch
    //遍历 watch 列表
    for (let key in watch) {
        const handler = watch[key];
        //可以传入数组
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
              createWatcher(vm, key, handler[i])
            }
        } else {
        	//创建 watch
            createWatcher(vm, key, handler)
        }
    }
}
function createWatcher(vm, expOrFn, handler, options) {
		// handler 如果是对象取出里面的 handler 函数
        if (typeof handler === "object") {
            options = handler
            handler = handler.handler
        }
        //如果监听的方法是 methods 里的方法直接取出来
        if (typeof handler === 'string') {
            handler = vm[handler]
        }
      return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
        let vm = this;
        options = options || {};
        //标识这个是用户 watch
        options.user = true;
        //使用 Watcher 类来创建渲染 Watcher
        const watcher = new Watcher(vm, expOrFn, cb, options);
}

监听 Watcher 类的实现

export class Watcher {
    constructor(vm,expOrFn,cb,options) {
        this.vm = vm
        this.expOrFn = expOrFn //监听的属性 如:a.b.c
        this.cb = cb // watch 回调
        if (options) {
            this.user = !!options.user //这是个 watch
            this.deep = !!options.deep //深度监听
        }
        this.deps = [];
        this.set = {}
        this.id = id++
        function parsePath(path) {
		    path = path.split('.')
		    return function (obj) {
		        path.forEach((key) => {
		            obj = obj[key]
		        })
		        return obj
		    }
		}
        if (typeof expOrFn === 'function') {
            this.getters = expOrFn
        } else {
        	//访问监听的变量 如:a.b.c.d
            this.getters = parsePath(this.expOrFn)
        }
        //留住 value
        this.value = this.get();
    }
    get() {
    	//深度访问对象内部每一个值
    	function traverse(val) {
		    let isA = Array.isArray(val);
		    if (!isA) {
		        let key = Object.keys(val);
		        let i = key.length
		        while (i--) {
		            traverse(val[key])
		        }
		    }
		}
        //标记target
        pushTarget(this)
        //访问监听的属性
        let value = this.getters.call(this.vm, this.vm);
        if (this.deep) {
            traverse(value)
        }
        //弹出target防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
        popTarget()
        return value
    }
    run() {
        let newValue = this.get()
        //取出旧值
        const oldValue = this.value
        //留住新值
        this.value = newValue
        //用户自己传入的watch
        if (this.user) {
        	//这里的 cb 就是传入的 watch 回调函数
             this.cb.call(this.vm, newValue, oldValue)
        }
    }
    addDep(dep) {
        let id = dep.id
        //去重防止dep添加watch多次
        if (!this.set[id]) {
            //watcher添加dep
            this.deps.push(dep)
            //给dep添加watch
            dep.addSub(this)
            this.set[id] = true;
        }
    }
    upDate() {
        this.run()
    }
}

核心也是利用 watcher 和 dep 两个类来实现的。区别就是这次 watcher 保留的是用户传入的 watch 回调函数,依赖发生更新时调用函数,传入新值和旧值if (this.user) { this.cb.call(this.vm, newValue, oldValue) }

5. computed实现

打开 src/core/instance/state.js 找到 initComputed

function initComputed(vm) {
	//获取传入的 computed
    let computed = vm.$options.computed;
    //创建一个 watchers 空对象
    const watchers = vm._computedWatchers = Object.create(null)
    //遍历 computed
    for (const key in computed) {
    	//拿到计算属性方法
        const userDef = computed[key];
        // 创建 Watcher lazy:true 默认不执行 ,看是否需要重新计算,computed 是有缓存的
        watchers[key] = new Watcher(vm, userDef, ()=>{}, {lazy:true});
        //监听 computed 方法
        defineComputed(vm, key, userDef)
    }
}
let sharedPropertyDefinition = {}
function defineComputed(target, key, userDef) {
    if (typeof userDef === 'function') {
    	//传入的是方法走这里
        sharedPropertyDefinition.get = createComputedGetter(key)
        sharedPropertyDefinition.set = ()=>{}
    } else {
    	//也可以传入一个对象,有 set 方法
        sharedPropertyDefinition.get = createComputedGetter(key)
        sharedPropertyDefinition.set = userDef.set
    }
    //包装计算属性的方法名,给 vm 上添加 computed 方法
    Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
	//重点!模板上访问计算属性才走这里
    return function () {
    	// 取出创建的 computedWatchers
        let watch = this._computedWatchers[key];
        // dirty = lazy 默认是 true
        if (watch.dirty) {
            //渲染的时候会进入这里
            watch.evaluate()
        }
        //此时渲染还未结束 Dep.target = 渲染 watcher,computed 函数内部的变量收集渲染 watcher
        if (Dep.target) {
            watch.depend()
        }
        //计算好的参数返回给用户
        return watch.value
    }
}

计算属性 watcher 类实现

export class Watcher {
    constructor(vm,expOrFn,cb,options) {
        this.vm = vm
        this.expOrFn = expOrFn //传入的 computed 方法
        this.cb = cb
        if (options) {
            this.lazy = !!options.lazy // true 默认不执行 这是一个 computed
        }
        this.dirty = this.lazy// computed 看是否需要从新求值
        this.deps = [];
        this.set = {}
        this.id = id++
        if (typeof expOrFn === 'function') {
            this.getters = expOrFn
        }
        //留住value
        this.value = this.lazy? undefined : this.get();
    }
    get() {
        //标记 computed target
        pushTarget(this)
        // 调用 computed 函数得到计算的值
        let value = this.getters.call(this.vm, this.vm);
        //弹出 target 防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
        popTarget()
        return value
    }
    run() {
        let newValue = this.get()
    }
    addDep(dep) {
        let id = dep.id
        //去重防止dep添加watch多次
        if (!this.set[id]) {
            //watcher添加dep
            this.deps.push(dep)
            //给dep添加watch
            dep.addSub(this)
            this.set[id] = true;
        }
    }
    upDate() {
    	//修改属性计算属性依赖的变量重置 dirty 
        if (this.lazy) {
            this.dirty = true
        }
    }
    evaluate() {
    	//当走到这里时,页面正在渲染中 Dep.target 已经有一个渲染 watcher 了
        this.value = this.get();
        //修改了计算属性里面脏值
        this.dirty = false
    }
    depend () {
    	//给 computed 函数内部的属性添加渲染 watcher
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
}

重点:计算属性方法内部变量的 Dep 上会有两个 watcher 分别是是计算属性 wathcer 和渲染 watcher,计算属性 watcher 的作用只需要控制是否需要重新计算,跟着调用依赖的渲染 watcher 重新计算属性

6. nextTick原理

打开 src/core/util/next-tick.js

const callbacks = []//储存 nextTick 回调函数
let pending = false //只开启一个 timerFunc
let timerFunc // 定时函数
/*
vue的降级策略(兼容)promise -> MutationObserver -> setImmediate -> setTimeout
原理:利用异步队列
在每个 macro-task 运行完以后,UI 都会重渲染,那么在 miscro-task (异步事件回调) 中就完成数据更新,当前 次事件循环 结束就可以得到最新的 UI 了。反之如果新建一个 macro-task 来做数据更新,那么渲染就会进行两次。
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //如果支持promise,使用promise实现
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (!isIE && typeof MutationObserver !== 'undefined') {
  //如果Promise不支持,但支持MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  //改变 counter 属性触发 flushCallbacks
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //上面两种都不支持,用setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //都不支持使用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
export function nextTick (cb, ctx) {
  let _resolve
  //压入 nextTick 回调,可存入多个
  callbacks.push(() => {
     cb && cb.call(ctx)
  })
  //开起定时函数
  if (!pending) {
    pending = true
    timerFunc()
  }
}
function flushCallbacks () {
  pending = false
  //复制 callback
  const copies = callbacks.slice(0)
  //清除 callback
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
  	//以此调用 nextTick 回调
    copies[i]()
  }
}

vue用异步队列的方式来控制DOM更新和nextTick回调先后执行

micro-task因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

因为兼容性问题,vue不得不做了microtask向macrotask的降级方案

7. Vue.extend原理

打开 src/core/global-api/extend.js

export function initExtend (Vue) {
 // 每一个组件实例都有一个唯一的 cid 
  Vue.cid = 0
  let cid = 1
  // Vue.extend 方法实体
  Vue.extend = function (extendOptions) {
    // 用户传进来的参数,包含组件实例的对象
    extendOptions = extendOptions || {}
    // 将 Super 指向父类 this ,也就是 Vue
    const Super = this
    // SuperId 使用父类的唯一cid
    const SuperId = Super.cid
    // 创建缓存,有缓存直接返回缓存
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    // 组件实例 name 
    const name = extendOptions.name || Super.options.name
    //子组件的构造函数
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 通过create继承父类原型
    Sub.prototype = Object.create(Super.prototype)
    // 修改子类 constructor 指向自己
    Sub.prototype.constructor = Sub
    // 创建子类唯一 cid
    Sub.cid = cid++
    // 合并父类 options 和子类自有 options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // 子类有props,初始化子类props,并创建监听
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 子类有 computed,初始化 computed,并创建监听,流程和上边 computed 实现一样
    if (Sub.options.computed) {
      initComputed(Sub)
    }
	//用于递归组件
    if (name) {
      //给组件的components添加上组件自己
      Sub.options.components[name] = Sub
    }
	// 上边说的缓存
    cachedCtors[SuperId] = Sub
    // 返回子类 , 实现官方案例那样,new一个实例,通过 $mount 挂载 。
    return Sub
  }
}

简单来说就是基于 Vue 构造函数,创建一个子类,然后继承父类的参数和方法,最后再返回这个子类,每个子组件都是一个 Sub 构造函数。子组件创建流程和 new Vue初始化流程 区别不大。

子组件创建过程中没有 el ,vue 渲染组件的时候内部自动调用 child.$mount(undefined) 不会挂载到页面上,而是放在 vnode.componentInstance.el 上,通过父组件压入到页面上。