1. 前言

大家好,我是若川。最近组织了源码共读活动《1个月,200+人,一起读了4周源码》,已经有超50+人提交了笔记,群里已经有超1200人,感兴趣的可以点此链接扫码加我微信 ruochuan12

之前写的《学习源码整体架构系列》jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4十余篇源码文章。其中最新的三篇是:

50行代码串行Promise,koa洋葱模型原来是这么实现?

Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一本好书,就是在和高尚的人谈话。同理可得:读源码,也算是和作者的一种学习交流的方式。

本文源于一次源码共读群里群友的提问,请问@若川,“为什么 data 中的数据可以用 this 直接获取到啊”,当时我翻阅源码做出了解答。想着如果下次有人再次问到,我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理,于是就有了这篇文章。

阅读本文,你将学到:

1. 如何学习调试 vue2 源码
2. data 中的数据为什么可以用 this 直接获取到
3. methods 中的方法为什么可以用 this 直接获取到
4. 学习源码中优秀代码和思想,投入到自己的项目中

本文不难,用过 Vue 的都看得懂,希望大家动手调试和学会看源码。

看源码可以大胆猜测,最后小心求证。

2. 示例:this 能够直接获取到 data 和 methods

众所周知,这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。

const vm = new Vue({data: {name: '我是若川',},methods: {sayName(){console.log(this.name);}},
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川

那么为什么 this.xxx 能获取到data里的数据,能获取到 methods 方法。

我们自己构造写的函数,如何做到类似Vue的效果呢。

function Person(options){}const p = new Person({data: {name: '若川'},methods: {sayName(){console.log(this.name);}}
});console.log(p.name);
// undefined
console.log(p.sayName());
// Uncaught TypeError: p.sayName is not a function

如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。

3. 准备环境调试源码一探究竟

可以在本地新建一个文件夹examples,新建文件index.html文件。在<body></body>中加上如下js

<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>const vm = new Vue({data: {name: '我是若川',},methods: {sayName(){console.log(this.name);}},});console.log(vm.name);console.log(vm.sayName());
</script>

再全局安装npm i -g http-server启动服务。

npm i -g http-server
cd examples
http-server .
// 如果碰到端口被占用,也可以指定端口
http-server -p 8081 .

这样就能在http://localhost:8080/打开刚写的index.html页面了。

对于调试还不是很熟悉的读者,可以看这篇文章《前端容易忽略的 debugger 调试技巧》

调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。

debugger

刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。

3.1 Vue 构造函数

function Vue (options) {if (!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword');}this._init(options);
}
// 初始化
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

值得一提的是:if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。一般而言,我们平时应该不会考虑写这个。

当然看源码库也可以自己函数内部调用 new 。但 vue 一般一个项目只需要 new Vue() 一次,所以没必要。

jQuery 源码的就是内部 new ,对于使用者来说就是无new构造。

jQuery = function( selector, context ) {// 返回new之后的对象return new jQuery.fn.init( selector, context );
};

因为使用 jQuery 经常要调用。其实 jQuery 也是可以 new 的。和不用 new 是一个效果。

如果不明白 new 操作符的用处,可以看我之前的文章。面试官问:能否模拟实现JS的new操作符

调试:继续在this._init(options);处打上断点,按F11进入函数。

3.2 _init 初始化函数

进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟datamethods相关的实现在initState(vm)函数里。

// 代码有删减
function initMixin (Vue) {Vue.prototype._init = function (options) {var vm = this;// a uidvm._uid = uid$3++;// a flag to avoid this being observedvm._isVue = true;// merge optionsif (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options);} else {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm);}// expose real selfvm._self = vm;initLifecycle(vm);initEvents(vm);initRender(vm);callHook(vm, 'beforeCreate');initInjections(vm); // resolve injections before data/props//  初始化状态initState(vm);initProvide(vm); // resolve provide after data/propscallHook(vm, 'created');};
}

调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。

3.3 initState 初始化状态

从函数名来看,这个函数主要实现功能是:

初始化 props
初始化 methods
监测数据
初始化 computed
初始化 watch
function initState (vm) {vm._watchers = [];var opts = vm.$options;if (opts.props) { initProps(vm, opts.props); }// 有传入 methods,初始化方法if (opts.methods) { initMethods(vm, opts.methods); }// 有传入 data,初始化 dataif (opts.data) {initData(vm);} else {observe(vm._data = {}, true /* asRootData */);}if (opts.computed) { initComputed(vm, opts.computed); }if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch);}
}

我们重点来看初始化 methods,之后再看初始化 data

调试:在 initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。继续按F11,先进入initMethods函数。

3.4 initMethods 初始化方法

function initMethods (vm, methods) {var props = vm.$options.props;for (var key in methods) {{if (typeof methods[key] !== 'function') {warn("Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +"Did you reference the function correctly?",vm);}if (props && hasOwn(props, key)) {warn(("Method \"" + key + "\" has already been defined as a prop."),vm);}if ((key in vm) && isReserved(key)) {warn("Method \"" + key + "\" conflicts with an existing Vue instance method. " +"Avoid defining component methods that start with _ or $.");}}vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);}
}

initMethods函数,主要有一些判断。

判断 methods 中的每一项是不是函数,如果不是警告。
判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。

除去这些判断,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。

这就是为什么我们可以通过this直接访问到methods里面的函数的原因

我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。

3.4.1 bind 返回一个函数,修改 this 指向

function polyfillBind (fn, ctx) {function boundFn (a) {var l = arguments.length;return l? l > 1? fn.apply(ctx, arguments): fn.call(ctx, a): fn.call(ctx)}boundFn._length = fn.length;return boundFn
}function nativeBind (fn, ctx) {return fn.bind(ctx)
}var bind = Function.prototype.bind? nativeBind: polyfillBind;

简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用callapply实现,据说是因为性能问题。

如果对于call、apply、bind的用法和实现不熟悉,可以查看我在面试官问系列面试官问:能否模拟实现JS的call和apply方法面试官问:能否模拟实现JS的bind方法

调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。

3.5 initData 初始化 data

initData 函数也是一些判断。主要做了如下事情:

先给 _data 赋值,以备后用。
最终获取到的 data 不是对象给出警告。
遍历 data ,其中每一项:
如果和 methods 冲突了,报警告。
如果和 props 冲突了,报警告。
不是内部私有的保留属性,做一层代理,代理到 _data 上。
最后监测 data,使之成为响应式的数据。
function initData (vm) {var data = vm.$options.data;data = vm._data = typeof data === 'function'? getData(data, vm): data || {};if (!isPlainObject(data)) {data = {};warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm);}// proxy data on instancevar keys = Object.keys(data);var props = vm.$options.props;var methods = vm.$options.methods;var i = keys.length;while (i--) {var key = keys[i];{if (methods && hasOwn(methods, key)) {warn(("Method \"" + key + "\" has already been defined as a data property."),vm);}}if (props && hasOwn(props, key)) {warn("The data property \"" + key + "\" is already declared as a prop. " +"Use prop default value instead.",vm);} else if (!isReserved(key)) {proxy(vm, "_data", key);}}// observe dataobserve(data, true /* asRootData */);
}

3.5.1 getData 获取数据

是函数时调用函数,执行获取到对象。

function getData (data, vm) {// #7573 disable dep collection when invoking data getterspushTarget();try {return data.call(vm, vm)} catch (e) {handleError(e, vm, "data()");return {}} finally {popTarget();}
}

3.5.2 proxy 代理

其实就是用 Object.defineProperty 定义对象

这里用处是:this.xxx 则是访问的 this._data.xxx

/*** Perform no operation.* Stubbing args to make Flow happy without leaving useless transpiled code* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).*/
function noop (a, b, c) {}
var sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
};function proxy (target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]};sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val;};Object.defineProperty(target, key, sharedPropertyDefinition);
}

