前言
源码阅读可能会迟到,但是一定不会缺席!
众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。
<html>
<head></head>
<body>
<div id="app">
{
{ message }}
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'See Vue again!'
},
})
</script>
</html>
源码cdn地址:https://cdn.jsdelivr.net/npm/vue/dist/vue.js,当下版本:v2.6.11。
本瓜选择生啃的原因是,可以更自主地选择代码段分轻重来阅读,一方面测试自己的掌握程度,一方面追求更直观的源码阅读。
当然你也可以选择在 https://github.com/vuejs/vue/tree/dev/src 分模块的阅读,也可以看各路大神的归类整理。
其实由于本次任务量并不算小,为了能坚持下来,本瓜将源码尽量按 500 行作为一个模块来形成一个 md 文件记录(分解版本共 24 篇感兴趣可移步),结合注释、自己的理解、以及附上对应查询链接来逐行细读源码,此篇为合并版本。
目的:自我梳理,分享交流。
最佳阅读方式推荐:先点赞👍再阅读📖,靴靴靴靴😁
正文
第 1 行至第 10 行
// init
(
function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());
}(
this,
function () {
'use strict';
//...核心代码...
}
)
);
// 变形
if (typeof exports === 'object' && typeof module !== 'undefined') { // 检查 CommonJS
module.exports = factory()
} else {
if (typeof define === 'function' && define.amd) { // AMD 异步模块定义 检查JavaScript依赖管理库 require.js 的存在 [link](https://stackoverflow.com/questions/30953589/what-is-typeof-define-function-defineamd-used-for)
define(factory)
} else {
(global = global || self, global.Vue = factory());
}
}
// 等价于
window.Vue=factory()
// factory 是个匿名函数,该匿名函数并没自执行 设计参数 window,并传入window对象。不污染全局变量,也不会被别的代码污染
第 11 行至第 111 行
// 工具代码
var emptyObject = Object.freeze({});// 冻结的对象无法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
// 接下来是一些封装用来判断基本类型、引用类型、类型转换的方法
-
isUndef//判断未定义
-
isDef// 判断已定义
-
isTrue// 判断为 true
-
isFalse// 判断为 false
-
isPrimitive// 判断为原始类型
-
isObject// 判断为 obj
-
toRawType // 切割引用类型得到后面的基本类型,例如:[object RegExp] 得到的就是 RegExp
-
isPlainObject// 判断纯粹的对象:“纯粹的对象”,就是通过 { }、new Object()、Object.create(null) 创建的对象
-
isRegExp// 判断原生引用类型
-
isValidArrayIndex// 检查val是否是一个有效的数组索引,其实就是验证是否是一个非无穷大的正整数
-
isPromise// 判断是否是 Promise
-
toString// 类型转成 String
-
toNumber// 类型转成 Number
第 113 行至第 354 行
- makeMap// makeMap 方法将字符串切割,放到map中,用于校验其中的某个字符串是否存在(区分大小写)于map中
e.g.
var isBuiltInTag = makeMap('slot,component', true);// 是否为内置标签
isBuiltInTag('slot'); //true
isBuiltInTag('slot1'); //undefined
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');// 是否为保留属性
-
remove// 数组移除元素方法
-
hasOwn// 判断对象是否含有某个属性
-
cached// ※高级函数 cached函数,输入参数为函数,返回值为函数。同时使用了闭包,其会将该传入的函数的运行结果缓存,创建一个cache对象用于缓存运行fn的运行结果。link
function cached(fn) {
var cache = Object.create(null);// 创建一个空对象
return (function cachedFn(str) {// 获取缓存对象str属性的值,如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
-
camelize// 驼峰化一个连字符连接的字符串
-
capitalize// 对一个字符串首字母大写
-
hyphenateRE// 用字符号连接一个驼峰的字符串
-
polyfillBind// ※高级函数 参考link
-
toArray// 将像数组的转为真数组
-
extend// 将多个属性插入目标的对象
-
toObject// 将对象数组合并为单个对象。
e.g.
console.log(toObject(["bilibli"]))
//{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ƒ}
-
no// 任何情况都返回false
-
identity // 返回自身
-
genStaticKeys// 从编译器模块生成包含静态键的字符串。TODO:demo
-
looseEqual//※高级函数 对对象的浅相等进行判断
//有赞、头条面试题
function looseEqual(a, b) {
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if(isObjectA && isObjectB) {
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
if(isArrayA && isArrayB) {
return a.length === b.length && a.every((e, i) => {
return looseEqual(e, b[i])
})
}else if(!isArrayA && !isArrayB) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
return keysA.length === keysB.length && keys.every(key => {
return looseEqual(a[key], b[key])
})
}else {
return false
}
} catch(e) {
return false
}
}else if(!isObjectA && !isObjectB) {
return String(a) === String(b)
}else {
return false
}
}
- looseIndexOf// 返回索引,如果没找到返回-1,否则执行looseEqual()
- once// 确保函数只被调用一次,用到闭包
阶段小结
- cached
- polyfillBind
- looseEqual
这三个函数要重点细品!主要的点是:闭包、类型判断,函数之间的互相调用。也即是这部分工具函数的精华!
第 356 行 至 第 612 行
// 定义常量和配置
- SSR_ATTR// 服务端渲染
- ASSET_TYPES// 全局函数 component、directive、filter
- LIFECYCLE_HOOKS// 生命周期,无需多言
- config // 全局配置 link
- unicodeRegExp//用于解析html标记、组件名称和属性pat的unicode字母
- isReserved// 检查变量的开头是 $ 或 _
- def// 在一个对象上定义一个属性的构造函数,其中 !!enumerable 强制转换 boolean
- parsePath// 解析一个简单路径 TODO:
- userAgent// 浏览器识别
- inBrowser
- _isServer//检测 vue的服务器渲染是否存在, 而且避免webpack去填充process
- isNative //这里判断 函数是否是系统函数, 比如 Function Object ExpReg window document 等等, 这些函数应该使用c/c++实现的。这样可以区分 Symbol是系统函数, 还是用户自定义了一个Symbol
- hasSymbol//这里使用了ES6的Reflect方法, 使用这个对象的目的是, 为了保证访问的是系统的原型方法, ownKeys 保证key的输出顺序, 先数组 后字符串
- _Set// 设置一个Set
第 616 行至第 706 行
//设置warn,tip等全局变量 TODO:
- warn
- tip
- generateComponentTrace// 生成组件跟踪路径(组件数规则)
- formatComponentName// 格式化组件名
第 710 行至第 763 行
Vue核心:数据监听最重要之一的 Dep
// Dep是订阅者Watcher对应的数据依赖
var Dep = function Dep () {
//每个Dep都有唯一的ID
this.id = uid++;
//subs用于存放依赖
this.subs = [];
};
//向subs数组添加依赖
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
//通知所有绑定 Watcher。调用watcher的update()
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
强烈推荐阅读:link
Dep 相当于把 Observe 监听到的信号做一个收集(collect dependencies),然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。
第 767 行至第 900 行
Vue核心:视图更新最重要的 VNode( Virtual DOM)
- VNode
- createEmptyVNode
- createTextVNode
- cloneVNode
把你的 template 模板 描述成 VNode,然后一系列操作之后通过 VNode 形成真实DOM进行挂载
更新的时候对比旧的VNode和新的VNode,只更新有变化的那一部分,提高视图更新速度。
e.g.
<div class="parent" style="height:0" href="2222">
111111
</div>
//转成Vnode
{
tag: 'div',
data: {
attrs:{href:"2222"}
staticClass: "parent",
staticStyle: {
height: "0"
}
},
children: [{
tag: undefined,
text: "111111"
}]
}
强烈推荐阅读:link
- methodsToPatch
将数组的基本操作方法拓展,实现响应式,视图更新。
因为:对于对象的修改是可以直接触发响应式的,但是对数组直接赋值,是无法触发的,但是用到这里经过改造的方法。我们可以明显的看到 ob.dep.notify() 这一核心。
阶段小结
这一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重点突破!!!
第 904 行至第 1073 行
Vue核心:数据监听最重要之一的 Observer
- 核心的核心!Observer(发布者) => Dep(订阅器) => Watcher(订阅者)
类比一个生活场景:报社将各种时下热点的新闻收集,然后制成各类报刊,发送到每家门口的邮箱里,订阅报刊人们看到了新闻,对新闻作出评论。
在这个场景里,报社发布者,新闻数据,邮箱订阅器,订阅报刊的人订阅者,对新闻评论==视图更新
- Observer//Observer的调用过程:initState()–>observe(data)–>new Observer()
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
- ※※ defineReactive 函数,定义一个响应式对象,给对象动态添加 getter 和 setter ,用于依赖收集和派发更新。
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()// 1. 为属性创建一个发布者
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get // 依赖收集
const setter = property && property.set // 派发更新
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)// 2. 获取属性值的__ob__属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()// 3. 添加 Dep
if (childOb) {
childOb.dep.depend()//4. 也为属性值添加同样的 Dep
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
第 4 步非常重要。为对象的属性添加 dep.depend(),达到监听对象(引用的值)属性的目的
重点备注
Vue对数组的处理跟对象还是有挺大的不同,length是数组的一个很重要的属性,无论数组增加元素或者删除元素(通过splice,push等方法操作)length的值必定会更新,为什么不直接操作监听length呢?而需要拦截splice,push等方法进行数组的状态更新?
原因是:在数组length属性上用defineProperty拦截的时候,会报错。
Uncaught TypeError: Cannot redefine property: length
再用Object.getOwnPropertyDescriptor(arr, ‘length’)查看一下://(Object.getOwnPropertyDescriptor用于返回defineProperty.descriptor)
{
configurable: false
enumerable: false
value: 0
writable: true
}
configurable为false,且MDN上也说重定义数组的length属性在不同浏览器上表现也是不一致的,所以还是老老实实拦截splice,push等方法,或者使用ES6的Proxy。
第 1075 行至第 1153 行
- set //在对象上设置一个属性。如果是新的属性就会触发更改通知(旧属性也会触发更新通知,因为第一个添加的时候已经监听了,之后自动触发,不再手动触发)
- del //删除一个属性,如果必要触发通知
- dependArray // 收集数组的依赖
link
第 1157 行至第 1568 行
// 配置选项合并策略
ar strats = config.optionMergeStrategies;
- mergeData
- strats.data
- mergeDataOrFn
- mergeHook
- mergeAssets
- strats.watch
- strats.computed
- defaultStrat
- checkComponents
- validateComponentName
- normalizeProps
- normalizeInject
- normalizeDirectives
- assertObjectType
- mergeOptions
这一部分代码写的就是父子组件配置项的合并策略,包括:默认的合并策略、钩子函数的合并策略、filters/props、data合并策略,且包括标准的组件名、props写法有一个统一化规范要求。
一图以蔽之
强烈推荐阅读:link
阶段小结
这一部分最重要的就是 Observer(观察者) ,这也是 Vue 核心中的核心!其次是 mergeOptions(组件配置项的合并策略),但是通常在用的过程中,就已经了解到了大部分的策略规则。
第 1570 行至第 1754 行
- resolveAsset// resolveAsset 全局注册组件用到
e.g.
我们的调用 resolveAsset(context. o p t i o n s , ′ c o m p o n e n t s ′ , t a g ) , 即 拿 v m . options, 'components', tag),即拿 vm. options,′components′,tag),即拿