当前篇:vue源码分析【1】-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>
</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>
1. 前言
此篇文章旨在说明new Vue之前
做了哪些操作
,给Vue构造函数挂载了哪些方法和属性,由于没有进入new Vue,所以不会执行挂载函数
,而仅仅是挂载操作。另外,在new Vue之前的代码,意味着此时都不会
拿到html
中new Vue的入参el,beforeCreate
等等。
因此,此文章最大的目的是作为后续文章的参考,注意,由于挂载函数不执行,所以我们在讲解挂载函数时会省略一些代码,详细代码将在后续文章中逐步讲解。
本文的结构依据点,线,面
来展开。
- 点即函数的作用
- 线即函数的执行流程
- 面即源码的详细解读
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
2. 整体流程
// 定义 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)
3. initMixin
作用:
初始化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 // 此是默认的
}
4. stateMixin
4-1. 基本信息
作用:
数据绑定,挂载 d a t a , data, data,props, s e t , set, set,delete,$watch方法
流程:
- 分别定义dataDef对象和propsDef对象,给它们各自挂载一个
get
方法,分别返回this._data和 this._props - 再给dataDef和propsDef分别挂载一个
set
方法,当改变数据时发出警告。- dataDef的set规定:
不能直接替换实例根$data
。 - propsDef的se规定:
不能更改props的值
。
- dataDef的set规定:
- 通过
Object.defineProperty
给Vue原型挂载对应data和props的get和set方法。 - 为确保更新数据或者删除数据更更新视图,挂载 s e t 和 set和 set和delete
- 挂载$watch
源码:
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();
}
};
}
4-2 说明
4-2-1 说明1:Object.defineProperty()
Object.defineProperty(Vue.prototype, ‘$data’, dataDef)的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。通过get进行依赖收集,而每个set方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
Object.defineProperty(obj, prop, desc)
- obj 需要定义属性的当前对象
- prop 当前需要定义的属性名
-
desc 属性描述符
一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性
同样,执行完这一行代码,我们再次打印Vue.prototype,如下:
{
_init: ƒ (options),
constructor: ƒ Vue(options),
__proto__: Object,
$data: undefined, // 本次加上的
get $data: ƒ (), // 本次加上的
set $data: ƒ (newData) // 本次加上的
}
4-2-2 说明2:Vue.prototype.$set = set
这个set方法如下:
function set(target, key, val) {...}
它做了如下动作:
- 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
- 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
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)来改变:
- target: 要改变的对象或者数组
- key: 数组的下标或者对象的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);
},
},
此时,我们再回到页面,可以看到控制台和页面都同步更新了
4-3. 结果
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,
}
5. eventsMixin
5-1. 基本信息
作用:
初始化事件绑定方法
流程:
- 依次挂载
$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) {...}
}
5-2. $on
作用:
监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm
流程:
- 如果传入的事件名event为数组,则遍历,递归调用$on
- 否则,如果传入的事件名event为单个事件名,把所有事件拆分存放到
_events
数组中,比如传入的event
为click
,fn
为addNum
,vm._events['click']
原本是[]
,则结果如下:vm._events = { click: [addNum] },然后返回vm- 如果事件名是以 hook: 开头的(如hook:mount=‘getNum’),则标记为vue系统内置钩子函数(比如vue 生命周期函数等)
源码:
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
};
5-3. $once
作用:
监听Vue自定义事件,先把传入的事件在vm上解绑,然后执行传入的事件。
虽然最终也触发$on,但once比较特殊,只触发一次,然后就会移除监听器
流程:
- 创建一个封装事件on,用于解绑和执行事件
- 将传入的事件挂载到封装事件on上,然后调用
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
};
5-4. $off
作用:
vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件。
返回删除了_events属性中相应事件的vm。
流程:
- 如果没有参数的情况下,移除所有监听器
- 否则,如果传入的event是数组事件 则循环回调递归Vue.$off
- 否则,vm._events中,不存在传入的事件名
- 否则,如果回调函数不存在则清空 _events对应的事件名中所有事件
- 否则,在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数
源码:
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