组件化
组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})
初始化一个 vue 页面也是 通过 render 函数去渲染的,不同的这次通过 createElement 传的参数是一个组件而不是一个原生的标签
组件 转 vNode
createComponent
如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode
一个 App 对象,它本质上是一个 Component 类型,那么它会走到上述代码的 else 逻辑,直接通过 createComponent 方法来创建 vnode。
针对组件渲染这个 case 主要就 3 个关键步骤:
构造子类构造函数,安装组件钩子函数和实例化 vnode。
构造子类构造函数
Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。(这里理解比较蛋疼)
安装组件钩子函数
Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用了这一点
实例化 VNode
最后一步非常简单,通过 new VNode 实例化一个 vnode 并返回。需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的
组件生成DOM的区别 patch
当我们通过 createComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm. _patch _ 去把 VNode 转换成真正的 DOM 节点。在 组件中创建方式与创建普通节点不太一样。
之前先 将组件 VUE实例化 通过 new Vue(options) 实例化子组件
因为实际上 JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。
再创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。
编写一个组件实际上是编写一个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在 _init 的最初阶段执行的就是 merge options 的逻辑
合并配置
到底啥是合并配置?
个人可以简单理解为 将Vue 的内置组件目前有 <keep-alive>、<transition> 和 <transition-group> 组件 与 自己用户编写的组件合并 (把一些内置组件扩展到 Vue.options.components 上)
对于钩子函数,他们的合并策略都是 mergeHook 函数。一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
通过执行 mergeField 函数,把合并后的结果保存到 options 对象中,最终返回它。
vm.$options 的值差不多是如下这样
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
关于合并$options
Vue 初始化阶段对于 options 的合并过程就是这样,我们需要知道对于 options 的合并有 2 种方式,子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。
纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。
生命周期
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。
callHook
源码中最终执行生命周期的函数都是调用 callHook 方法,它的定义在 src/core/instance/lifecycle 中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook 函数的逻辑很简单,根据传入的字符串 hook,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候把 vm 作为函数执行的上下文
在上一节中,我们详细地介绍了 Vue.js 合并 options 的过程,各个阶段的生命周期的函数也被合并到 vm.$options 里,并且是一个数组。因此 callhook 函数的功能就是调用某个生命周期钩子注册的所有回调函数。
beforeCreate & created
beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props、data、methods、watch、computed 等属性
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // created
// ...
}
beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等属性
那么显然 beforeCreate 的钩子函数中就不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数
在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM
如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数。
等下要说的 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreatd 钩子函数。
beforeMount & mounted
顾名思义,beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent 函数中,定义在 src/core/instance/lifecycle.js 中
在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mouted 钩子。
这里对 mouted 钩子函数执行有一个判断逻辑,vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程
beforeUpdate & updated
beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候。 这个以后再章节中再提
beforeDestroy & destroyed
beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段。
beforeDestroy 钩子函数的执行时机是在 destroy 函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent 的 $children 中删掉自身,删除 watcher,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函数。
组件注册
在 Vue.js 中,除了它内置的组件如 keep-alive、component、transition、transition-group 等,其它用户自定义组件在使用前必须注册。
全局注册
Vue.component('my-component', {
// 选项
// ***要注册一个全局组件,可以使用 Vue.component(tagName, options)***
})
局部注册
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
异步组件
在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力。
Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve 和 reject,函数内部用 setTimout 模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve 方法,它的参数就是我们的异步组件对象。
高级异步组件 可以扩展为3种方式 暂时先不搞~~~