https://juejin.im/post/5b38830de51d455888216675

Vue现在是中国三大网站之一,也是我主要的技术栈之一。在日常使用中,我知道它为什么神奇。此外,最近的社区中出现了大量的vue源代码阅读文章。我想借此机会从大家的文章和讨论中学到一些营养。同时,我在阅读源代码时总结了一些想法,并产生了一些文章。作为自己思考的总结,水平有限。请留言讨论~

目标Vue版本:2.5.17-beta.0

Vue源代码评论:https://github.com/SHERlocked93/vue-analysis

声明:文章中的源代码语法使用Flow,源代码根据需要进行删节。如果你想看完整版,请到上面的github地址。本文是系列文章。见文末地址~

0.先验知识

FlowES6语法常用的设计模式柯里化等函数式编程思想

下面是一些预备文章:JS静态类型检查工具Flow,ECMA 6简介——阮一峰,JS中的Corization,JS观察器模式,JS使用高阶函数实现函数缓存

1.文件结构

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

├──s-包含与施工相关的脚本和配置文件

│ ├ ── alias.js模块导入源代码中使用的别名

│ ├ ──工程的施工配置

├ ──建筑相关文件。一般来说,我们不需要移动

├ ──建造后文件的输出目录

├ ──示例-存储一些使用Vue开发的应用案例

├──js静态类型检查工具的流类型声明

├──包. json

├ ──测试文件

├ ── src来源目录

│ ├ ──编译器代码,用于将模板编译成渲染函数

│ │ ├ ──解析器-存储将模板字符串转换为元素抽象语法树的代码

││├──codegen-存储从抽象语法树生成渲染函数的代码

││├──optimizer . js-分析静态树并优化vdom渲染

│ ├ ──核心存储通用和独立于平台的运行时代码

│ │ ├ ──响应观测者的实现,包括数据观测的核心代码

││├──vdom-虚拟dom的创建和修补代码

│ │ ├ ──实例-vue构造函数和原型相关代码

│ │ ├ ──全局-API-将全局方法或属性挂载到Vue构造函数的代码

│ │ ├ ──组件——包括抽象的一般组件,目前只有保活

│ ├ ──服务器端渲染的服务器相关代码

│ ├ ──不同平台独有的平台相关代码

│ │ ├ ── WEEX平台支持

│ │ ├ ──网络平台支持

│ │ ├ ─ ─入口-运行时. js-运行时构造的入口

││├──entry-runtime-with-compiler . js-独立构建版本的入口

││├──entry-compiler . js-vue的入口文件-模板-编译器包

│ │ ├ ──入口-服务器-渲染器. js-vue的入口文件-服务器-渲染器包

│├──sfc-包含单文件组件的解析逻辑,用于vue-模板-编译器包

│ ├ ──共享-整个代码库共有的代码

几个重要目录:

compiler: 编译,用来将template转化为render函数core: Vue的核心代码,包括响应式实现、虚拟DOM、Vue实例方法的挂载、全局方法、抽象出来的通用组件等platform: 不同平台的入口文件,主要是 web 平台和 weex 平台的,不同平台有其特殊的构建过程,当然我们的重点是 web 平台server: 服务端渲染的相关代码,SSR 主要把组件直接渲染为 HTML 并由 Server 端直接提供给 Client 端sfc: 主要是 .vue 文件解析的逻辑shared: 一些通用的工具方法,有一些是为了增加代码可读性而设置的

平台下,src/platforms/web/entry-runtime . js文件作为运行时构造的入口,dist/vue.runtime.esm.js以esm模式输出,dist/vue.runtime.common.js以CJS模式输出,UMD输出dist/vue.runtime.js,编译器src/platforms/web/entry-runtime-with-compiler . js文件没有模板来渲染函数作为运行时构造的入口,ESM输出dist/vue.esm.js,CJS模式输出dist/runtime

2.入境文件

