欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ES6 Proxy实现Vue的变化检测问题

程序员文章站 2022-11-20 09:06:40
vue变化检测object使用defineproperty、数组使用方法拦截实现。最近,vue3.0将采用es6 proxy的形式重新实现vue的变化检测,在官方还没给出新...

vue变化检测object使用defineproperty、数组使用方法拦截实现。最近,vue3.0将采用es6 proxy的形式重新实现vue的变化检测,在官方还没给出新方法之前,我们先实现一个基于proxy的变化检测。

模块划分

参照之前vue变化检测的代码,将vue 变化检测的功能分为以下几个部分。

  • observer
  • dep
  • watcher
  • utils

首先,我们要确定的问题是,将dep依赖搜集存在哪里。vue 2.x里,object的依赖收集放在defineractive,array的依收集存入到observer中。es6 proxy里,考虑到让handler访问dep,我们将依赖放入到observer中。

observer

observer.js功能代码如下:

import dep from './dep';
import { isobject } from './utils';
export default class observer {
  constructor (value) {
    // 递归处理子元素
    this.obeserve(value);
    // 实现当前元素的代理
    this.value = this.proxytarget(value);
  }
  proxytarget (targetbefore, keybefore) {
    const dep = new dep();
    targetbefore.__dep__ = dep;
    let self = this;
    const filtersatrr = val => ['__dep__', '__parent__'].indexof(val) > -1;
    return new proxy(targetbefore, {
      get: function(target, key, receiver){
        if (filtersatrr(key)) return reflect.get(target, key, receiver);
        if (!array.isarray(target)) {
          dep.depend(key);
        }
        // sort/reverse等不改变数组长度的,在get里触发
        if (array.isarray(target)) {
          if ((key === 'sort' || key === 'reverse') && target.__parent__) {
            target.__parent__.__dep__.notify(keybefore);
          }
        } 
        return reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver){
        if (filtersatrr(key)) return reflect.set(target, key, value, receiver);
        // 新增元素,需要proxy
        const { newvalue, ischanged } = self.addproxytarget(value, target, key, self);
        // 设置key为新元素
        reflect.set(target, key, newvalue, receiver);
        // notify
        self.depnotify(target, key, keybefore, dep, ischanged);
        return true;
      },
    });
  }
  addproxytarget(value, target, key, self) {
    let newvalue = value;
    let ischanged = false;
    if (isobject(value) && !value.__parent__) {
      self.obeserve(newvalue);
      newvalue = self.proxytarget(newvalue, key);
      newvalue.__parent__ = target;
      ischanged = true;
    }
    return {
      newvalue,
      ischanged,
    }
  }
  depnotify(target, key, keybefore, dep, ischanged) {
    if (ischanged && target.__parent__) {
      target.__parent__.__dep__.notify(keybefore);
      return;
    }
    if (array.isarray(target)) {
      if (key === 'length' && target.__parent__) {
        target.__parent__.__dep__.notify(keybefore);
      }
    } else {
      dep.notify(key);
    }
  }
  obeserve(target) {
    // 只处理对象类型,包括数组、对象
    if (!isobject(target)) return;
    for (let key in target) {
      if (isobject(target[key]) && target[key] !== null) {
        this.obeserve(target[key]);
        target[key] = this.proxytarget(target[key], key);
        // 设置__parent__,方便子元素调用
        target[key].__parent__ = target;
      }
    }
  }
}

在observer中,针对对象,只需要执行 dep.depend(key) dep.notify(key) 即可。添加 key 是为了能正确的触发收集,不知道怎么说明白为什么要这样做,只能一切尽在不言中了。

array, 如何实现依赖的收集和触发那。依赖收集与object类似, dep.depend(key) 完成数组的收集。关于触发,可以分为两个方面,一是改变数组长度、二未改变数组长度的。改变数组长度的,在set里,通过长度属性的设置触发父级元素的notify。为什么要使用父级元素的notify那?我们可以分析以下,在你设置数组的长度时,这时候的target\key\value分别是[]\length*, 这个时候,数组的依赖收集是没有的,你watcher的是数组,并不是数组本身。这个时候只能通过 target.__parent__.__dep__.notify(keybefore) 触发父级的收集,完成数据变化的检测。二对于未改变数组长度的,这里的做法,虽然是直接 target.__parent__.__dep__.notify(keybefore) 触发依赖,但是有个严重的问题,实际上更新的数据不是最新的,这个地方暂时还没想到比较好的方法,欢迎大家讨论。

