|
| 1 | +--- |
| 2 | + title: Vue源码阅读——Vue内部的初始化流程 |
| 3 | + date: 2023-12-23T12:18:35Z |
| 4 | + summary: |
| 5 | + tags: [] |
| 6 | +--- |
| 7 | + |
| 8 | + ## new 一个 Vue 实例 |
| 9 | +``` |
| 10 | +<!DOCTYPE html> |
| 11 | +<html lang="en"> |
| 12 | + <head> |
| 13 | + <meta charset="UTF-8" /> |
| 14 | + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
| 15 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 16 | + <title>Document</title> |
| 17 | + </head> |
| 18 | + <body> |
| 19 | + <div id="app"></div> |
| 20 | + <script src="../dist/vue.js"></script> |
| 21 | + <script> |
| 22 | + new Vue({ el: "#app", template: "<span>Hello World</span>" }); |
| 23 | + </script> |
| 24 | + </body> |
| 25 | +</html> |
| 26 | +
|
| 27 | +``` |
| 28 | +从`dist/vue.js`引入了打包后的 vue,传入要挂载的 DOM 的 id,template参数,vue 就成功渲染出来了,但今天 |
| 29 | + |
| 30 | +## Vue 的版本 |
| 31 | +接下来我们从外往里地往下看,Vue 使用的 rollup 打包,配置文件位于 script 文件夹的 config.js 中,这里面着各种版本的打包配置,不同的版本有着不同的功能,runtime 表示包含 Vue 运行时的版本,compiler 表示包含编译器的版本,编译器可以识别我们写的 template,如果不包含 compiler 就仅能处理 rander 函数,我找到当前使用的同时有 runtime 和 compiler 的版本,也就是`web-full-dev` |
| 32 | +``` |
| 33 | +// @ scripts/config.js |
| 34 | +// Runtime+compiler development build (Browser) |
| 35 | + 'web-full-dev': { |
| 36 | + // 入口路径 |
| 37 | + entry: resolve('web/entry-runtime-with-compiler.js'), |
| 38 | + // 出口路径与文件名 |
| 39 | + dest: resolve('dist/vue.js'), |
| 40 | + // 打包输出格式 |
| 41 | + format: 'umd', |
| 42 | + // 环境 |
| 43 | + env: 'development', |
| 44 | + alias: { he: './entity-decoder' }, |
| 45 | + banner |
| 46 | + }, |
| 47 | +``` |
| 48 | +配置的 entry 没有直接使用路径,而是为了代码的简洁集中配置了别名 |
| 49 | +``` |
| 50 | +// @ scripts/alias.js |
| 51 | +module.exports = { |
| 52 | + vue: resolve('src/platforms/web/entry-runtime-with-compiler'), |
| 53 | + compiler: resolve('src/compiler'), |
| 54 | + core: resolve('src/core'), |
| 55 | + shared: resolve('src/shared'), |
| 56 | + web: resolve('src/platforms/web'), |
| 57 | + weex: resolve('src/platforms/weex'), |
| 58 | + server: resolve('src/server'), |
| 59 | + sfc: resolve('src/sfc') |
| 60 | +} |
| 61 | +``` |
| 62 | +这里是我们当前版本(entry-runtime-with-compiler)的入口,此处就是做了编译(compiler)工作: |
| 63 | +``` |
| 64 | +@ src/platforms/web/entry-runtime-with-compiler |
| 65 | +
|
| 66 | +在Vue原型上添加了 $mount 方法 |
| 67 | +const mount = Vue.prototype.$mount |
| 68 | +
|
| 69 | +Vue.prototype.$mount = function ( |
| 70 | + el?: string | Element, |
| 71 | + hydrating?: boolean |
| 72 | +): Component { |
| 73 | + ... |
| 74 | +// 模板编译相关操作 |
| 75 | + ... |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +我们再往里看,此处是 运行时(runtime)模块的入口: |
| 80 | +``` |
| 81 | +@ src/platforms/web/runtime/index |
| 82 | +... |
| 83 | +Vue.prototype.$mount = function ( |
| 84 | + el?: string | Element, |
| 85 | + hydrating?: boolean |
| 86 | +): Component { |
| 87 | + // inBrowser 是 Vue 封装的一个工具函数 |
| 88 | + // const inBrowser = typeof window !== 'undefined' |
| 89 | + // 用来判断当前环境是否为浏览器(根据需要安装devtools) |
| 90 | + el = el && inBrowser ? query(el) : undefined; |
| 91 | + // 进入挂载阶段 |
| 92 | + return mountComponent(this, el, hydrating); |
| 93 | +}; |
| 94 | +... |
| 95 | +``` |
| 96 | +如果使用的版本是 runtime 版本,是没有 compoiler 模块,也就是无法对 template 进行编译的,所以我们需要根据实际需求选择版本。 |
| 97 | + |
| 98 | +## Vue 的初始化 |
| 99 | +接下来就是 Vue 的核心代码了 |
| 100 | +``` |
| 101 | +initGlobalAPI(Vue) |
| 102 | +
|
| 103 | +Object.defineProperty(Vue.prototype, '$isServer', { |
| 104 | + get: isServerRendering |
| 105 | +}) |
| 106 | +
|
| 107 | +Object.defineProperty(Vue.prototype, '$ssrContext', { |
| 108 | + get () { |
| 109 | + /* istanbul ignore next */ |
| 110 | + return this.$vnode && this.$vnode.ssrContext |
| 111 | + } |
| 112 | +}) |
| 113 | +
|
| 114 | +// expose FunctionalRenderContext for ssr runtime helper installation |
| 115 | +Object.defineProperty(Vue, 'FunctionalRenderContext', { |
| 116 | + value: FunctionalRenderContext |
| 117 | +}) |
| 118 | +
|
| 119 | +Vue.version = '__VERSION__' |
| 120 | +``` |
| 121 | + |
| 122 | +这里调用了 initGlobalAPI 函数,并传入了 Vue 的构造函数 |
| 123 | +``` |
| 124 | +// @src/core/global-api/index |
| 125 | +export function initGlobalAPI(Vue: GlobalAPI) { |
| 126 | + const configDef = {} |
| 127 | + configDef.get = () => config |
| 128 | + if (process.env.NODE_ENV !== 'production') { |
| 129 | + configDef.set = () => { |
| 130 | + warn( |
| 131 | + 'Do not replace the Vue.config object, set individual fields instead.' |
| 132 | + ) |
| 133 | + } |
| 134 | + } |
| 135 | + Object.defineProperty(Vue, 'config', configDef) |
| 136 | +``` |
| 137 | +我们知道,传入的参数是 Vue 的构造函数,此处使用 Object.defineProperty 方法在Vue构造函数上新增了 config 属性,并定义该属性的<a target="_blank" |
| 138 | +href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty">存取描述符</a>,仅允许获取,赋值时进行警告。 |
| 139 | + |
| 140 | +然后挂载一系列的工具方法,这些相信我们大多都用过,我们在后面再做详细的了解: |
| 141 | +``` |
| 142 | + Vue.util = { |
| 143 | + warn, |
| 144 | + extend, |
| 145 | + mergeOptions, |
| 146 | + defineReactive, |
| 147 | + }; |
| 148 | + Vue.set = set; |
| 149 | + Vue.delete = del; |
| 150 | + Vue.nextTick = nextTick; |
| 151 | + // 2.6 explicit observable API |
| 152 | + Vue.observable = <T>(obj: T): T => { |
| 153 | + observe(obj); |
| 154 | + return obj; |
| 155 | + }; |
| 156 | +``` |
| 157 | +紧接着,在Vue上创建了一个空对象options: |
| 158 | +``` |
| 159 | +Vue.options = Object.create(null); |
| 160 | + // 变量常量数组 ASSET_TYPES = ['component','directive','filter'],在 Vue.option 中创建空的对象 |
| 161 | + ASSET_TYPES.forEach((type) => { |
| 162 | + Vue.options[type + "s"] = Object.create(null); |
| 163 | + }); |
| 164 | + // 将 Vue.options._base 属性指向自身,此属性在下文中被用来判断是否为根实例 |
| 165 | + Vue.options._base = Vue; |
| 166 | + // 这是shared中封装的一个工具函数,比较简单, |
| 167 | + // 两参数都是对象,作用是将参数二中的属性插入到参数一中 |
| 168 | + // 此处将 Keep-alive 中缓存的数据合并到了 option |
| 169 | + extend(Vue.options.components, builtInComponents); |
| 170 | + |
| 171 | + initUse(Vue); |
| 172 | + initMixin(Vue); |
| 173 | + initExtend(Vue); |
| 174 | + initAssetRegisters(Vue); |
| 175 | +``` |
| 176 | +此处调用了四个 init 开头的函数,我们先大致的做一下了解,这四个函数做的操作就是在 Vue 上添加相应的方法(use,mixin,extend),initAssetRegisters 内部通过数组的变量添加了三个方法。 |
| 177 | +## Vue 的构造函数 |
| 178 | +此处就是 Vue 开始的地方 |
| 179 | +``` |
| 180 | +@ src/core/instance/index.js |
| 181 | +import { initMixin } from "./init"; |
| 182 | +import { stateMixin } from "./state"; |
| 183 | +import { renderMixin } from "./render"; |
| 184 | +import { eventsMixin } from "./events"; |
| 185 | +import { lifecycleMixin } from "./lifecycle"; |
| 186 | +import { warn } from "../util/index"; |
| 187 | +
|
| 188 | +function Vue(options) { |
| 189 | + //判断是否以new关键字创建的vue实例,否则抛出警告 |
| 190 | + if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) { |
| 191 | + warn("Vue is a constructor and should be called with the `new` keyword"); |
| 192 | + } |
| 193 | + // 调用_init |
| 194 | + this._init(options); |
| 195 | +} |
| 196 | +
|
| 197 | +// 挂载_init方法 |
| 198 | +initMixin(Vue); |
| 199 | +stateMixin(Vue); |
| 200 | +eventsMixin(Vue); |
| 201 | +lifecycleMixin(Vue); |
| 202 | +renderMixin(Vue); |
| 203 | +
|
| 204 | +export default Vue; |
| 205 | +``` |
| 206 | +那么问题来了,这个_init是哪来的?在下面调用的initMixin函数中,此处为Vue构造函数挂载了_init方法: |
| 207 | +``` |
| 208 | +Vue.prototype._init = function (options?: Object) { |
| 209 | + const vm: Component = this; |
| 210 | + // uid是每个 Vue 实例的唯一标识 |
| 211 | + vm._uid = uid++; |
| 212 | + // 一个避免被观察到的标记 |
| 213 | + vm._isVue = true; |
| 214 | + // 对 option 进行合并操作,将相关的属性和方法合并到 vm.$options 对象之上 |
| 215 | + if (options && options._isComponent) { |
| 216 | + // _isComponent 是Vue在创建组件流程中声明的属性 |
| 217 | + // 如果是子组件初始化时走这里,这里只做了一些性能优化 |
| 218 | + initInternalComponent(vm, options); |
| 219 | + } else { |
| 220 | + // 将用户传入配置合并到vm |
| 221 | + vm.$options = mergeOptions( |
| 222 | + // 合并 mixin 以及 extend 操作下影响的 option |
| 223 | + resolveConstructorOptions(vm.constructor), |
| 224 | + options || {}, |
| 225 | + vm |
| 226 | + ); |
| 227 | + } |
| 228 | +
|
| 229 | + vm._self = vm; |
| 230 | + // 初始化组件实例关系属性 |
| 231 | + initLifecycle(vm); |
| 232 | + // 初始化自定义事件 |
| 233 | + initEvents(vm); |
| 234 | + // 初始化 rander 和插槽 |
| 235 | + initRender(vm); |
| 236 | + // 执行生命周期钩子beforeCreate |
| 237 | + callHook(vm, "beforeCreate"); |
| 238 | + // 注入实例化 |
| 239 | + initInjections(vm); |
| 240 | + // 数据响应式的实例化 |
| 241 | + initState(vm); |
| 242 | + // 解析provide |
| 243 | + initProvide(vm); // resolve provide after data/props |
| 244 | + // 执行生命周期钩子created |
| 245 | + callHook(vm, "created"); |
| 246 | +
|
| 247 | + //最后,判断是否是否传入`el`,如果有就调用$mount进入模板编译阶段 |
| 248 | +
|
| 249 | +if (vm.$options.el) { |
| 250 | + vm.$mount(vm.$options.el); |
| 251 | + } |
| 252 | + |
| 253 | +``` |
| 254 | + |
| 255 | +## 总结 |
| 256 | +经过上面的分析,可以归纳为一下几个流程 |
| 257 | + |
| 258 | +1. 初始化 Vue 构造函数上的方法 |
| 259 | +2. 实例化对象 vm |
| 260 | +3. 调用 $mount 方法进入模板解析阶段 |
| 261 | + |
| 262 | + |
0 commit comments