Vue 阅读笔记
看一下 vue 的源码,顺便做个笔记
- 作为模块加载
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
}(this, function () { 'use strict';
...
}));
- set 方法 用于为对象添加属性
function set(obj, key, val) {
// 如已有该属性,覆盖 [hasOwn](#)
if (hasOwn(obj, key)) {
obj[key] = val;
return;
}
// 如对象为 vueModel,设置 _data 中的值
if (obj._isVue) {
set(obj._data, key, val);
return;
}
var ob = obj.__ob__;
// 如果不是 vueModel 则简单设置值
if (!ob) {
obj[key] = val;
return;
}
// 如果是 vueModel 须要额外处理工作
// 将属性设置为可响应的 [convert](#)
ob.convert(key, val);
// 通知依赖该数据的对象 [notify](#)
ob.dep.notify();
// 如果有上级的 vueModel,更新键值和 watcher
if (ob.vms) {
var i = ob.vms.length;
while (i--) {
var vm = ob.vms[i];
vm._proxy(key);
vm._digest();
}
}
return val;
}
- del 方法 为对象删除属性,如果有必要,触发 change 事件
function del(obj, key) {
// 检查是否已有该属性 [hasOwn](#)
if (!hasOwn(obj, key)) {
return;
}
delete obj[key];
var ob = obj.__ob__;
if (!ob) {
if (obj._isVue) {
delete obj._data[key];
// 更新所有 watcher [_digest](#)
obj._digest();
}
return;
}
// 通知依赖此属性的对象 [notify](#)
ob.dep.notify();
// 如果有上级的 vueModel,更新键值和 watcher
if (ob.vms) {
var i = ob.vms.length;
while (i--) {
var vm = ob.vms[i];
vm._unproxy(key);
vm._digest();
}
}
}
- 字符串检查 检查一个字符串是否能被转为字面量
// 开头空格+(布尔值|正负数|字符串)结尾空格
var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
function isLiteral(exp) {
return literalValueRE.test(exp);
}
检查是否为保留字,即是否以 $ 或 _ 开头
- 类型转换
对特殊情况做了处理
- _toString 转为字符串,确保空值输出为空字符串
- toNumber 转为数字,无法转为数字时返回原值
- toBoolean 转为布尔值,’false’ 被认为是假值
- 字符串格式化
- stripQuotes 去除字符串首尾引号
- camelize 将用 - 拼接的字符串转为驼峰式字符串
- hyphenate 将驼峰式的字符串转为用连字符连接
- classify 将用 - 或 _ 或 / 连接的字符串转为驼峰式的类名
- 辅助函数
- bind 代替原生的 bind 方法
- toArray 将类数组对象转为真正的数组
- extend 类似于 jQuery 的 extend 不过不提供深层递归功能
- isObject 判断是否为对象(排除 null),主要用于判断是否能被 json 编码
- isPlainObject 判断是否为简单的对象即 toString 返回 [object Object]
- def 为对象设置带描述符的属性
- _debounce 防抖函数,防止函数被过于频繁地触发
- indexOf 在数组中查询对象的索引值
- cancellable 将一个函数包装成一个可取消的版本,主要用于回调函数
function cancellable(fn) {
var cb = function cb() {
if (!cb.cancelled) {
return fn.apply(this, arguments);
}
};
// 可以通过调用 cancel 方法来静默原函数
cb.cancel = function () {
cb.cancelled = true;
};
return cb;
}
- 辅助函数
- looseEqual 检查两个参数是否相等,如果均为对象且 json 编码后相同也视为相等
- hasProto 检查是否有 proto 属性
- isBrowser 检查运行环境是否在浏览器中
- 浏览器检查
- isIE
- isIE9
- isAndroid
- isIos
- iosVersion
- 浏览器兼容
- hasMutationObserverBug 检查 iOS 版本是否高于 9.3
- 根据浏览器设置 transition 和 animation 的前缀和事件名
- 辅助函数
- nextTick 设置一个异步任务,通过 nextTick(callbackfun, context) 使用
var nextTick = (function () {
var callbacks = [];
var pending = false;
var timerFunc;
function nextTickHandler() {
pending = false;
var copies = callbacks.slice(0);
callbacks = [];
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
/* istanbul ignore if */
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
// 如果可以使用 MutationObserver 的话使用一个不断改变内容的文字节点来触发异步事件
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(counter);
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = counter;
};
} else {
// webpack attempts to inject a shim for setImmediate
// if it is used as a global, so we have to work around that to
// avoid bundling unnecessary code.
// 无法使用 MutationObserver 的时候使用 setTimeout
var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
timerFunc = context.setImmediate || setTimeout;
}
return function (cb, ctx) {
var func = ctx ? function () {
cb.call(ctx);
} : cb;
callbacks.push(func);
if (pending) return;
pending = true;
timerFunc(nextTickHandler, 0);
};
})();
- 数据类型
- _Set 如果没有原生的集合 Set,则提供一个实现
- Cache 缓存队列 可以通过 limit 设置队列最大长度
- put 在队列尾部加入元素,如果队列已满,先调用一次 shift
- shift 在队列首部移除元素
- get 根据键名,在队列任意位置提取一个元素,并插到队列末尾
- 命令解析
- pushFilter 将一个 filter 字符串解析成过滤器名和参数,推入数组
- processFilterArg 解析 filter 参数
function processFilterArg(arg) {
if (reservedArgRE.test(arg)) {
// 经检查为保留字 /^in$|^-?\d+/, in 或者正负数
return {
value: toNumber(arg),
dynamic: false
};
} else {
// 检查是否被引号包裹
var stripped = stripQuotes(arg);
var dynamic = stripped === arg;
// 如果被引号包裹则认为是字符串,否则为变量
return {
value: dynamic ? arg : stripped,
dynamic: dynamic
};
}
}
- 命令解析
- parseDirective 解析指令
// 解析形如 "a + 1 | uppercase" 这样的字符串
// 返回一个对象
// {
// expression: 'a + 1',
// filters: [{name: 'uppercase', args: null}]
// }
function parseDirective(s) {
// 检查是否解析过
var hit = cache$1.get(s);
if (hit) {
// 如已经解析过,直接使用缓存队列中的结果
return hit;
}
// reset parser state
str = s;
inSingle = inDouble = false;
curly = square = paren = 0;
lastFilterIndex = 0;
dir = {};
for (i = 0, l = str.length; i < l; i++) {
// 前一个字符
prev = c;
// 当前字符
c = str.charCodeAt(i);
if (inSingle) {
// check single quote
// 0x27 ' 0x5C \
if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
} else if (inDouble) {
// check double quote
// 0x27 " 0x5C \
if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
} else if (c === 0x7C && // pipe
// 0x7C |
str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
if (dir.expression == null) {
// first filter, end of expression
lastFilterIndex = i + 1;
// 分离出表达式部分
dir.expression = str.slice(0, i).trim();
} else {
// already has filter
// 解析处理 filter 部分
pushFilter();
}
} else {
switch (c) {
case 0x22:
inDouble = true;break; // "
case 0x27:
inSingle = true;break; // '
case 0x28:
paren++;break; // (
case 0x29:
paren--;break; // )
case 0x5B:
square++;break; // [
case 0x5D:
square--;break; // ]
case 0x7B:
curly++;break; // {
case 0x7D:
curly--;break; // }
}
}
}
// 收尾
if (dir.expression == null) {
dir.expression = str.slice(0, i).trim();
} else if (lastFilterIndex !== 0) {
pushFilter();
}
cache$1.put(s, dir);
return dir;
}
- 命令解析
- 正则字符串处理
- escapeRegex 转义字符串中的特殊字符,生成一个能用于构造正则的字符串
- compileRegex 生成识别边界界定符的正则 (默认是 })
- parseText 字符串解析,将字符串中的 token 提取出来 (双括号或三括号包裹的内容) 如输入 ‘a c’ 输出 [ {value: ‘a ‘}, {tag: true, value: ‘b’, html: false, oneTime: false}, {value: ‘ c’} ]
- 正则字符串处理
function parseText(text) {
if (!cache) {
compileRegex();
}
var hit = cache.get(text);
// 如有缓存使用缓存
if (hit) {
return hit;
}
if (!tagRE.test(text)) {
// 如果没有识别到边界符,直接返回
return null;
}
var tokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, html, value, first, oneTime;
/* eslint-disable no-cond-assign */
while (match = tagRE.exec(text)) {
/* eslint-enable no-cond-assign */
index = match.index;
if (index > lastIndex) {
// 不在双括号或三括号中的内容
tokens.push({
value: text.slice(lastIndex, index)
});
}
// tag token
// 是否有三大括号内容
html = htmlRE.test(match[0]);
// 获取括号中间的内容
value = html ? match[1] : match[2];
first = value.charCodeAt(0);
oneTime = first === 42; // *
value = oneTime ? value.slice(1) : value;
tokens.push({
tag: true,
value: value.trim(),
html: html,
oneTime: oneTime
});
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push({
value: text.slice(lastIndex)
});
}
cache.put(text, tokens);
return tokens;
}
- 命令解析
- tokensToExp 将多个 token 组合到表达式字符串中 如输入 ‘a c’ 输出 ‘“a “ + b + “ c”’
- formatToken 处理单个 token 如果不是 tag (即双括号/三括号包裹内容),则直接返回,如果是,进行求值操作
- inlineFilters 行内 filter 解析
var filterRE = /[^|]\|[^|]/;
function inlineFilters(exp, single) {
if (!filterRE.test(exp)) {
// 如果没有 filter,直接返回
return single ? exp : '(' + exp + ')';
} else {
// 有 filter 的话对字符串进行解析
var dir = parseDirective(exp);
if (!dir.filters) {
return '(' + exp + ')';
} else {
return 'this._applyFilters(' + dir.expression + // value
',null,' + // oldValue (null for read)
JSON.stringify(dir.filters) + // filter descriptors
',false)'; // write?
}
}
}
-
config 全局配置 定义了 config 变量,保存配置信息,包括各种开关(debug, warning …),常量,分隔符等 设置分隔符会让命令解析的缓存失效
-
warning 根据 config 配置生成 warn 函数
- transition 带过渡动画操作
- appendWithTransition 添加元素
- beforeWithTransition 在元素之前插入
- removeWithTransition 删除元素
- applyTransition 过渡动画实现
- 节点操作相关
- query 查询一个元素
- inDoc 检查一个节点是否在文档中(nodetype 须为 1)
- getAttr 获取一个节点的一个属性,并将属性从节点上移除
- getBindAttr 获取一个 v-bind 属性
- hasBindAttr 检查一个节点是否有绑定属性
- before 在指定节点前插入节点
- after 在指定节点后插入节点
- remove 移除节点
- prepend 在节点最前面插入子节点
- replace 替换节点
- on 在节点上绑定事件,实际上是调用 addEventListener
- off 移除事件绑定
- getClass 获取 css 类,包含对 IE9 的兼容
- setClass 设置 css 类,包含对 IE9 的兼容
- addClass 添加 css 类
- removeClass 移除 css 类
- extractContent 提取节点中的内容,生成 fragment 或 element
- trimNode 去除首尾空格,注释
- isTrimable 判断节点是否须要删除
- isTemplate 检查节点是否为 template 标签
- crateAnchor 创建锚点,主要用于节点的插入和删除
- findRef 获取一个节点的 v-ref 属性,返回其驼峰形式
- mapNodeRange 在一系列节点上 map 函数,参数为起始节点,结束节点和操作函数 这一组节点须为同一个节点的子节点并且是连续的
- removeNodeRange 删除一系列节点,可以捕获被删除的节点,删除完成后可触发回调
- isFragment 检查是否为 fragment,即 nodeType 为 11
- getOuterHTML 获取 outerHtml 属性
- isUnknownElement 检查是否为未知的标签元素
- 组件识别
- checkComponentAttr 检查一个元素是否为 vue 组件,如果是的话返回组件 id
function checkComponentAttr(el, options) {
// 获取标签名
var tag = el.tagName.toLowerCase();
// 检查标签是否有属性
var hasAttrs = el.hasAttributes();
if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
// 如果不是原生标签也不是 vue 标签(slot|partial|component...)
if (resolveAsset(options, 'components', tag)) {
return { id: tag };
} else {
// 获取 is 属性(is 用于在 tr/option 之类的标签上,表示其为组件)
var is = hasAttrs && getIsBinding(el, options);
if (is) {
return is;
} else if ('development' !== 'production') {
// 发出错误信息
var expectedTag = options._componentNameMap && options._componentNameMap[tag];
if (expectedTag) {
warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
} else if (isUnknownElement(el, tag)) {
warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
}
}
}
} else if (hasAttrs) {
return getIsBinding(el, options);
}
}
- 组件识别
- getIsBinding 获取 is 绑定,返回值为 {id: 组件id, dynamic: 是否为动态绑定}
- strats 配置重写策略
- mergeData 辅助 merge 函数,合并策略为补充,不重写。递归合并
- strats.data data 的合并策略,对于组件的 data,检查其是否为函数,如果是函数, 返回一个函数,返回值为将父 data 调用结果 mergeData 合并到子 data 调用结果中
- strats.el 合并策略是优先使用子值,如果没有再使用父值,不进行合并
- strats.init strats.created strats.ready … 等钩子函数,合并策略为多个函数拼接, 得到一个函数数组
- mergeAssets 合并 assets 属性(component/directive/elementDirective/filter/transition/partial) 合并策略是先将 assets 数组转为键值对的形式,然后用 extend 合并, 写入复数形式属性(components/directives/…)中
- strats.watch strats.events 像钩子函数一样,也是作为数组拼接
- strats.props strats.methods strats.computed 这些的策略是子属性覆盖父属性
- defaultStrat 默认策略,当第二个参数存在是使用第二个参数否则使用第一个
- 组件配置
- guardComponents 确保组件的选项正确生效
function guardComponents(options) {
if (options.components) {
// 将 components 转为 key-value 形式
var components = options.components = guardArrayAssets(options.components);
// 获取组件 id
var ids = Object.keys(components);
var def;
if ('development' !== 'production') {
var map = options._componentNameMap = {};
}
for (var i = 0, l = ids.length; i < l; i++) {
var key = ids[i];
if (commonTagRE.test(key) || reservedTagRE.test(key)) {
// 检查命名是否非法
'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
continue;
}
// record a all lowercase <-> kebab-case mapping for
// possible custom element case error warning
if ('development' !== 'production') {
map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
}
def = components[key];
if (isPlainObject(def)) {
// 注册子组件
components[key] = Vue.extend(def);
}
}
}
}
- 组件配置
- guardProps 将所有 props 转为 key-value 形式
- guardArrayAssets 将数组形式的 assets 转为 key-value 形式,key 为 name 或者 id
- mergeOptions 合并两个 option
function mergeOptions(parent, child, vm) {
// 对子选项进行预处理
guardComponents(child);
guardProps(child);
if ('development' !== 'production') {
if (child.propsData && !vm) {
warn('propsData can only be used as an instantiation option.');
}
}
var options = {};
var key;
// 如果有用 extends 声明扩展另一个组件
if (child['extends']) {
parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
}
// 如果使用 mixins 其他的配置项
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
var mixin = child.mixins[i];
var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
parent = mergeOptions(parent, mixinOptions, vm);
}
}
// 使用合适的合并策略合并各个配置项
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
- 组件配置
- resolveAsset 获取资源
function resolveAsset(options, type, id, warnMissing) {
/* istanbul ignore if */
if (typeof id !== 'string') {
return;
}
var assets = options[type];
var camelizedId;
// 尝试多种格式
var res = assets[id] ||
// camelCase ID
assets[camelizedId = camelize(id)] ||
// Pascal Case ID
assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
if ('development' !== 'production' && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
}
return res;
}
- 依赖处理
- Dep 依赖对象
- 构造函数,递增获得一个 id,维持一个 subs 数组
- target 当前的 watcher
- prototype.addSub 添加一个订阅
- prototype.removeSub 删除一个订阅
- prototype.depend 将自己添加到 target 中
- prototype.notify 通知所有订阅自己的对象进行更新
- 在原生的数组变异方法上挂载钩子
- Dep 依赖对象
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length;
var args = new Array(i);
while (i--) {
args[i] = arguments[i];
}
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
inserted = args;
break;
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 如果新插入了值,将其加入监控
if (inserted) ob.observeArray(inserted);
// 通知依赖对象进行更新
ob.dep.notify();
return result;
});
});
- 依赖处理
- 添加数组原型方法
- $set 通过索引设置数组某一项的值,其实调用的 splice
- $remove 从数组中移除一项,其实调用的 splice
- shouldConvert 监控配置
- 添加数组原型方法
// 配置开关,是否进行监听
var shouldConvert = true;
function withoutConversion(fn) {
shouldConvert = false;
fn();
shouldConvert = true;
}
- 依赖处理
- Observer 对象
- 构造函数
- Observer 对象
// 附加到每一个须要被监听的对象上,将其属性转为 getter 和 setter
// 通过 getter 和 setter 实现依赖和更新的处理
function Observer(value) {
this.value = value;
this.dep = new Dep();
// 添加 __ob__ 属性
def(value, '__ob__', this);
if (isArray(value)) {
var augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
// 设置 getter 和 setter
this.walk(value);
}
}
- prototype.walk 将对象的属性转为 getter 和 setter
- prototype.observeArray 监控数组值
- prototype.convert 将对象的一个属性转为响应数据
- prototype.addVm 添加拥有此数据的 vue 实例
- prototype.removeVm 删除所有者 vue 实例
- 辅助函数
- protoAugment 给一个对象添加 __proto__ 属性
- copyAugment 复制函数,从一个对象复制属性到另一个对象,并设为响应式数据
- observe 为一个值添加监控对象,如果已被监控直接返回,如果配置开关关闭返回 undefinded
- defineReactive 为一个对象的某个属性设置 getter 和 setter 使其成为响应式的数据
function defineReactive(obj, key, val) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
if (newVal === value) {
return;
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = observe(newVal);
dep.notify();
}
});
}
- util 辅助对象
- initMixin 初始化
- 设置 Vue.prototype._init vue 实例初始化函数,每个实例都会调用,包括使用 extend 创建的实例
- path 解析
- getPathCharType 检查字符类型,分为几类: [, ], ., ‘, “, ident, ws, number, else
- formatSubPath
-
util 对象
-
Vue 初始化方法 initMixin -. 将 options 写入实例属性,设定唯一 uid -. 绑定事件 -. fragment 存储 -. context, scope, frag 的设定,确定组件的层级关系
-
表达式编译状态机,提供编译过程中各种状态
-
parse 函数,用于将字符串编译成片段数组,而且提供了缓存机制
-
expression 对象,如果不是简单的变量,赋予生成 getter 和 setter 的能力
-
Watcher 对象,负责触发 watcher 函数。
-
模板解析,将字符或节点串转成 DocumentFragment -. Fragment 对象,拥有自己的 scop,有控制自己子节点的方法,联系 vue 实例的方法
-
内置命令 v-for -. 参数列表 -. bind 方法 -. diff 方法,用于减少列表更新时的重绘。 -. create 方法 -. updateRef 方法 -. updateModel 方法 -. 针对 fragemnt 的创建、删除、移动、缓存等操作 未完待续