dep

dep.js

let uid = 0;
export default class dep {
  constructor () {
    this.subs = {};
    this.id = uid++;
  }
  addsub(prop, sub) {
    this.subs[prop] = this.subs[prop] || [];
    this.subs[prop].push(sub);
  }
  removesub(prop, sub) {
    this.remove(this.subs[prop] || [], sub);
  }
  depend(prop) {
    if (dep.target) {
      // 传入的是当前依赖
      dep.target.adddep(prop, this)
    }
  }
  notify(prop) {
    const subs = (this.subs[prop] || []).slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
  remove(arr, item) {
    if (arr.length) {
      const index = arr.indexof(item);
      if (index > -1) {
        return arr.splice(index, 1);
      }
    }
  }
}
dep.target = null
const targetstack = []
export function pushtarget (_target) {
  if (dep.target) targetstack.push(dep.target)
  dep.target = _target
}
export function poptarget () {
  dep.target = targetstack.pop()
}

dep 添加prop实现类型的绑定,为什么要这么做那?使用proxy代理后,你假如wahcter对象下的几个元素,此时的deps将同时存在这几个元素,你触发依赖的时候,这些依赖都会执行。因此,通过key值绑定观察事件,触发时,能完成对象的正确触发。

watcher、utils

import { parsepath } from './utils';
import { pushtarget, poptarget } from './dep'
export default class watcher {
  constructor(vm, exporfn, cb) {
    // dep id集合
    this.depids = new set();
    this.vm = vm;
    this.getter = parsepath(exporfn);
    this.cb = cb;
    this.value = this.get();
  }
  get () {
    pushtarget(this);
    let value = this.getter.call(this.vm, this.vm);
    poptarget();
    return value;
  }
  update() {
    const oldvalue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldvalue);
  }
  adddep (prop, dep) {
    const id = dep.id;
    if (!this.depids.has(id)) {
      this.depids.add(id);
      dep.addsub(prop, this);
    }
  }
}

utils.js

/**
 * 解析简单路径
 */
const bailre = /[^\w.$]/;
export function parsepath (path) {
  if (bailre.test(path)) {
    return;
  }
  const segments = path.split('.');
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return;
      obj = obj[segments[i]];
    }
    return obj;
  };
}
/**
 * define a property.
 */
