Hello, 这里是Link🤩, 从今天开始, 我打算写一份Vue源码分析的系列文章, 但是请放心, 绝对不会是单纯枯燥的源码, 我们从一些面试题, 场景出发, 我们每篇文章只去关注Vue实现的某些功能, 以及解决一些面试常问的问题. 之后再去理解具体的实现. 让你不必直接面对枯燥的源码.💪💪
后续, 我们会讲到Vue是如何用一个 watcher
去完成组件实例化, 计算属性, 监听属性等核心功能的, 并且你会理解到, Vue设计的很多精妙之处, 比如最为人熟知的响应式系统等, 一定会收获颇丰哦😋
Vue 实例化的流程图
这是我自己写的一份关于Vue的流程图笔记, 能够非常直观方便的了解整个实例化做的事情. 并且这张图已经放在我的Vue源码项目vue-core-analyse, 并且这个项目的源码有我个人读源码时的备注, 相信对你也会有一定的帮助😝, 欢迎star✨
这个图涵盖了整个Vue实例数据与组件实例化的流程
- 这个流程图是vscode插件
Draw.io Integration
提供的, 你需要下载本插件才能启用
场景
为什么可以直接通过
this.message
的形式去访问到data函数中的值?
<script src="../../dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#el',
data () {
return {
message: "Hello Vue"
}
},
mounted() {
const message = this.message
}
})
</script>
在我们看来, 似乎mouted
和 data
是两个不同的作用域, 我们应该先访问共同的作用域, 然后再去访问这个data
对象才合理🤔, 就像这样
mounted() {
const message = this.data.message
}
原理
实际上这个处理也很好理解, 我们一起从入口来看一下
Vue
对data
属性的处理
_init函数
是在Vue这整个项目初始化的时候通过initMixin(Vue)
挂载上去的, 当我们 new Vue({})
的时候会执行下面这个函数, 这个options
, 就是上面那个demo中我们传给 Vue 构造函数
的大对象, 里面有data
, mounted
等配置
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
// 要求 new 执行
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 👈
}
- 在 Vue 中的最后一行执行了一个
this._init(options)
函数就是Vue在被实例化的时候触发一系列行为 这里我对源码进行的删减, 我们只关注data
部分的初始化. 这样的一个_init
函数 做了一下几件事情
-
定义一个
vm
等于当前实例 -
初步挂载
$options
并合并配置选项这里我们先简单的理解为
vm.$options = options
-
执行
initState(vm)
初始化全部状态
步骤3是我们最需要关注的. 在这个函数中负责初始化配置项中的
props
methods
data
等属性
// src/core/intance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a flag to avoid this being observed
vm._isVue = true
// merge options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// expose real self
vm._self = vm
// 初始化状态
initState(vm)
}
- 这个函数这里也就到了我们的终极目标
initData(vm)
在这里负责初始我们的数据(data)
// src/core/intance/state.js
// 初始化状态
export function initState (vm: Component) {
const opts = vm.$options
// 基于我们options中存在的属性进行初始化
if (opts.data) {
// 初始化数据
initData(vm)
}
}
initData函数
首先会判断我们的data
是否为一个函数, 是的话执行他并返回一个对象. 如果不是则直接使用.- while循环首先对我们的
data
与pros
和method
做一层重名校验, 这里的逻辑十分简单. 我们无需关注 - 将这个对象赋值一份给
data
和vm._data
- 然后调用 proxy(vm, _data, key)
// src\core\instance\state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
/**
* 对data中的key与 props 和 method 做对比, 要求不能够 key 重复
*/
if (process.env.NODE_ENV !== 'production') {
//..
}
if (props && hasOwn(props, key)) {
//...
} else if (!isReserved(key)) {
// 代理
proxy(vm, `_data`, key) // 👈
}
}
}
重点在于proxy(vm, _data
, key)
- proxy函数首先为
sharedPropertyDefinition
对象定义了一个get
和set
方法 - 通过defineProperty去对vm对象做拦截操作
也就是说 当我们通过
vm
也就是我们常用的this.message
的形式 去访问数据时候, 会通过defineProperty
函数做的拦截操作将this._data.message
转发给我们(赋值同理)
// \src\core\instance\state.js
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
/**
* 为什么 在其他属性里, 可以直接通过 this.message 就能拿到 data 中的值?
* 答案就在这里, vue 在 初始化 data 的时候会通过这个代理函数
* 将 data 中的 key 值直接放到 vm 实例上进行监控,然后基于上面的对象进行监控
* 访问 this.message 相当于访问了 this._data.message
*/
Object.defineProperty(target, key, sharedPropertyDefinition)
}
流程图展示
new Vue({
el: '#app',
data:{
msg: 'parent-Vue'
},
mounted () {
console.log(this.msg);
console.log(this._data.msg); // 当然并不推荐通过这种方式访问数据
}
})
感谢😘
如果觉得文章内容对你有帮助:
-
❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁
往期: