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. setdeletenextTick 赋值,上面是加在原型上 $ 开头,这是加在 Vue 上,都是同一个方法

4. observable,传入 object, 调用 observe,然后把响应处理后的 object 返回

5. options 初始化,赋值为 null

6. componentfilterdirective, 挂到 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. componentfilterdirective, 都是暴露给用户的,用来注册或获取自定义组件、过滤器、指令

这不是 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 > 显示绑定 > 隐式绑定 > 默认绑定

关于这个问题,在后续阅读过程中如果解惑,会在此更新

 

 

本文内容为笔者自身理解,在源码基础上做的笔记记录,如有疏漏或不足之处欢迎指出

 

posted @ 2021-04-09 21:11  yuhui_yin  阅读(150)  评论(0编辑  收藏  举报