Vue源码起步

搞事!搞事!

截止2017.5.16,终于把vue的源码全部抄完,总共有9624行,花时大概一个月时间,中间迭代了一个版本(2.2-2.3),部分代码可能不一致,不过没关系!

上一个链接https://github.com/pflhm2005/vue

进入阶段2:尝试一下,从小案例看一看代码在vue源码中的走向,Go!(语文不好,将就看看)

从最简单的案例开始,摘抄官网的起步:

1 <body> 2 <div id='app'> {{message}} </div> 3 </body> 4 <script src='./vue.js'></script> 5 <script> 6 var app = new Vue({ 7 el: '#app', 8 data: { 9 message: 'Hello Vue!' 10 } 11 }); 12 </script>

打断点,开始执行!

初始化函数

html代码中,包含2大部分,挂载DOM节点,以及初始化vue的js代码。

有几个小地方,虽然按照官网的案例不会出现问题,但是还是说明一下:

(1)、el不能挂载到html或者body标签上

1 // Line-9547 2 if (el === document.body || el === document.documentElement) { "development" !== 'production' && warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead." ); return this }

(2)、关于代码各处可见的"development" !== 'production'

这个是dev模式才有,vue.js文件中对所有警告的代码判断条件进行了替换,报错方便调试,在发布模式中会自动清除,方便开发。

与jQuery不一样,这里需要手动new才会创建一个vue实例。

直接上源码。

jQuery:

1 // Line-94 jQuery 3.2.1 2 // 顺便吐槽一下 这个版本终于把初始化提前了 代码结构应该棒棒的 3 var jQuery = function(selector, context) { // The jQuery object is actually just the init constructor 'enhanced' 4 // Need init if jQuery is called (just allow error to be thrown if not included) 5 return new jQuery.fn.init(selector, context); 6 }

Vue:

1 // Line-9622 2 return Vue$3;

但是我们看到源码最后其实返回的是Vue$3,至于为什么new的是Vue也行呢?看一下源码开头的整个IIFE表达式也就明白了。