export function def (obj, key, val, enumerable) {
  object.defineproperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
/**
 * quick object check - this is primarily used to tell
 * objects from primitive values when we know the value
 * is a json-compliant type.
 */
export function isobject (obj) {
  return obj !== null && typeof obj === 'object'
}
/**
 * check whether an object has the property.
 */
const hasownproperty = object.prototype.hasownproperty
export function hasown (obj, key) {
 return hasownproperty.call(obj, key)
}

utils.js/watchers.js与vue 2.x类似,这里就不多介绍了。

测试一下

test.js

import observer from './observer';
import watcher from './watcher';
let data = {
  name: 'lijincai',
  password: '***********',
  address: {
    home: '安徽亳州谯城区',
  },
  list: [{
    name: 'lijincai',
    password: 'you know it object',
  }], 
};
const newdata = new observer(data);
let index = 0;
const watchername = new watcher(newdata, 'value.name', (newvalue, oldvalue) => {
  console.log(`${index++}: name newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
const watcherpassword = new watcher(newdata, 'value.password', (newvalue, oldvalue) => {
  console.log(`${index++}: password newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
const watcheraddress = new watcher(newdata, 'value.address', (newvalue, oldvalue) => {
  console.log(`${index++}: address newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
const watcheraddresshome = new watcher(newdata, 'value.address.home', (newvalue, oldvalue) => {
  console.log(`${index++}: address.home newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
const watcheraddprop = new watcher(newdata, 'value.addprop', (newvalue, oldvalue) => {
  console.log(`${index++}: addprop newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
const watcherdataobject = new watcher(newdata, 'value.list', (newvalue, oldvalue) => {
  console.log(`${index++}: newvalue:`, newvalue, ', oldvalue:', oldvalue);
});
newdata.value.name = 'resetname';
newdata.value.password = 'resetpassword';
newdata.value.name = 'hello world name';
newdata.value.password = 'hello world password';
newdata.value.address.home = 'hello home';
newdata.value.address.home = 'hello home2';
newdata.value.addprop = 'hello addprop';
newdata.value.addprop ={
  name: 'ceshi',
};
newdata.value.addprop.name = 'ceshi2';
newdata.value.list.push('1');
newdata.value.list.splice(0, 1);
newdata.value.list.sort();
newdata.value.list.reverse();
newdata.value.list.push('1');
newdata.value.list.unshift({name: 'nihao'});
newdata.value.list[0] = {
  name: 'lijincai',
  password: 'you know it array',
};
newdata.value.list[0].name = 'you know it array after';
newdata.value.list.pop();
newdata.value.list.shift();
newdata.value.list.length = 1;

我们使用对象、数组测试一下我们的es6 proxy检测。

20:17:44.725 index.js?afc7:20 0: name newvalue: resetname , oldvalue: lijincai
20:17:44.725 index.js?afc7:24 1: password newvalue: resetpassword , oldvalue: ***********
20:17:44.725 index.js?afc7:20 2: name newvalue: hello world name , oldvalue: resetname
20:17:44.725 index.js?afc7:24 3: password newvalue: hello world password , oldvalue: resetpassword
20:17:44.726 index.js?afc7:32 4: address.home newvalue: hello home , oldvalue: 安徽亳州谯城区
20:17:44.726 index.js?afc7:32 5: address.home newvalue: hello home2 , oldvalue: hello home
20:17:44.726 index.js?afc7:36 6: addprop newvalue: hello addprop , oldvalue: undefined
20:17:44.727 index.js?afc7:36 7: addprop newvalue: proxy {name: "ceshi", __dep__: dep, __parent__: {…}} , oldvalue: hello addprop
20:17:44.727 index.js?afc7:40 0: newvalue: proxy {0: proxy, 1: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: proxy, 1: "1", __dep__: dep, __parent__: {…}}
20:17:44.728 index.js?afc7:40 1: newvalue: proxy {0: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", __dep__: dep, __parent__: {…}}
20:17:44.729 index.js?afc7:40 2: newvalue: proxy {0: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", __dep__: dep, __parent__: {…}}
20:17:44.731 index.js?afc7:40 3: newvalue: proxy {0: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", __dep__: dep, __parent__: {…}}
20:17:44.734 index.js?afc7:40 4: newvalue: proxy {0: "1", 1: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", 1: "1", __dep__: dep, __parent__: {…}}
20:17:44.735 index.js?afc7:40 5: newvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}}
20:17:44.735 index.js?afc7:40 6: newvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}}
20:17:44.736 index.js?afc7:40 7: newvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: proxy, 1: "1", 2: "1", __dep__: dep, __parent__: {…}}
20:17:44.737 index.js?afc7:40 8: newvalue: proxy {0: proxy, 1: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: proxy, 1: "1", __dep__: dep, __parent__: {…}}
20:17:44.738 index.js?afc7:40 9: newvalue: proxy {0: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", __dep__: dep, __parent__: {…}}
20:17:44.738 index.js?afc7:40 10: newvalue: proxy {0: "1", __dep__: dep, __parent__: {…}} , oldvalue: proxy {0: "1", __dep__: dep, __parent__: {…}}

我们看到了es6 proxy后实现了object/array的检测,虽然还存在一些问题,但是基本的侦测变化的功能都已经具备了。

总结

以上所述是小编给大家介绍的es6 proxy实现vue的变化检测问题,希望对大家有所帮助