3.5.3 Object.defineProperty 定义对象属性

Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)

Object.defineProperty 涉及到比较重要的知识点,面试也常考。

value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。

详细举例见此链接

3.6 文中出现的一些函数,最后统一解释下

3.6.1 hasOwn 是否是对象本身拥有的属性

调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。

/*** Check whether an object has the property.*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {return hasOwnProperty.call(obj, key)
}hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

3.6.2 isReserved 是否是内部私有保留的字符串$  和 _ 开头

/*** Check if a string starts with $ or _*/
function isReserved (str) {var c = (str + '').charCodeAt(0);return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false

4. 最后用60余行代码实现简化版

function noop (a, b, c) {}
var sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
};
function proxy (target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]};sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val;};Object.defineProperty(target, key, sharedPropertyDefinition);
}
function initData(vm){const data = vm._data = vm.$options.data;const keys = Object.keys(data);var i = keys.length;while (i--) {var key = keys[i];proxy(vm, '_data', key);}
}
function initMethods(vm, methods){for (var key in methods) {vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);}
}function Person(options){let vm = this;vm.$options = options;var opts = vm.$options;if(opts.data){initData(vm);}if(opts.methods){initMethods(vm, opts.methods)}
}const p = new Person({data: {name: '若川'},methods: {sayName(){console.log(this.name);}}
});console.log(p.name);
// 未实现前:undefined
// '若川'
console.log(p.sayName());
// 未实现前:Uncaught TypeError: p.sayName is not a function
// '若川'

5. 总结

本文涉及到的基础知识主要有如下:

构造函数
this 指向
call、bind、apply
Object.defineProperty
等等基础知识。

本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。

解答文章开头提问:

通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。

通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx

Vue的这种设计,好处在于便于获取。也有不方便的地方,就是propsmethodsdata三者容易产生冲突。

文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。

启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。

你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。

最后欢迎加我微信 ruochuan12源码共读 活动,大家一起学习源码,共同进步。

