赞
踩
这篇博客的内容是讲讲在 Vue2 中,组件在底层的本质。
在这里,直接抛出结论:组件的本质就是一个个的构造函数,这些函数以组件的定义 options 对象为参数,在 Vue2 中,最顶级的组件就是我们从 vue.js 中导入的 Vue 函数,而子组件是 Vue 底层通过 extend 函数创建出来的 VueComponent 函数。通过 new 这些组件的构造函数,我们可以创建出组件实例。
Vue 通过 initGlobalAPI(Vue) 向 Vue 上赋值静态属性,源码如下所示:
- // 向 Vue 上挂载一些静态的属性和方法
- initGlobalAPI(Vue)
- export function initGlobalAPI (Vue: GlobalAPI) {
- // 应用于 Vue 源码内部的工具函数,不建议程序员直接使用。
- Vue.util = {
- warn,
- extend,
- mergeOptions,
- defineReactive
- }
-
- // 定义 options 对象,该对象用于存储一系列的资源,如:组件、指令和过滤器
- Vue.options = Object.create(null)
- ASSET_TYPES.forEach(type => {
- Vue.options[type + 's'] = Object.create(null)
- })
-
- // 将与平台无关的内建组件存储到 options.components 中
- extend(Vue.options.components, builtInComponents)
- }
在这里,比较重要的是名为 options 的静态属性,这个属性保存着组件能够使用的资源(组件、指令、过滤器),并且我们声明的组件描述对象(.vue 文件中导出的对象)中的信息也会被保存到 options 静态属性中。
Vue 通过 initGlobalAPI(Vue) 向 Vue 上赋值静态方法,源码如下所示:
- // 向 Vue 上挂载一些静态的属性和方法
- initGlobalAPI(Vue)
- export function initGlobalAPI (Vue: GlobalAPI) {
- // 定义全局 API。set、delete、nextTick
- Vue.set = set
- Vue.delete = del
- Vue.nextTick = nextTick
-
- // 初始化 Vue.use()
- initUse(Vue)
- // 初始化 Vue.mixin()
- initMixin(Vue)
- // 初始化 Vue.extend()
- initExtend(Vue)
- // 初始化 Vue.component()、Vue.directive()、Vue.filter(),用于向 Vue 中注册资源
- initAssetRegisters(Vue)
- }
-
- export function initUse (Vue: GlobalAPI) {
- Vue.use = function (plugin: Function | Object) {
- ......
- }
- }
Vue 原型方法的定义在 src/core/instance/index.js 文件中,源码如下所示:
- function Vue (options) {
- // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
- // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
- if (process.env.NODE_ENV !== 'production' &&
- !(this instanceof Vue)
- ) {
- warn('Vue is a constructor and should be called with the `new` keyword')
- }
- // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
- this._init(options)
- }
-
- // 下面函数的作用是:往 Vue 的原型上写入原型函数,这些函数是给 Vue 的实例使用的
- // 这些函数分为两类:一类是 Vue 内部使用的,特征是函数名以 '_' 开头;
- // 还有一类是给用户使用的,特征是函数名以 '$' 开头,这些函数可以在 Vue 的官方文档中看到;
- // 写入 vm._init
- initMixin(Vue)
- // 写入 vm.$set、vm.$delete、vm.$watch
- stateMixin(Vue)
- // 写入 vm.$on、vm.$once、vm.$off、vm.$emit
- eventsMixin(Vue)
- // 写入 vm._update、vm.$forceUpdate、vm.$destroy
- lifecycleMixin(Vue)
- // 写入 vm.$nextTick、vm._render
- renderMixin(Vue)
-
- export default Vue
在这里,vue 首次声明了 Vue 构造函数,函数的主体是执行原型上的 _init 方法进行实例的初始化。
然后在下面以 Vue 构造函数为参数执行一系列的函数,这些函数的作用是:往 Vue 构造函数的原型对象上添加新的函数,这些函数是给组件实例调用的,以 initMixin() 函数为例,源码如下所示:
- export function initMixin (Vue: Class<Component>) {
- Vue.prototype._init = function (options?: Object) {
- ......
- }
- }
在底层,子组件是借助 extend 方法创建出来的,这部分源码可以看我的这篇博客。
组件(构造函数)只有一个,但是我们可以在模板字符串中多次使用某一个组件,每使用一次组件,Vue 的底层便会通过 new Ctor(options) 创建出一个对应的组件实例。
我们以一个简单的例子来看看组件的实例化。
- <script>
- // 声明一个组件
- let componentOption = {
- data: {
- name: 'tom',
- age: 27
- },
- mounted(){
- setTimeout(() => {
- this.updateAge()
- }, 2000)
- },
- methods: {
- updateAge(){
- this.age++
- }
- },
- template: `
- <div id="app">
- {{name}} {{age}}
- </div>
- `,
- render() {
- // 这里的 this 是指 Vue 组件实例
- with (this) {
- return _c(
- 'div',
- {attrs: {"id": "app"}},
- [_v(_s(name) + " " + _s(age))]
- )
- }
- }
- }
- // 创建一个组件实例
-
- let app = new Vue(componentOption)
- // 调用 $mount 函数进行页面挂载操作
- app.$mount('#app')
- </script>
我们首先定义了一个组件配置对象 componentOption,至此我们还没有使用 Vue,只是按照 Vue 的规范声明了一个组件配置对象。
接下来,我们以 componentOption 对象为参数执行 new Vue(),这会创建出一个组件实例。Vue 构造函数的函数体如下所示:
- function Vue (options) {
- // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
- // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
- if (process.env.NODE_ENV !== 'production' &&
- !(this instanceof Vue)
- ) {
- warn('Vue is a constructor and should be called with the `new` keyword')
- }
- // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
- this._init(options)
- }
Vue 函数内部借助 _init 原型函数进行组件实例的初始化,接下来看 _init 函数。
- export function initMixin (Vue: Class<Component>) {
- Vue.prototype._init = function (options?: Object) {
- // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
- const vm: Component = this
- // 赋值唯一的 id
- vm._uid = uid++
-
- // a flag to avoid this being observed
- // 一个标记,用于防止 vm 变成响应式的数据
- vm._isVue = true
- // 下面这个 if else 分支需要注意一下。
- // 在 Vue 中,有两个时机会创建 Vue 实例,一个是 main.js 中手动执行的 new Vue({}),还有一个是当我们
- // 在模板中使用组件时,每使用一个组件,就会创建与之相对应的 Vue 实例。也就是说 Vue 的实例有两种,一种是
- // 手动调用的 new Vue,还有一种是组件的 Vue 实例。组件的 Vue 实例会进入下面的 if 分支,而手动调用的
- // new Vue 会进入下面的 else 分支。
- //
- // 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
- if (options && options._isComponent) {
- // optimize internal component instantiation
- // since dynamic options merging is pretty slow, and none of the
- // internal component options needs special treatment.
- initInternalComponent(vm, options)
- } else {
- // options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
- // 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
- // 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
- // 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
- vm.$options = mergeOptions(
- // resolveConstructorOptions 函数的返回值是 Vue 的 options
- resolveConstructorOptions(vm.constructor),
- options || {},
- vm
- )
- }
- /* istanbul ignore else */
- if (process.env.NODE_ENV !== 'production') {
- initProxy(vm)
- } else {
- vm._renderProxy = vm
- }
- // expose real self
- vm._self = vm
- // 初始化与生命周期有关的内容
- initLifecycle(vm)
- // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法
- initEvents(vm)
- // 初始化与插槽和渲染有关的内容
- initRender(vm)
- // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
- // 执行 beforeCreate 生命周期函数
- callHook(vm, 'beforeCreate')
- // 解析初始化当前组件的 inject
- initInjections(vm) // resolve injections before data/props
- // 初始化 state,包括 props、methods、data、computed、watch
- initState(vm)
- // 初始化 provide
- initProvide(vm) // resolve provide after data/props
- // 在 created 回调函数中,可以访问到实例中的数据
- // 执行 created 回调函数
- callHook(vm, 'created')
- // beforeCreate 和 created 生命周期的区别是:能否访问到实例中的变量
-
- // 如果配置中有 el 的话,则自动执行挂载操作
- if (vm.$options.el) {
- vm.$mount(vm.$options.el)
- }
- }
- }
源码的具体解释看注释即可,我写的很详细,_init 函数的主要作用是对 vm 组件实例进行一系列的初始化操作,我们以 data 的初始化为例。
- // 初始化 state,包括 props、methods、data、computed、watch
- initState(vm)
- export function initState (vm: Component) {
- const opts = vm.$options
- ......
- if (opts.data) {
- initData(vm)
- }
- ......
- }
- // 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
- function initData (vm: Component) {
- // 取出配置对象中的 data 字段
- let data = vm.$options.data
- // data 字段有两种写法:(1)函数类型;(2)普通对象类型
- // 在这一步,还会将 data 赋值给 vm._data
- data = vm._data = typeof data === 'function'
- // 如果 data 是函数类型的话,借助 getData 函数拿到最终的 data 对象
- ? getData(data, vm)
- // 否则的话,直接返回 data 对象,如果没有配置 data 的话,就返回后面的 {}
- : data || {}
- // 如果 data 不是普通的对象({k:v})的话
- if (!isPlainObject(data)) {
- // 将 data 设为 {}
- data = {}
- // 如果实在开发环境的话,打印出警告
- process.env.NODE_ENV !== 'production' && warn(
- 'data functions should return an object:\n' +
- 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
- vm
- )
- }
- // proxy data on instance
- // 拿到 data 对象中的 key
- const keys = Object.keys(data)
- // 拿到我们定义的 props 和 methods
- const props = vm.$options.props
- const methods = vm.$options.methods
- // 获取 data 中 key 的个数
- let i = keys.length
- // 遍历 data 中的 key
- while (i--) {
- // 拿到当前的key
- const key = keys[i]
- if (process.env.NODE_ENV !== 'production') {
- if (methods && hasOwn(methods, key)) {
- // 如果是在开发模式下,并且我们自定义的 methods 中有和 key 同名的方法时,在这发出警告
- warn(
- `Method "${key}" has already been defined as a data property.`,
- vm
- )
- }
- }
- if (props && hasOwn(props, key)) {
- // 如果是在开发模式下,并且 props 有和 key 同名的属性时,在此发出警告
- process.env.NODE_ENV !== 'production' && warn(
- `The data property "${key}" is already declared as a prop. ` +
- `Use prop default value instead.`,
- vm
- )
- // isReserved 函数用于检查字符串是不是 $ 或者 _ 开头的
- // 如果不是 $ 和 _ 开头的话
- } else if (!isReserved(key)) {
- // 将 vm.key 代理到 vm['_data'].key
- // 也就是说当我们访问 this.message 的时候,实际上值是从 this['_data'].message 中获取到的(假设 data 中有 message 属性)
- proxy(vm, `_data`, key)
- }
- }
- // observe data
- observe(data, true /* asRootData */)
- }
initData 主要做了三件事:
通过 new Vue() 我们创建出了一个组件实例,如果我们在组件配置对象中声明了 el 属性的话,Vue 会自动的帮我们进行组件实例的挂载,源码如下所示:
- export function initMixin (Vue: Class<Component>) {
- Vue.prototype._init = function (options?: Object) {
- // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
- const vm: Component = this
-
- ......
-
- // 如果配置中有 el 的话,则自动执行挂载操作
- if (vm.$options.el) {
- vm.$mount(vm.$options.el)
- }
- }
- }
如果我们没有声明 el 属性的话,则需要我们手动的执行 $mount() 函数进行挂载。
- let app = new Vue(componentOption)
- // 调用 $mount 函数进行页面挂载操作
- app.$mount('#app')
接下来一起看看 $mount 方法,这个函数是一个原型方法,源码如下所示:
- // 运行时版本代码使用的 $mount 函数。调用这个 $mount 函数,模板字符串必须已经编译成 render 函数
- Vue.prototype.$mount = function (
- el?: string | Element,
- hydrating?: boolean
- ): Component {
- el = el && inBrowser ? query(el) : undefined
- return mountComponent(this, el, hydrating)
- }
$mount 函数的内部通过 mountComponent 函数进行组件的挂载,mountComponent 的源码如下所示:
- export function mountComponent (
- vm: Component,
- el: ?Element,
- hydrating?: boolean
- ): Component {
- // 将 el 设值到 vm 中的 $el
- vm.$el = el
-
- // 触发执行 beforeMount 生命周期函数(挂载之前)
- callHook(vm, 'beforeMount')
-
- // 一个更新渲染组件的方法
- let updateComponent = () => {
- // vm._render() 函数的执行结果是一个 VNode
- // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
- vm._update(vm._render(), hydrating)
- }
-
- // 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
- vm._watcher = new Watcher(vm, updateComponent, noop)
- hydrating = false
-
- // manually mounted instance, call mounted on self
- // mounted is called for render-created child components in its inserted hook
- if (vm.$vnode == null) {
- vm._isMounted = true
- callHook(vm, 'mounted')
- }
- return vm
- }
这里首先声明了一个 updateComponent 函数,这个函数的功能是:执行组件实例的 render 函数获取到最新的 vnode,然后以最新的 vnode 为参数执行 update 函数进行组件真实 DOM 的挂载或更新渲染。
然后以 updateComponent 函数为参数实例化一个渲染 Watcher,在实例化渲染 Watcher 的过程中,底层会执行 updateComponent 函数以及进行依赖的收集,这部分内容可以看我的这篇博客。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。