当前篇:vue源码分析【1】-new Vue之前
以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="./../../oldVue.js">script>
head>
<body>
<div id="app">
<h2>开始存钱h2>
<div>每月存 :¥{{ money }}div>
<div>存:{{ num }}个月div>
<div>总共存款: ¥{{ total }}div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点button>
div>
<script>
debugger;
var app = new Vue({
el: '#app',
beforeCreate() { },
created() { },
beforeMount() { },
mounted: () => { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { },
data: function () {
return {
money: 100,
num: 12,
arryList: [{name:'子树'}]
}
},
computed: {
total() {
return this.money * this.num;
}
},
methods: {
getMoreMoney() {
this.money = this.money * 2
this.arryList.unshift({name: '大树'})
}
}
})
script>
body>
html>
此篇文章旨在说明new Vue之前
做了哪些操作
,给Vue构造函数挂载了哪些方法和属性,由于没有进入new Vue,所以不会执行挂载函数
,而仅仅是挂载操作。另外,在new Vue之前的代码,意味着此时都不会
拿到html
中new Vue的入参el,beforeCreate
等等。
因此,此文章最大的目的是作为后续文章的参考,注意,由于挂载函数不执行,所以我们在讲解挂载函数时会省略一些代码,详细代码将在后续文章中逐步讲解。
本文的结构依据点,线,面
来展开。
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
// 定义 Vue.prototype._init 方法
initMixin(Vue)
/**
* 定义:
* Vue.prototype.$data
* Vue.prototype.$props
* Vue.prototype.$set
* Vue.prototype.$delete
* Vue.prototype.$watch
*/
stateMixin(Vue)
/**
* 定义 事件相关的 方法:
* Vue.prototype.$on
* Vue.prototype.$once
* Vue.prototype.$off
* Vue.prototype.$emit
*/
eventsMixin(Vue)
/**
* 定义:
* Vue.prototype._update
* Vue.prototype.$forceUpdate
* Vue.prototype.$destroy
*/
lifecycleMixin(Vue)
/**
* 执行 installRenderHelpers,在 Vue.prototype 对象上安装运行时便利程序
*
* 定义:
* Vue.prototype.$nextTick
* Vue.prototype._render
*/
renderMixin(Vue)
作用:
初始化vue,挂载_init方法。
在Vue原型上挂载一个_init方法,这一步只是挂载方法,并没有执行
源码:
//初始化vue
initMixin(Vue);
var uid$3 = 0;
function initMixin(Vue) {
Vue.prototype._init = function (options) { //初始化函数
...
}
console.log(Vue.prototype)
执行完initMixin,我们看下此时Vue的原型,ok,符合预期,就只挂载了一个_init方法:
{
_init: ƒ (options),
constructor: ƒ Vue(options), // 此项是默认的
__proto__: Object // 此是默认的
}
作用:
数据绑定,挂载 d a t a , data, data,props, s e t , set, set,delete,$watch方法
流程:
get
方法,分别返回this._data和 this._propsset
方法,当改变数据时发出警告。
不能直接替换实例根$data
。不能更改props的值
。Object.defineProperty
给Vue原型挂载对应data和props的get和set方法。源码:
stateMixin(Vue);
function stateMixin(Vue) {
debugger
//直接声明的定义对象生成的流在某种程度上有问题
//在使用Object.defineProperty时,我们必须循序渐进地进行构建
// 定义一个data对象
var dataDef = {};
//重新定义get 和set方法
dataDef.get = function () {
return this._data //获取data中的数据
};
// 定义一个props对象
var propsDef = {};
propsDef.get = function () {
return this._props// 获取props 数据
};
{
dataDef.set = function (newData) {
// 修改数据时,避免直接替换实例根$data。
/**
* 以下直接给根$data赋值会警告:
* const list={
name:"子树"
}
this.$data = list;
正确的写法:
const list={
name:"子树"
}
Object.assign(this.$data,list);
*
*/
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
);
};
propsDef.set = function () {
/**
* props 是只读数据,不能更改。
* 这里也属于Vue单向数据流的范畴,如父组件能通过props改变子组件,
* 但子组件改变传入的props就会警告,只允许单向流动
*/
warn("$props is readonly.", this);
};
}
// 数据响应式的关键方法
// 【说明1】
Object.defineProperty(Vue.prototype, '$data', dataDef);
Object.defineProperty(Vue.prototype, '$props', propsDef);
/** 【说明2】
* 确保设置值能更新视图
* 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
* 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
*/
Vue.prototype.$set = set;
/**
* 确保删除能更新视图
* 通过 Vue.delete 或者 vm.$delete 删除 target 对象的指定 key
* 数组通过 splice 方法实现,对象则通过 delete 运算符删除指定 key,并执行依赖通知
*/
Vue.prototype.$delete = del;
/**
* 挂载$watcher,返回 unwatch函数:
创建 watcher 实例,如果设置了 immediate,则立即执行一次 cb
* 返回 unwatch
* unwatch 函数: 用于取消 watch 监听
*/
Vue.prototype.$watch = function (
expOrFn, //用户手动监听
cb, // 监听 变化之后 回调函数
options //参数
) {
var vm = this;
if (isPlainObject(cb)) { //判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
// 转义handler 并且为数据 创建 Watcher 观察者
return createWatcher(
vm,
expOrFn,
cb,
options
)
}
options = options || {};
options.user = true; //用户手动监听, 就是在 options 自定义的 watch
//实例化Watcher 观察者
var watcher = new Watcher(
vm, //vm vode
expOrFn, //函数 手动
cb, //回调函数
options //参数
);
if (options.immediate) {
//回调触发函数
cb.call(vm, watcher.value);
}
return function unwatchFn() { //卸载观察者,解除监听
//从所有依赖项的订阅方列表中删除self。
watcher.teardown();
}
};
}
Object.defineProperty(Vue.prototype, ‘$data’, dataDef)的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。通过get进行依赖收集,而每个set方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
Object.defineProperty(obj, prop, desc)
同样,执行完这一行代码,我们再次打印Vue.prototype,如下:
{
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
$data: undefined, // 本次加上的
get $data: ƒ (), // 本次加上的
set $data: ƒ (newData) // 本次加上的
}
这个set方法如下:
function set(target, key, val) {...}
它做了如下动作:
vue开发时经常会遇到当vue实例已经创建好了,有时候需要再次给数据赋值时,并不能在视图中改变。
比如:
<template>
<div>
测试
{{ list }}
{{ obj }}
<Button @click="change">改变数组</Button>
<Button @click="addValue">改变对象</Button>
</div>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3],
obj: { name: "子树" },
};
},
methods: {
change() {
this.list[1] = 666; // 通过下标直接改变数组
console.log(this.list);
},
addValue() {
this.obj.value = true; // 给对象添加一个属性
console.log(this.obj);
},
},
};
</script>
显示如图:
我们会发现,控制台打印的数据已经更新了,但是,视图并没有同步更新。那么,我们怎么让视图同步更新呢?
通过Vue.set(target, key, val)来改变:
methods: {
change() {
// this.list[1] = 666;
Vue.set(this.list, 1, 666)
console.log(this.list);
},
addValue() {
// this.obj.value = true;
Vue.set(this.obj, 'value', true)
console.log(this.obj);
},
},
此时,我们再回到页面,可以看到控制台和页面都同步更新了
stateMixin(Vue)执行完后,我们再打印Vue.prototype结果如下:
{
$delete: ƒ del(target, key), //本次加上的
$set: ƒ (target, key, val), //本次加上的
$watch: ƒ ( expOrFn), //本次加上的
$data: undefined, //本次加上的
$props: undefined, //本次加上的
get $data: ƒ (), //本次加上的
set $data: ƒ (newData), //本次加上的
get $props: ƒ (), //本次加上的
set $props: ƒ (), //本次加上的
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
}
作用:
初始化事件绑定方法
流程:
$on,$once,$off,$emit
事件相关的属性。源码:
// 以下各个挂载事件将在各个小节讲解
function eventsMixin(Vue) {
var hookRE = /^hook:/; //开头是hook: 的字符串
/**
* 监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm
*/
Vue.prototype.$on = function (){...}
/**
* 监听Vue自定义事件,返回Vm。
* once比较特殊,只触发一次,然后就会移除监听器
*/
Vue.prototype.$once = function (event, fn) {...}
/**
* vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件
*/
Vue.prototype.$off = function (event, fn) {...}
/**
* 触发事件
*/
Vue.prototype.$emit = function (event) {...}
}
作用:
监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm
流程:
_events
数组中,比如传入的event
为click
,fn
为addNum
,vm._events['click']
原本是[]
,则结果如下:vm._events = { click: [addNum] },然后返回vm
源码:
Vue.prototype.$on = function (
event, // 单个的事件名称或者有多个事件名组成的数组
fn // 回调
) {
var this$1 = this;
var vm = this;
// 【逻辑 1】 event为多个事件组成的数组
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
// event为事件数组,则遍历,递归调用$on
this$1.$on(event[i], fn);
}
}
// 【逻辑 2】 event为单个事件名
else {
/**
* 把所有事件拆分存放到_events 数组中
* vm._events本身是个对象,
* 比如传入的event为click,fn为addNum,vm._events['click']原本是[],则结果如下:
* vm._events = { click: [addNum] }
*/
(vm._events[event] || (vm._events[event] = [])).push(fn);
/**
* 如果事件名是以 hook: 开头的(如hook:mount='getNum'),
* 标记为vue系统内置钩子函数, 比如vue 生命周期函数等
*/
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
作用:
监听Vue自定义事件,先把传入的事件在vm上解绑,然后执行传入的事件。
虽然最终也触发$on,但once比较特殊,只触发一次,然后就会移除监听器
流程:
vm.$on(event, on);
$on
的逻辑了,也就是说,$once
最终还是会走到$on
源码:
Vue.prototype.$once = function (event, fn) {
var vm = this;
function on() {
// 解绑事件
vm.$off(event, on);
// 执行回调事件
fn.apply(vm, arguments);
}
// 将传入的回调函数fn挂载到我们定义on函数的fn上。
on.fn = fn;
// 再把on添加到vm上
vm.$on(event, on);
return vm
};
作用:
vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件。
返回删除了_events属性中相应事件的vm。
流程:
源码:
Vue.prototype.$off = function (event, fn) {
var this$1 = this;
var vm = this;
// 【逻辑 1】 如果没有参数的情况下,移除所有监听器
if (!arguments.length) {
// 赋值一个没有原型的空对象
vm._events = Object.create(null);
return vm
}
// 【逻辑 2】 如果传入的event是数组事件 则循环回调递归Vue.$off
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this$1.$off(event[i], fn);
}
return vm
}
// 【逻辑 3】,vm._events中,不存在传入的事件名
var cbs = vm._events[event];
if (!cbs) {
return vm
}
// 【逻辑 4】,如果回调函数不存在则清空 _events对应的事件名中所有事件
if (!fn) {
vm._events[event] = null;
return vm
}
// 【逻辑 5】在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数
if (fn) {
var cb;
var i$1 = cbs.length;
while (i$1--) {
// 拿到vm._events中event事件名下每个事件
cb = cbs[i$1];
// cb.fn为了兼容$once中on.fn = fn;
if (cb === fn || cb.fn === fn) {
cbs.splice(i$1, 1);
break
}
}
}
return vm
};
作用:
触发事件
流程:
源码:
Vue.prototype.$emit = function (event) {
var vm = this;
{
var lowerCaseEvent = event.toLowerCase(); //转成小写 例:onClick=》onclick
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
// 注意 html属性 是大小写敏感的, 你不能利用 v-on 来监听驼峰写法的事件, 在模板中
// 你应该使用 get-num 来替代 getNum
// 实际使用中这两者写法没有区别的,这个提示有点疑惑
tip(
"Event \"" + lowerCaseEvent + "\" is emitted in component " +
(formatComponentName(vm)) + " but the handler is registered for \"" +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"" + (hyphenate(event)) + "\" instead of \""
);
}
}
//获取传入的事件名的值
var cbs = vm._events[event];
if (cbs) {
//如果长度大于1 将它变成一个真正的数组
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
//将参数变成一个真正数组
var args = toArray(arguments, 1);
//循环事件
for (var i = 0, l = cbs.length; i < l; i++) {
try {
//执行触发事件
cbs[i].apply(vm, args);
} catch (e) {
//如果发生错误则发出报错警告
handleError(e, vm, ("event handler for \"" + event + "\""));
}
}
}
return vm
};
}
(Vue)执行完后,我们再打印Vue.prototype结果如下:
{
$delete: ƒ del(target, key),
$set: ƒ (target, key, val),
$watch: ƒ ( expOrFn),
$data: undefined,
$props: undefined,
get $data: ƒ (),
set $data: ƒ (newData),
get $props: ƒ (),
set $props: ƒ (),
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
$emit: ƒ (event) // 本次加上的
$off: ƒ (event, fn) // 本次加上的
$on: ƒ ( event, fn) // 本次加上的
$once: ƒ (event, fn) // 本次加上的
}
作用:
挂载初始化(_update
), 更新 ($forceUpdate
),销毁($destroy
) 函数
流程:
_update,$forceUpdate,$destroy
,方法属性源码:
// 以下各个挂载事件将在各个小节讲解
function lifecycleMixin(Vue) {
/**
* 更新数据函数
*/
Vue.prototype._update = function (vnode, hydrating) { ... }
/**
* 更新数据 观察者数据
*/
Vue.prototype.$forceUpdate = function () { ... }
/**
* 销毁组建周期函数
*/
Vue.prototype.$destroy = function () { ... }
}
作用:
更新视图,更新数据函数,包括首次渲染和后续更新, 也是 patch 的入口
流程:
源码:
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
// 是否 触发过 钩子Mounted
// Todo
if (vm._isMounted) {
//触发更新数据 触发生命周期函数
callHook(vm, 'beforeUpdate');
}
// 标志上一个 el 节点
var prevEl = vm.$el;
// 标志上一个 vonde
var prevVnode = vm._vnode;
// 活动实例
var prevActiveInstance = activeInstance;
activeInstance = vm;
//标志上一个 vonde
vm._vnode = vnode;
// 【逻辑 1】 执行初始化
//如果不存在表示上一次没有创建过vnode,即当前是初始化,第一次进来
if (!prevVnode) {
// 这里通过patch函数,是创建真实dom
// 注意,patch在vue中非常核心,我们将在后面用单独文章来解析
// Todo
vm.$el = vm.__patch__(
vm.$el, //真正的dom
vnode, //vnode
hydrating, // 空
false /* removeOnly */,
vm.$options._parentElm, //父节点 空
vm.$options._refElm //当前节点 空
);
// 初始补丁之后不需要ref节点,这可以防止在内存中保留分离的DOM树
vm.$options._parentElm = vm.$options._refElm = null;
}
// 【逻辑 2】 执行数据更新
else {
// 如果这个prevVnode存在,表示vno的已经创建过,只是更新数据而已
// 比较新旧节点,生成新的dom
vm.$el = vm.__patch__(prevVnode, vnode);
}
// 更新全局的activeInstance原来的旧值
activeInstance = prevActiveInstance;
// 更新vue参考
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) { // 更新真实dom上对虚拟dom的指向
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
作用:
vue强制更新,会更新视图和数据,触发updated生命周期
使用场景
:因为数据层次太多,render函数没有自动更新,需手动强制刷新。
使用语法
:直接调用 this.$forceUpdate()
源码:
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
作用:
销毁实例,断开连接,卸载全部指令和事件监听器。
流程:
源码:
Vue.prototype.$destroy = function () {
var vm = this;
// 如果是已经销毁过退出
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy');
// 进入这个函数,执行销毁事件,需要更新销毁标记
vm._isBeingDestroyed = true;
var parent = vm.$parent;
// 从父节点的$children移除自身
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
// 卸载依赖监听
if (vm._watcher) {
vm._watcher.teardown();
}
// 卸载依赖队列的监听
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
//从数据ob中删除引用
// 被冻结的对象可能没有观察者。
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--;
}
// 调用最后一个钩子
vm._isDestroyed = true;
// 调用 __patch__,销毁节点
vm.__patch__(vm._vnode, null);
// 销毁实例
callHook(vm, 'destroyed');
// 关闭事件监听器
vm.$off();
// 删除vue 参数
if (vm.$el) {
vm.$el.__vue__ = null;
}
// 释放循环引用 销毁父节点
if (vm.$vnode) {
vm.$vnode.parent = null;
}
};
此时Vue.prototype为:
{
$delete: ƒ del(target, key),
$set: ƒ (target, key, val),
$watch: ƒ ( expOrFn),
$data: undefined,
$props: undefined,
get $data: ƒ (),
set $data: ƒ (newData),
get $props: ƒ (),
set $props: ƒ (),
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
$emit: ƒ (event),
$off: ƒ (event, fn),
$on: ƒ ( event, fn),
$once: ƒ (event, fn),
$destroy: ƒ (), // 本次加上去的
$forceUpdate: ƒ (), // 本次加上去的
_update: ƒ (), // 本次加上去的
}
作用:
挂载_render,生成 VNode
挂载_$nextTick函数,将回调延迟到下次 DOM 更新循环之后执行
流程:
_render,_$nextTick
,方法属性源码:
// 以下各个挂载事件将在各个小节讲解
function renderMixin(Vue) {
// 在实例上安装渲染工具函数
installRenderHelpers(Vue.prototype);
Vue.prototype.$nextTick = function (fn) { ... }
Vue.prototype._render = function () { ... }
}
作用:
在实例上安装渲染工具函数
流程:
将一系列工具函数,通过简写属性key挂载到实例上。例如:target._o = markOnce
源码:
function installRenderHelpers(target) {
target._o = markOnce; //实际上,这意味着使用唯一键将节点标记为静态。* 标志 v-once. 指令
target._n = toNumber; //字符串转数字,如果失败则返回字符串
target._s = toString; // 将对象或者其他基本数据 变成一个 字符串
target._l = renderList; //根据value 判断是数字,数组,对象,字符串,循环渲染
target._t = renderSlot; //用于呈现的运行时帮助程序 创建虚拟slot vonde
target._q = looseEqual; //检测a和b的数据类型,是否是不是数组或者对象,
//对象的key长度一样即可,数组长度一样即可
target._i = looseIndexOf; //或者 arr数组中的对象,或者对象数组 是否和val 相等
target._m = renderStatic;//用于呈现静态树的运行时助手。 创建静态虚拟vnode
target._f = resolveFilter; // 用于解析过滤器的运行时助手
target._k = checkKeyCodes; // 检查两个key是否相等,如果不想等返回true 如果相等返回false
target._b = bindObjectProps; //用于将v-bind="object"合并到VNode的数据中的运行时助手。
//检查value 是否是对象,并且为value 添加update 事件
target._v = createTextVNode; //创建一个文本节点 vonde
target._e = createEmptyVNode; // 创建一个节点 为注释节点 空的vnode
target._u = resolveScopedSlots; // 解决范围槽 把对象数组事件分解成 对象
target._g = bindObjectListeners; //判断value 是否是对象,并且为数据 data.on 合并data和value 的on 事件
}
作用:
回调延迟到下次 DOM 更新循环之后执行
使用场景:
比如我们现在需要获取接口数据去渲染一个列表,并且把列表的第一项设置为高亮。
mounted: function () {
this.loadData()
},
methods: {
loadData () {
getList.then(res => {
this.list = res.data
// 获取list的第一个节点,高亮
this.$refs.list.getElementsByTagName('li')[0].style.color = 'blue'
})
},
}
此时就会遇到个问题,我们获取节点li的时候,DOM并没有更新完成,ul下面还没有li节点,这时就会报错。此时就需要使用$nextTick方法,在DOM更新之后才执行回调。
如下,改动mounted:
mounted: function () {
this.$nextTick(() => {
this.loadData()
})
},
methods: {...}
流程:
为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数
源码:
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
function nextTick(
cb, // 回调函数
ctx // this的指向
) {
var _resolve;
// 添加当前回调函数到回调函数队列里面去,等待适当的时机执行
callbacks.push(function () {
if (cb) {
// 如果cb存在 并且是一个函数就执行
try {
cb.call(ctx);
} catch (e) {
// 如果不是函数则报错
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
//_resolve 如果存在则执行
_resolve(ctx);
}
});
// 默认false
if (!pending) {
pending = true;
//执行异步宏任务
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
if (!cb && typeof Promise !== 'undefined') {
//如果回调函数不存在 则声明一个Promise 函数
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
作用:
渲染函数,生成vnod。
流程:
源码:
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
//重置槽上的_render标记,以检查重复槽
{
for (var key in vm.$slots) {
//标志位
vm.$slots[key]._rendered = false;
}
}
if (_parentVnode) {
// 获取插槽
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
}
//设置父vnode。这允许呈现函数具有访问权限,拿到占位符节点上的数据。
vm.$vnode = _parentVnode;
var vnode;
try {
// 创建一个空的组件
// vm.$options.render = createEmptyVNode;
// _renderProxy 代理拦截
vnode = render.call(
vm._renderProxy,
vm.$createElement
);
} catch (e) { //收集错误信息 并抛出
handleError(e, vm, "render");
// 返回错误渲染结果,或以前的vnode,以防止渲染错误导致空白组件
{
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
} catch (e) {
handleError(e, vm, "renderError");
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
}
}
// 如果render函数生成的不是Vnode,则返回空的vnode
if (!(vnode instanceof VNode)) {
if ("development" !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
);
}
vnode = createEmptyVNode();
}
vnode.parent = _parentVnode;
return vnode
};
{
$delete: ƒ del(target, key),
$set: ƒ (target, key, val),
$watch: ƒ ( expOrFn),
$data: undefined,
$props: undefined,
get $data: ƒ (),
set $data: ƒ (newData),
get $props: ƒ (),
set $props: ƒ (),
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
$emit: ƒ (event),
$off: ƒ (event, fn),
$on: ƒ ( event, fn),
$once: ƒ (event, fn),
$destroy: ƒ (),
$forceUpdate: ƒ (),
_update: ƒ (),
$nextTick: ƒ (fn), // 本次加上去的
_render: ƒ (), // 本次加上去的
_b: ƒ bindObjectProps(), // 本次加上去的
_e: ƒ (text), // 本次加上去的
_f: ƒ resolveFilter(id), // 本次加上去的
_g: ƒ bindObjectListeners(data, value, // 本次加上去的
_i: ƒ looseIndexOf(arr, val), // 本次加上去的
_k: ƒ checkKeyCodes(), // 本次加上去的
_l: ƒ renderList(), // 本次加上去的
_m: ƒ renderStatic(), // 本次加上去的
_n: ƒ toNumber(val), // 本次加上去的
_o: ƒ markOnce(tree, index, key), // 本次加上去的
_q: ƒ looseEqual(a, b), // 本次加上去的
_s: ƒ toString(val), // 本次加上去的
_t: ƒ renderSlot(), // 本次加上去的
_u: ƒ resolveScopedSlots(fns), // 本次加上去的
_v: ƒ createTextVNode(val), // 本次加上去的
}
作用:
kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存。
在平常开发中,有部分组件没有必要多次初始化,这时,我们需要将组件进行持久化,使组件的状态维持不变,在下一次展示时,也不会进行重新初始化组件。
使用场景:
如列表页面,点击第二页,选择一项进入详情。然后从详情页面返回列表页面时,打开的依然是第二页,且滚动条的位置也不会变。
//只有路径匹配到的 name 为 list 组件会被缓存
流程:
源码:
var patternTypes = [String, RegExp, Array]; //类型
// KeepAlive的属性就这些了
var KeepAlive = {
name: 'keep-alive',
abstract: true,
props:{
include: patternTypes, // 类型允许[String, RegExp, Array] 缓存匹配的组件
exclude: patternTypes, // 类型允许[String, RegExp, Array] 不缓存匹配的组件
max: [String, Number] // 类型允许 [String, Number] 缓存组件的最大值
},
created: function created() {
this.cache = Object.create(null); //创建一个缓存的空对象
this.keys = []; // 缓存key数组
},
destroyed: function destroyed() {
var this$1 = this;
for (var key in this$1.cache) {
// 销毁所有组件 【说明名1】
pruneCacheEntry(
this$1.cache, key,
this$1.keys
);
}
},
mounted: function mounted() {...},
render: function render() {...}
}
var builtInComponents = {
KeepAlive: KeepAlive
}
【说明1 pruneCacheEntry】:
检测缓存中的组件,如果不是当前激活的组件则销毁
function pruneCacheEntry(
cache, // 缓存对象
key, // 单个key 缓存对象this.cache中的key
keys, // 多个key
current //当前虚拟dom或者空
) {
var cached$$1 = cache[key];
if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
// 如果exclude中的值不是当前组件(当前激活组件是不能销毁的),则让他销毁
cached$$1.componentInstance.$destroy();
}
// 置空,移除key所在的对象和数组
cache[key] = null;
remove(keys, key);
}
作用:
组件初始化 生命周期
流程:
源码:
mounted: function mounted() { //组件初始化 生命周期
var this$1 = this;
this.$watch(
'include', //监听 include 数据是否有变化
function (val) { //监听为完后更新的值
pruneCache(
this$1,
function (name) {
// 判断include属性中name是否存在 如:include:['a','b'], name: 'a',此时返回true
return matches(val, name);
});
});
this.$watch(
'exclude', //监听 exclude 数据是否有变化
function (val) {
pruneCache(
this$1,
function (name) {
//如果exclude 对象中存在name 不存在了 就 调用 检测缓存中的组件,如果不是当前激活的组件则销毁
return !matches(val, name);
});
});
},
【说明1 matches】
判断pattern 中是否还有 name
function matches(pattern, name) {
if (Array.isArray(pattern)) { //如果是数组
return pattern.indexOf(name) > -1 // 是否存在
} else if (typeof pattern === 'string') { //如果是字符串
return pattern.split(',').indexOf(name) > -1 //判断是否存在
} else if (isRegExp(pattern)) { // 如果是正则 则用正则表示
return pattern.test(name)
}
return false
}
【说明2 pruneCache】
删除缓存,销毁exclude中不是当前激活的组件
function pruneCache(
keepAliveInstance, //当前保持活动的实例
filter // 函数过滤器
) {
var cache = keepAliveInstance.cache; // 缓存对象
var keys = keepAliveInstance.keys; // key数组
var _vnode = keepAliveInstance._vnode;
for (var key in cache) {
var cachedNode = cache[key]; // 获取缓存对象的值
if (cachedNode) {
var name = getComponentName(cachedNode.componentOptions); // 获取组件的名称
/**
* 例: include的情况下:
* name为 'article',
* filter(name)为 matches(['article','articleList'], 'article'),
* 则filter(name)为true,!filter(name)为false,即不执行
* 也就是说,当是include时,获取组件的名称在include的值中,就不删除
* 当是exclude时,获取组件的名称在exclude的值中,则执行相反操作,删除
*/
if (name && !filter(name)) { //如果name已经被销毁掉
pruneCacheEntry(
cache,
key,
keys,
_vnode
);
}
}
}
}
作用:
渲染 keepAlive 组件
流程:
源码:
// 渲染 keepAlive 组件
render: function render() {
var slot = this.$slots.default; //获取插槽
var vnode = getFirstComponentChild(slot); // 获取插槽子组件
var componentOptions = vnode && vnode.componentOptions; //获取组件参数
if (componentOptions) {
var name = getComponentName(componentOptions); //获取组件名称
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// exclude优先于include
if (
/**
* 如果name不存在include中,或者name存在exclude中,则进if
* 例: include为:['a','b'],name为'a' false
* exclude为:['a','b','c'] true
*/
(include && (!name || !matches(include, name))) ||
// 如果exclude存在 并且name存在 并且name存在exclude对象中
(exclude && name && matches(exclude, name))
) {
return vnode //返回虚拟dom
}
var ref$1 = this;
var cache = ref$1.cache; //缓存的对象
var keys = ref$1.keys; //获取keys 所有的key
//同一个构造函数可以注册为不同的本地组件
//单靠cid是不够的(#3269)
//这里三目是 判断组件是否有cid 如果有 则 判断 是否有组件标签,如果有组件标签则返回 '::'+组件标签,
//如果没有组件标签则返回空。如果没有 判断组件是否有cid 则返回 vnode.key
var key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ?
("::" + (componentOptions.tag)) :
'') :
vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance; //直接获取组件实例化
remove(keys, key);
keys.push(key);
} else {
//将虚拟dom缓存起来
cache[key] = vnode;
keys.push(key); //key缓存起来
//删除最老的条目
//设定最大的缓存值
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(
cache,
keys[0], //第一个key
keys, //keys[]
this._vnode //当前活跃的组件
);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0])
}
作用:
初始化全局api 并且暴露 一些静态方法
流程:
Vue构造函数
上挂载util,set,delete,nextTick方法Vue.options
挂载相应指令,然后设置_base属性值为Vue源码:
function initGlobalAPI(Vue) {
// 声明配置对象
var configDef = {};
configDef.get = function () {
// 就是一列的警告,提示等配置项
return config;
};
{
configDef.set = function () {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
);
};
}
/**
* 在这里,为Vue的构造函数,添加一个要通过Object.defineProperty监听的属性config,
* 获取的时候,获取到的是上面描述的那个config对象,如果对这个config对象直接做变更,
* 就会提示“不要替换vue.config对象,而是设置单个字段”,说明,
* vue不希望我们直接去替换和变更整个config对象,如果有需要,希望去直接修改我们需要修改的值
*/
Object.defineProperty(Vue, 'config', configDef);
// 暴露的util方法。
// 注意:这些不是公共API的一部分——避免依赖,除非你意识到其中的风险。
Vue.util = {
warn: warn, //警告函数
extend: extend, //继承方式
mergeOptions: mergeOptions, //合并参数
// 该方法在object上定义一个响应式的属性
defineReactive: defineReactive
};
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;
Vue.options = Object.create(null);
//添加components ,directives, filters 指令组件 控对象
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
//用来标识扩展所有普通对象的“基”构造函数
// Weex的多实例场景中的组件。
Vue.options._base = Vue;
extend(Vue.options.components, builtInComponents); //合并 KeepAlive参数中的组件对象
initUse(Vue); // 初始化vue 安装插件函数
initMixin$1(Vue); //初始化vue mixin 函数
initExtend(Vue); //初始化 vue extend 函数
initAssetRegisters(Vue); //为vue 添加 静态方法component,directive,filter
}
作用:
给Vue挂载一个use插件函数
流程:
源码:
function initUse(Vue) {
//安装 Vue.js 插件。
Vue.use = function (plugin) {
/**
* 安装的插件放到了 installedPlugins
* Vue会去寻找这个插件在已安装的插件列表里有没有,
* 如果没有,则进行安装插件,如果有则跳出函数,这保证插件只被安装一次。
*/
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) { //判断是否已经安装过插件了
return this
}
// 转换为真的数组
var args = toArray(arguments, 1);
args.unshift(this); //在前面添加
if (typeof plugin.install === 'function') { //如果plugin.install 是个函数 则执行安装
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') { //如果plugin 是个函数则安装
plugin.apply(null, args);
}
installedPlugins.push(plugin); // 将已经安装过的插件添加到队列去
return this
};
}
作用:
给Vue挂载一个mixin函数,合并mixin参数到Vue的options上,并返回合并对象
源码:
function initMixin$1(Vue) {
Vue.mixin = function (mixin) {
// 合并 对象
this.options = mergeOptions(this.options, mixin);
return this
};
}
作用:
在Vue上挂载一个extend方法
使用场景:
Vue.extend返回的是一个扩展实例构造器,预设了部分选项的Vue实例构造器。一般用来用来生成组件。比如:以组件名称作为标签的自定义元素时,会自动调用Vue.extend来生产组件实例,并挂载到自定义元素上。
如:
// 方式1
// 方式2
流程:
this._init
生成组件对象Sub源码:
function initExtend(Vue) {
/**
* 每个实例构造函数(包括Vue)都有一个唯一的cid。
* 这使我们能够创建包装的“子对象”用于原型继承和缓存它们的构造函数。
*/
Vue.cid = 0;
var cid = 1;
/**
* Vue.extend//使用基础 Vue 构造器,创建一个“子类”。
* 参数是一个包含组件选项的对象。合并继承new 实例化中的拓展参数或者是用户直接使用
* Vue.extend 的拓展参数。把对象转义成组件构造函数。创建一个sub类 构造函数是VueComponent,
* 合并options参数,把props属性和计算属性添加到观察者中。
* 如果组件含有名称 则 把这个对象存到 组件名称中, 在options拓展参数的原型中
* 能获取到该数据Sub.options.components[name] = Sub 简称Ctor,返回该构造函数
*/
//使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); //组件构造函数
if (cachedCtors[SuperId]) { //父类 超类id
return cachedCtors[SuperId] //获取 超类
}
var name = extendOptions.name || Super.options.name; //获取组件的name
if ("development" !== 'production' && name) {
// 验证组件名称 必须是大小写,并且是-横杆
validateComponentName(name);
}
//实例化 组件 对象
var Sub = function VueComponent(options) {
// vue中的_init 函数 Vue.prototype._init
this._init(options);
};
//创建一个对象 继承 超类的原型
Sub.prototype = Object.create(Super.prototype);
//让他的构造函数指向回来,防止继承扰乱。
Sub.prototype.constructor = Sub;
//标识 不同的组件
Sub.cid = cid++;
//合并参数
Sub.options = mergeOptions(
Super.options,
extendOptions
);
//记录超类
Sub['super'] = Super;
//对于道具和计算属性,我们定义代理getter
//在扩展原型上的扩展时的Vue实例。这避免为创建的每个实例调用Object.defineProperty。
if (Sub.options.props) { //获取props属性 如果有
//初始化属性 并且把组件的属性 加入 观察者中
initProps$1(Sub);
}
if (Sub.options.computed) { //组件计算属性
//定义计算属性 并且 把属性的数据 添加到对象监听中
initComputed$1(Sub);
}
// 允许进一步的扩展/混合/插件使用
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
//如果组件含有名称 则 把这个对象存到 组件名称中, 在options拓展参数的原型中能获取到该数据
if (name) {
Sub.options.components[name] = Sub;
}
//在扩展时保留对超级选项的引用。
//稍后在实例化时,我们可以检查Super的选项是否具有更新。
Sub.superOptions = Super.options; //超类 父类的拓展参数
Sub.extendOptions = extendOptions; //子类拓参数
Sub.sealedOptions = extend({}, Sub.options); //合并
// 当前缓存的构造函数
cachedCtors[SuperId] = Sub;
return Sub
};
}
作用:
静态方法component,directive,filter
使用场景:
vue.prototype没有变更了,vue.options为:
{
components: {KeepAlive: {…}}, //执行extend(Vue.options.components, builtInComponents),添加的
directives: {},
filters: {},
_base: ƒ Vue(options),
}
// ssr相关
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
});
Object.defineProperty(Vue.prototype, '$ssrContext', {
get: function get() {
return this.$vnode && this.$vnode.ssrContext
}
});
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
});
Vue.version = '2.5.16'; //版本号
// 以下都是通过makeMap函数返回一个函数,去查找map中是否存在val,存在返回true,否则返回false
isReservedAttr // 判断'style,class'
acceptValue // 判断'input,textarea,option,select,progress'
mustUseProp // 判断属性和标签是否匹配
isEnumeratedAttr // 判断'contenteditable(编辑),draggable(拖动),spellcheck(拼写)'
isBooleanAttr // 检查是否是html中的布尔值属性(属性值只有 true 和 false)
isXlink // 判断是否是xmlns 属性
getXlinkProp // 获取xml link的属性
isFalsyAttrValue // 判断val 是否是 null 或者 false
var namespaceMap = {
svg: 'http://www.w3.org/2000/svg', //svg标签命名xmlns属性
math: 'http://www.w3.org/1998/Math/MathML' //math 中的xmlns属性声明 XHTML 文件
};
isFalsyAttrValue // 判断val 是否是 html中的原始标签
isSVG // 判断svg 标签 以及 svg子元素标签
isPreTag // 判断标签是否是pre
isReservedTag // 判断是不是 html 原生的标签 或者svg标签
getTagNamespace // 判断 是否是svg 或者 math 标签
unknownElementCache // 判断 是否是svg 或者 math 标签
isTextInputType // 判断 是否是文本输入框 判断属性:text,number,password,search,email,tel,url
作用:
Object.freeze()
方法可以冻结一个对象,它是作用是:
这里是冻结一系列操作dom的 方法
源码:
var nodeOps = Object.freeze({
createElement: createElement$1, //创建一个真实的dom
createElementNS: createElementNS, //创建一个真实的dom svg方式
createTextNode: createTextNode, // 创建文本节点
createComment: createComment, // 创建一个注释节点
insertBefore: insertBefore, //插入节点 在xxx dom 前面插入一个节点
removeChild: removeChild, //删除子节点
appendChild: appendChild, //添加子节点 尾部
parentNode: parentNode, //获取父亲子节点dom
nextSibling: nextSibling, //获取下一个兄弟节点
tagName: tagName, //获取dom标签名称
setTextContent: setTextContent, // //设置dom 文本
setStyleScope: setStyleScope //设置组建样式的作用域
});
作用:
创建一个真实的dom
流程:
源码:
function createElement$1(tagName, vnode) {
// 通过标签名创建一个真实的dom
var elm = document.createElement(tagName);
//如果不是select标签则返回dom出去, 退出函数
if (tagName !== 'select') {
return elm
}
// 否则,如果是select标签 判断是否设置了multiple属性。如果设置了就重置
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple');
}
return elm
}
作用:
创建一个真实的dom svg方式,document.createElementNS
方法可创建带有指定命名空间
的元素节点。
用法:
var c=document.createElementNS('http://www.w3.org/2000/svg','svg') //创建svg节点
document.body.appendChild(c);
可以看到这dom中插入了一对svg标签
源码:
//创建一个真实的dom svg方式
function createElementNS(namespace, tagName) {
/** namespaceMap,前面定义值为 {
svg: 'http://www.w3.org/2000/svg',
math: 'http://www.w3.org/1998/Math/MathML'
};
例如:document.createElementNS('http://www.w3.org/2000/svg','svg');
*/
return document.createElementNS(namespaceMap[namespace], tagName)
}
作用:
创建文本节点
源码:
function createTextNode(text) {
return document.createTextNode(text)
}
作用:
创建注释节点
用法:
var c=document.createComment("My personal comments"); // 创建注释
document.body.appendChild(c); //插入节点
可以看到这dom中插入了一条注释
源码:
function createTextNode(text) {
return document.createTextNode(text)
}
作用:
在父节点的某个子节点前插入一个新的节点
用法:
node.insertBefore(newnode, existingnode) 方法可在已有的子节点前插入一个新的子节点。
node:
被插入的父节点。newnode:
必须。要插入的节点对象existingnode:
必须。要添加新的节点前的子节点。例如:
- 上海
- 深圳
在第一个li前面插入一个节点
var newCity = document.createElement("li") //创建元素
var textnode = document.createTextNode("北京") // 创建元素
newCity.appendChild(textnode) // 添加文本
var list = document.getElementById("List")
// 在父节点ul的第一个节点li前添加一个newCity节点
list.insertBefore(newCity, list.childNodes[0]);
执行完后:
- 北京
- 上海
- 深圳
源码:
function insertBefore(parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
剩下的比较简单,就放一起讲了
//删除子节点
function removeChild(node, child) {
node.removeChild(child);
}
//添加子节点 尾部
function appendChild(node, child) {
node.appendChild(child);
}
//获取父亲子节点dom
function parentNode(node) {
return node.parentNode
}
//获取下一个兄弟节点
function nextSibling(node) {
return node.nextSibling
}
//获取dom标签名称
function tagName(node) {
return node.tagName
}
//设置dom 文本
function setTextContent(node, text) {
node.textContent = text;
}
//设置组建样式的作用域,设置后只在当前组件生效
function setStyleScope(node, scopeId) {
node.setAttribute(scopeId, '');
}
到这里,nodeOps冻结节点就讲完了,得到一个包含各种操作dom函数的对象。
作用:
对ref进行一系列的操作,比如对ref 创建,更新 和 销毁
流程:
源码:
var ref = {
create: function create(_, vnode) {
//创建注册一个ref
registerRef(vnode);
},
update: function update(oldVnode, vnode) {
//更新ref
if (oldVnode.data.ref !== vnode.data.ref) {
registerRef(oldVnode, true); //先删除
registerRef(vnode); //在添加
}
},
destroy: function destroy(vnode) {
registerRef(vnode, true); //删除销毁ref
}
}
作用:
元素或子组件
注册引用信息。引用信息将会注册在父组件
的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。这里,用来注册ref或者删除ref。比如标签上面设置了ref=‘abc’ ,那么该函数就是为this.$refs.abc 注册ref 把真实的dom存进去,最终得到更新后的refs
源码:
function registerRef(
vnode, // 虚拟dom对象
isRemoval // 是否销毁ref, true为销毁,否则为注册
) {
var key = vnode.data.ref;
if (!isDef(key)) { // 未定义ref则退出函数
return
}
var vm = vnode.context; // context 上下文
// ref指向组件的时候,是获取组件实例;指向标签的时候,是获取DOM节点
// 优先获取vonde的组件实例(对于组件来说),或者el(该Vnode对应的DOM节点,非组件来说)
var ref = vnode.componentInstance || vnode.elm;
var refs = vm.$refs;
// 如果需要销毁ref,从refs上删除对应的ref属性
if (isRemoval) {
if (Array.isArray(refs[key])) {
remove(refs[key], ref);
} else if (refs[key] === ref) {
refs[key] = undefined;
}
} else {
if (vnode.data.refInFor) { //当在v-for之内时,则保存为数组形式
if (!Array.isArray(refs[key])) { //refs[key] 不是数组 则变成一个数组
refs[key] = [ref];
} else if (refs[key].indexOf(ref) < 0) { //如果ref 不存在 refs的时候则添加进去
// $flow-disable-line
refs[key].push(ref);
}
} else {
refs[key] = ref; //如果是单个直接赋值
}
}
}
//判断数据是否定义(注意!值为0或者false也是定义过了的)
function isDef(v) {
return v !== undefined && v !== null
}
作用:
Vnode是一个构造函数,我们用它来生成虚拟节点。挂载了一系列的节点相关的属性和标识。
源码:
//创建一个空的vnode
var emptyNode = new VNode('', {}, []);
// 此时tag为'', data为{},children为[]
var VNode = function VNode(
tag, // 当前节点的标签名
data为{},, // 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型
children, // 子节点
text, // 文本
elm, // 当前节点的dom
context, // 编译作用域
componentOptions, // 组件的option选项
asyncFactory // 异步工厂
){
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined; // 当前节点的名字空间
this.context = context;
this.fnContext = undefined; // 函数上下文
this.fnOptions = undefined; // 函数Options选项
this.fnScopeId = undefined; // 函数范围id
this.key = data && data.key; // 节点的key属性,被当作节点的标志,用以优化
this.componentOptions = componentOptions;
this.componentInstance = undefined; // 当前节点对应的组件的实例
this.parent = undefined; // 当前节点的父节点
this.raw = false;
this.isStatic = false; // 静态节点标志
this.isRootInsert = true; // 是否作为跟节点插
this.isComment = false; // 是否为注释节点
this.isCloned = false; // 是否为克隆节点
this.isOnce = false; // 是否有v-once指令
this.asyncFactory = asyncFactory; // 异步工厂
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};
结果:
我们看下生成的emptyNode的结构:
{
asyncFactory: undefined,
asyncMeta: undefined,
isAsyncPlaceholder: false,
children: [], // 本次入参
componentInstance: undefined,
componentOptions: undefined,
context: undefined,
data: {}, // 本次入参
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "", // 本次入参
text: undefined,
child: undefined,
__proto__: Object
}
作用:
directives和ref对象很相似,都是一个包含create,update,destroy函数属性的对象。
它是用来封装自定义指令用的,vue允许我们去创建自定义指令,例如: v-getval,
那么,指令是用来干嘛的,例如: v-modal, v-for,它们都是更新dom的,所以它是起了一个更新的作用。
源码:
// 这三个属性其实用到的是同一个函数,updateDirectives
var directives = {
create: updateDirectives, //创建指令
update: updateDirectives, //更新指令
destroy: function unbindDirectives(vnode) { //销毁指令
updateDirectives(vnode, emptyNode);
}
}
作用:
通过_update来更新 新旧vnode,所以我们后面会分析_update
源码:
//更新指令
function updateDirectives(
oldVnode, //oldVnode 老数据
vnode //vnode 新数据
) {
// 只要新旧vnode任何一个有指令存在,就更新
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}
作用:
更新指令 比较oldVnode和vnode,根据oldVnode和vnode的情况
触发指令钩子函数bind,update,inserted,insert,componentUpdated,unbind钩子函数
流程:
源码:
function _update(
oldVnode, //oldVnode 老数据
vnode //vnode 新数据
) {
// 如果旧节点是空节点,就表示当前操作为创建,首次创建
var isCreate = oldVnode === emptyNode;
//如果新节点是空节点,就表示当前操作为销毁
var isDestroy = vnode === emptyNode;
//规范化的指令,为指令属性修正变成规范的指令数据。返回指令数据集合
// 这个方法请看下面的解析
var oldDirs = normalizeDirectives$1(
oldVnode.data.directives, //vonde指令对象集合
oldVnode.context //vm vne实例化对象,或者是组件实例化的对象
);
//规范化的指令,为指令属性修正变成规范的指令数据。返回指令数据集合
var newDirs = normalizeDirectives$1(
vnode.data.directives, //vonde指令对象集合
vnode.context //vm vne实例化对象,或者是组件实例化的对象
);
var dirsWithInsert = []; // 触发inserted指令钩子函数的指令列表。
var dirsWithPostpatch = []; // 触发componentUpdated钩子函数的指令列表。
var key, oldDir, dir;
for (key in newDirs) { //循环新的指令集合
oldDir = oldDirs[key]; //获取旧的单个指令值
dir = newDirs[key]; //获取新的单个指令值
if (!oldDir) { //新增指令,触发bind
// new directive, bind 新指令,绑定
callHook$1( //例:v-focus指令的bind函数
dir, //新的指令值
'bind', //触发bind钩子函数
vnode,//新的vonde
oldVnode //旧的vonde
);
if (dir.def && dir.def.inserted) {
// 如果有插入指令,加入列表
dirsWithInsert.push(dir);
}
} else {
// 指令已存在触发update
// existing directive, update 现有的指令,更新
// 如有指令 value=123. 如果更新了123 就是更新值
dir.oldValue = oldDir.value;
callHook$1(
dir,
'update', //触发更新钩子函数
vnode,
oldVnode
);
// 如果有更新指令,加入列表
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
// 此时完成bind和update钩子函数,列表更新完成
if (dirsWithInsert.length) {
// 定义一个函数,该函数会执行dirsWithInsert里的每个函数
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook$1(
dirsWithInsert[i], //新的指令值 也就是上面的dir(新的单个指令值)
'inserted', //触发inserted钩子函数
vnode, //新的vonde
oldVnode //旧的vonde
);
}
};
if (isCreate) {
//如果是初始化
mergeVNodeHook(
vnode,
'insert',//合并钩子函数
callInsert
);
} else {
callInsert();
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode,
'postpatch',
function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook$1(
dirsWithPostpatch[i],
'componentUpdated',
vnode, oldVnode);
}
});
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) { //新的vonde 中没有了指令
// no longer present, unbind 不再存在,解除束缚
callHook$1(
oldDirs[key],
'unbind', //触发unbind 钩子
oldVnode,
oldVnode,
);
}
}
}
}
作用:
它修正指令属性变成规范的指令数据,返回指令数据集合
源码:
function normalizeDirectives$1(
dirs, //vonde 指令集合
vm //vm vne实例化对象,或者是组件实例化的对象
) {
//创建一个空的对象
var res = Object.create(null);
//如果 指令 名称dirs 不存在 则返回一个空的对象
if (!dirs) {
// $flow-disable-line
return res
}
var i, dir;
for (i = 0; i < dirs.length; i++) { //循环遍历指令集合
dir = dirs[i];
if (!dir.modifiers) { //判断是否有修饰符
// $flow-disable-line
dir.modifiers = emptyModifiers; //空对象
}
//返回指令名称 或者属性name名称+修饰符
res[getRawDirName(dir)] = dir;
/**
*给当前指令挂载自定义指令属性,该属性由用户自定义如
*bind,inserted,update,componentUpdated,unbind这些
*/
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
}
// $flow-disable-line
return res
}
作用:
返回指令名称 或者属性name名称+修饰符
源码:
function getRawDirName(dir) {
//rawName 视图中的 指令如 就是v-hello
//name 视图中的 指令如 就是hello
//name 视图中的 指令如有修饰符 就是hello.native
//modifiers 修饰符
return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
}
此时res[getRawDirName(dir)] = dir
,已经将指令名作为res的属性了,并且将指令作为属性值。
接着检测指令是否在 组件对象上面 ,返回注册指令或者组建的对象
function resolveAsset(
options, //参数 例:vm.$options
type, // 类型 例:'directives' , 'filters' ,'components'
id, // 指令,组件的key 属性 例:dir.name
warnMissing //开启警告的信息 例:true
) {
/* istanbul ignore if 如果id不是字符串,退出函数 */
// 返回逻辑【1】
if (typeof id !== 'string') {
return
}
var assets = options[type]; // 例: vm.$options['components']
// check local registration variations first
/**
* 首先检查本地注册的变化 判断id(组件等的name)是否是assets自有属性
* 否则判断将id驼峰后的key,是否是assets自有属性
* 否则判断将id驼峰后,再首字母变大写的key,是否是assets自有属性
*/
// 例:判断v-modal 指令,在不在options['directives']中
// 例:判断my-header 组件,在不在options['components']中
/**
* 所以,我们在Vue引入某个组件时候,我们可以在template写组件标签用驼峰的方式,
* 也可以是首字母大写,或是直接用组件名来当作组件的标签,就是因为这里做了这样的扩展处理
*/
// 执行返回逻辑【2】
if (hasOwn(assets, id)) {
return assets[id]
}
// 可以让这样的的属性 v-model 变成 vModel 变成驼峰
var camelizedId = camelize(id);
// 执行返回逻辑【3】
if (hasOwn(assets, camelizedId)) {
return assets[camelizedId]
}
// 将首字母变成大写 即 vModel 变成 VModel
var PascalCaseId = capitalize(camelizedId);
// 执行返回逻辑【4】
if (hasOwn(assets, PascalCaseId)) {
return assets[PascalCaseId]
}
// fallback to prototype chain 回到原型链
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
// 如果以上都不成立且是开发环境则警告
if ("development" !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
//返回注册指令或者组建的对象(原型上的)
return res
}
接着将我们的指令name变成驼峰的写法,camelize:
/**
把横线-的转换成驼峰写法
这个正则可以让这样的的属性 v-model 变成 vModel
把名称格式为“xx-xx”的变为“xxXx”,这里接收的是当前的props属性值,一个字符串
*/
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
return str.replace(camelizeRE, function (_, c) {
/**
* var str="Hello World!"
str.toUpperCase() //HELLO WORLD! 将小写转为大写
*/
return c ? c.toUpperCase() : '';
})
});
接着看下cached函数:
// 它将我们需要调用的一些函数给封装到一个对象里面,需要的时候就去对象取
function cached(fn) {
var cache = Object.create(null); //这样创建的没有原型的空对象
return (function cachedFn(str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
为了更直接观的查看camelize
函数,我们就将cached
引入过来,再回到我们前面的camelizeRE
函数,是不是清晰了很多:
var camelizeRE = /-(\w)/g;
var camelize = function () {
var cache = Object.create(null); //这样创建的没有原型的空对象
// 这个str就是某些属性的key,如id,v-modal
// 也就是说,会先去缓存对象cache中判断有没有存在,id属性对应的函数,有就返回,没有的就设置
return (function cachedFn(str) {
var hit = cache[str];
return hit
|| (cache[str] = str.replace(camelizeRE, function (_, c) { //后面这一块就是cached的入参fn
return c ? c.toUpperCase() : '';
})(str))
})
};
ok,此时我们已经分析完camelize
函数了,现在resolveAsset
已经分析完了,拿到了指令集合。normalizeDirectives$1
也执行完了,指令属性修正变成规范的指令数据.
接着回到我们的_update
函数,新旧的指令集合我们都拿到了:
我们已经分析完了这个函数了,它实现了更新新旧节点指令和执行钩子函数。
回推回去,directives
也分析完了,指令章节也分析完了。
var events = {
create: updateDOMListeners,
update: updateDOMListeners
}
作用:
更新dom事件
源码:
function updateDOMListeners(oldVnode, vnode) {
// 边界处理
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm; //真实的dom
normalizeEvents(on); //为事件 多添加 change 或者input 事件加进去
// 更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
updateListeners(
on, //新的事件对象
oldOn, //旧的事件对象
add$1, //添加真实dom的事件函数
remove$2, //删除真实dom的事件函数
vnode.context //vue 实例化的对象 new Vue 或者组件 构造函数实例化的对象
);
target$1 = undefined;
}
作用:
为事件 多添加 change 或者input 事件加进去
源码:
function normalizeEvents(on) {
/* istanbul ignore if */
if (isDef(on[RANGE_TOKEN])) {
// IE input[type=range] only supports `change` event
// 判断是否是ie 浏览器,如果是则选择 change 事件,如果不是则选择input事件
var event = isIE ? 'change' : 'input';
// 连接事件 把change或者input 事件添加进去
on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);
delete on[RANGE_TOKEN]; //删除旧的事件
}
// This was originally intended to fix #4521 but no longer necessary
// after 2.5. Keeping it for backwards compat with generated code from < 2.4
/* istanbul ignore if */
//最初的目的是修复#4521,但现在已经没有必要了
// 2.5之后。保留它以便与< 2.4生成的代码进行反向比较
//添加change事件
if (isDef(on[CHECKBOX_RADIO_TOKEN])) {
on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);
delete on[CHECKBOX_RADIO_TOKEN];
}
}
作用:
更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
源码:
function updateListeners(
on, //新的事件
oldOn, //旧的事件
add, //添加事件函数
remove$$1, //删除事件函数
vm//vue 实例化对象
) {
var name, def, cur, old, event;
for (name in on) {
def = cur = on[name]; //on 新的事件值
old = oldOn[name]; // 旧的值
event = normalizeEvent(name); //normalizeEvent 如果是事件,则过滤 事件修饰符
/* istanbul ignore if */
// isUndef 值是空的 undefined || null
if (isUndef(cur)) {
//如果不是生产环境
"development" !== 'production' && warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) { //如果函数不存在 则绑定函数
//函数 获取钩子函数
// 创建函数调用器并重新复制给cur和on[name]
cur = on[name] = createFnInvoker(cur); //这个时候cur.fns就存在了
}
name = '&' + name; // mark the event as passive 将事件标记为被动的
//添加事件
add(
event.name, //事件名称
cur, // 转义过的事件 执行静态类
event.once, //是否只触发一次的状态
event.capture, // 事件俘获或是冒泡行为
event.passive, // 检测事件修饰符 是否是 '&'
event.params //事件参数
);
} else if (cur !== old) {
//如果新的值不等于旧的值
//则更新新旧值
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
//循环旧的值 为空的时候
if (isUndef(on[name])) {
//获取事件
event = normalizeEvent(name);
//删除旧的值的事件
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
updateDOMListeners分析完成。
更新真实dom的props属性
var domProps = {
create: updateDOMProps, //更新真实dom的props 属性值
update: updateDOMProps
}
作用:
更新真实dom的props属性
源码:
function updateDOMProps(oldVnode, vnode) {
if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
return
}
var key, cur;
var elm = vnode.elm;
var oldProps = oldVnode.data.domProps || {}; //获取旧的props属性
var props = vnode.data.domProps || {}; //获取新的props
// clone observed objects, as the user probably wants to mutate it
// 克隆观察到的对象,因为用户可能希望对其进行修改
// 如果是props添加了观察者,重新克隆他,这样就可以修改了
if (isDef(props.__ob__)) {
props = vnode.data.domProps = extend({}, props);
}
for (key in oldProps) {
if (isUndef(props[key])) {
elm[key] = '';
}
}
for (key in props) {
cur = props[key];
// ignore children if the node has textContent or innerHTML,
// as these will throw away existing DOM nodes and cause removal errors
// on subsequent patches (#3360)
//忽略子节点,如果节点有textContent或innerHTML,
//因为这将丢弃现有的DOM节点并导致删除错误
//其后的修补程式(#3360)
if (
key === 'textContent' ||
key === 'innerHTML'
) {
if (vnode.children) {
vnode.children.length = 0;
}
if (cur === oldProps[key]) {
continue
}
// #6601 work around Chrome version <= 55 bug where single textNode
// replaced by innerHTML/textContent retains its parentNode property
// #6601解决Chrome版本<= 55的bug,其中只有一个textNode
//被innerHTML/textContent替换后,保留了它的parentNode属性
if (elm.childNodes.length === 1) { //文本节点
elm.removeChild(elm.childNodes[0]);
}
}
if (key === 'value') {
// store value as _value as well since
// non-string values will be stringified
//将value存储为_value以及since
//非字符串值将被字符串化
elm._value = cur;
// avoid resetting cursor position when value is the same
// 当值相同时,避免重置光标位置
var strCur = isUndef(cur) ? '' : String(cur); //转义成字符串
if (shouldUpdateValue(
elm, //真实的dom
strCur //value
)) {
elm.value = strCur; //赋值
}
} else {
elm[key] = cur; //直接赋值
}
}
}
作用:
把style 字符串
转换成对象
比如'width:100px;height:200px;'
转化成 {width:100px,height:200px}
源码:
var parseStyleText = cached(function (cssText) {
var res = {};
//匹配字符串中的 ;符号。但是不属于 (;)的 符号 如果是括号中的;不能匹配出来
var listDelimiter = /;(?![^(]*\))/g;
var propertyDelimiter = /:(.+)/; //:+任何字符串
cssText.split(listDelimiter).forEach(function (item) {
if (item) {
var tmp = item.split(propertyDelimiter);
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim());
}
});
return res
});
封装工具模板
var platformModules = [
// attrs包含两个方法create和update都是更新设置真实dom属性值
// {create: updateAttrs, /*创建属性*/ update: updateAttrs /*更新属性 */}
attrs,
// klass包含类包含两个方法create和update都是更新calss。
// 其实就是updateClass方法。 设置真实dom的class
klass,
events, //更新真实dom的事件
domProps, //更新真实dom的props 属性值
// 更新真实dom的style属性。有两个方法create 和update 不过函数都是updateStyle更新真实dom的style属性值.
// 将vonde虚拟dom的css 转义成并且渲染到真实dom的css中
style,
transition // 过度动画
]
var modules = platformModules.concat(baseModules);
//path 把vonde 渲染成真实的dom
var patch = createPatchFunction(
{
nodeOps: nodeOps,
modules: modules
}
);
封装指令
// 定义插入更新指令函数
var directive = {
inserted: function inserted(el, binding, vnode, oldVnode) {...},
componentUpdated: function componentUpdated(el, binding, vnode) {...}
};
var show = {
bind: function bind(el, ref, vnode) {...},
update: function update(el, ref, vnode) {...},
unbind: function unbind(el,
binding,
vnode,
oldVnode,
isDestroy) {...}
}
// 封装指令
var platformDirectives = {
model: directive,
show: show
}
Vue.prototype.$mount = function (el, hydrating) {
debugger
// query(el) 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
el = el && inBrowser ? query(el) : undefined;
// 安装组件
return mountComponent(
this, // Vue实例
el, // 真实dom
hydrating
)
};