1 (function(global, factory) { // 兼容各种宿主环境 2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : // 浏览器环境 3 (global.Vue = factory()); 4 }(this, /\*vue\*/ ));

基本上框架都是这个套路,引入一个宿主环境的对象以及框架本身。

上述代码形参中,global在浏览器环境中相当于window,由于有时会在node、webpack等环境中运行,所以需要进行兼容处理,于是有很长的typeof。

对于浏览器来讲,上述代码其实就是window.Vue = Vue$3({options}),所以这就很明了了。

起步流程两个框架都是一样的,首先通过一个init函数进行全局初始化。

1 // Line-4055 2 function Vue$3(options) { if ("development" !== 'production' && 3 !(this instanceof Vue$3)) { 4 warn('Vue is a constructor and should be called with the \`new\` keyword'); 5 } this.\_init(options); 6 }

这里的options参数,很明显就是我们在new对象的时候传进去的对象,目前只有el和data两个。

入口函数只是简单的判断了一下有没有new,然后自动调用了原型函数_init。

_init函数的定义地点有点意思,是在一个函数内部定义,然后在后面调用了这个函数。

1 // Line-3924 2 function initMixin(Vue) { 3 Vue.prototype.\_init \= function(options) { //.... 4 }; 5 } // Line-4063 6 initMixin(Vue$3);

  整个函数只定义了_init这个初始化原型函数,原因在某个注释中写,直接定义原型会出现问题,所以采用这种方法进行规避。

  至于具体什么问题,我找不到那行注释了。。。

接下来看看初始化函数里面都干了啥事。

1 // Line-3926 2 // 生成的实例保存为别名vm 3 var vm = this; // 全局记数 表示有几个vue实例 4 vm.\_uid = uid$1++; var startTag, endTag; // 这里的config.performance开发版默认是false 5 if ("development" !== 'production' && config.performance && mark) { 6 startTag \= "vue-perf-init:" + (vm.\_uid); 7 endTag \= "vue-perf-end:" + (vm.\_uid); 8 mark(startTag); 9 } // 代表这是一个vue实例 10 vm.\_isVue = true; // 非组件 跳过 11 if (options && options.\_isComponent) { 12 initInternalComponent(vm, options); 13 } // 正常实例初始化 14 // 在这里对参数进行二次加工 15 else { 16 vm.$options \= mergeOptions( 17 resolveConstructorOptions(vm.constructor), 18 options || {}, 19 vm 20 ); 21 } // ...more

  前面基本上没做什么事,对于config对象,在开发版中的默认参数如下:

1 // 开发版的默认配置 2 var config = ({ 3 optionMergeStrategies: Object.create(null), 4 silent: false, 5 productionTip: "development" !== 'production', 6 devtools: "development" !== 'production', 7 performance: false, 8 errorHandler: null, 9 ignoredElements: \[\], 10 keyCodes: Object.create(null), 11 isReservedTag: no, 12 isReservedAttr: no, 13 isUnknownElement: no, 14 getTagNamespace: noop, 15 parsePlatformTagName: identity, 16 mustUseProp: no, // 历史遗留 17 \_lifecycleHooks: LIFECYCLE\_HOOKS 18 });

由于提示信息不是重点,所以第一步直接可以走到mergeOptions这里,从名字就可以看出这是一个参数合并的函数,接受3个参数:

1、resolveConstructorOptions(vm.constructor)

这个函数属于内部初始化,接受的参数就是Vue函数自身,如下:

1 // Line-4136 2 Sub.prototype.constructor = Sub;

  跳进去看一眼这个函数做了什么:

1 // Line-3998 2 function resolveConstructorOptions(Ctor) { // Ctor=Constructor 3 // options为所有vue实例基础参数 4 // 包含components,directives,filters,\_base 5 var options = Ctor.options; // 这个属性比较麻烦 暂时没有 跳过 6 if (Ctor.super) { //... 7 } // 返回修正后的options 8 return options 9 }

如果忽略那个super属性的话,返回的其实就是Vue$3.constructor.options,该对象包含4个属性,如图所示。

  

1 // Line-4368 2 // Vue函数自身的引用 3 Vue.options.\_base = Vue; // Line-7523 4 extend(Vue$3.options.directives, platformDirectives); 5 extend(Vue$3.options.components, platformComponents); // Line-7161 6 // 指令相关方法 7 var platformDirectives = { 8 model: model$1, 9 show: show 10 }; // Line-7509 11 // 组件相关 12 var platformComponents = { 13 Transition: Transition, 14 TransitionGroup: TransitionGroup 15 };

其中filters属性暂时是空的,其余3个属性在2个地方有定义,一个是组件、指令方法集,一个是vue函数自身引用。

2、options || {} => 传进来的参数

3、vm => 当前vue实例

最后,总览3个参数如下:

   带着3个小东西,跳进了mergeOptions函数进行参数合并。

1 // Line-1298 2 // 父子组件合并参数 本案例父组件为默认对象 3 function mergeOptions(parent, child, vm) { // 检测components参数中键是否合法 4 checkComponents(child); if (typeof child === 'function') { 5 child \= child.options; 6 } // 格式化props,directives参数 7 normalizeProps(child); 8 normalizeDirectives(child); // 格式化extends参数 9 var extendsFrom = child.extends; if (extendsFrom) { 10 parent \= mergeOptions(parent, extendsFrom, vm); 11 } // mixins参数 12 if (child.mixins) { for (var i = 0, l = child.mixins.length; i < l; i++) { 13 parent \= mergeOptions(parent, child.mixins\[i\], vm); 14 } 15 } // 本案例中上面的都会跳过 16 var options = {}; var key; // 遍历父组件对象 合并键 17 for (key in parent) { 18 mergeField(key); 19 } // 遍历子组件对象 若有父组件没有的 合并键 20 for (key in child) { if (!hasOwn(parent, key)) { 21 mergeField(key); 22 } 23 } // 合并函数 24 function mergeField(key) { var strat = strats\[key\] || defaultStrat; 25 options\[key\] \= strat(parent\[key\], child\[key\], vm, key); 26 } return options 27 }

这个函数中前半部分可以跳过,因为只有简单的el、data参数,所以直接从mergeField开始执行。

上面已经列举出父组件的键,有components、directives、_filters、_base四个。

这里又多出一个新的东西,叫strats,英文翻译成战略,所以应该怎么叫我也是很懵逼的。这个对象内容十分丰富,从生命周期到data、computed、methods都有,如下所示:

  方法太多,就不一个一个讲了,说说本案例相关的几个方法。

  看起来非常吓人,其实定义简单粗暴,上代码看看就明白了。

1 // Line-281 2 var ASSET\_TYPES = \[ 'component', 'directive', 'filter' \]; // Line-1182 3 ASSET\_TYPES.forEach(function(type) { 4 strats\[type \+ 's'\] = mergeAssets; 5 }); // Line-1175 6 function mergeAssets(parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : 7 res 8 }

  简单讲就是,3个键对应的是同一个方法,接受2个参数,方法还贼简单。

  所以,对上面的mergeOptions函数进行简化,可以转换成如下代码:

1 // parent键:components、directives、\_filters、\_base 2 // child键:data、el 3 function mergeOptions(parent, child, vm) { var options = {}; var key; // 父子对象键没有重复 参数直接可以写undefined 一步一步简化 4 for (key in parent) { //options\[key\] = mergeAssets(parent\[key\], child\[key\], vm, key); 5 //options\[key\] = mergeAssets(parent\[key\], undefined); 6 options\[key\] = Object.create(parent\[key\]); 7 } // 子键data和el需要额外分析 第一个参数同样可以写成undefined 8 for (key in child) { if (!hasOwn(parent, key)) { //options\[key\] = strats\[key\](parent\[key\], child\[key\], vm, key); 9 options\[key\] = strats\[key\](undefined, child\[key\], vm, key); 10 } 11 } return options 12 } function mergeAssets(parentVal, childVal) { var res = Object.create(parentVal || null); return childVal ? extend(res, childVal) : 13 res 14 }

  遍历父对象其实啥也没做,直接把几个方法加到了options上面,然后开始遍历子对象,子对象包含我们传进去的el、data。

  el比较简单,只是做个判断然后丢回来。

1 // Line-1064 2 // 简单判断是否是vue实例挂载的el 3 strats.el = strats.propsData = function(parent, child, vm, key) { if (!vm) { 4 warn( "option "" + key + "" can only be used during instance " + 5 'creation with the \`new\` keyword.' ); 6 } return defaultStrat(parent, child) 7 };

  data则分两种情况,一种是未挂载的组件,一种是实例化的vue。

  不管未挂载,直接看实例化vue是如何处理data参数。

1 // Line-1098 2 strats.data = function(parentVal, childVal, vm) { // 未挂载 3 if (!vm) { //... 4 } // new出来的 5 // 传进来的parentVal、childVal分别为undefined、{message:'Hello Vue!} 6 else if (parentVal || childVal) { return function mergedInstanceDataFn() { var instanceData = typeof childVal === 'function' ? childVal.call(vm) : 7 childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : 8 undefined; if (instanceData) { return mergeData(instanceData, defaultData) 9 } else { return defaultData 10 } 11 } 12 } 13 };

这里直接返回了一个函数,暂时不做分析,后面执行时候再来看。

到此,整个mergeOptions函数执行完毕,返回一个处理过的options,将这个结果给了实例的$options属性:

最后,用一张图结束这个乱糟糟的源码小跑第一节吧。

  撒花!撒花!

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )