Vue现在是国内前端Web端的三分之一世界之一,同时也是本人的主要技术栈之一,从日常使用中知道的也很奇妙,另外最近社区涌现了一票vue源码阅读类的文章,借此机会从大家的文章和讨论中汲取一些营养,同时总结一些源代码阅读时的想法,作为部分文章、自己思考的总结。

目标Vue版本:2.5.17-be

Vue源注释:

声明:句子中源代码的语法全部使用Flow,源代码根据需要删除(为了避免混淆@ _ @)。要查看完整版本,请进入上面的github地址。本文是系列文章,文章地址在底部~

0.词典知识

FlowES6语法中常用的设计模式韩国化等函数式编程思想

以下是一些初步句子:JS静态类型检查工具Flow、ECMAScript 6入门-单峰、JS的KORLID、JS观察者模式、JS使用高级函数实现函数缓存(备忘录模式)。

1.文件结构

vue的CONTRIBUTING.md中介绍了文件结构。在这里直接翻译。

-脚本-包括施工相关脚本和剖面目的构建配置-构建-相关文档构建一般我们不需要移动-DIST--JS静态类型检查工具[Flow](https://flowtype.org/)类型声明-。 包括数据观测在内的核心代码

一些重要目录:

Compiler:用于将template转换为render函数core: Vue的核心代码,包括响应实现、虚拟DOM、Vue实例方法装载、全局方法、抽象通用组件等。平台:多个平台的门户文件当然,我们的重点是web平台服务器:服务器端渲染(SSR)相关代码。SSR主要将组件直接呈现为HTML,服务器端直接提供给客户端sfc。主要是。用于解析vue文件的逻辑共享:一些常用工具方法是为了添加代码。

其中,平台下的src/platforms/web/en文件用作运行时构建的入口,ESM输出di,CJS输出di,UMD输出di,不包含模板template-render函数的编译器src/platform不输出

2.门户文件

所有前端项目都可以在文件中看到。首先,让我们看一下运行NPM run dev时的命令行。

scripts ' : { ' dev ' : ' roll up-w-c scripts/con-environment target : web-full-;

这里的rollup是一个JS模块包,类似于web包。事实上,在Vue-v1.0.10版本之前也使用了web pack,之后变成了rollup。如果想知道为什么改成rollup,可以看看刘雨溪本人的回答。

可以看到这里 rollup 去运行 scripts/con 文件,并且给了个参数 TARGET:web-full-dev,那来看看 scripts/con 里面是啥

// scripts/con const builds = { 'web-full-dev': { entry: resolve('web/en'), // 入口文件 dest: resolve('di'), // 输出文件 format: 'umd', // 参看下面的编译方式说明 env: 'development', // 环境 alias: { he: './entity-decoder' }, // 别名 banner // 每个包前面的注释-版本/作者/日期.etc },}

format 编译方式说明:

  • es: ES Modules,使用ES6的模板语法输出
  • cjs: CommonJs Module,遵循CommonJs Module规范的文件输出
  • amd: AMD Module,遵循AMD Module规范的文件输出
  • umd: 支持外链规范的文件输出,此文件可以直接使用script标签

这里的 web-full-dev 就是对应刚刚我们在命令行里传入的命令,那么 rollup 就会按下面的 entry 入口文件开始去打包,还有其他很多命令和其他各种输出方式和格式可以自行查看一下源码。

因此本文主要的关注点在包含 compiler 编译器的 src/platforms/web/en 文件,在生产和开发环境中我们使用 vue-loader 来进行 template 的编译从而不需要带 compiler 的包,但是为了更好的理解原理和流程还是推介从带 compiler 的入口文件看起。

先看看这个文件,这里导入了个 Vue ,看看它从哪来的

// src/platforms/web/enimport Vue from './runtime/index'

继续看

// src/platforms/web/runtimeimport Vue from 'core/index'

keep moving

// src/coreimport Vue from './instance/index'

keep moving*2

// src/core/instance /* 这里就是vue的构造函数了,不用ES6的Class语法是因为mixin模块划分的方便 */function Vue(options) { (options) // 初始化方法,位于 initMixin 中} // 下面的mixin往Vue.prototype上各种挂载initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue) export default Vue

当我们 new Vue( ) 的时候,实际上调用的就是这个构造函数,可以从这里开始看了。

3. 运行机制

这里我用xmind粗略的画了一张运行机制图,基本上后面的分析都在这张图上面的某些部分了

本文 Vue 实例都是用 vm 来表示

上面这个图可以分为多个部分细加阅读,具体的实现我们在后面的文章中详细讨论,这里先贴一部分源码尝尝鲜

3.1 初始化 _init( )

当我们在 main.js 里 new Vue( ) 后,Vue 会调用构造函数的 _init( ) 方法,这个方法是位于 core/instance 的 initMixin( ) 方法中定义的

// src/core/instance /* 这里就是Vue的构造函数 */function Vue(options) { (options) // 初始化方法,位于 initMixin 中} // 下面的mixin往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的initMixin(Vue) // 给Vue.prototype添加:_init函数,...stateMixin(Vue) // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...eventsMixin(Vue) // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...lifecycleMixin(Vue) // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...renderMixin(Vue) // 给Vue.prototype添加: $nextTick函数, _render函数,... export default Vue

我们可以看看 init( ) 这个方法到底进行了哪些初始化:

// src/core/instance Vue. = function(options?: Object) { const vm: Component = this initLifecycle(vm) // 初始化生命周期 src/core/instance initEvents(vm) // 初始化事件 src/core/instance initRender(vm) // 初始化render src/core/instance callHook(vm, 'beforeCreate') // 调用beforeCreate钩子 initInjections(vm) // 初始化注入值 before data/props src/core/instance initState(vm) // 挂载 data/props/methods/watcher/computed initProvide(vm) // 初始化Provide after data/props callHook(vm, 'created') // 调用created钩子 if (vm.$o) { // $options可以认为是我们传给 `new Vue(options)` 的options vm.$mount(vm.$o) // $mount方法 }}

这里 _init() 方法中会对当前 vm 实例进行一系列初始化设置,比较重要的是初始化 State 的方法 initState(vm) 的时候进行 data/props 的响应式化,这就是传说中的通过 Object.defineProperty() 方法对需要响应式化的对象设置 getter/setter,以此为基础进行依赖搜集(Dependency Collection),达到数据变化驱动视图变化的目的。

最后检测 vm.$options 上面有没有 el 属性,如果有的话使用 vm.$mount 方法挂载 vm,形成数据层和视图层的联系。这也是如果没有提供 el 选项就需要自己手动 vm.$mount('#app') 的原因。

我们看到 created 钩子是在挂载 $mount 之前调用的,所以我们在 created 钩子触发之前是无法操作 DOM 的,这是因为还没有渲染到 DOM 上。

3.2 挂载 $mount( )

挂载方法 vm.$mount( ) 在多个地方有定义,是根据不同打包方式和平台有关的,src/platform/web/en、src/platform/web/runtime、src/platform/weex/runtime,我们的关注点在第一个文件,但在 en 文件中会首先把 runtime 中的 $mount 方法保存下来,并在最后用 call 运行:

// src/platform/web/en const mount = Vue.prototype.$mount // 把原来的$mount保存下来,位于 src/platform/web/runtimeVue.prototype.$mount = function( el?: string | Element, // 挂载的元素 hydrating?: boolean // 服务端渲染相关参数): Component { el = el && query(el) const options = this.$options if (!o) { // 如果没有定义render方法 let template = o // 把获取到的template通过编译的手段转化为render函数 if (template) { const { render, staticRenderFns } = compileToFunctions(template, {...}, this) o = render } } return mount.call(this, el, hydrating) // 执行原来的$mount}

在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法。这里的 compileToFunctions 就是把 template 编译为 render 的方法,后面会介绍。

// src/platform/weex/runtime Vue.prototype.$mount = function ( el?: string | Element, // 挂载的元素 hydrating?: boolean // 服务端渲染相关参数): Component { el = el && inBrowser ? query(el) : undefined // query就是document.querySelector方法 return mountComponent(this, el, hydrating) // 位于core/instance}

这里的 el 一开始如果不是DOM元素的话会被 query 方法换成DOM元素再被传给 mountComponent 方法,我们继续看 mountComponent 的定义:

// src/core/instance export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { vm.$el = el if (!vm.$o) { vm.$o = createEmptyVNode } callHook(vm, 'beforeMount') // 调用beforeMount钩子 // 渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图 let updateComponent updateComponent = () => { vm._update(), hydrating) } // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数 // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数 new Watcher(vm, updateComponent, noop, { before () { if ) { callHook(vm, 'beforeUpdate') // 调用beforeUpdate钩子 } } }, true /* isRenderWatcher */) // 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例 if (vm.$vnode == null) { vm._isMounted = true // 表示这个实例已经挂载 callHook(vm, 'mounted') // 调用mounted钩子 } return vm}

在 mountComponent 方法里实例化了一个渲染 Watcher,并且传入了一个 updateComponent ,这个方法:() => { vm._update(), hydrating) } 首先使用 _render 方法生成 VNode,再调用 _update 方法更新DOM。可以看看视图更新部分的介绍

