Vue 源码解析-组件化
Vue
其中一个核心思想是组件化,把页面拆分成多个组件,组件是可复用的 Vue
实例,所以它们与 new Vue
接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像 el
这样根实例特有的选项。
createComponent
1 | var app = new Vue({ |
createComponent
最终会调用 _createElement
方法, 通过判断传入的参数是否是组件,而选择使用 createComponent
方法创建一个 VNode
。
源码: src/core/vdom/create-component.js
该方法针对组件渲染有三个关键步骤:
构造子类构造函数mergeOptions
方法将 Vue
构造函数的 options
和用户传入的 options
合并到同一层,到 vm.$options
上
Vue.extend
的作用就是将一个纯对象转换成一个继承于 Vue
的构造器 Sub
并返回。
并且对 Sub
这个对象做了一些扩展,如扩展 options
、全局 API
等;对配置中的 props
、 computed
进行初始化工作;最后对 Sub
的构造函数缓存,避免多次 extend
同一个子组件的时候重复执行。
安装组件的钩子函数
1 | // install component management hooks onto the placeholder node/ instal |
将 componentVNodeHooks
的钩子函数合并到 data.hook
中,在 VNode
执行 patch
的过程中执行相关的钩子函数。
实例化 VNode
1 | const name = Ctor.options.name || tag; |
最后通过 new VNode
实例化一个 VNode
并返回,与普通元素节点不同的是,组件的参数中是没有 children
的,在之后的 patch
过程中有关键性作用。
patch
上一节中 createComponent
创建的组件的 VNode
,之后会执行 vm._update
, 执行 vm.__patch__
(包含diff
算法)把 VNode
转换成真正的 DOM
节点。
patch
的过程会调用 createElm
创建元素节点
1 | if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { |
组件的初始化时不传 el
的,因为自己接管了 vm$mount
.
在完成组件的整个 patch
过程后,最后执行 insert(parentElm, vnode.elm, refElm)
完成组件的 DOM
插入,如果组件 patch
过程中又创建了子组件,那么 DOM
的插入顺序是先子后父。
配置合并
new Vue
通常有两种情况,一种是外部代码中主动调用 new Vue(opitons)
去实例化一个 Vue
对象,另一种是在组件过程中内部通过调用 new Vue(opitons)
实例化子组件。
无论哪种场景,都会首先执行 merge options
的逻辑。
外部调用场景
合并前
1 | Vue.mixin({ |
合并后
1 | vm.$options = { |
组件场景
组件的构造函数是通过 Vue.extend
继承自 Vue
。其中最关键的逻辑是,将 Vue.options
和组件定义的对象合并到 Sub.options
中。
子组件初始化过程通过 initInternalComponent
方式要比外部初始化 Vue
通过 mergeOptions
的过程要快,合并完的结果保留在 vm.$options
中。
options
就是new vue
大括号里的内容
使用mergeOptions
合并配置,在Vue options
扩展配置,之后执行new Vue()
生命周期
每个 Vue
实例(new Vue
、子组件内部调用的 new Vue
)被创建都需要经过一系列初始化过程。其中需要设置数据监听、编译模板、挂载实例到 DOM
、在数据更新时更新 DOM
。在这个过程中会调用相关的钩子方法。
callHook
最终执行生命周期是通过调用 callHook
方法,根据传入的字符串 hook
,去拿到 vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候把 vm
作为函数执行的上下文。
在之前的配置合并中,这些钩子函数都被合并到了 vm.$options
中并且是一个数组。
调用时机
beforeCreate & created
1 | Vue.prototype._init = function(options?: Object) { |
beforeCreate
和 created
的钩子调用是在 initState
的前后,initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性,之后我们会详细分析。那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的函数。
这两个方法执行完后,并没有渲染 DOM
,此时是不能访问 DOM
。 一把来说后端的接口请求,放在这里两个钩子函数中都可以,如果需要访问 props
、data
等数据,就需要在 created
中执行了。
vue-router
、 vuex
都混合了 beforeCreatd
钩子函数。
beforeMount & mounted
这一对方法是以 DOM
挂载前后为分界线。
在执行 vm._render
方法前,执行了 beforeMount
, 当在执行完 vm.update
方法后,把 VNode patch
到真是 DOM
后,执行了 mouted
钩子。
mounted
钩子函数的执行顺序也是先子后父。
beforeUpdate & updatedbeforeUpdate
的执行时机是在渲染 Watcher 的 before
函数中,组件已经 mounted
之后,才会去调用这个钩子函数。
update
的执行时机是在 flushSchedulerQueue
函数调用的时候。
组件 mount
的过程中,会实例化一个渲染的 Watcher
去监听 vm
上的数据变化重新渲染。
beforeDestroy & destroyedbeforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode
执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数。
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
activated & deactivatedactivated
和 deactivated
钩子函数是专门为 keep-alive
组件定制的钩子。
总结
在 created
中可以访问到数据,在 mounted
钩子函数中可以访问到 DOM
,在destroyed
钩子函数中做定时器销毁
组件注册
在 Vue.js
中,除了它内置的组件如 keep-alive
、component
、transition
、transition-group
等,其它用户自定义组件在使用前必须注册。
全局注册
可以在任意地方使用,扩展在Vue options
1 | Vue.component('my-component', { |
局部注册
在当前组件内,扩展在 Sub options
1 | import HelloWorld from './components/HelloWorld'; |
总结
局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options
下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。
异步组件
开发中,为了减少首屏代码体积,往往把一些非首屏的组件设计成异步组件,按需加载。
本质是两次渲染,先渲染成注释节点,在组件加载成功了,在通过 foreceRender
重新渲染。
普通函数异步组件
1 | Vue.component('async-example', function(resolve, reject) { |
component
的第二个参数传入的是函数, 普通组件传入的是对象。
⚠️ 在整个异步组件加载过程中是没有数据发生变化的,所以通过执行
$forceUpdate
可以强制组件重新渲染一次。
Promise 异步组件
1 | Vue.component( |
判断是否是 Promise
的方法:typeof res.then === 'function'
高级异步组件
由于异步加载组件需要动态加载 JS
,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading
组件和 error
组件,并在适当的时机渲染它们。
1 | const AsyncComponent = () => ({ |