最近组建了一个湖南人的前端交流群,如果你是湖南人可以加我微信 ruochuan12 私信 湖南 拉你进群。


推荐阅读

1个月,200+人,一起读了4周源码
我读源码的经历

老姚浅谈:怎么学JavaScript?

我在阿里招前端,该怎么帮你(可进面试群)

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动

识别方二维码加我微信、拉你进源码共读

今日话题

略。欢迎分享、收藏、点赞、在看我的公众号文章~

为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!相关推荐

  1. php天气源码_PHP获取城市天气API接口源码

    PHP获取城市天气API接口源码,接口调用的是微鲤的接口,2020年9月1日 10:11:07测试接口还可以正常使用. API查询接口代码<?php function tian(){ $city ...

  2. 只需两步获取任何微信小程序源码

    只需两步获取任何微信小程序源码 kedwan关注 12018.09.15 09:32:43字数 2,428阅读 3,946 转载自百家号作者:不忘初心lucy 第一次在掘金这样高大上的社区写文章,忐忑 ...

  3. VC++获取磁盘剩余空间(附源码)

      VC++开发常用功能一系列文章 (欢迎订阅,持续更新...) 第23章:VC++获取磁盘剩余空间(附源码) 源代码demo已上传到百度网盘:永久生效  ,代码实现了获取任一磁盘的剩余空间,返回MB ...

  4. 获取整个网页的html源码

    #region 获取整个网页的html源码         public static string GetStringByUrl(string strUrl,string strEncode)   ...

  5. 易语言免登录获取QQ/昵称/头像/在线状态源码

    易语言免登陆获取q资料卡的源码. 例子: 取得登录q  获取状态:在线 昵称 x7st 获取本人描述等 没有调用第三方接口,纯QQpost稳定,需要的可以下载查看 下载地址:https://6yunp ...

  6. 使用Python获取数字货币价格(附完整源码)

    使用Python获取数字货币价格(附完整源码) 如果你想要及时了解到加密货币的价格变化,你可以使用Python获取数字货币的实时价格.本文将介绍如何使用Python和CoinGecko API获取数字 ...

  7. 【SeaJS】【3】seajs.data相关的源码阅读

    在SeaJS官网上推荐了源码阅读顺序,本文并没有采用这个顺序,而是按个人习惯以调试官方示例的方式进行源码阅读.早期版本作者玉伯使用了几个闭包形式,本文源码版本为2.1.1,它的编码方式个人认为更加脚本 ...

  8. java开发_mysql中获取数据库表描述_源码下载

    功能描述: 在mysql数据库中,有两张表: data_element_config , test_table 我们需要获取表:test_table表的描述信息,然后把描述信息插入到表:data_el ...

  9. 反编译获取任何微信小程序源码——看这篇就够了(最新)

    一 准备工具 1 node.js 运行环境 下载地址:https://nodejs.org/en/ 2 反编译的脚本 链接:https://pan.baidu.com/s/1InxRoozDDb-C- ...

最新文章

  1. 离职交接文档_如何写好离职工作交接文档?
  2. Kafka实现细节(下)
  3. window下lamp环境搭建
  4. Angular form学习笔记
  5. 误用.Net Redis客户端工具CSRedisCore,自己挖坑自己填
  6. c语言字母表等腰三角,【原创】CS必修课——C语言基础编程实战26“C语言输出等腰三角形”...
  7. 理想汽车市值逼近蔚来,王兴曾多次在饭否为其站台
  8. 简单介绍Javascript匿名函数和面向对象编程
  9. latex longtable caption长度提前换行解决方案
  10. 20210107WEB渗透学习之信息收集
  11. 日常使用计算机如何进行病毒防范,电脑日常生活中怎么防范电脑病毒
  12. space-between时尾行排版优化
  13. Servlet入门到入坟 一站式基础及进阶——SpringMVC没它都不行 你确定不来看看——囊括初学基础以及进阶
  14. mount挂载基础点
  15. 用Python教训盗号骗子
  16. 按键精灵调用python插件_【Python 教程】使用 Python 和大漠插件进行文字识别
  17. 利用iText.jar操作pdf文档
  18. 电商代运营是做什么的
  19. 三分钟了解阿里云产品:对象存储OSS概述
  20. 07——驾校科目一考试系统——布局题库

热门文章

  1. java 循环标记_深入浅析Java 循环中标签的作用
  2. jq js json 转字符串_JS中JSON对象和String之间的互转及处理技巧
  3. python文件读取方法read(size)的含义是_在Python中可使用read([size])来读取文件中的数据,如果参数size省略,则读取文件中的()。(4.0分)_学小易找答案...
  4. 《深入浅出nodejs》读书笔记(3)
  5. shell编辑crontab任务
  6. activemq生产者和消费者的双向通信
  7. android项目 之 记事本(6)----- 加入手写
  8. 关于Java中的随机数产生
  9. JavaScript——以简单的方式理解闭包
  10. VS 2008 生成操作中各个选项的差别