任何前端项目都可以从package.json文件中看到。让我们看看它的。dev,这是我们运行npm运行dev时的命令行:

" s": {

" dev ":" roll up-w-c s/config . js-environment TARGET:web-full-dev "

}

这里的Rollup是一个类似webpack的JS模块打包器。其实Vue-v1.0.10之前用的是webpack,后来改成了rollup。如果你想知道为什么它被改为汇总,你可以看看尤雨溪自己的答案。一般来说是为了让包更小,初始化更快。

您可以在这里看到,rollup运行s/config.js文件,并给出了一个参数TARGET:web-full-dev。让我们看看s/config.js中有什么。

// s/config.js

constbuilds= {

web-full-dev :{

条目:解析,//条目文件

Dest: resolve,//输出文件

格式:《UMD》。//参见下面对编译方法的描述

环境:'发展',//环境

别名:{he:'。/entity-decoder'},//alias

每个包前面的横幅//注释-版本/作者/日期。等等

},

}

格式编制方法说明:

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

这里的web-full-dev对应的是我们刚刚从命令行传入的命令,所以汇总会根据下面的入口文件开始打包,还有很多其他的命令和其他的输出模式和格式,可以自己查源码。

因此,本文主要关注的是包含编译器的src/platforms/web/entry-runtime-with-compiler . js文件。在生产和开发环境中,我们使用vue-loader来编译模板,所以我们不需要带有编译器的包。然而,为了更好地理解原理和过程,我们应该推荐带有编译器的入口文件。

先看看这个文件,在这里导入一个Vue看看它来自哪里

//src/platforms/web/entry-runtime-with-compiler . js

从“”导入Vue。“/runtime/index”

继续看

//src/platforms/web/runtime/index . js

从“核心/索引”导入Vue

保持前进

// src/core/index.js

从“”导入Vue。“/instance/index”

继续前进*2

// src/core/instance/index.js

/*这里是vue的构造函数,由于mixin模块分区的方便,不使用ES6的Class语法*/

functionVue{

这个。_init//初始化方法,位于initMixin

}

Vue.prototype上安装了以下mixin

initMixin

stateMixin

eventsMixin

生命周期信息

渲染混合

导出defaultVue

当我们新建Vue时,我们实际上调用了这个构造函数,所以我们可以从这里开始。

3.传动机构

在这里,我用xmind大致画了一个运行机制图,基本上下面的分析就在这个图的某些部分

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

上图可以分成几个部分仔细阅读。具体实现将在下面的文章中详细讨论。在这里,我们将发布一些源代码,先试用一下

3.1初始化_初始化

当我们在main.js中新增Vue时,Vue会调用构造函数的_init方法,这个方法是在core/instance/index.js的initMixin方法中定义的。

// src/core/instance/index.js

/*这里是Vue的构造函数*/

functionVue{

这个。_init//初始化方法,位于initMixin

}

//以下mixin以各种方式安装在Vue.prototype上,加载时已经安装好了。

InitMixin//添加:_init函数到Vue.prototype,...

StateMixin// Add $data属性,$props属性,$set函数,$delete函数,$watch函数到Vue.prototype,...

EventsMixin// add $on函数,$once函数,$off函数,$emit函数,$watch方法到Vue.prototype,...

life cycle emixin//Add:_ update方法,$forceUpdate函数,$destroy函数到Vue.prototype,...

RenderMixin//Add:$ next tick函数,_render function to Vue.prototype,...

导出defaultVue

我们可以看看init的初始化:

// src/core/instance/index.js

Vue.prototype._init= function{

组件=这个

InitLifecycle//初始化生命周期src/core/instance/lifecycle.js

init Events//Initialize Events src/core/instance/Events . js

init Render//initialize render src/core/instance/render . js。

CallHook//调用BeforeCreate钩子

InitInjections//在data/props src/core/instance/inject . js之前初始化注入值。

InitState//挂载数据/道具/方法/观察器/计算

InitProvide//初始化后提供数据/道具

CallHook//调用已创建的钩子

If{// $options可以看作是我们传递给` new Vue`。

VM。$ mount/$ mount方法

}

}

这里,init方法将为当前虚拟机实例执行一系列初始化设置。在初始化State的initState方法时,执行数据/道具响应更为重要。这就是传说中的通过Object.defineProperty为需要响应的对象设置getter/setter的方法,作为Dependency Collection的基础,从而达到数据变更驱动视图变更的目的。

最后,检查虚拟机上是否有el属性。$options,如果有的话,使用vm。$mount方法来挂载vm,形成数据层和视图层之间的连接。这就是为什么你需要手动虚拟机。$ mount,如果您不提供el选项。

我们看到创建的钩子是在挂载$mount之前调用的,所以在创建的钩子被触发之前,我们不能操作DOM,因为它还没有在DOM上呈现。

3.2挂载$mount

装载方法虚拟机。$mount在很多地方都有定义,根据不同的封装方式与平台有关。src/platform/web/entry-runtime-with-compiler . js,src/platform/web/runtime/index . js,src/platform/WEEX/runtime/index . js,我们的重点是第一个文件。但是在entry-runtime-with-compiler.js文件中,runtime/index.js中的$mount方法会先保存,然后在最后用call运行:

//src/platform/web/entry-runtime-with-compiler . js

Constmount = vue。原型。$mount//保存原$ mount,位于src/platform/web/runtime/index . js。

Vue.prototype.$mount= function:组件{

el = el & amp& amp查询

constoptions= this。$选项

if{//如果没有定义render方法,

让模板=选项。模板

//通过编译的方式将得到的模板转换成渲染函数

if{

const{render,static renderfns } = CompileToFunctions

options.render= render

}

}

返回安装。调用//执行最初的$mount

}

在vue的2.0版本中,Vue所有组件的渲染最终都需要render方法。不管我们是不是以单个文件的形式开发组件。vue或write el或template属性,它最终将被转换为render方法。这里的CompileToFunctions是把模板编译成render的方法,后面会介绍。

//src/platform/weex/runtime/index . js

Vue.prototype.$mount= function:组件{

el = el & amp& amp英国人?Query: undefined// query是document.querySelector方法

returnmountcomponent //位于core/instance/lifecycle.js。

}

如果这里的el一开始不是DOM元素,就会被查询方法的DOM元素替换,然后传递给mountComponent方法。让我们继续看看安装组件的定义:

//src/core/instance/life cycle . js

导出函数安装组件:组件{

vm。$el= el

if{

vm。$ options . render = createEmptyVNode

}

CallHook//调用BeforeMount钩子

//渲染观察器,当数据发生变化时,更新组件作为观察器对象的getter函数,依赖于集合和渲染视图

让更新组件

updateComponent= =>。{

vm。_更新,补水)

}

//渲染守望者,守望者在这里扮演两个角色。一是初始化时执行回调函数

//,另一种是在vm实例中的被监控数据发生变化时执行回调函数

newWatcher{

if{

CallHook//调用BeforeUpdate钩子

}

}

},true/* isRenderWatcher */)

//这里注意vm。$vNode表示Vue实例的父虚拟节点,因此如果它为空,则意味着它当前是根Vue实例

if{

Vm。_isMounted= true//表示此实例已装载

CallHook//调用已挂载的钩子

}

returnvm

}

渲染观察器在装载组件方法中被实例化,并且更新组件被传入。此方法:= >: {VM。_更新,补水)}首先使用_ render方法生成VNode,然后调用_ update方法更新DOM。您可以看到视图更新部分的介绍

这里调用了几个钩子,可以观察它们的时序。

3.3编译编译

如果需要转换渲染的场景,比如我们写的模板,会被编译器转换成渲染函数,这将由几个步骤组成:

条目位于src/platform/web/entry-runtime-with-compile . js的compileToFunctions方法中:

//src/platforms/web/compiler/index . js

const{compile,CompileToFunctions } = CreateCompiler

导出{compile,compileToFunctions}

请继续在此处查看创建编译器方法:

// src/compiler/index.js

导出常量创建编译器=创建编译器创建者:CompiledResult{

constast= parse,选项)

if{

优化

}

constcode= generate

返回{

ast,

render: code.render,

static renderfns:code . static renderfns

}

})

这里我们可以看到有三个重要的过程:解析、优化和生成,然后生成render方法的代码。

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

3.4响应观察

作为一个MVVM框架,我们知道Vue的模型层和视图层之间的桥梁是数据驱动的关键。Vue的响应是通过Object.defineProperty实现的,object . define property为响应的对象设置getter/setter。当呈现函数被呈现时,它将触发getter读取响应对象以收集依赖关系,而当修改响应对象时,它将触发setter的设置。setter方法将通知它之前收集的每个观察器,告诉它们它们的值已经被更新,从而触发观察器的更新来修补更新视图。

响应条目位于src/core/instance/init.js的initState:

// src/core/instance/state.js

导出功能状态{

vm。_观察者=

constopts= vm。$选项

ifinitProps

ifinitMethods

if{

initData

}else{

观察

}

ifinit computed

if{

initWatch

}

}

它定义了几个方法来初始化道具,方法,数据,计算的,非常有规律的调用。在这里,只要看看initData方法就能看到一只豹子

// src/core/instance/state.js

功能初始化数据{

让数据= vm。$options.data

数据= vm。_ data = typeof data = = = ' function '

?getData

:数据|| {}

观察//响应数据

}

首先判断数据是否是函数。如果是,取返回值,然后取本身。然后是一个观察的方法来处理数据。看看这个方法

// src/core/observer/index.js

导出函数observe:观察者| void{

观察者|空

ob=新观察者

returnob

}

这个方法主要使用数据来实例化一个Observer对象实例。观察者是一个类。观察者的构造函数使用定义活动方法来响应对象的键。它递归地将getter/setter添加到依赖项集合的对象属性中,并通知更新。这个方法大概是这样的。

// src/core/observer/index.js

functiondefineReactive{

Object.defineProperty{

/*收集依赖项*/

returnval

},

set:FunctiOnRestorveSetter{

if返回;

notify;//触发更新

}

});

}

3.5查看更新补丁

当使用defineReactive方法响应对象时,当呈现函数被呈现时,它将读取响应对象的getter,这将触发getter收集观察器依赖项,而当修改响应对象的值时,它将触发setter通知收集的依赖项,然后通知它已被修改,因此请根据需要重新呈现视图。被通知的观察器调用update方法来更新视图,该方法在上述传递给新的观察器的updateComponent方法中,该方法将调用update方法来修补和更新视图。

//src/core/instance/life cycle . js

让更新组件

updateComponent= =>。{

vm。_更新,补水)

}

//渲染守望者,守望者在这里扮演两个角色。一是初始化时执行回调函数

//,另一种是在vm实例中的被监控数据发生变化时执行回调函数

newWatcher

此渲染方法生成一个虚拟节点,并且在更新方法中将新的虚拟节点号与旧的虚拟节点号一起传递到补丁中

//src/core/instance/life cycle . js

vue . prototype . _ update = function{//调用此方法更新视图

组件=这个

constprevVnode = vm。v node

vm。_vnode= vnode

if{

//初始化

vm。$el= vm。__patch__

}else{

//更新

vm。$el= vm。_ _ patch _ _

}

}

_update调用__patch__方法,主要比较新旧VNode patchVnode,通过diff算法发现它们的区别是直接的。最后,更新对应于这些差异的DOM。

这里基本上介绍了一个主要的流程。从构造函数的实例化中,我们对Vue是如何工作的有了一个大致的了解。后面我们会讨论各个部分的内容。太浅,不好学,欢迎大家讨论~

如果有原创的好文章投稿,请直接发消息到官方号。

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

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

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