vue 源码之 Vue 定义
本文地址: https://www.cnblogs.com/veinyin/p/14638541.html
1 定义位置 core/instance/index
在 core/instance/index 中,定义了 function Vue,作为构造器
并向原型上绑定了一些方法,如下所示
1 import { initMixin } from './init' 2 import { stateMixin } from './state' 3 import { renderMixin } from './render' 4 import { eventsMixin } from './events' 5 import { lifecycleMixin } from './lifecycle' 6 import { warn } from '../util/index' 7 8 function Vue (options) { 9 if (process.env.NODE_ENV !== 'production' && 10 !(this instanceof Vue) 11 ) { 12 warn('Vue is a constructor and should be called with the `new` keyword') 13 } 14 this._init(options) 15 } 16 17 initMixin(Vue) // 在 Vue.prototype 上添加 _init 方法,做一些初始化的事情,一直到 mounted 18 stateMixin(Vue) // 状态管理相关,也是添加到 Vue.prototype 上,包括 $data, $props, $set, $delete, $watch 19 eventsMixin(Vue) // vue 自定义事件相关,$on, $once, $off, $emit 20 lifecycleMixin(Vue) // 生命周期相关,_update, $forceUpdate, $destroy,这是放在原型上的,对每个实例会有相应的 initLifecycle 初始化生命周期钩子 21 renderMixin(Vue) // 渲染相关,先给 vue.prototype 加上 runtime 常用方法,如 toNumber、createTextVnode 等,然后是 $nextTick 和 _render 23 export default Vue // 把丰富原型方法之后的 Vue 导出
疑问1:为什么不用 ES6 的 class,而是仍采用 ES5 的 function 形式
A:在 vue 的开发场景下,class 添加原型方法没有 function 方便
从上面代码可以看出,不同模块的代码是分开维护的,这样看上去清晰简洁,也便于维护
class 的原型方法是在 class 内部定义的,如下所示,由于需要在 Vue 的原型对象上扩展很多属性和方法,如果使用 class,将在这个文件中糅合大量代码,看起来分层不够清晰,也不利于阅读和维护
当然,也可以通过 vue.prototype._init = function () {} 给 class 的原型扩展,但这样就与 class 的使用初衷违背了,不如直接用 function
1 class Vue { 2 constructor() { 3 _init() {} 4 } 5 6 // 原型方法要写在 class 内部,原型方法很多时,就都糅合在一个单文件里了 7 }
2 增强_1 core/index
在 1 基础上功能增强了,代码如下,重点加粗,主要是静态属性和方法的扩展,以及原型对象上属性的扩展
1 import Vue from './instance/index' 2 import { initGlobalAPI } from './global-api/index' 3 import { isServerRendering } from 'core/util/env' 4 import { FunctionalRenderContext } from 'core/vdom/create-functional-component' 5 6 initGlobalAPI(Vue) // 给 Vue 添加了一些静态属性和方法,util, set, delete, nextTick, observable, options. 其它文件做的导入的 use, mixin, extend, 这几个定义都是导入的 compoent, directive, filter 7 8 Object.defineProperty(Vue.prototype, '$isServer', { 9 get: isServerRendering 10 }) 11 12 Object.defineProperty(Vue.prototype, '$ssrContext', { 13 get () { 14 /* istanbul ignore next */ 15 return this.$vnode && this.$vnode.ssrContext 16 } 17 }) 18 19 // expose FunctionalRenderContext for ssr runtime helper installation 20 Object.defineProperty(Vue, 'FunctionalRenderContext', { 21 value: FunctionalRenderContext 22 }) 23 24 Vue.version = '__VERSION__' 25 26 export default Vue
3 增强_2 platforms/web/runtime/index
web 构建时用到,代码如下,核心加粗,主要就是对 Vue.config 和 Vue.options 增强,并在原型对象上添加了 __patch__ 和 $mount 方法,在最后启动 devtool,对生产环境使用开发模式做出检查和提示
1 /* @flow */ 2 3 import Vue from 'core/index' 4 import config from 'core/config' 5 import { extend, noop } from 'shared/util' 6 import { mountComponent } from 'core/instance/lifecycle' 7 import { devtools, inBrowser } from 'core/util/index' 8 9 import { 10 query, 11 mustUseProp, 12 isReservedTag, 13 isReservedAttr, 14 getTagNamespace, 15 isUnknownElement 16 } from 'web/util/index' 17 18 import { patch } from './patch' 19 import platformDirectives from './directives/index' 20 import platformComponents from './components/index' 21 22 // install platform specific utils 23 Vue.config.mustUseProp = mustUseProp // 标签和属性一定要搭配使用的检查,如 video-muted,input-checked,option-selected 24 Vue.config.isReservedTag = isReservedTag // 判断是否是 HTML 保留标签 25 Vue.config.isReservedAttr = isReservedAttr // 判断是不是保留属性 26 Vue.config.getTagNamespace = getTagNamespace // 获取标签命名空间,暂时不是特别清楚作用,后续更新 27 Vue.config.isUnknownElement = isUnknownElement // 是否为位置元素,运行平台相关 28 29 // install platform runtime directives & components 30 extend(Vue.options.directives, platformDirectives) // 指令相关逻辑 31 extend(Vue.options.components, platformComponents) // 组件,动画相关,transition,transition-group 32 33 // install platform patch function 34 Vue.prototype.__patch__ = inBrowser ? patch : noop 35 36 // public mount method 37 Vue.prototype.$mount = function ( 38 el?: string | Element, 39 hydrating?: boolean 40 ): Component { 41 el = el && inBrowser ? query(el) : undefined 42 return mountComponent(this, el, hydrating) 43 } 44 45 // devtools global hook 46 /* istanbul ignore next */ 47 if (inBrowser) { 48 setTimeout(() => { 49 if (config.devtools) { 50 if (devtools) { 51 devtools.emit('init', Vue) 52 } else if ( 53 process.env.NODE_ENV !== 'production' && 54 process.env.NODE_ENV !== 'test' 55 ) { 56 console[console.info ? 'info' : 'log']( 57 'Download the Vue Devtools extension for a better development experience:\n' + 58 'https://github.com/vuejs/vue-devtools' 59 ) 60 } 61 } 62 if (process.env.NODE_ENV !== 'production' && 63 process.env.NODE_ENV !== 'test' && 64 config.productionTip !== false && 65 typeof console !== 'undefined' 66 ) { 67 console[console.info ? 'info' : 'log']( 68 `You are running Vue in development mode.\n` + 69 `Make sure to turn on production mode when deploying for production.\n` + 70 `See more tips at https://vuejs.org/guide/deployment.html` 71 ) 72 } 73 }, 0) 74 } 75 76 export default Vue
4 最终导出
如果使用的 runtime only 版,导出的就是 3 中的 Vue
如果使用的是 runtime + compile 版,有两点不同:1. Vue.prototype.$mount 又再次增强,做了一些逻辑判断; 2. 在 Vue 上添加了 compile
runtime only 是通过 webpack 的 vue-loader 在编译过程中,将 template 编译为 JS,只包含运行时 vue 代码,更轻量,开发时更推荐使用
runtime + compile 是运行时编译,这个编译是有时间成本的,但是如果在 new Vue 时,用到了 template,就需要使用这个,如果在 new Vue 是用的是 render,就不需要
5 总结
以下内容,方法用斜体,属性用下划线
先总结下大致流程,具体细节会在后续完善
从 instance/index 导出的 Vue 构造函数,已经在原型上拓展了以下几大类的 方法 或 属性,_ 开头的是内部使用,$ 开头的可以在工程中使用
被导出后,只被 core/index 导入过
这些是 import Vue from 'vue' 时做的
1. 初始化相关,_init, 具体内容下面有补充
2. 状态相关,$data, $props, $set, $delete, $watch。 $data, $props 重写了描述符,set 时警告,禁止修改
3. 生命周期相关,_update, $forceUpdate, $destory
4. render 相关,一些 render 函数(_v, _s, _l 等 ),$nextTick, _render
5. 事件相关,$on, $once, $off, $emit
以上是 instance/index 里做的事情,接下来是 core/index,在 Vue 上添加 静态方法 和 静态属性
这个会被两个地方用到,weex/runtime 和 web/runtime
下面这些是通过 initGlobalAPI 添加的
1. config 初始化,只能读,不能写,包括用户可以配置的全局配置,_lifecycleHooks 就是实例的生命周期(mounted 等),内部要用的配置等
2. util 初始化,包括 warn,extend, mergeOptions, defineReactive 这几个方法
3. set, delete, nextTick 赋值,上面是加在原型上 $ 开头,这是加在 Vue 上,都是同一个方法
4. observable,传入 object, 调用 observe,然后把响应处理后的 object 返回
5. options 初始化,赋值为 null
6. component, filter, directive, 挂到 Vue.options 上,都初始化为 null
7. Vue.options._base = Vue
8. 内置组件 keep-alive 挂到 Vue.options.component 上
9. use,插件注册时用到的,Vue.use(xxx)
10. mixin,合并 options 实现的混入
11. extend,生成子类要用的
12. component, filter, directive, 都是暴露给用户的,用来注册或获取自定义组件、过滤器、指令
这不是 initGlobalAPI 里的
13. 在 Vue.prototype 上加了 $isServer,$ssrContext,在 Vue 上加了 FunctionalRenderContext, version
以上是 core/index 里做的内容,下面是 web/runtime/index 内做的事情
1. 在 Vue.config 上加了些内部要用的方法
2. 在 Vue.options.directives 上定义了 model 和 show 两个指令
3. 在 Vue.options.components 上定义了动画相关的内置组件 transition , transition-group
4. 在 Vue.prototype 上添加 __patch__ 方法,用来更新 VNode 的
5. 在 Vue.prototype 上添加 $mount 方法,在 $mounted 里会调用 mountComponent , beforeMount、beforeUpdate、mounted 就是在这里被触发的
6. 如果是开发环境,还会调用 devtools
new Vue() 时,在构造函数中,调用 _init 函数进行初始化,添加了原型方法和静态方法
1. 给一个 uid,设置标志位 _isVue,把 所有传进去的 options 合并到 $options 里,在 this.$options 里可以访问到
2. 初始化代理 proxy
3. 把 _self 赋值为 this
4. 初始化 lifeCycle, 跟 vue 实例的生命周期不同,是指 $parent, $children, $refs, $root, _watcher, _inactive, _isMounted, _isDestroyed 等
5. 初始化 events, _events, _hasHookEvent, 以及父组件提供的事件。
6. 初始化 render,_vnode, _staticTrees,$slots, $scopedSlots, _c, $createElement, 后面两个创建 element 的方法区别是对子节点的处理不同,接着给 $attrs, $listeners 定义响应式
这时调用 beforeCreate 钩子
7. 初始化 inject, 对 inject 里的属性定义响应式
8. 初始化 state,state 包含多种状态, props, methods, data, computed, watch
初始化时都做了代理或混入,如把 this._data.xx 代理到 this 上,可以用 this.xx 直接访问,把 methods 每一项都混入到 vm 实例上,vm.xx 可以直接调用
对 props 做检查,然后响应式,代理 this._props.xx -> this.xx
methods 做检查,然后绑定 this,混入
data 可能有数组,所以 observe,在这里面判断分别响应式处理
computed 对每个属性实例化 watcher, 计算每一个属性的值
watch 根据数组和其它类型分别处理,数组就是遍历后对每一个处理.. ,调用 vm.$watch
9. 初始化 provider,其实就是绑一下 this
这时调用 created 钩子
10. 调用 vm.$mount
2021.04.12 update:
关于构造函数使用 function 而不是 class,还有个说法是 this 指向问题
在很多地方都涉及到修改 this 指向,class 创建的函数不能修改,function 可以
有个相关的 JS 知识点,在这写一下:this 指向的优先级:new > 显示绑定 > 隐式绑定 > 默认绑定
关于这个问题,在后续阅读过程中如果解惑,会在此更新
本文内容为笔者自身理解,在源码基础上做的笔记记录,如有疏漏或不足之处欢迎指出
感谢您的阅读及指正,让我们一起进步。
欢迎联系我交流:veinyin@gmail.com
作者:yuhui_yin
博客地址:https://www.cnblogs.com/veinyin/
如转载请注明出处。