当前篇:vue源码分析【2】-new Vue的过程
以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<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>
<msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
</div>
<script>
debugger;
// 定义一个新组件
var a = {
props:['msginfo', 'totalnum'],
data: function () {
return {
count: 0
}
},
template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
}
var app = new Vue({
el: '#app',
components: { msgTip: a},
beforeCreate() { },
created() { },
beforeMount() { },
mounted: () => { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { },
data: function () {
return {
money: 100,
num: 12,
arryList: [{name:'子树'}],
msgText: "优秀的乃古:"
}
},
computed: {
total() {
return this.money * this.num;
}
},
methods: {
getMoreMoney() {
this.money = this.money * 2
this.arryList.unshift({name: '大树'})
}
}
})
</script>
</body>
</html>
1. 前言
本文的结构依据点,线,面来展开。
- 点即函数的作用
- 线即函数的执行流程
- 面即源码的详细解读
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
2. 整体流程
3. 触发new Vue
在html的js中执行new Vue,此时会进入vue.js中的Vue构造函数中。
options参数是html中new Vue的入参。
源码:
// html中触发
var app = new Vue({...})
// vue.js中的Vue构造函数
function Vue(options) {
// 在vue的webpack版本中这里是:development是process.env.NODE_ENV
if ("development" !== 'production' && !(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
我们先看下options的入参是什么:
{
beforeCreate: ƒ beforeCreate(),
beforeDestroy: ƒ beforeDestroy(),
beforeMount: ƒ beforeMount(),
beforeUpdate: ƒ beforeUpdate(),
components: { // 组件
msgTip: { // 组件名
data: ƒ (),
props: [
msginfo,
totalnum
],
template: "<div>{{ msginfo }}存了¥{{ totalnum }}</div>"
}
},
computed: {total: ƒ}
created: ƒ created()
data: ƒ ()
destroyed: ƒ destroyed()
el: "#app"
methods: {getMoreMoney: ƒ}
mounted: () => { }
updated: ƒ updated()
__proto__: Object
}
4. this._init
作用:
- 初始化 vm.$options(合并选项)
- 一堆初始化的工作(包括初始化生命周期、事件、渲染函数、data、prop、methods、Computed、watch 等等)
- mount 生命钩子挂载元素(完整版构建就会有 Compiler 编译过程来生成渲染函数)
源码:
// 它执行的时候,会走Vue.prototype._init
this._init(options);
var uid$3 = 0;
//初始化vue
function initMixin(Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// 递增给每一个组件加一个唯一的uid
// uid$3本身递增了1,但是表达式没有增加,所以vm._uid初始化为0
vm._uid = uid$3++;
//开始,结束标签
var startTag, endTag;
// 浏览器性能监控
// 【说明1 config】
if ("development" !== 'production' && config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// 一个避免被观察到的标志
vm._isVue = true;
// 如果当前文件是个组件,执行子组件初始化
if (options && options._isComponent) {
// 将new Vue入参options的属性放到 vm.$options 中
initInternalComponent(vm, options);
} else {
/**
* 这一块都是围绕合并options,然后返回合并后的options
*
* 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如
* Vue.component 注册的全局组件会合并到 根实例的 components 选项中
*/
//合并参数 将父值对象和子值对象合并在一起
vm.$options = mergeOptions(
// 解析constructor上的options属性的
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
{
//初始化 代理 监听
initProxy(vm);
}
// 执行一系列钩子函数等
vm._self = vm;
// 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
initLifecycle(vm);
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上
* 注册的事件,监听者不是父组件,
* 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm);
// 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
initRender(vm);
callHook(vm, 'beforeCreate');
// 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象
// 然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initInjections(vm);
initState(vm); // 数据响应式的重点,处理 props、methods、data、computed、watch
//provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,用于组件之间通信。
initProvide(vm);
callHook(vm, 'created');
//浏览器 性能监听
if ("development" !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需
// 要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
说明1:config
config也是new Vue之前定义的,一系列配置性的对象:
4-1. resolveConstructorOptions
作用:
从组件构造函数中解析配置对象 options,并合并基类选项
执行流程:
- 如果构造函数上有super才执行
- 递归执行resolveConstructorOptions,传入构造函数的super,返回新的options
- 当构造函数的superOptions不等于递归获取的options,重置构造函数的superOptions为递归获取的options
- 如果options修改了,将构造函数的extendOptions和修改项合并
- 将归获取的options和已经合并过的extendOptions,再次合并
- 返回修改后的options
源码:
function resolveConstructorOptions(
Ctor // vm.constructor 就是构造函数本身
) {
debugger
/**
* new Vue之前,执行initGlobalAPI的时候,Vue.options变成了
* {
* components: {
KeepAlive: {…}, Transition: {…}, TransitionGroup: {…}},
directives: {model: {…}, show: {…}},
filters: {},
_base: ƒ Vue(options)
}
*/
var options = Ctor.options;
// 有super属性,说明Ctor是Vue.extend构建的子类 继承的子类
if (Ctor.super) { // 基类
// 回调 基类 表示继承 基类,返回options
var superOptions = resolveConstructorOptions(Ctor.super); // 取Vue.options,参考上面展示对象
var cachedSuperOptions = Ctor.superOptions; // 取Vue.options,参考上面展示对象
if (superOptions !== cachedSuperOptions) { // 判断如果 基类的options不等于子类的options 的时候
// 说明基类构造函数选项已经发生改变,需要重新设置
Ctor.superOptions = superOptions; //让他的基类选项赋值Ctor.superOptions
// 检查是否有任何后期修改/附加选项(#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// 如果存在被修改或增加的选项,则合并两个选项
if (modifiedOptions) {
//extendOptions合并拓展参数
extend(Ctor.extendOptions, modifiedOptions);
}
// 优先取Ctor.extendOptions 将两个对象合成一个对象
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
/**返回参数格式
* {
components: {},
data: ƒ (),
directives: {},
filters: {},
template: "<div>detail message is :{{msg}}</div>",
_Ctor: {0: ƒ},
_base: ƒ Vue(options),
__proto__: Object
}
*/
}
4-1-1. mergeOptions
作用:
将两个对象合成一个对象 将父对象和子对象合并在一起,并且相同的key优先取值子对象
执行流程:
- 规范子组件的名称,不符合则警告
- 格式化child里面的Props、Inject、Direcitives,分别处理成对应规范的格式
- 如果child是存在extends,则递归extends和parent,拿到合并后的新的parent
- 如果child是存在mixins(数组),则递归mixins的每一项和parent,拿到合并后的新的parent
- 通过父选项或者子选项的key,去获取strats中对应的merge方法,合并父选项和子选项中对应属性值,再把合并的值重新赋值给options对应的key。
- 返回合并后的options(父子选项的最终合并项)
源码:
function mergeOptions(
parent, // 例:{components: {…}, directives: {…}, filters: {…}, _base: ƒ}
child, // 子值 new Vue的传参, 例:{el: "#app", beforeCreate: ƒ, …}
vm // Vue实例
) {
{
//检验子组件
checkComponents(child);
}
if (typeof child === 'function') {
child = child.options;
}
// 规范child里面的Props、Inject、Direcitives
//规范属性,确保所有的props的规范都是基于对象的
normalizeProps(child, vm);
// 将数组转化成对象 比如 [1,2,3]转化成
normalizeInject(child, vm);
// * normalizeDirectives获取到指令对象值。循环对象指令的值,如果是函数则把它变成dirs[key] = {bind: def, update: def} 这种形式
normalizeDirectives(child);
/**
*看child是否存在extends(存在表示当前组件扩展了另外一个组件),
递归当前的mergeOptions方法,
parent就是当前的parent,child就是当前child的extends的值(扩展的那个组件);
调用后覆盖parent
*/
var extendsFrom = child.extends;
if (extendsFrom) {
//如递归
parent = mergeOptions(parent, extendsFrom, vm);
}
/**
* 检测child是否存在mixins,如果存在的话,递归当前的mergeOptions方法,
* 并把最新的结果,去覆盖上一次调用mergeOptions方法的parent;
*/
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
// 作用是更新合并options
// parent和child合并字段后组成新的options
// 通过parent中的key,去取strats对应的方法,然后把值作为options对应key的值
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
/**
* 合并字段,更新options
* strats类 有方法 el,propsData,data,provide,watch,props,
* methods,inject,computed,components,directives,filters 。
*
* 通过父选项或者子选项的key,去获取strats中对应的merge方法,然后,
* 合并复选项和子选项中对应属性值,再把合并的值重新赋值给options对应的key。
*/
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
// 递归时,返回的options就是下一次函数的入参parent
return options
}
4-1-1-1. checkComponents
作用:
验证我们的组件名称符不符合规范。
验证组件名称 只能包含字母数字字符和连字符,必须以字母开头。
且组件名不是内置标签 slot,component 或者 html 原生的标签 或者svg标签
源码
function checkComponents(options) {
for (var key in options.components) {
validateComponentName(key); // 传入组件名
}
}
function validateComponentName(name) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
// 组件名只能包含字母数字字符和连字符,必须以字母开头。
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);
}
// 是否是内置标签 slot,component 或者 html 原生的标签 或者svg标签
if (isBuiltInTag(name) || config.isReservedTag(name)) {
// 不要将内置或保留的HTML元素用作组件名
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
//检查标记是否为内置标记。
var isBuiltInTag = makeMap('slot,component', true);
//保留标签 判断是不是 html 原生的标签 或者svg标签
var isReservedTag = function (tag) {
return isHTMLTag(tag) || isSVG(tag)
};
4-1-1-2. normalizeProps
作用:
这个函数是处理子组件的props的
给出限制:props只支持对象和数组
并对象的props进行统一格式处理,然后重新赋值给子组件的props属性
执行流程:
- options即传入的子组件child的配置项,例如:
{ data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}" }。 - 如果options没有props项则退出
- props如果是数组,则把子组件接收的props,从
props:['myName', 'detail-id']格式变成:props: {myName: {type: null},detailId: {type: null}} - props如果对象,则把子组件接收的props,也处理成统一格式
- 最后把child里面所有的props给规范化了,覆盖子组件的props属性
源码:
function normalizeProps(
options, // child组件配置,例:data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}"
vm // Vue实例
) {
debugger
// 如果子组件child里面没有props(只有子组件需要和父组件通信才需要写),退出
var props = options.props;
if (!props) {
return
}
var res = {};
var i, val, name;
/**
* 数组形式的处理
* 例: 把子组件接收的props,从props:['myName', 'detail-id']变成:
* props: {
myName: {
type: null
},
detailId: {
type: null
}
}
*/
if (Array.isArray(props)) {
i = props.length;
while (i--) {
val = props[i];
if (typeof val === 'string') {
/*
把含有'-'的字符串 变成驼峰写法
把名称格式为“detail-id”的变为“detailId
*/
name = camelize(val);
res[name] = { type: null };
} else {
// props虽然是数组,但是数组项不是字符串的话,会警告你“使用数组语法时,props必须是字符串
warn('props must be strings when using array syntax.');
}
}
} else if (isPlainObject(props)) {
/**
* 对象形式的处理
* 例: props: {
* myName: String,
portlist: {
type: Array,
required: true,
default: ()=>[]
},
}
格式化后,变成:
props: {
myName: { type: String },
portlist: {
type: Array,
required: true,
default: ()=>[]
},
}
*/
for (var key in props) {
val = props[key];
name = camelize(key);
res[name] = isPlainObject(val)
? val
: { type: val };
}
} else {
//如果不是对象和数组则警告,所以这里也可以看出,props只支持对象和数组
warn(
"Invalid value for option \"props\": expected an Array or an Object, " +
"but got " + (toRawType(props)) + ".",
vm
);
}
// 这里,就把child里面所有的props给规范化了,最后覆盖了源child的props属性
options.props = res;
}
4-1-1-3. normalizeInject
作用:
确保所有inject选项语法都规范化为对象格式,检查 inject 数据类型,我们知道vue接收inject的写法可以是数组的形式,也可以是对象的形式,这个函数就对格式进行处理,确保返回统一的格式
执行流程:
- options即传入的子组件child的配置项,例如:
{ data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}" }。 - 如果options没有inject项则退出
- 给出限制:inject只支持对象和数组
- 并对inject进行统一格式处理,然后重新赋值给子组件的inject属性
源码:
function normalizeInject(
options, // child组件配置,例:data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}"
vm // Vue实例
) {
// provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
/**
* 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,
* 不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
*/
var inject = options.inject;
if (!inject) {
return
}
// 如果存在inject,需要置空
// 保存格式化后的inject
var normalized = options.inject = {};
/**
* 数组格式的处理
*/
if (Array.isArray(inject)) {
for (var i = 0; i < inject.length; i++) {
/* 将数组转化成对象 例如inject为:['foo','bar'],转换过程为:
* normalized['foo']={from: 'foo'}
* normalized['bar']={from: 'bar'}
结果:{
foo:{from: 'foo'},
bar:{from: 'bar'}
}
*/
normalized[inject[i]] = { from: inject[i] };
}
}
/**
* 对象格式的处理
*/
else if (isPlainObject(inject)) {
for (var key in inject) {
/**如果inject为:
{
foo: {
from: 'name1',
default: 'name1'
},
bar
}
* 转换过程为:
第一次遍历,key为 foo,val为{from: 'name1',default: 'name1'},是个对象,
则执行extend合并{ from: 'foo' }和{from: 'name1',default: 'name1'}
结果:
{
foo: {
from: 'name1',
default: 'name1'
},
bar:{from: bar}
}
*/
var val = inject[key];
normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val };
}
} else {
warn(
"Invalid value for option \"inject\": expected an Array or an Object, " +
"but got " + (toRawType(inject)) + ".",
vm
);
}
}
4-1-1-4. normalizeDirectives
作用:
将原始函数指令归一化为对象格式。循环对象指令的值,如果是函数转换。
例如:
options.directives为:
{ getList: a(), delete: b()}
转换为:
{
getList: { bind: a(), update: a() },
delete: { bind: b(), update: b() }
}
执行流程:
- 如果指令存在
- 遍历指令数组,如果遍历项是函数,才对它进行转换
源码:
function normalizeDirectives(
options // 入参就是child
) {
debugger
//获取参数中的指令
var dirs = options.directives;
if (dirs) { //如果指令存在
for (var key in dirs) {
var def = dirs[key]; //获取到指令的值
if (typeof def === 'function') { //如果是函数
//为该函数添加一个对象和值
/**例如:options.directives为{ getList: a(), delete: b()},转换为
{
getList: { bind: a(), update: a() },
delete: { bind: b(), update: b() }
}
*/
dirs[key] = { bind: def, update: def };
}
}
}
}
4-1-1-5. mergeField
作用:
合并更新options。
通过父选项或者子选项的key,去获取strats中对应的merge方法。
然后,合并父选项和子选项中对应属性值,再把合并的值重新赋值给options对应的可以。
源码:
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
strats定义在new Vue之前,它可以说了包含了vue所需要的所有东西,如生命周期,data,watch,el等一系列merge函数。
4-2. initProxy
proxy比较重要,后续我们将单独开一篇文章讲解proxy
作用:
如果Proxy属性存在,则把包装后的vm属性赋值给_renderProxy属性值,否则把vm是实例本身赋值给_renderProxy属性。
执行流程:
- 判断 系统内置 函数有没有 es6的Proxy 代理对象api,无则将 vm直接赋值到 vm._renderProxy
- 有则继续,根据vm.$options是否存在render来选择代理处理程序
源码:
//初始化 代理 监听
initProxy = function initProxy(vm) {
// 判断 系统内置 函数有没有 es6的Proxy 代理对象api
// var hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy);
if (hasProxy) {
// 确定使用哪个代理处理程序
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler // 获取值
: hasHandler; // 判断内部函数,这样vue中模板就可以使用内置函数
//实例化 代理对象,只是这里添加了 警告的日志而已
vm._renderProxy = new Proxy(vm, handlers);
} else {
//如果不能代理直接赋值
vm._renderProxy = vm;
}
};
/**
* hasHandler方法的应用场景在于查看vm实例是否拥有某个属性—比如调用for in循环遍历vm实例属性时会
* 触发hasHandler方法
*/
var hasHandler = {
// in 操作符的捕捉器,has 方法返回一个 boolean 属性的值.
has: function has(target, key) {
debugger
var has = key in target;
//是否含有全局api 就是window 的内置函数
//全局api
// var allowedGlobals = makeMap(
// 'Infinity,undefined,NaN,isFinite,isNaN,' +
// 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
// 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
// 'require' // for Webpack/Browserify
// );
var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
//如果 key 在target对象中 不存在, 且不是(全局api 或者 第一个字符不是_的时候) 发出警告
// 确定属性名称是否可用
if (!has && !isAllowed) {
warnNonPresent(target, key);
}
// 返回true
return has || !isAllowed
}
};
// 该方法可以在开发者错误的调用vm属性时,提供提示作用。
var getHandler = {
// 属性读取操作的捕捉器。
get: function get(target, key) {
// key如果是字符串 并且 key不在target中发出警告
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key);
}
//返回target值
return target[key]
}
};
4-3. initLifecycle
作用:
初始化vm实例中和生命周期相关的属性,例如:
- 挂载
$parent,可以通过this.$parent来直接访问到父组件 - 挂载
$refs,可以通过this.$refs来直接访问已注册过 ref 的所有子组件 - 以及其它属性
源码:
initLifecycle(vm)
function initLifecycle(vm) {
debugger
var options = vm.$options;
// 子组件的父实例
var parent = options.parent;
// 当父实例存在,且该实例不是抽象组件
if (parent && !options.abstract) {
// 判断parent父亲节点是否存在,并且判断抽象节点是否存在
while (parent.$options.abstract && parent.$parent) {
// 找到顶层的parent
parent = parent.$parent;
}
// 子节点添加 vm,父实例里卖弄存放当前实例
parent.$children.push(vm);
}
//指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent访问父实例
vm.$parent = parent;
//当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
vm.$root = parent ? parent.$root : vm;
vm.$children = []; //当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。
vm.$refs = {}; // 一个对象,持有已注册过 ref 的所有子组件。
vm._watcher = null; // 组件实例相应的 watcher 实例对象。
vm._inactive = null; // 表示keep-alive中组件状态,如被激活,该值为false,反之为true。
vm._directInactive = false; // 不活跃 禁用的组件标志 也是表示keep-alive中组件状态的属性。
vm._isMounted = false; // 当前实例是否完成挂载(对应生命周期图示中的mounted)。
vm._isDestroyed = false; //当前实例是否已经被销毁(对应生命周期图示中的destroyed)。
//当前实例是否正在被销毁,还没有销毁完成(介于生命周期图示中deforeDestroy和destroyed之间)。
vm._isBeingDestroyed = false;
}
4-4. initEvents
作用:
当vm.$options._parentListeners(父组件绑定在当前组件上的事件)存在时才会执行组件事件更新操作。
更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
源码:
initEvents(vm)
function initEvents(vm) {
debugger
// 父组件绑定在当前组件上的事件
vm._events = Object.create(null);
vm._hasHookEvent = false;
// 父组件绑定在当前组件上的事件
var listeners = vm.$options._parentListeners;
if (listeners) {
//更新组件事件
updateComponentListeners(vm, listeners);
}
}
function updateComponentListeners(
vm, // vue实例对象
listeners, // 父组件绑定在当前组件上的事件对象
oldListeners // 当前组件上旧的事件对象
) {
debugger
target = vm;
//更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
target = undefined;
}
这里最终走到updateListeners函数,有关这个函数的讲解,在【文章1里面】
4-5. initRender
作用:
这个函数的主要功能是:
1、获取_parentVnode对象(子组件存在)
2、将vnode解析为slot对象
3、挂载 '_c'函数(生成vnode)
4、调用defineReactive(将在响应式章节讲解)函数拦截实例的 'listeners' 属性的操作
源码:
function initRender(vm) {
vm._vnode = null; // 上一个 vonde
vm._staticTrees = null; // v-once缓存的树
var options = vm.$options; // 获取参数
var parentVnode = vm.$vnode = options._parentVnode; // 父树中的占位符节点
var renderContext = parentVnode && parentVnode.context; // this 上下文
// 执行resolveSlots获取占位符VNode下的slots信息
//
debugger
/**
* 执行resolveSlots获取占位符VNode下的slots信息,如这里的div节点。
* renderContext是Vue实例,options._renderChildren是一个slot对应vnode数组。
* 例如:
* [
{
{ tag: "div", data: {attrs: {…}, slot: "you"}...},
{ tag: undefined, data: undefined...},
{ tag: "div", data: {attrs: {…}, slot: "my"}...},
{ tag: undefined, data: undefined...}
}
]
执行后vm.$slots格式为: { you: [VNode], my: [VNode], default: [VNode, VNode] }
*/
vm.$slots = resolveSlots(options._renderChildren, renderContext);
vm.$scopedSlots = emptyObject;
//将createElement fn绑定到这个实例
//这样我们就得到了合适的渲染上下文。
//内部版本由模板编译的呈现函数使用
//创建虚拟dom的数据结构
vm._c = function (a, b, c, d) {
return createElement(
vm, //vm new Vue 实例化的对象
a, //有可能是vonde或者指令
b,
c,
d,
false
);
};
//用户编写的渲染功能。
vm.$createElement = function (a, b, c, d) {
return createElement(vm, a, b, c, d, true);
};
// $attrs和$listener将被公开,以便更容易地进行临时创建。
var parentData = parentVnode && parentVnode.data;
{
// 在object上定义一个响应式的属性,这个方法,就是把对象obj里的属性key变成一个getter/setter形式的
// 响应式的属性 同时在getter的时候收集依赖,并在setter的时候触发依赖。
// 我们将在响应式的章节分析它
defineReactive(
vm,
'$attrs',
parentData && parentData.attrs || emptyObject,
function () {
!isUpdatingChildComponent && warn("$attrs is readonly.", vm);
},
true
);
// 通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
!isUpdatingChildComponent && warn("$listeners is readonly.", vm);
}, true);
}
}
4-5-1. resolveSlots
以下分析基于下面的例子。 父组件
// 插入了2个具名插槽,和一个非具名插槽
<msg-tip :msginfo='msgText' :totalnum='total'>
<div slot='you'>你</div>
<div slot='my'>我</div>
他
</msg-tip>
子组件
<div>
{{ msginfo }}存了¥{{ totalnum }}
<slot name='you'></slot>
<slot name='my'></slot>
<slot></slot>
</div>
作用:
【此函数为slot插槽的核心】
收集父组件里的所有插槽,并对具名插槽和非具名插槽分类,同时将插槽对应的Vnode放置在对应的插槽下面,返回一个封装对象。
执行流程:
- 传入slot占位符对应的Vnode的集合(例:
[{tag: "div", data: {…}...})如:[ { { tag: "div", data: {attrs: {…}, slot: "you"}...}, { tag: undefined, data: undefined...}, { tag: "div", data: {attrs: {…}, slot: "my"}...}, { tag: undefined, data: undefined...} } ] - 声明一个slots来放入所有的插槽
- 遍历children,拿到slot节点的data值,如果节点含有slot属性,即具名插槽,需要删除attrs中的slot属性
- 判断是否为具名插槽,如果为具名插槽,将具名插槽的name存放到对应的属性名下,如果为非具名插槽,统一放在default数组下
- 最终返回slots(例:
{ you: [VNode], my: [VNode], default: [VNode, VNode] })
源码:
function resolveSlots(
children, //存放 slot占位符对应的Vnode 的集合
context // 上下文,vue实例
) {
var slots = {}; // 缓存插槽
// 如果没有子节点 则返回一个空对象
// 一个组件可以有多个slot,所以children是个数组
if (!children) {
return slots
}
//循环slot的vnode集合
// slots是对象,slot是数组(存储vnode),slots存储slot
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
// slot占位符对应数据
// data 例: { attrs: {slot: "you"}, slot: "you" }
var data = child.data;
// 如果节点含有slot属性,即具名插槽,需要删除attrs中的slot属性
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
/* 判断是否为具名插槽,如果为具名插槽,将具名插槽的name存放到对应的属性名下,
如果为非具名插槽,统一放在default数组下
*/
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
var name = data.slot; // 插槽名
// 如果slots[name]不存在,则slots设置插槽名属性,初始化为一个空数组
// 并设置slot为空数组,这里用引用类型将slots和slot关联起来了,当我们设置slot时,
// 就会自动同步到slots对应的插槽下面
var slot = (slots[name] || (slots[name] = []));
if (child.tag === 'template') { // 此例tag为div
//把子节点的 子节点 添加 到slot插槽中
slot.push.apply(slot, child.children || []);
} else {
// 把slot占位符对应的Vnode 添加 到slot插槽中
slot.push(child);
}
} else {
// 如果是非具名slot,则把slot对应的节点插入到slots默认的default数组中
(slots.default || (slots.default = [])).push(child);
}
}
//忽略只包含空白的槽
for (var name$1 in slots) {
//删除空的插槽
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
debugger
return slots
}
4-6. callHook(vm)
callHook属于声明周期相关的函数了,我们将在声明周期篇详情讲解。
4-7. initInjections(vm)
用的不多,暂时不讲
4-8. initProvide(vm)
用的不多,暂时不讲
4-9. initState(vm)
这个函数会在【vue源码分析【3】- vue响应式】中讲解
4-10. $mount(vm)
作用
首先缓存了原型上的 $mount 方法,再重新定义该方法,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。
如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。 compileToFunctions是对我们最后生成的模板进行解析,生成render函数。
其实vue源码在2个地方定义了$mount 方法,分别是文件中间的位置和文件末尾的文字,这里先执行的末尾的位置的 $mount 方法。为什么要这样做呢?
末尾的是适用于 Runtime+Compiler 版本的。中间的mount 的 Vue.prototype.$mount 方法是适用于 Runtime Only 版本的。
我们这里先解析末尾的$mount,因为按照我们代码的流程是会先走这里
源码:
vm.$mount(vm.$options.el);
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el, // 例:#app
hydrating // 服务端渲染相关,在浏览器环境下我们不需要传
) {
// 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
el = el && query(el);
/* istanbul ignore if */
//如果el 是body 或者文档 则警告
if (el === document.body || el === document.documentElement) {
"development" !== 'production' && warn(
/**
* 不要将<html>或<body>挂载到vue的mount,而是需要挂载普通元素
* 因为挂载是覆盖的,如果挂载在body或html上, 覆盖后就没有body和html节点了
* 所以我们一般采用的都是挂载在div上的形式。
*/
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
//获取参数
var options = this.$options;
// resolve template/el and convert to render function
//解析模板/el并转换为render函数
if (!options.render) {
//获取模板字符串
var template = options.template;
if (template) { //如果有模板
if (typeof template === 'string') {
//模板第一个字符串为# 则判断该字符串为 dom的id
if (template.charAt(0) === '#') {
template = idToTemplate(template); //获取字符串模板的innerHtml
/* istanbul ignore if */
if ("development" !== 'production' && !template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) { //如果template 是don节点 则获取他的html
template = template.innerHTML;
} else {
//如果什么都是不是则发出警告
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
//如果模板没有,dom节点存在则获取dom节点中的html 给模板
/** template 例子如下:
* "<div id="app">
<!--this is comment--> {{ message }}
</div>"
*/
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
//监听性能监测
if ("development" !== 'production' && config.performance && mark) {
mark('compile');
}
//创建模板
var ref = compileToFunctions(
template, //模板字符串
{
//IE在属性值中编码换行,而其他浏览器则不会
shouldDecodeNewlines: shouldDecodeNewlines, //flase
//true chrome在a[href]中编码内容
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
//改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}}
//delimiters: ['${', '}']之后变成这样 ${mgs}
delimiters: options.delimiters,
//当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
comments: options.comments
},
this
);
//ast 模板
//code 虚拟dom需要渲染的参数函数
//staticRenderFns 【说明1 staticRenderFns】
//这样赋值可以有效地 防止 引用按地址引用,造成数据修改而其他对象也修改问题,
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
/*
render 是 虚拟dom,需要执行的编译函数
*/
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
// 调用原先原型上的 $mount 方法挂载
// 再次执行mount,执行的是中间位置定义的$mount
// 这一步执行完,data就已经渲染到页面了
return mount.call(
this, // Vue实例
el, //真实的dom 例:el = div#app {__vue__: null, align: "", title: "", lang: "",
// translate: true, …}
hydrating //undefined
)
};
【说明1 staticRenderFns】
这里的staticRenderFns目前是一个空数组,其实它是用来保存template中,静态内容的render,比如模板为:
<div id="app">
<p>这是<span>静态内容</span></p>
<p>{{message}}</p>
</div>
staticRenderFns为:
staticRenderFns = function () {
with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])
}
5. compileToFunctions
5-1. 基本信息
作用:
编译相关函数
为了方便查看,我调整了声明的顺序,并且简化了代码,我们先对调用结构有个了解。这里的函数调用看着有点绕,实际就是做了柯里化,实现了默认传参。
源码:
// 【第一步】
// 调用compileToFunctions函数,传进3个参数
// 下一步我们先分析compileToFunctions做了什么操作
var ref = compileToFunctions(
template,
{
shouldDecodeNewlines: shouldDecodeNewlines,
linesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
);
// 【第二步】
// 由第一步知道,compileToFunctions是个函数,所以
// ref$1.compileToFunctions返回的也肯定是个函数
// 同时也说明ref$1本身是返回一个对象
var compileToFunctions = ref$1.compileToFunctions;
// 转换为:var ref = ref$1.compileToFunctions(template,{...},this)
// 【第三步】
// 说明 createCompiler函数返回的是一个对象
// 入参baseOptions是在new Vue之前定义的
var ref$1 = createCompiler(baseOptions);
// 继续转换为:var ref = createCompiler(baseOptions).compileToFunctions(template,{...},this)
// 【第四步】
// 由第三步,可以知道,createCompilerCreator函数返回的也是一个对象
var createCompiler = createCompilerCreator(
function baseCompile(template,options) {
return {
ast: ast, //ast 模板
render: code.render, //code 虚拟dom需要渲染的参数函数
staticRenderFns: code.staticRenderFns //空数组
}
});
}
/**
*继续转换为:
var ref = createCompilerCreator(function baseCompile(){template,options})
(baseOptions).compileToFunctions(template,{...},this)
现在终于调用到了createCompilerCreator函数,我们接着看createCompilerCreator返回了什么
*/
// compile函数返回了compiled,compiled又是由入参函数baseCompile返回的,
// 即前面的createCompiler 里面的参数函数function baseCompile(template,options) {});
// 返回一个函数,该函数又返回了一个封装对象:{ ast: ast, render...,staticRenderFns:... },
// 即compile函数也返回了这个对象
function createCompilerCreator(
baseCompile
) {
return function createCompiler(baseOptions) {
function compile(template,options) {
var compiled = baseCompile(
template,
finalOptions
);
return compiled
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
// 由上一步可知,compile是个函数,并且返回一个对象
/**
* createCompilerCreator(function baseCompile(){template,options})
(baseOptions).compileToFunctions其实就是取createCompilerCreator返回的对象的compileToFunctions
属性,即createCompileToFunctionFn(compile)的值
所以,可以再次转换:
* var ref = createCompileToFunctionFn(function baseCompile(){template,options})
(baseOptions).compileToFunctions(template,{...},this)
* 没看错,这就是我们终极转换的函数执行链,现在就很清晰了,createCompileToFunctionFn函数的入参compile
* 就是function baseCompile(){template,options},
* compileToFunctions的入参就是上面compileToFunctions函数的参数(template,{...},this),即第一步的入参
*/
function createCompileToFunctionFn(compile) {
return function compileToFunctions(
template,
options,
vm
) {
var compiled = compile(
template,
options
);
return (cache[key] = res)
}
}
接下来我们就可以仔细分析了
5-2. createCompilerCreator
作用:
在render 函数中编译模板字符串
执行流程:
来看下函数是createCompilerCreator怎么定义的,这里的baseCompile就是上面createCompilerCreator函数的入参:baseCompile,里面的baseOptions(new Vue之前定义的)也是从前面的函数传递过来的。最终所有的逻辑都会走到这个函数,前面的逻辑可以认为是提供函数参数的操作。
- 传入一个编译函数,返回一个对象
- 这个对象有2个属性,分别是:
- 定义的compile函数:编译器,在 render 函数中编译模板字符串,负责将模版字符串(即你编写的类 html 语法的模版代码)编译为 JavaScript 语法的 render 函数。
- createCompileToFunctionFn: 入参为定义的compile函数,解析模板字符串成为函数
所以这个函数的核心其实就是里面定义的compile函数。
源码:
// 最终返回compile函数
function createCompilerCreator(
baseCompile // 基本的编译函数,传入这个参数是为了让我们在当前函数
// 能调用传入函数,同时给他传参
) {
return function createCompiler(baseOptions) { //[图1]
debugger
function compile(
template, // 例:"<div id="app">{{ message }} </div>" [图2]
options // 例:{shouldDecodeNewlines: false...} [图3]
) {
debugger
// 创建一个对象 拷贝baseOptions 拷贝到 原型 protype 中
// [图4]
var finalOptions = Object.create(baseOptions); //为虚拟dom添加基本需要的属性
var errors = [];
var tips = [];
// 警告函数
finalOptions.warn = function (msg, tip) {
(tip ? tips : errors).push(msg);
};
if (options) {
// 合并modules到finalOptions
if (options.modules) {
finalOptions.modules = (baseOptions.modules || []).concat(options.modules);
}
// 合并指令到finalOptions
if (options.directives) {
finalOptions.directives = extend(Object.create(baseOptions.directives || null), options.directives);
}
// 复制其他选项到finalOptions
for (var key in options) {
if (key !== 'modules' && key !== 'directives') {
//浅拷贝
finalOptions[key] = options[key];
}
}
}
/**
* 这2个参数会作为参树传入到:createCompiler =
* createCompilerCreator(function baseCompile(template, options) {})中的
* template, options中
*/
//【图4 compiled】
var compiled = baseCompile(
template,
finalOptions // 为虚拟dom添加基本需要的属性
);
{
errors.push.apply(errors, detectErrors(compiled.ast));
}
compiled.errors = errors;
compiled.tips = tips;
return compiled
}
/*
* compile
*在 render 函数中编译模板字符串。只在独立构建时有效
var res = Vue.compile('<div><span>{{ msg }}</span></div>')
new Vue({
data: {
msg: 'hello'
},
render: res.render,
staticRenderFns: res.staticRenderFns
})
*
*
*
* */
debugger
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
[图1] baseOptions:
[图2] template:
[图3] options:
【图4 compiled】
[图4] finalOptions:
5-3. baseCompile
作用:
baseCompile函数是回调时才触发的。
源码:
//编译器创建的创造者
var createCompiler = createCompilerCreator(
//把html变成ast模板对象,然后再转换成 虚拟dom 渲染的函数参数形式。
// 返回出去一个对象
// {ast: ast, //ast 模板
// render: code.render, //code 虚拟dom需要渲染的参数函数
//staticRenderFns: code.staticRenderFns } //空数组
function baseCompile(
template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
options // 这里已经baseOptions和options的合并项了 【图1 】
) {
//返回ast模板对象
// parse函数非常关键,我们将在下一节解析
// 【图2 ast】
var ast = parse(template.trim(), options);
// optimize 的主要作用是标记 static 静态节点
// optimize 也将在后面的章节解析
if (options.optimize !== false) {
// * 循环递归虚拟node,标记是不是静态节点
//* 根据node.static或者 node.once 标记staticRoot的状态
// 我们将在下一节解析
optimize(ast, options);
}
//初始化扩展指令,on,bind,cloak,方法, dataGenFns 获取到一个数组,
// 数组中有两个函数genData和genData$1
//genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,
// 或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
//返回对象{ render: ("with(this){return " + code + "}"),staticRenderFns: state.staticRenderFns} //空数组
// 我们将在下一节解析
// 【图3 code】
var code = generate(ast, options);
return {
ast: ast, //ast 模板
render: code.render, //code 虚拟dom需要渲染的参数函数
staticRenderFns: code.staticRenderFns // 静态渲染函数,数组
}
});
【图1 options】
【图2 ast】
【图3 code】
5-4. createCompileToFunctionFn
作用:
createCompilerCreator最后面就是执行createCompileToFunctionFn
源码:
function createCompileToFunctionFn(compile) {
var cache = Object.create(null);
/**
* 这里又返回了一个函数,父函数作为属性值在createCompilerCreator中:
* {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
* 所以createCompileToFunctionFn(compile)可以看作是执行了当前的返回函数:
compileToFunctions(template, options, vm )
// * 我们往前推,可以看到上面那个对象的compileToFunctions属性是在mount的ref中
// 调用的,因此,compileToFunctions的入参就是定义ref时传入的
// *
*/
return function compileToFunctions(
// 这几个入参就是compileToFunctions: createCompileToFunctionFn(compile)中
// 的compile返回的三个参数
template, // "<div id=\"app\">
options, // {shouldDecodeNewlines: false,...}
vm // Vue实例
) {
//浅拷贝参数
options = extend({}, options);
//警告
var warn$$1 = options.warn || warn;
//删除参数中的警告
delete options.warn;
/*
*这个选项只在完整构建版本中的浏览器内编译时可用。
* 详细:改变纯文本插入分隔符。
*
* 示例:
new Vue({
delimiters: ['${', '}']
})
// ['${', '}'] String后变成了 "${,}"
*/
var key = options.delimiters ? String(options.delimiters) + template : template;
if (cache[key]) {
return cache[key]
}
// compile 传进来的函数, 返回值就是baseCompile函数返回值
var compiled = compile(
template, //模板字符串
options //参数
);
// turn code into functions 将代码转换为函数
var res = {};
var fnGenErrors = [];
//将compiled.render创建一个函数,如果发生错误则记录fnGenErrors错误
//把字符串 转化成真正的js并且以 函数的方式导出去
// 【说明1 createFunction】
// 【图1 res】
res.render = createFunction(
compiled.render,
fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
// 【图2 3】
return (cache[key] = res)
}
}
5-6. createFunction
作用:
把字符串 转成真正的js 并且以一个函数形式导出去
源码:
function createFunction(
code, // 例:"with(this){return _c('div',{attrs:{"id":"app"}},[_v(" "+_s(message)+"\n ")])}"
errors
) {
debugger
try {
return new Function(code)
/**
* 转换成函数:
* (function anonymous() {
with(this){return _c('div',{attrs:{"id":"app"}},[_v(" "+_s(message)+"\n ")])}
})
*/
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}
【图1 res.render,可以看到res.render已经是个函数了
【图2】cache,以template为key的对象
【图3】res,渲染函数对象
到这里,整个编译的过程就解析完成了。
5-8. mount
当我们在末尾的mount执行:
return mount.call(
this, // Vue实例
el, //真实的dom 例:el = div#app {__vue__: null, align: "", title: "", lang: "", translate: true, …}
hydrating //undefined
)
它会继续执行mount,不过此时的mount是中间定义的mount,如下:
Vue.prototype.$mount = function (
el,
hydrating
) {
// query(el) 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
el = el && inBrowser ? query(el) : undefined;
return mountComponent(
this, // Vue实例
el, // 真实dom 例:el = div#app {align: "", title: "", lang: "", translate: true, dir: "", …}
hydrating
)
};
mountComponent:
如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程
mountComponent 核心就是先调用 vm._render 方法,先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。
//安装组件
function mountComponent(
vm, //Vue 实例
el, //真实dom
hydrating //新的虚拟dom vonde
) {
debugger
// 将真实dom挂载到vue实例上去
vm.$el = el;
/*
Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,
前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,
需要借助 webpack 的 vue-loader 事先把模板编译成 render函数。
*/
//如果参数中没有渲染函数,说明使用的是 Runtime only
if (!vm.$options.render) { //实例化vm的渲染函数,虚拟dom调用参数的渲染函数
//创建一个空的组件
vm.$options.render = createEmptyVNode;
{
/* istanbul ignore if */
//如果参数中的模板第一个不为# 号则会 警告,因为Runtime only版本的代码是这种格式:el: '#app',
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
/*
您使用的是 Runtime only 生成的Vue,其中模板编译器不可用。
或者将模板预编译为渲染函数,或使用内置的编译器。
*/
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
);
} else {
// 无法装载组件:未定义template或render函数
warn(
'Failed to mount component: template or render function not defined.',
vm
);
}
}
}
//执行生命周期函数 beforeMount
callHook(vm, 'beforeMount');
//更新组件
var updateComponent;
/* istanbul ignore if */
//如果开发环境
/*
Vue.config.performance为true的话,以在浏览器开发工具的性能/时间线面板中
启用对组件初始化、编译、渲染和打补丁的性能追踪
*/
if ("development" !== 'production' && config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag); //插入一个名称 并且记录插入名称的时间
var vnode = vm._render();
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag); //浏览器 性能时间戳监听
//更新组件
vm._update(vnode, hydrating);
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
//直接更新view试图
// 【在第一篇 new Vue有讲解】
vm._update(
/*
render 是 虚拟dom,需要执行的编译函数 类似于这样的函数
(function anonymous( ) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:
[{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],
attrs:{"type":"text"}}),_v(" "),_m(0)])}
})
*/
vm._render(), //先执行_render,返回虚拟 Node
hydrating
);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
//我们将其设置为vm。在观察者的构造函数中
//因为观察者的初始补丁可能调用$forceUpdate(例如inside child)
//组件的挂载钩子),它依赖于vm。_watcher已经定义
//创建观察者
new Watcher(
vm, //vm vode
updateComponent, //数据绑定完之后回调该函数。更新组件函数 更新 view试图
noop, //回调函数
null, //参数
true //是否渲染过得观察者
/* isRenderWatcher */
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
//手动挂载实例,调用挂载在self上
// 在插入的钩子中为呈现器创建的子组件调用// mount
if (vm.$vnode == null) {
vm._isMounted = true;
//执行生命周期函数mounted
// 渲染data
callHook(vm, 'mounted');
}
debugger
return vm
}
5-9 总结
最终我们的ref是这样的:
然后挂载ref上的属性到options上:
6. optimize
作用: 对parse解析后的AST进行了优化,标记了静态节点和静态根节点。当一个节点staticRoots(静态根节点)为true,并且不在v-for中,那么第一次render的时候会对以这个节点为根的子树进行缓存,等到下次再render的时候直接从缓存中拿,避免再次render。
优化器的目标:遍历生成的模板AST树,检测纯静态的子树,即永远不需要更改的DOM。一旦我们检测到这些子树,我们可以:
- 把它们变成常数,这样我们就不需要在每次重新渲染时为它们创建新的节点;
- 在修补过程中完全跳过它们。循环递归虚拟node,标记是不是静态节点。根据node.static或者 node.once 标记staticRoot的状态
源码:
var createCompiler = createCompilerCreator(
function baseCompile(
template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
options // 这里已经baseOptions和options的合并项了
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// * 循环递归虚拟node,标记是不是静态节点
//* 根据node.static或者 node.once 标记staticRoot的状态
optimize(ast, options);
}
function optimize(
root, // 转换后的ast树
options // 配置项
) {
if (!root) {
return
}
//匹配type,tag,attrsList,attrsMap,plain,parent,children,attrs + staticKeys 字符串
// 例:options.staticKeys = "staticClass,staticStyle"
isStaticKey = genStaticKeysCached(options.staticKeys || '');
//保留标签 判断是不是真的是 html 原有的标签 或者svg标签
isPlatformReservedTag = options.isReservedTag || no;
// 第一步: 标记所有非静态节点。
markStatic$1(root);
//第二步: 标记所有的静态根节点
markStaticRoots(root, false);
}
6-1 markStatic$
作用:
第一步,标记所有非静态节点。
分别循环递归子节点和ifConditions虚拟node,标记是否静态节点,如果子节点是非静态,则父节点也是非静态的。
源码:
function markStatic$1(node) {
debugger
// 初步判断这个节点的是否可以为静态节点。
node.static = isStatic(node);
if (node.type === 1) {
// 不要将组件插槽内容设置为静态。这就避免了:
// 1.组件无法更改插槽节点
// 2.静态插槽内容无法热加载
if (
!isPlatformReservedTag(node.tag) && //判断是不是html 原有标签(div等) 或者svg标签
node.tag !== 'slot' && //当前标签不等于slot
node.attrsMap['inline-template'] == null // 也不是inline-template 内联模板
) {
return
}
// 递归循环子节点,如果子节点不是静态节点,则父节点也不是
for (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
markStatic$1(child);
if (!child.static) {
node.static = false;
}
}
/**
* if标记,是一个数组,如input标签的type:
* node.ifConditions = [
{exp: "(_f(\"recordType\")(texts))==='checkbox'", block: {…}}
{exp: "(_f(\"recordType\")(texts))==='radio'", block: {…}}
{exp: undefined, block: {…}}
]
判断if数组的虚拟dom是不是静态标记,如果不是,那么则父节点也不是
*/
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
var block = node.ifConditions[i$1].block; //虚拟dom
markStatic$1(block);
if (!block.static) {
node.static = false;
}
}
}
}
}
//判断是否是静态的ast虚拟dom type必须不等于2和3,pre必须为真
function isStatic(node) {
debugger
if (node.type === 2) { // node是一个表达式的话,直接就标记成【非静态】节点
return false
}
if (node.type === 3) { // text 文本节点或者是空注释节点 【静态】
return true
}
// 如果既不是表达式也不是文本节点,就说明这是一个标签,有子节点,就根据这个标签上的一些属性
// 或者标签名等判断是不是一个静态节点。
return !!( // 其它节点,如'div'标签
node.pre || //标记 标签是否还有 v-pre 指令 ,如果有则为真
(
!node.hasBindings && // 没有动态标记元素
!node.if && !node.for && // 没有 v-if 或者 v-for 或者 v-else
!isBuiltInTag(node.tag) && // 没有 slot,component
isPlatformReservedTag(node.tag) && // 是保留标签,html 原有的标签 或者svg标签
//判断当前ast 虚拟dom 的父标签 如果不是template则返回false,如果含有v-for则返回true
!isDirectChildOfTemplateFor(node) &&
//node的key必须每一项都符type,tag,attrsList,attrsMap,plain,parent等的字符串
Object.keys(node).every(isStaticKey)
)
)
}
6-2 markStaticRoots
作用:
第二步,标记标记静态根,逻辑和第一步差不多
源码:
function markStaticRoots(node, isInFor) {
debugger
if (node.type === 1) {
if (
node.static || //静态节点
node.once // v-once 只渲染一次节点。
) {
node.staticInFor = isInFor;
}
if (
node.static && //如果是静态节点
node.children.length && //如果是有子节点
!(
node.children.length === 1 && //如果只有一个子节点
node.children[0].type === 3 //文本节点
)) {
node.staticRoot = true; //标记静态根节点
return
} else {
node.staticRoot = false;
}
if (node.children) {
for (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(
node.children[i],
isInFor || !!node.for
);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(
node.ifConditions[i$1].block,
isInFor
);
}
}
}
}
最后,我们看一下,打完标记的虚拟dom:
7. generate
7-1. 基本信息
作用:
generate,生成render表达式。
经过前面对AST进行了优化之后,需要将整个AST变成一个可执行的代码块,也就是render函数。于是模板编译器使用了generate对AST进行了代码生成。
源码:
var createCompiler = createCompilerCreator(
function baseCompile(
template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
options // 这里已经baseOptions和options的合并项了
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 循环递归虚拟node,标记是不是静态节点
// 根据node.static或者 node.once 标记staticRoot的状态
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render, //code 虚拟dom需要渲染的参数函数
staticRenderFns: code.staticRenderFns //空数组
}
}
返回的code为下面的格式,他分为渲染函数render和静态渲染函数staticRenderFns
{
render: "with(this){return _c('div',{attrs:{\"id\":\"app\"}},
[_c('div',[_v(\"我是静态节点\")]),_v(\" \"),(showDom)?_m(0):_e()])}",
staticRenderFns: ["with(this){return _c('div',[_v(\"我只渲染一次\")])}"]
}
generate
function generate(
ast,
options
) {
// 生成状态
// * 扩展指令,on,bind,cloak,方法
// * dataGenFns 获取到一个数组,数组中有两个函数genData和genData$1
var state = new CodegenState(options);
/**
* 根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,
* 或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
* code,例:"_c('div',{attrs:{"id":"app"}},[_c('div',[_v("我是静态节点")]),_v(" "),(showDom)?_m(0):_e()])"
*/
debugger
var code = ast ? genElement(ast, state) : '_c("div")';
debugger
return {
//with 绑定js的this 缩写
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns //空数组
}
}
先看下生成的state,dataGenFns是个数组,数组中有两个函数genData和genData$1
7-2. CodegenState
作用:
扩展指令,on,bind,cloak,方法,dataGenFns 获取到一个数组,数组中有两个函数genData和genData$1
源码:
var CodegenState = function CodegenState(
options // 基础配置
) {
this.options = options;
this.warn = options.warn || baseWarn; //警告日志输出函数
// 拿到数组中,所有key为transformCode的对象项组成的数组 []
this.transforms = pluckModuleFunction(options.modules, 'transformCode');
//获取到一个数组,数组中有两个函数genData和genData$1 [ƒ genData(el), ƒ genData$1(el)]
this.dataGenFns = pluckModuleFunction(options.modules, 'genData');
// 扩展指令,on,bind,cloak,方法
this.directives = extend(
extend(
{},
baseDirectives
),
options.directives
);
//保留标签 判断是不是真的是 html 原有的标签 或者svg标签
var isReservedTag = options.isReservedTag || no;
//不是原生标签就也许是组件
this.maybeComponent = function (el) {
return !isReservedTag(el.tag); // 原生标签
};
this.onceId = 0;
//静态渲染方法
this.staticRenderFns = [];
};
7-3. genElement
作用:
genElement的逻辑先对当前元素的属性部分进行了代码生成,比如v-if、v-for等。 之后又对元素的body部分进行了代码块生成
源码:
function genElement(
el, //ast对象或者虚拟dom
state //渲染虚拟dom的一些方法
) {
debugger
if (el.staticRoot && !el.staticProcessed) {
//将子节点导出虚拟dom 渲染函数的参数形式。静态渲染
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
//参考文档 https://cn.vuejs.org/v2/api/#v-once
// v-once
// 不需要表达式
// 详细:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能
// <!-- 单个元素 -->
// <span v-once>This will never change: {{msg}}</span>
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
// v-for
//判断标签是否含有v-for属性 解析v-for指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { //判断标签是否有if属性
// v-if
//判断标签是否含有if属性 解析 if指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
//标签是模板template
//获取虚拟dom子节点
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
//如果标签是插槽
return genSlot(el, state)
} else {
// component or element
//组件或元素
var code;
if (el.component) { //如果是组件
//创建一个虚拟dom 的参数渲染的函数
code = genComponent(
el.component,
el,
state
);
} else {
var data = el.plain ? //如果标签中没有属性则这个标志为真
undefined :
genData$2(el, state);
var children = el.inlineTemplate ? //是不是内联模板标签
null :
genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
//返回 虚拟dom需要的参数js渲染函数
return code
}
}
7-4. genStatic
作用:
打静态标记
源码:
function genStatic(el, state) {
debugger
//标记已经静态处理过
el.staticProcessed = true;
//添加渲染函数
// 由于上面设置el.staticProcessed为true,这时候递归调用genElement,则会进入
// 判断的else if里面
state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));
//返回虚拟dom渲染需要的参数格式
return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
}
7-5. genOnce
作用:
源码:
function genOnce(el, state) {
//标志已经处理过的
el.onceProcessed = true;
//【说明1】
if (el.if && !el.ifProcessed) {
//判断标签是否含有if属性
return genIf(el, state)
} else if (el.staticInFor) {
var key = '';
var parent = el.parent;
while (parent) {
if (parent.for) {
key = parent.key;
break
}
parent = parent.parent;
}
if (!key) {
"development" !== 'production' && state.warn(
"v-once can only be used inside v-for that is keyed. "
);
//genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
return genElement(el, state)
}
//genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
} else {
//将子节点导出虚拟dom 渲染函数的参数形式
return genStatic(el, state)
}
}
【说明1】,el.if ,el.if定义在function processIf函数中
//获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
function processIf(el) {
var exp = getAndRemoveAttr(el, 'v-if'); //获取v-if属性
if (exp) {
// <div v-if='showDom'>我只渲染一次</div>,这里拿到的el.if就是'showDom'
el.if = exp;
addIfCondition(el, { //为if指令添加标记
exp: exp,
block: el
});
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
var elseif = getAndRemoveAttr(el, 'v-else-if');
if (elseif) {
el.elseif = elseif;
}
}
}
7-6. genIf
作用:
解析 if指令中的参数 如果存在,会返回一个三元表达式,这里就是v-if的源码了
源码:
function genIf(
el,
state,
altGen,
altEmpty
) {
el.ifProcessed = true; // avoid recursion 标记已经处理过 避免递归
//el.ifConditions.slice() if条件参数
//解析 if指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions(
conditions, //[{exp: view中的if属性,block: el当前渲染的虚拟组件}] 例:[{exp: "showDom", block: {…}}]
state, // CodegenState 如function generate中通过new CodegenState(options)生成的 例:{dataGenFns: (2) [ƒ, ƒ]...}
altGen, // undefined 没看到哪里有入参
altEmpty // undefined 没看到哪里有入参
) {
debugger
if (!conditions.length) { //如果conditions 不存在 则返回一个空的虚拟dom参数
return altEmpty || '_e()'
}
var condition = conditions.shift(); //取第一项
/**
* 判断if指令参数是否存在
* 如果存在则递归condition.block 数据此时ifProcessed 变为true 下次不会再进来
*
*
*/
if (condition.exp) {
/**
* genTernaryExp(condition.block) 例: "_m(0)"
* genIfConditions(conditions, state, altGen, altEmpty) 例:"_e()"
* conditions为删除了第一项后剩余的数组项
* 最终表达式:(showDom)?_m(0):_e()
*/
return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
} else {
return ("" + (genTernaryExp(condition.block))) //没有表达式直接生成元素 像v-else
}
//如果用v-once生成像(a)?_m(0):_m(1)这样的代码
function genTernaryExp(el) {
//数据此时ifProcessed 变为true 下次不会再进来
return altGen ?
altGen(el, state) //altGen 一个自定义函数吧
: el.once ? //静态标签标志 存在么 不存在
genOnce(el, state) //导出一个静态标签的虚拟dom参数
: genElement(el, state) //递归el 数据此时ifProcessed 变为true 下次不会再进来
}
}