这里调用了几个钩子,他们的时机可以关注一下。

3.3 编译 compile( )

如果在需要转换 render 的场景下,比如我们写的 template ,将会被 compiler 转换为 render 函数,这其中会有几个步骤组成:

入口位于刚刚 src/platform/web/en 的 compileToFunctions 方法:

// src/platforms/web/compiler const { compile, compileToFunctions } = createCompiler(baseOptions)export { compile, compileToFunctions }

继续看这里的 createCompiler 方法:

// src/compiler export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions): CompiledResult { const ast = parse(), options) if !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns }})

这里可以看到有三个重要的过程 parse、optimize、generate,之后生成了 render 方法代码。

  • parse:会用正则等方式解析 template 模板中的指令、class、style等数据,形成抽象语法树 AST
  • optimize:优化AST,生成模板AST树,检测不需要进行DOM改变的静态子树,减少 patch 的压力
  • generate:把 AST 生成 render 方法的代码

3.4 响应式化 observe( )

Vue作为一个MVVM框架,我们知道它的 Model 层和 View 层之间的桥梁 ViewModel 是做到数据驱动的关键,Vue的响应式是通过 Object.defineProperty 来实现,给被响应式化的对象设置 getter/setter ,当 render 函数被渲染的时候会触发读取响应式化对象的 getter 进行依赖收集,而在修改响应式化对象的时候会触发设置 setter,setter 方法会 notify 它之前收集到的每一个 watcher 来告诉他们自己的值更新了,从而触发 watcher 的 update 去 patch 更新视图。

