首先我们准备一份测试代码:
在dist/index.html文件下:
引入我们自己的vue.js,创建一个Vue类的实例
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>Documenttitle>
head>
<body>
<div id="app" style="color:aqua; background: yellow">
<div style="color: red">{{ firstname }}div>
div>
<script src="vue.js">script>
<script>
//响应式的数据变化,数据变化了可以监控到
//数据的取值 和 更改值我们要监控到
const vm = new Vue({
data: { //代理数据
firstname: '珠',
lastname: '峰'
},
el: '#app', //将数据解析到el元素上
computed: {
fullname(){
return this.firstname + this.lastname;
}
}
})
script>
body>
html>
在src/index.js 文件中初始化代码:
在vue源码中,并没有使用一个vue的class类,而是使用了一个构造函数,然后在构造函数的原型上增加方法。
function Vue(options){ //options是用户的选项
this._init(options)
}
Vue.prototype._init = function(){ //扩展方法
}
export default Vue;
思路是将扩展方法抽离出来,封装成每个文件。
接下来创建一个init.js,用来初始化文件。
//init.js
export function initMixin(Vue){
Vue.prototype._init = function(){ //扩展方法
}
}
然后在index.js文件中引入init.js文件:
//index.js
import { initMxin } from './init';
function Vue(options){ //options是用户的选项
this._init(options)
}
initMxin(Vue); //扩展了init方法,执行该函数,Vue构造函数原型上就有了_init方法
export default Vue;
接下来我们就要开始在init.js文件中编写初始化操作的相关代码了。
首先我们需要将用户传入的数据绑定到vue上。
默认vue的属性使用 开头: 开头: 开头:options, $attr, $set…
开始初始化状态:就是挂载属性,方法,计算属性… 封装一个initState函数,参数为我们的vue实例vm
如何初始化:将用户的数据绑定到vm实例上 -> vm._data = vm.$options.data
我们的options上就是用户传入的数据
然后我们对data数据进行劫持 -> defineProperty
劫持完毕后,访问不太方便:vm._data.name。
如何解决?将 vm._data 用 vm 来代理
//将 vm._data 用 vm 来代理
for(let key in data){
proxy(vm, '_data', key);
}
实现将 vm._data 用 vm 来代理的函数:
//将 vm._data 用 vm 来代理
function proxy(vm, target, key){
Object.defineProperty(vm, key, {
get(){
return vm[target][key];
},
set(newValue){
vm[target][key] = newValue
}
})
}
一个问题:
关于将 vm._data 用 vm 来代理:
我们对data进行劫持,关于data的这一坨代码进行劫持完毕后,我们需要访问到这些数据:
如何访问?我们当时是将data从$options中取出,然后赋给了一个属性_data上,那么也就是说,我们的访问data是通过 vm.-data
实现的,为了方便,我们再次进行代理。
//init.js
export function initMixin(Vue) { //就是给Vue增加init方法的
Vue.prototype._init = function(options){ //用于初始化话操作
// vm.$options 就是获取用户的配置
const vm = this;
vm.$options = options; //将用户的选项挂载到实例上
//初始化状态:就是挂载属性,方法,计算属性...
initState(vm);
}
}
function initState(vm){
const opts = vm.$options; //获取所有选项
if(opts.data) { //是否有data属性,执行初始化data的函数
initData(vm);
}
if(opts.computed){
initComputed(vm);//初始化计算属性
}
//这后面还可以往后续...
if(opts.watch){
initWatch(vm);
}
}
//执行初始化data的函数
function initData(vm){
let data = vm.$options.data; //data可能是函数或者对象
// 这里我们只是拿到了用户的数据,但是得把数据绑定到vm上,如何绑定呢?用 _data
data = typeof data === 'function' ? data.call(vm) : data; //data是用户返回的对象
vm._data = data //我将返回的对象放到了_data上
//现在已经成功绑定了,但是访问不太方便:vm._data.name
//怎么变方便呢? -> 将 vm._data 用 vm 来代理
// 数据绑定完之后该干什么呢?接下来是对数据进行劫持 vue2里采用了一个api defineProperty
observe(data)
//将 vm._data 用 vm 来代理
for(let key in data){
proxy(vm, '_data', key);
}
}
//将 vm._data 用 vm 来代理
function proxy(vm, target, key){
Object.defineProperty(vm, key, {
get(){
return vm[target][key];
},
set(newValue){
vm[target][key] = newValue
}
})
}
上面说到,我们对数据进行初始化,并且,实现了通过vm.data来访问。
在初始化的过程中,我们要对数据进行劫持。
这里使用observe(data)函数。
export function observe(data) {
//只对对象进行劫持
if(typeof data !== 'object' || data == null){
return;
}
//如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个属性,用属性来判断是否被劫持过)
//...
return new Observer(data);
}
接下来我们声明一个Observer的类,用于劫持data
class Observer { // 观测值
constructor(value){
//Object.defineProperty只能劫持已经存在的属性(vue里面会为此单独写一些api $set $delete)
this.walk(value);
}
walk(data){ // 让对象上的所有属性依次进行观测
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++){
let key = keys[i];
let value = data[key];
defineReactive(data,key,value);
}
}
}
function defineReactive(data,key,value){
observe(value); //如果值是对象的话,再次进行劫持(就是对象里面包含着对象) -> 递归
// 使用 Object.defineProperty来进行劫持
Object.defineProperty(data,key,{
get(){
return value
},
set(newValue){
if(newValue == value) return;
observe(newValue);//如果设置的值是对象,那就需要继续观测
value = newValue
}
})
}
上面我们对对象进行了劫持,现在要开始对数组进行劫持。
数组不可能对数组的每一项都进行代理,这样会浪费性能,并且用户很少通过vm.arr[888]
的方式来修改数组(即用户很少直接使用索引来修改数组)。
一般修改数组的方法:push shift…
在进入constructor函数中时,上面我们是直接开始劫持,现在我们修改一下,在劫持之前进行判断:是数组还是对象。
如果数组的某一项是对象,我们也需要监测
实现数组的监测:我们需要重写数组的方法,使用户调用数组的常见方法(push unshift pop…)时,我们可以监测到
import {arrayMethods} from './array';
class Observer { // 观测值
constructor(value){
if(Array.isArray(value)){
value.__proto__ = arrayMethods; // 重写数组原型方法
this.observeArray(value); //这里实现了对数组某一项是对象进行监测的情况
}else{
this.walk(value);
}
}
observeArray(value){
for(let i = 0 ; i < value.length ;i ++){
observe(value[i]); //如果数组的某一项是对象,那就继续进行监测
}
}
}
重新数组原型方法
let oldArrayProtoMethods = Array.prototype;//获取数组的原型
export let arrayMethods = Object.create(oldArrayProtoMethods);//这里就实现了保留数组原有的方法
let methods = [//找到所有变异方法(这些方法会修改原数组)
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
];
methods.forEach(method => {
arrayMethods[method] = function (...args) { //这里重写了数组的方法
const result = oldArrayProtoMethods[method].apply(this, args); //内部调用原来的方法
let inserted;//我们需要对新增的数据再次进行劫持,该变量存放新增的数据值
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2)
default:
break;
}
if (inserted){
// 对新增的每一项进行观测
//...
}
return result
}
})
在上面的代码中,我们提到: 需要对对新增的每一项进行观测。
那就需要继续调用Observer类上的observeArray(*data*)
方法,
但是我们在这里又调用不到,如何实现呢?
既然要对新增的数据进行观测:那么还是要调用最初监测数组的方法来对新增数据进行观测
那个新增的方法在c实例的observeArray函数,所以我们要拿到Observe类上的data
怎么拿? 在Observe类中,我们需要绑定了this到__ob__
属性上,即data.__ob__ = this;
,其中this指的就是Observe实例,这样我们就将Observe实例绑定到了data的__ob__
属性上,并且在这里:根据这里被调用的位置可知,这里的this就是class Observe的实例对象
所以,我们直接拿一个变量来承接Observe实例 -> let ob = this.__ob__;
(在上面已经声明过了)
此时ob
身上就有了Observer实例,那么也就有了observeArray函数,现在就可以实现对新增的数据进行观测了。
首先在Observer类上绑定一下__ob__
属性:
class Observer{
constructor(data){
data.__ob__ = this;//绑定了this到`__ob__`属性上
if(Array.isArray(data)){ //是数组
//...
}else{ //是对象
this.walk(data);
}
}
walk(data){
//...
}
}
然后在重新数组方法的遍历中实现对新增数据的观测
const ob = this.__ob__;
if (inserted) ob.observeArray(inserted); // 对新增的每一项进行观测
关于__ob__
属性
data.ob = this; //将this实例绑定到__ob__属性上
不仅是将this绑定到__ob__
属性上,方便再次对新增的数组内容进行观测(具体观测在array.js文件中,上面已经体现过了)
而且也给数据加了一个标识:因为在最初的入口中,我们需要判断当前数据是否被劫持过了,如果已经被劫持了,那就直接返回:
如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
既然是用一个实例来判断当前数据是否被劫持过了:
那么这个 __ob__
属性就是一个标识,如果数据已经被劫持了,那么它的实例身上一定有一个__ob__
的标识
//__ob__的具体代码在 observe(data)函数中体现
export function observe(data) {
//对对象进行劫持
if(typeof data !== 'object' || data == null){
return; //只对对象进行劫持
}
if(data.__ob__ instanceof Observer){ //说明这个对象被代理过了
return data.__ob__;
}
//如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
return new Observer(data); //我们返回的是一个被劫持过的对象
}
增加这个__ob__
属性,其实会有一点问题 -> 会产生死循环
因为给当前类的实例增加了这个属性的值是this代指当前对象
当我们对进行属性是对象时进行劫持并且遍历各个属性值时,走到defineReactive函数(这个函数是对对象的每一项进行监测的函数),进行set设置时,如果出现设置的值是一个新的对象,又会走observe函数,然后又会进到类的实例中,在类的实例中,又会对对象进行劫持,然后就会产生死循环
如何解决? 将这个属性变成不可枚举型的,
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false //将__ob__变成不可枚举型(循环时无法获取到)
})
//上面就是一个定义属性,不必要再写 data.__ob__ = this; 这句代码