本综述主要是对 new Vue() & JSX new Vue() & 组件 两章内容进行归纳总结。
第一章、new Vue() & JSX
该章主要讲述的是简单的 new Vue() 历程,是在用于手写 render() 渲染函数以及 JSX。在初始化函数内先合并配置 vm.$options,然后又初始化生命周期、事件中心、属性:data/props/methods/...、执行生命周期钩子函数:beforeCreate/created,最后执行挂载 $mount() 函数。
在挂载阶段先判断是否有 render() 函数,如果没有则用属性 template 对应的html 内容生成该渲染函数。最后若是没有生成该函数,则直接返回一个空的 vnode 对象。若有该函数,则接着执行生命周期钩子函数 beforeMount,然后实例化一个渲染 watcher,在求值 watcher.value 时候,会执行 vm.update(vm.render()); 语句。
在 vm.render() 函数内,会递归调用手写 render() && JSX 内容,最终生成对应的虚拟 DOM Tree。生成过程顺序:先生成 childVnode 对象,然后将 childVnode 传入渲染函数内作为子节点再生成 parentVnode 对象,依次类推最终形成 VNode Tree。
在 vm.update() 函数内,拿到vm.render() 函数最新生成的 vnode 对象,然后通过 patch() 函数递归的调用 createElm() 函数生成对应的真实 DOM 节点对象。生成过程顺序:先通过 parenVnode.tag 属性生成对应的 dom 节点并赋值到 parenVnode.elm 属性上,然后再遍历 parenVnode.children 属性并调用 createElm() 函数再传入 parenVnode.elm 作为父节点参数,最后依次生成 childVnode 对应的 dom 节并赋值 childVnode.elm 属性上。此过程是一个递归调用过程,最终的子节点有可能是文本节点或注释节点,然后生成对应的节点并执行插入语句,插入到父节点,然后父节点再插入到父父节点内,依次类推,最终形成 dom tree。最后将生成的新的 dom 节点插入到页面上并删除旧的 dom 节点,最后更新 vm.$el 属性的值为最新的 dom 节点对象。
第二章、new Vue() & 组件
该章主要简述单位件组件化其实就是复杂的 new Vue() 历程,因为是手写 render() 函数传入组件对象 ctor,所以再执行初始化的过程与第一章节一致。然后在 $mount() 挂载阶段有所不同。因为是组件所以 render 过程生成组件 vnode,并且对应的 patch 过程也有所不同。
在 render 阶段因为是组件,所以执行 vnode=createComponent(ctor) 生成组件 vnode 对象。然而在函数 createComponent() 内首先通过原型继承 Object.create(Vue.prototype) 函数生成构造器 VueComponent 即为子类 Sub 。然后又合并属性 Sub.options = Vue.options + ctor;,又从父类上复制了一些静态方法,目的是让子类 Sub 拥有与 Vue 一样的能力。最后又缓存 Vue.options、ctor 在 Sub 的静态属性上,最后又缓存了该子类在 ctor 上,防止下次访问该组件时再次执行该过程创建构造器,会直接返回子类。
然后又安装了组件的构钩子函数 init、prepatch、insert、destroy 在 vnode.data.hook 属性上。最后设置组件名字 vue-component-{cid}-{name} 生成组件 vnode 对象,同时缓存 vnode.componentOptions = {Ctor...} 属性,其中 Ctor 为子组件构造器 Sub 类,最后该 vnode.children 必须为空。
在组件 patch 阶段内 vm.$el = vm.__patch(vm.$el,vnode)__ 更新当前实例的 dom。由于是组件 vnode 所以直接执行 createComponent() 函数,接着执行组件的钩子 init() 函数生成组件的实例 vnode.componentInstance = child; 也可以理解为子实例。
在 init() 函数内容主要是执行 const child = new Sub(options) 进行初始化,在此过程中关联当前 vm 与 child 为父子关系。然后执行组件的 $mount() 过程,此过程与第一章 $mount() 过程基本一致,目前只说不一致的地方。
在 child.render() 函数内会递归执行编译生成的 render && JSX 生成该组件的 html 内容对应的 c_vnode 对象,在此过程中会先关联父子 vnode 对象。
在 child.update(c_vnode) 函数内执行 patch() 再递归执行 createElm() 函数生成真实的 dom 节点并返回该节点并进行赋值 child.$el= child.__patch(c_vnode)__ 属性,最后再更新 child.$parent.$el = child.$el; vnode.elm = child.$el; 父实例的 $el 属性、组件 vnode.elm 为对应真实的 dom 节点。
最会回归到 patch 阶段内,会插入新的 dom 节点并删除旧的 dom 节点。最会组件挂载完成并执行生命周期钩子 mounted,组件挂载结束。
为什么组件包含子组件的时候会从 createElm() 开始呢?
因为子孙组件大多都是通过 import 导入的,赋值到父组件的 components 属性上,这些子孙组件在父组件经过 vue-loader 编译生成的 render() 函数后,自动生成组件 vnode 对象,当然父组件也生成了组件 vnode,然后在递归遍历父组件 children 的时候会进入 createElm() 函数,然后走 createComponent() 函数,然后走 vnode.data.hook.init() 函数,依次执行下去。
最后组件的 patch 结束,通过 insert() 替换旧 DOM 节点。