仿写vue依赖收集笔记__Vue.js
发布于 3 年前 作者 banyungong 1141 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

1.Vue的基本原理

https://blog.csdn.net/weixin_42429718/article/details/104472989

当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

由此仿写vue内部对数据的监听 1.png

  1. 基于发布订阅模式。
  2. 创建一个Observer来劫持所有属性,充当‘发布者’角色,通知变化。
  3. 创建一个Watcher充当订阅者角色,订阅data数据,当data数据发生变化的时候,更新视图。
  4. 创建一个Dep的类用于给Watcher和Observer之间解耦合。
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el);
        this.$data = options.data
        observer(this.$data)

        //获取插值表达式
        let mustach = this.$el.innerText;
        console.log(mustach);
        let regExp = /\{\{(.+)\}\}/
        let prop = regExp.exec(mustach);
        prop = prop[1]
        this.$el.innerText = this.$data[prop]
        
        
        //Watcher 订阅者 订阅某个属性的变化,当属性值发生变更的时候,触发视图更新
        class Watcher {
            constructor(data, key, fn) {
                Dep.target = this;
                this.data = data;
                this.key = key;
                this.fn = fn;
                this.value = data[key] //添加订阅 交给了defineProperty中的get完成
                Dep.target = null; //添加完订阅之后 清空target
            }

            update() { //更新视图
                //获取更新之后的值
                this.value = this.data[this.key];//获取更新后的值
                this.fn(this.value)
            }
        }
        
        
        let _this = this;
        new Watcher(this.$data, 'name', function (value) {
            //更新视图
            console.log(this);
            _this.$el.innerHTML = value
        })
        this.proxy_data() //属性代理
    }
    proxy_data() { //将$data里面的属性代理到根实例
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                configurable: true,// 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
                enumerable: true,// 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    if (newVal !== this.$data[key]) {
                        this.$data[key] = newVal
                    }
                }
            })
        })
    }
}

//Observer 发布者,用于劫持data中的属性

function observer(data) {
    if (typeof data !== "object") {
        return
    }
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key])
    })
}

//Dep 一个用于解耦合的类

class Dep {
    constructor() {
        this.subs = [];//储存订阅者
    }
    addSub(watcher) { //添加订阅
        this.subs.push(watcher)
    }

    notify() { //通知变化
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

//defineReactive 定义响应
function defineReactive(obj, key, value) {
    observer(value)
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log(`${key}被访问了`);
            Dep.target && dep.addSub(Dep.target)
            return value
        },
        set(newVal) {
            value = newVal
            dep.notify()//通知变化
        }
    })
}`

以上仿写vue2.X采用了Object.defineProperty() 有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。 在Vue中,响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以我们需要对这些操作进行hack,让Vue能监听到其中的变化。 具体实现: 改写其中部分方法,如下图

1B40B500F54945B5A7043607E905B101.jpg

  1. 存储数组原型上的方法
  2. 改写数组原型方法
  3. 再调用原生方法,并将结果返回
// 缓存数组原型 
const arrayProto = Array.prototype; 
// 实现 arrayMethods.__proto__ === Array.prototype 
export const arrayMethods = Object.create(arrayProto); 
// 需要进行功能拓展的方法 
const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ]; 
/** * Intercept mutating methods and emit events */ 
methodsToPatch.forEach(function(method) { 
// 缓存原生数组方法 
const original = arrayProto[method]; 
def(arrayMethods, method, function mutator(...args) { 
// 执行并缓存原生数组功能 
const result = original.apply(this, args); 
// 响应式处理 
const ob = this.__ob__; 
let inserted; switch (method) { 
// push、unshift会新增索引,所以要手动observer 
case "push": 
case "unshift": 
inserted = args; 
break; 
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 
case "splice": inserted = args.slice(2); break; } 
// 
if (inserted) ob.observeArray(inserted);
// 获取插入的值,并设置响应式监听 
// notify change 
ob.dep.notify();
// 通知依赖更新 
// 返回原生数组方法的执行结果 
return result; 
  }); 
});


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。

Proxy

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。

5013ED525DE546E7BC103C60EBDE73C1.jpg

什么是 Proxy?

Proxy 这个词翻译过来就是“代理”,用在这里表示由它来“代理”某些操作。 Proxy 会在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可 以对外界的访问进行过滤和改写。

先来看下 proxy 的基本语法 const proxy = new Proxy(target, handler)

target :您要代理的原始对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

handler :一个对象,定义将拦截哪些操作以及如何重新定义拦截的操作

一个简单的Demo

 let array = [1, 2, 3, 4, 5, 6];

    let p = new Proxy(array, {
        get() {
            console.log('array get被触发了');
        },
        set(val) {
            console.log('set被触发了');
            console.log(val);
        }
    })

Proxy 还可以做什么?

我们已经看到了 Proxy 在 Vue3 中的应用场景,其实在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。

实现访问日志

let api = {
  getUser: function(userId) {
    /* ... */
  },
  setUser: function(userId, config) {
    /* ... */
  }
};
// 打日志
function log(timestamp, method) {
  console.log(`${timestamp} - Logging ${method} request.`);
}
api = new Proxy(api, {
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...arguments) {
      log(new Date(), key); // 打日志
      return Reflect.apply(value, target, arguments);
    };
  }
});
api.getUsers();

校验模块

let numObj = { count: 0, amount: 1234, total: 14 };
numObj = new Proxy(numObj, {
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error('Properties in numObj can only be numbers');
    }
    return Reflect.set(target, key, value, proxy);
  }
});

// 抛出错误,因为 "foo" 不是数值
numObj.count = 'foo';
// 赋值成功
numObj.count = 333;

    版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    作者: 佩吉吉
    原文链接:<a href='https://juejin.im/post/6960953125117263886'>https://juejin.im/post/6960953125117263886</a>
  </p>
回到顶部