【系列 1】手写vue响应式原理

手写vue响应式原理

首先我们看看原生 vue 做了什么

<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
<script>
    const vm = new Vue({
        data: {
            name: '小米',
            arr:[{a:2}] // 检测深度响应式
        }
    })
    console.log('vm', vm)
</script>

可见 vm 第一层与 _data 内都能获取到 data 数据, 并且其数值都进行了 get set 响应式处理
在这里插入图片描述
在这里插入图片描述
接下来我们动手实现 vue 的这个步骤!

vue 响应式原理

目录结构:

├── dist   // 打包存放的目录
├── public  // 静态资源文件
│   └── index.html
├── src
│   ├── observe
│   │  ├── array.js  // 操作数组数据响应式
│   │  └── index.js  // 数据响应式
│   ├── index.js     // 导出 vue 构造函数
│   ├── init.js      // 初始化vue状态
│   └── state.js     // 据不同属性进行初始化操作
├── .babelrc         // babel打包配置
├── package.json
└── rollup.config.js 

使用Rollup搭建开发环境-目录说明

/src/index.js (导出vue构造函数)

import {initMixin} from './init';

function Vue(options) {
    this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;

/src/init.js (init方法中初始化vue状态)

import { initState } from "./state";

export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        const vm = this;
        vm.$options = options // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项
        // 对于实例的数据源 props data methods computed watch
        initState(vm);
    }

/src/state.js (根据不同属性进行初始化操作)

这里开始对数据进行响应式处理

import { observe } from "./observe/index";

export function initState(vm) {
    const options = vm.$options

    // 后续实现计算属性 、 watcher 、 props 、methods
    if (options.data) {
        initData(vm);
    }
}

function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[source][key]
        },
        set(newValue) {
            vm[source][key] = newValue;
        }
    })
}

function initData(vm) {
    let data = vm.$options.data;
    // 如果是函数就拿到函数的返回值 否则就直接采用data作为数据源
    data = vm._data = typeof data === 'function' ? data.call(vm) : data

    // 属性劫持 采用defineProperty将所有的属性进行劫持

    // 我期望用户可以直接通过 vm.xxx 获取值, 也可以这样取值 vm._data.xxx
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    observe(data)
}

src/observe/index.js (实现数据响应式)

对数据递归操作实现所有数据都响应式

import arrayPrototype from "./array";

class Observer{
    constructor(data){
        // 如果是数组的话也是用defineProperty会浪费很多性能 很少用户会通过arr[1000] = 1234
        // vue3 中的 polyfill 直接就给数组做代理了
        // 改写数组的方法,如果用户调用了可以改写数组方法的api 那么我就去劫持这个方法
        // 变异方法 push pop shift unshift reverse sort splice 
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false
        })
         // 如果有__ob__属性 说明被观测过了
        // 修改数组的索引和长度是无法更新视图的
        if(Array.isArray(data)){
            // 需要重写这7个方法
            data.__proto__ = arrayPrototype; 
            // 直接将属性赋值给这个对象
            // 如果数组里面放的是对象类型 我期望他也会被变成响应式的
            this.observeArray(data);
        }else{
            this.walk(data)
        }
    }
    observeArray(data){
        data.forEach(item=> observe(item)); //如果是对象我才进行观测了  
    }
    walk(data){ // 循环对象 尽量不用for in (会遍历原型链)
        let keys = Object.keys(data); // [0,1,2]
        keys.forEach(key=> { //没有重写数组里的每一项
            defineReactive(data,key,data[key])
        })
    }
}
// 性能不好的原因在于 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理 性能差
function defineReactive(data,key,value){ //  闭包
    // 属性会全部被重写增加了get和set
    observe(value); // 递归代理属性
    Object.defineProperty(data,key,{
        get(){ // vm.xxx
            return value;
        },
        set(newValue){ // vm.xxx = {a:1} 赋值一个对象的话 也可以实现响应式数据
            if(newValue === value) return
            observe(newValue)
            value = newValue;
        }
    })
}
export function observe(data) {
    if(typeof data !== 'object' || data == null){
        return ; // 如果不是对象类型,那么不要做任何处理
    }
    if(data.__ob__){
        // 说明这个属性已经被代理过了
        return data
    }

    // 我稍后要区分 如果一个对象已经被观测了,就不要再次被观测了
    // __ob__ 标识是否有被观测过

    return new Observer(data)
};

src/observe/array.js (操作数组数据响应式)

目的就是对数组内新增的数据再次进行观测 避免里面出现没有监听到的对象数据

let oldArrayPrototype = Array.prototype;
// arrayProptotype.__proto__ = Array.prototype;

let arrayPrototype = Object.create(oldArrayPrototype);
let methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
]
// 重写数组 7 方法 (目的就是对新增的数据再次进行观测 避免里面出现没有监听到的对象数据)
// 如 arr[1000] = 1234 更改数组会响应式是通过 $set 实现的
methods.forEach(method => { // 用户调用push方法会先经历我自己重写的方法,之后调用数组原来的方法
    arrayPrototype[method] = function(...args) {
        let inserted;
        let ob = this.__ob__;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args; // 数组
                break;
            case 'splice': // arr.splice(1,1,xxx)
                inserted = args.slice(2); // 接去掉前两个参数
            default:
                break
        }
        if (inserted) {
            // 对新增的数据再次进行观测
            ob.observeArray(inserted)
        }
        return oldArrayPrototype[method].call(this, ...args)
    }
})
export default arrayPrototype
手写 vue 代码仓库链接
GitHubhttps://github.com/shunyue1320/vue-resolve/tree/vue-01
Giteehttps://gitee.com/shunyue/vue-resolve/tree/vue-01/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值