在上一篇已经讲了使用Rollup搭建开发环境,从本篇开始就可以手撸vue源码了。话不多说从对象属性劫持开始。
最开始学习Vue的时候,先在页面引入vue.js文件,然后创建Vue实例,并接收参数。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./vue.js"></script>
<script>
const vm = new Vue( // 创建 Vue 的一个实例
{ // 参数
data() {
return {
a: '1',
b: 2,
c: 3
}
},
computed: {
addbc() {
return b + c;
}
},
methods: {
addab() {
return a + b;
}
}
}
)
</script>
</body>
</html>
由此看来,vue.js无非是创建了一个Vue的构造函数,且这个构造函数就是对接收的参数做一系列操作。
所以,在这里我们先写一个Vue的构造函数:
// index.js 入口文件
function Vue(options) { // options就是接收的参数,即用户的选项
// 一系列操作
}
export default Vue
然后我们开始顺一顺构造函数的一系列操作是怎么操作的。首先得初始化操作,也就是把用户的选项挂载到当前实例上。
为了更好的对比,先贴出没有挂载之前的实例(如果不做任何初始化操作,当前实例就什么都没有):
// index.js 入口文件
import { initMixin } from "./init"
function Vue(options) { // options就是用户的选项,new Vue 的时候就会执行这个 Vue 的构造函数
this._init(options) // 初始化操作
}
initMixin(Vue); // 给Vue扩展init方法
export default Vue
// init.js 初始化操作
export function initMixin(Vue) { // 给Vue增加init方法的
Vue.prototype._init = function(options) { // 用于初始化操作
const vm = this;
vm.$options = options; // 将用户的选项挂载到实例上
}
}
挂载之后的实例,可以看到用户传的参数都放到实例的$options内:
挂载完用户的选项之后,接下来就是初始化状态(就是data、props、computed、watch、methods等的初始化)。我们知道Vue的核心特点是响应式的数据变化,也就是数据的取值和更改值我们要监控到,然后更新视图,所以本篇先撸一撸data的初始化状态。
通常data可能是个方法,也可能是个对象,所以要想拿到data需要判断下data = typeof data === 'function' ? data.call(vm) : data,然后丢到实例vm上,方便存取。这个时候存取都在vm._data上存取,通常我们使用的时候都直接在vm上存取,所以需要将vm._data用vm来代理。
// init.js 初始化操作
import { initState } from "./state";
export function initMixin(Vue) { // 给Vue增加init方法的
Vue.prototype._init = function(options) { // 用于初始化操作
const vm = this;
vm.$options = options; // 将用户的选项挂载到实例上
// 初始化状态(data、computed、watch、props、methods等等, 本篇以data为例)
initState(vm);
}
}
// observe/state.js
import { observe } from "./observe/index";
export function initState(vm) {
const opts = vm.$options; // 获取所有的选项
if(opts.data) {
initData(vm);
}
}
function proxy(vm, target, key) {
Object.defineProperty(vm, key, { // vm.age
get() {
return vm[target][key]; // vm._data.name
},
set(newVal) {
vm[target][key] = newVal;
}
})
}
function initData(vm) {
let data = vm.$options.data; // data可能是函数和对象
data = typeof data === 'function' ? data.call(vm) : data; // this指向当前实例
vm._data = data; // 将返回的对象放到实例vm上
// 对数据进行劫持 vue2 里采用了一个 api defineProperty
observe(data); // 观测data,主要是为了实现响应式
// 将vm._data 用vm来代理(取_data上的属性就可以直接vm.取)
for(let key in data) {
proxy(vm, '_data', key);
}
}
代理完了之后就到了最重要的实现响应式,想要实现响应式,就需要观测(监听)data内的所有属性(包括对象的所有属性。
// ./observe/index.js
class Observer {
constructor(data) {
// Object.defineProperty只能劫持已经存在的属性,后增的或者删除的Object.defineProperty是不知道的(vue里面会为此单独写一些api:$set $delete)
this.walk(data);
}
walk(data) { // 循环对象 对属性一次劫持
// “重新定义”属性 性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key])) // defineReactive 响应式设置
}
}
export function defineReactive(target, key, value) { // 闭包 属性劫持
observe(value); // 对所有的对象都进行属性劫持
Object.defineProperty(target, key, {
get() { // 取值的时候执行get
console.log("用户取值了")
return value
},
set(newValue) { // 修改的时候执行set
if(newValue === value) return
console.log("用户设置值了")
value = newValue
}
})
}
export function observe(data) {
// 对这个对象进行劫持,所以需要判断下data是否是个对象,是对象才能劫持
if(typeof data != 'object' || data == null) {
return; // 只对对象进行劫持
}
// 如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
return new Observer(data);
}