一:前言
众所周知,Vue框架是一个渐进式的响应式框架,它可以做到数据的实时监听以及动态修改,而在vue的底层中究竟是如何实现的呢?在本篇文章中将会一一介绍如何监听数据的变化,以及两层数据劫持等代码实现。本文是基于(1)来接着写的,有时间的小伙伴可以看一下(1)哦,当然不看也不会有太大的影响。链接如下
二:响应式源码
1、项目目录
以下是项目的目录,其中dist文件夹是打包后生成的,index.html是新增的,也是实际的主页。
src文件夹下,index.js是打包后的入口文件,即去使用new Vue()时运行index.js文件,其余的是响应式的一些监听方法。接下来会对这些一一讲解。
2、src下的index.js文件
该文件是使用new Vue()时所调用的文件,这里主要是进行一个初始化的方法,比如挂载一些用户选项等,目前这里写的是一个初始化data的方法,代码如下。
import { initMixin } from "./init"
function Vue(options){ // options就是用户的选项
// debugger//可以调试代码
this._init(options)//默认调用_init()
}
initMixin(Vue)//扩展了init方法
export default Vue
3、src下的init.js文件
该文件是一个initMixin方法,用于进一步封装初始化的方法,为了方便理解,这里我们将this使用vm来代替,并且把用户的选项挂载到vm实例上,然后进行一个状态的初始化。
import { initState } from "./state"
//这是个初始化的文件,通过这方法传入Vue然后进行初始化操作,方便解耦
export function initMixin(Vue){//给Vue增加init方法的
Vue.prototype._init = function(options){ // 用于初始化操作
// 在vue中,vm.$options 就是获取用户的配置
// 很多api都是这样,使用$XXX来写,所以$开头都是vue自己的属性
const vm = this
vm.$options = options//将用户的选项挂载到实例上,即这些是data里的数据
//初始化状态
initState(vm)
}
}
4、src下的state.js文件
这个文件是用于初始化状态,首先进入initState()方法,判断用户的data里是否有数据,有数据的时候才会进行初始化。
而后在initData()中,由于Vue2的data可能是返回一个对象,也可能是返回一个方法,这是无法确定的,所以先进行数据的处理,将数据处理成一个对象。并且在这个方法中我们进行第一次劫持,将字段添加,使用户可以直接this.name调用而不需要this._data.name。而后进行数据的监听。
没有进行劫持的时候,调用data里的name实例,需要:this._data.name
进行第一次劫持后,只需要this.name就可以调用了
import { observe } from "./observe/index";
export function initState(vm){
const opts = vm.$options;//获取所有的选项
// if(opts.props){
// initProps()
// }
if(opts.data){
initData(vm)
}
}
function proxy(vm,target,key){
Object.defineProperty(vm,key,{ // vm.name
get(){
return vm[target][key] // vm._data.name
},
set(newValue){
vm[target][key] = newValue
}
})
}
function initData(vm){
// debugger
//假如用户输入的是函数,这里就是判断后变成对象
let data = vm.$options.data;//data可能是函数,也可能是对象
data = typeof data === 'function' ? data.call(vm) : data//这样就处理了data数据
// console.log(data)
//data是用户的对象,将放回的对象放在了实例的_data上,并且进行观测
vm._data = data
//对数据进行劫持 vue2里采用了一个api defineProperty
observe(data)
// 将vm._data用vm来代理
for(let key in data){
proxy(vm,'_data',key)
}
}
5、src下observe的index.js文件
该文件主要是对数据进行劫持,给对象里的属性添加get和set方法,并且判断是否已经进行过劫持。实现思路如下:
首先进入observe()方法,进行一系列判断后,如果没有被劫持过,那么我们去new一个Observe()类的对象,这时候会调用这个类里面的构造器,其中传入的参数data就是我们在state.js中处理过一次的对象。
在这个构造器里,我们先将他本身挂载到一个不可枚举的__ob__实例上,然后判断这个数据是否为数组,这时候有两种情况如下:
1. 是数组的时候,我们需要重写常用的七个变异方法,这里重写的代码会放在下面第六条。重写完方法后,由于数组中是可以存放对象的,那么我们也要将数组里的对象劫持,所以定义一个for循环,将数组里的数据判断是不是对象,如果是对象的话重新进行劫持。
注意:因为在oberve()方法中进行过判断,所以当数组里存放的不是对象的时候会return出去。
2.是对象的时候,这种情况就比较简单了,只需要进行数据劫持,赋予get和set方法就好了,当然同理对象中可能嵌套对象,所以我们也是进行for循环来判断。
注意:由于这里是“重新定义属性”,所以在性能方面就有落后,因此Vue2的性能是不如Vue3的。
import { newArrayProto } from "./array";
class Observer {
constructor(data) {
// Object.defineProperty只能劫持已存在的属性,后面增加或者删除的属实是无法劫持的(vue2里会为此单独写一些api,比如$set $delete)
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 将__ob__变成不可枚举(循环的时候无法获取)
});
// data.__ob__ = this; // 给数据加了一个表示,如果数据上有__ob__,则说明该数据被检测过 将Observe的实例放在data的对象上
if (Array.isArray(data)) {//判断,如果是一个数组,就不需要劫持了
//这里重写数组中的七个变异方法,这几个方法是可以修改数组本身的
data.__proto__ = newArrayProto // 需要保留数组原有的特性,并且可以重写部分方法
this.observerArray(data) // 如果数组中放的是对象,可以监听到数据的变化
} else {
this.walk(data);
}
}
walk(data) { // 让这个data循环对象,让属性依次劫持
// '重新定义'属性,因为是重新定义,所以性能受到影响
Object.keys(data).forEach(key => defineReactive(data, key, data[key]));
}
observerArray(data) { // 观测数组
data.forEach(item => observe(item))
}
}
// 第一个参数,要定义的是谁,key值,value值
export function defineReactive(target, key, value) { // 闭包 属性劫持
observe(value) // 采用递归 如果这个值是对象,会递归再次进行一次劫持
Object.defineProperty(target, key, {
get() { // 取值的时候执行get
return value
},
set(newValue) { // 修改的时候执行set
// 判断相同的时候,就不进行操作,不同的时候,进行赋值
if (newValue === value) return
value = newValue
}
})
}
export function observe(data) {
//对这个对象进行劫持
//先判断是不是对象
if (typeof data !== 'object' || data == null) {
return;//只对对象进行劫持
}
if(data.__ob__ instanceof Observer){ // 如果有这个实例,说明已经被代理过了
return data.__ob__
}
//如果一个对象被劫持过了,那就不惜要再次劫持了(要判断是否被劫持过,可以增添一个实例来判断是否被劫持)
return new Observer(data);
}
6、src下observe的array.js文件
这个文件是重写了数组的变异方法,所有能够修改原数组的方法都是变异方法,下面一共是七个,因为不能直接覆盖数组的原型,所以我们采取新数组的方式进行重写。
//对数组中的部分方法进行重写
let oldArrayProto = Array.prototype; // 获取数组的原型
// newArrayProto.__proto__ = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto)
let methods = [//找到所有的变异(能修改原数组的方法都是变异方法)方法
'push',
'pop',
'shift',//向后追加
'unshift',
'reverse',//反序
'sort',//排序
'splice'//删除
] // concat slice都不会改变原来的数组
methods.forEach(method=>{
// arr.push(1,2,3)
newArrayProto[method] = function(...args){//这里重写了数组的方法
// 内部调用原来的方法,这叫做函数的劫持 ,切片变成
// push.call(arr)
const result = oldArrayProto[method].call(this,...args)//这里的this就是19行的arr,
// console.log(method)
//需要对新增的数据再次进行劫持
let inserted
let ob = this.__ob__;
switch(method){
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break
default:
break;
}
console.log(inserted) // 新增的内容
if(inserted){
// 对新增的数组再次进行观测
ob.observerArray(inserted);
}
return result
}
})
三:总结
vue2的响应式可以说是入门源码的第一步,所以是一个十分重要的知识点,学习源码可以让我们对编写代码有一个更加清晰的了解和认知,同时在开发过程中遇到了bug能够更容易发现产生的原因。好啦本文大概就这些内容,如果有写的不好的地方希望能够指正出来,祝大家能够写出自己满意的代码~需要代码的可以私聊,暂时还没有上传git。