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


Posted in Javascript onJune 11, 2019

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的变化检测问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
JavaScript全局函数使用简单说明
Mar 11 Javascript
jQuery事件 delegate()使用方法介绍
Oct 30 Javascript
JS解析json数据并将json字符串转化为数组的实现方法
Dec 25 Javascript
jquery 卷帘效果实现代码(不同方向)
Feb 05 Javascript
在服务端(Page.Write)调用自定义的JS方法详解
Aug 09 Javascript
jquery siblings获取同辈元素用法实例分析
Jul 25 Javascript
原生JS轮播图插件
Feb 09 Javascript
令按钮悬浮在(手机)页面底部的实现方法
May 02 Javascript
AngularJs用户输入动态模板XSS攻击示例详解
Apr 21 Javascript
详解JS转换数值函数Number()、parseInt()、parseFloat()
Aug 24 Javascript
JSONP原理及应用实例详解
Sep 13 Javascript
VUE 组件转换为微信小程序组件的方法
Nov 06 Javascript
webpack实践之DLLPlugin 和 DLLReferencePlugin的使用教程
Jun 10 #Javascript
vue2 中二级路由高亮问题及配置方法
Jun 10 #Javascript
JS中实现一个下载进度条及播放进度条的代码
Jun 10 #Javascript
vuex 中插件的编写案例解析
Jun 10 #Javascript
使用webpack搭建vue项目及注意事项
Jun 10 #Javascript
详解iview的checkbox多选框全选时校验问题
Jun 10 #Javascript
前端路由&amp;webpack基础配置详解
Jun 10 #Javascript
You might like
SONY SRF-40W电路分析
2021/03/02 无线电
PHP读取txt文本文件并分页显示的方法
2015/03/11 PHP
PHP实现的限制IP投票程序IP来源分析
2016/05/04 PHP
javascript下有关dom以及xml节点访问兼容问题
2007/11/26 Javascript
ajax中get和post的说明及使用与区别
2012/12/23 Javascript
js中继承的几种用法总结(apply,call,prototype)
2013/12/26 Javascript
jQuery实现渐变下拉菜单的简单方法
2015/03/11 Javascript
JavaScript Promise 用法
2016/06/14 Javascript
jQuery中on方法使用注意事项详解
2017/02/15 Javascript
js实现产品缩略图效果
2017/03/10 Javascript
Angular js 实现添加用户、修改密码、敏感字、下拉菜单的综合操作方法
2017/10/24 Javascript
Vue组件的使用教程详解
2018/01/05 Javascript
详解vue组件开发脚手架
2018/06/15 Javascript
详解vue在项目中使用百度地图
2019/03/26 Javascript
使用Node.js写一个代码生成器的方法步骤
2019/05/10 Javascript
详解Vue2.5+迁移至Typescript指南
2019/08/01 Javascript
JSONP解决JS跨域问题的实现
2020/05/25 Javascript
解决vue的touchStart事件及click事件冲突问题
2020/07/21 Javascript
python判断字符串是否包含子字符串的方法
2015/03/24 Python
用Python编写分析Python程序性能的工具的教程
2015/04/01 Python
基于Python在MacOS上安装robotframework-ride
2018/12/28 Python
Python设计模式之原型模式实例详解
2019/01/18 Python
python实现H2O中的随机森林算法介绍及其项目实战
2019/08/29 Python
Python any()函数的使用方法
2019/10/28 Python
Python算法中的时间复杂度问题
2019/11/19 Python
解决pycharm debug时界面下方不出现step等按钮及变量值的问题
2020/06/09 Python
Python selenium键盘鼠标事件实现过程详解
2020/07/28 Python
django表单中的按钮获取数据的实例分析
2020/07/31 Python
纯CSS3发光分享按钮的实现教程
2014/09/06 HTML / CSS
ECCO爱步加拿大官网:北欧丹麦鞋履及皮具品牌
2017/07/08 全球购物
潘多拉珠宝俄罗斯官方网上商店:PANDORA俄罗斯
2020/09/22 全球购物
shell程序中如何注释
2012/01/28 面试题
超市后勤自我鉴定
2014/01/17 职场文书
大学生优秀团员事迹材料
2014/01/30 职场文书
多媒体专业自我鉴定
2014/02/28 职场文书
HTML中的表单元素介绍
2022/02/28 HTML / CSS