Vue2源码解析(2)——响应式详解

一:前言

        众所周知,Vue框架是一个渐进式的响应式框架,它可以做到数据的实时监听以及动态修改,而在vue的底层中究竟是如何实现的呢?在本篇文章中将会一一介绍如何监听数据的变化,以及两层数据劫持等代码实现。本文是基于(1)来接着写的,有时间的小伙伴可以看一下(1)哦,当然不看也不会有太大的影响。链接如下

        vue2源码解析(1)——rollup详解-CSDN博客

二:响应式源码 

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暴怒的代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值