我们知道在使用vue的时候,要new vue({}),因此可以得出vue就是一个构造函数,只是我们要传一个options作为参数即可,因此我们先把初始结构搭建起来。
初始化导出Vue


搭建好之后,我们就开始准备来自己动手写vue2源码啦。
Vue基本结构搭建
我们知道,Vue在初始化的时候要做很多事,比如watch,methods,computed以及各种生命周期钩子,而这些方法肯定都是挂载在vue的原型上,因此我们先分类把结构处理好。
js
//src/index.js
import {initMixin} from './init'
function Vue(options){
this._init(options);//在Vue的原型身上要增加一个_init方法,把配置对象挂载在实例身上
}
initMixin(Vue)
export default Vue
js
//src/init.js
import {initState} from './initState'
function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this; //声明一个vm=this,这样方便后续拿值,且由于this是实例对象,根据地址引用,操作vm就相当于操作this
vm.$options = options; //把new Vue传进来的options配置对象挂载在vue的实例身上
initState(vm);
};
}
export { initMixin };
js
//src/initState.js
import { observer } from "./observer/object.js";
export function initState(vm) {
let opts = vm.$options;
if (opts.props) {
//如果配置对象里传入了props
initProps(vm);
}
if (opts.data) {
//如果配置对象里传入了data
initData(vm);
}
if (opts.watch) {
//如果配置对象里传入了watch
initWatch(vm);
}
if (opts.computed) {
//如果配置对象里传入了computed
initComputed(vm);
}
if (opts.methods) {
//如果配置对象里传入了methods
initMethods(vm);
}
}
// 初始化data,注意这里要把data先放在_data中,方便后边做数据代理
function initData(vm){
let data = vm.$options.data;
// 判断这里data是对象形式还是函数形式
vm._data=data=typeof data=='function'? data.call(vm):data
// 这里定义好data之后,就需要对这个data进行数据劫持,在src文件夹下新建observe文件夹
observer(data)
}
function initProps(vm){
}
function initWatch(vm){
}
function initComputed(vm){
}
function initMethods(vm){
}
数据劫持
数据劫持的本质其实就是要监测对象的变化,然后告诉视图,我更新了,然后让视图更新即可。 数据劫持和代理的相关文章Vue原理学习 - 实现数据代理和数据劫持 - 掘金 (juejin.cn)
而Vue2的官方文档里说,Object.defineProperty只能监听对象的变化,而不能监听数组的变化,这其实是不对的,因为是可以监听数组的变化的,但是由于性能原因, Object.defineProperty
只能劫持已有属性,要监听数组变化,必须预设数组长度,遍历劫持,但数组长度在实际引用中是不可预料的,因此Vue2中没有采用 Object.defineProperty
对数组进行劫持,这也是为什么在Vue2中,且由于Object.defineProperty
有一个缺点,就是对象新增或者删除的属性无法被 set 监听到 只有对象本身存在的属性修改才会被劫持 ,所以在Vue2中才会有$set
这个方法来增加属性
参考文献: vue2为什么不用Object.defineProperty劫持数组 - 掘金 (juejin.cn)
第一层数据的劫持
我们知道,data是一个对象,其数据可能是这样: { name:'张三' }
也可能是这样 { person:{ name:'张三'} }
即数据分为一层数据或者嵌套数据,当然我们实际工作中肯定是嵌套数据用的多啦,这里分为两个模块,先对一层数据进行劫持,再用递归对多层数据进行劫持。
js
// src/observer/object.js
// observer函数就是对data进行数据劫持
export function observer(data) {
//1判断data是否是对象或者存在,因为Object.defineProperty只对对象有用
if (typeof data != "object" || data == null) {
return data;
}
Observer(data);
}
function Observer(data) {
walk(data); //对data进行遍历
}
function walk(data) {
//1. 首先获取data对象中的所有keys
const keys = Object.keys(data);
for (let key of keys) {
// 2. 然后对data进行数据劫持,数据劫持的目的是进行数据视图的响应式更新
// 比如,当数据发生改变,我们可以监听到,然后去更新视图,这样就做到了数据驱动视图
defineReactive(data, key, data[key]);
}
}
function defineReactive(data, key, data_key) {
if (typeof data !== "object" || data == null) {
return;
}
Object.defineProperty(data, key, {
get() {
console.log("我被读取了");
return data_key; //当想读取data中的值时,就return对应的值即可
},
set(newValue) {
if (newValue === data_key) return; //如果设置的新值和旧值相同,就跳出去
console.log("我被设置新值了");
data_key = newValue;
},
});
}

如上,我们完成了一层数据的劫持。
递归劫持所有数据
我们把上面的walk函数中的代码修改为如下,即可进行一个简单的递归
js
function walk(data) {
//1. 首先获取data对象中的所有keys
const keys = Object.keys(data);
for (let key of keys) {
// 2. 然后对data进行数据劫持,数据劫持的目的是进行数据视图的响应式更新
// 比如,当数据发生改变,我们可以监听到,然后去更新视图,这样就做到了数据驱动视图
if(typeof data[key]=='object'){
walk(data[key])//在这里进行递归
}else{
defineReactive(data, key, data[key]);
}
}
}
js
//index.html
<div id="app">hello Vue</div>
<script src="/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "张三",
person: { name: "李四" },
arr: [1, 2, 3, 4],
},
});
console.log(vm._data);
</script>


对数组进行劫持
在上面的操作中,我们对数组和对象都进行了劫持,但是对数组进行劫持是非常消耗性能的,因此这里我们要重新数组的原型方法,以至于不让数组被劫持
数据代理
数据代理就比较好理解了,我们在options传入的data,Vue首先帮我们放在了_data上,然后通过数据代理把_data上的数据又放在了this身上,这也是为什么我们可以通过this和vm访问到时数据,如this.a,vm.a