手写vue2源码系列之初始化Vue及数据劫持和代理(二)

296 阅读7分钟

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

初始化导出Vue

image.png

image.png

image.png 搭建好之后,我们就开始准备来自己动手写vue2源码啦。

Vue基本结构搭建

我们知道,Vue在初始化的时候要做很多事,比如watch,methods,computed以及各种生命周期钩子,而这些方法肯定都是挂载在vue的原型上,因此我们先分类把结构处理好。

//src/index.js
import {initMixin} from './init'
function Vue(options){
    this._init(options);//在Vue的原型身上要增加一个_init方法,把配置对象挂载在实例身上
}
initMixin(Vue)
export default Vue
//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 };
//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:'张三'} } 即数据分为一层数据或者嵌套数据,当然我们实际工作中肯定是嵌套数据用的多啦,这里分为两个模块,先对一层数据进行劫持,再用递归对多层数据进行劫持。

// src/observer/object.js
// observer函数就是对data进行数据劫持

// data数据可能有如下类型,因此都要考虑到
// data{
// name:'张三',
// person:{name:'李四'},
// arr:[1,2,3],
// arr2:[{name:'张三'},{age:18}]
// }

export function observer(data) {
  return new Observer(data);
}

class Observer {
  constructor(data) {
    if (typeof data != "object" || data == null) {
      return data;
    }
       this.walk(data)
  }

  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) {
  observer(data_key); //递归
  Object.defineProperty(data, key, {
    get() {
      console.log("我被读取了");
      return data_key; //当想读取data中的值时,就return对应的值即可
    },
    set(newValue) {
      if (newValue === data_key) return; //如果设置的新值和旧值相同,就跳出去
      console.log("我被设置新值了");
      observer(newValue) //如果用户设置的值是对象,要对新设置的对象再进行数据劫持
      data_key = newValue;
    },
  });
 
}

递归劫持所有数据

我们把上面的walk函数中的代码修改为如下,即可进行一个简单的递归

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]);
    }
   
  }
}

image.png image.png

数据劫持已经完成了,本质就是利用 Object.defineProperty进行劫持,一旦被读取或者发生了变化,我们就可以监测到,这时我们再通知视图去更新即可。

对数组函数进行劫持

在上面的操作中,我们对数组和对象都进行了劫持,如上所述,其实我们可以通过vm._data.arr[1]=999来改变数组,而且也是可以被监测到的,但是对数组进行劫持是非常消耗性能的,这样对于性能来说是承担不起的,因此我们不对数组根据索引改变(vm._data.arr[1]=999)进行数据劫持

但是这里我们要做两件事:

  1. 对数组里的对象进行劫持,比如数组是[name:'张三'{}]
  2. 对数组的基本方法来进行劫持,比如push,pop,shift,unshift等

对数组里的对象进行劫持

class Observer {
  constructor(data) {
    if (typeof data != "object" || data == null) {
      return data;
    }
    if (Array.isArray(data)) {
      // 如果数据是数组,且数组里是对象,我们需要对里边的对象进行劫持,[{name:'张三'}]
      this.observeArray(data); // 处理数组对象
    } else {
      // 如果数据是对象
      this.walk(data);
    }
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let key of keys) {
           defineReactive(data, key, data[key]);
    }
  }
   observeArray(data) {
    data.forEach((item) => {
      // 对数组里的每个对象进行遍历,然后对其做数据劫持
      observer(item);
    });
  }
}

对数组函数进行劫持

首先要重写数组的函数,在observer文件夹下新建arr.js文件,暴露出一个ArrayMethods方法,在同目录下的index.js中引入这个方法,作为数组对象实例的新的原型方法

image.png

// observer/arr.js
//(1) 首先我们获取原来的数组方法
let oldArrayProtoMethods = Array.prototype;
//(2)创建一个ArrayMethods对象并且继承原来数组的方法
export let ArrayMethods = Object.create(oldArrayProtoMethods);
// (3)对方法进行劫持,以下方法是改变原数组的方法,所以需要对其劫持
let methods = ["push", "pop", "unshift", "shift", "splice"];
methods.forEach((item) => {
  ArrayMethods[item] = function (...args) {
    // {list:[]} list.push()
    console.log("劫持数组");
    let result = oldArrayProtoMethods[item].apply(this, args);//这里的this是谁?谁调用的就是谁,其实就是data
    // console.log(args) // [{b:6}]
    //问题 :数组追加对象的情况  arr.push({a:1})
    let inserted;
    switch (item) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.splice(2); //arr.splice(0,1,{a:6})
        break;
    }
    // console.log( inserted) 是数组新增的内容
    // console.log(this)
    let ob = this.__ob__; //这里是this是谁?就是谁调用push就是谁,因此其实就是data
    if (inserted) {
      ob.observeArray(inserted); // 对我们的添加的对象进行劫持
    }
    return result;
  };
});

我们在data身上定义一个__ob__,并且让其等于实例本身,这样实例就可以用this.methods身上的方法了,其实这个__ob__就是给上面代码中的函数用的,这样如果数组新增了一个对象,那么就可以对这个对象进行数据劫持了

除此之外,还有个意义就是,一旦数据被监测后,身上就会有个__ob__属性,就说明这个数据被监测过了,以后就不需要再被监测了

//observer/index.js
class Observer {
  constructor(data) {
    if (typeof data != "object" || data == null) {
      return data;
    }
        if(data.__ob__ instanceof Observer){
      // 说明这个对象被代理过了
      return data.__ob__
    }
    // Object.defineProperty只能劫持已经存在的属性
    //data.__ob__=this //这里给data增加一个属性,属性就是实例本身自己,这样实例就可以调用自身的方法,但是这里要写成不可枚举的
     Object.defineProperties(data,'__ob__',{
      value:this,
      enumerable:false
    })
    // 为什么用__ob__呢?因为怕用户重名哈哈
    // 这个属性的作用是什么呢?就是让data实例可以使用自己身上的一些方法
    if (Array.isArray(data)) {
      // ArrayMethods是重写数组中的7个可以修改数组本身的方法,需要保持原有的属性,再进行劫持
      data.__proto__ = ArrayMethods;
      this.observeArray(data); // 处理数组对象
    } else {
      // 如果数据是对象
      this.walk(data);
    }
  }

  walk(data) {
    //1. 首先获取data对象中的所有keys
    const keys = Object.keys(data);
    for (let key of keys) {
      // 2. 然后对data进行数据劫持,数据劫持的目的是进行数据视图的响应式更新
      // 比如,当数据发生改变,我们可以监听到,然后去更新视图,这样就做到了数据驱动视图
      defineReactive(data, key, data[key]);
    }
  }
  observeArray(data) {
    data.forEach((item) => {
      // 对数组里的每个对象进行遍历,然后对其做数据劫持
      observer(item);
    });
  }
}

综上,我们已经对数组和对象都进行了数据劫持,之后就可以在数据劫持的过程中,进行一些自己的操作,比如通知视图去修改内容

数据代理

数据代理就比较好理解了,我们在options传入的data,Vue首先帮我们放在了_data上,然后通过数据代理把_data上的数据又放在了this身上,这也是为什么我们可以通过this和vm访问到时数据,如this.a,vm.a

//initState.js
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);
  for (let key in data) {
    proxy(vm, "_data", key);
  }
}
//当读取vm.a时,就是读取vm._data.a,当设置vm.a时,就是设置vm._data.a
function proxy(vm, target, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[target][key];
    },
    set(newValue) {
      vm[target][key] = newValue;
    },
  });
}

至此,我们完成了数据劫持和数据代理,下面就是在数据劫持中,通知视图去更新页面啦~