响应式化的入口位于 src/core/instance 的 initState 中:

// src/core/instance export function initState(vm: Component) { vm._watchers = [] const opts = vm.$options if ) initProps(vm, o) if ) initMethods(vm, o) if ) { initData(vm) } else { observe = {}, true /* asRootData */) } if ) initComputed(vm, o) if && o !== nativeWatch) { initWatch(vm, o) }}

它非常规律的定义了几个方法来初始化 props、methods、data、computed、wathcer,这里只看 initData 方法,来窥一豹

// src/core/instance function initData(vm: Component) { let data = vm.$o data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} observe(data, true /* asRootData */) // 给data做响应式处理}

首先判断了下 data 是不是函数,是则取返回值不是则取自身,之后有一个 observe 方法对 data 进行处理,看看这个方法

// src/core/observer export function observe (value: any, asRootData: ?boolean): Observer | void { let ob: Observer | void ob = new Observer(value) return ob}

这个方法主要用 data 去实例化一个 Observer 对象实例,Observer 是一个 Class,Observer 的构造函数使用 defineReactive 方法给对象的键响应式化,它给对象的属性递归添加 getter/setter,用于依赖收集和 notify 更新,这个方法大概是这样的

// src/core/observer function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { /* 进行依赖收集 */ return val; }, set: function reactiveSetter (newVal) { if (newVal === val) return; notify(); // 触发更新 } });}

3.5 视图更新 patch( )

当使用 defineReactive 方法将对象响应式化后,当 render 函数被渲染的时候,会读取响应化对象的 getter 从而触发 getter 进行 watcher 依赖的收集,而在修改响应化对象的值的时候,会触发 setter 通知 notify 之前收集的依赖,通知自己已被修改,请按需重新渲染视图。被通知的 watcher 调用 update 方法去更新视图,位于上面介绍过的传递给 new Watcher( ) 的 updateComponent 方法中,这个方法会调用 update 方法去 patch 更新视图。

// src/core/instance let updateComponentupdateComponent = () => { vm._update(), hydrating)} // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数// ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数new Watcher(vm, updateComponent, noop, {...}, true /* isRenderWatcher */)

这个 _render 方法生成虚拟 Node, _update 方法中的会将新的 VNode 与旧的 VNode 一起传入 patch

// src/core/instance Vue. = function(vnode: VNode, hydrating?: boolean) { // 调用此方法去更新视图 const vm: Component = this const prevVnode = vm._vnode vm._vnode = vnode if (!prevVnode) { // 初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { //更新 vm.$el = vm.__patch__(prevVnode, vnode) }}

_update 调用 __patch__ 方法,它主要是对新老 VNode 进行比较 patchVnode,经过 diff 算法得出它们的差异直接,最后这些差异的对应 DOM 进行更新。

到这里基本上一个主要的流程就介绍完了,我们大概了解了一个 Vue 从一个构造函数的实例化开始是如何运转的,后面会展开来讨论一下各个部分的内容,在下才疏学浅,未免纰漏,欢迎大家讨论~

作者:SHERlocked93

1.《.vue 如何写注释?总结很全面速看!Vue源码阅读:文件结构与运行机制》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《.vue 如何写注释?总结很全面速看!Vue源码阅读:文件结构与运行机制》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/gl/2128457.html