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 相关文章推荐
jQuery formValidator表单验证插件开源了 含API帮助、源码、示例
Aug 14 Javascript
Array.prototype.slice 使用扩展
Jun 09 Javascript
关于js拖拽上传 [一个拖拽上传修改头像的流程]
Jul 13 Javascript
5个最佳的Javascript日期处理类库分享
Apr 15 Javascript
JS 获取滚动条高度示例代码
Oct 24 Javascript
框架页面高度自动刷新的Javascript脚本
Nov 01 Javascript
你知道setTimeout是如何运行的吗?
Aug 16 Javascript
利用百度地图API获取当前位置信息的实例
Nov 06 Javascript
使用RN Animated做一个“添加购物车”动画的方法
Sep 12 Javascript
JavaScript学习教程之cookie与webstorage
Jun 23 Javascript
详解Angular Karma测试的持续集成实践
Nov 15 Javascript
基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能
Feb 23 Vue.js
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
php中stream(流)的用法
2014/03/25 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(六)
2014/06/23 PHP
PHP安全上传图片的方法
2015/03/21 PHP
ZendFramework2连接数据库操作实例
2017/04/18 PHP
PHP处理bmp格式图片的方法分析
2017/07/04 PHP
Javascript优化技巧(文件瘦身篇)
2008/01/28 Javascript
jQuery学习4 浏览器的事件模型
2010/02/07 Javascript
jQuery中将函数赋值给变量的调用方法
2012/03/23 Javascript
JQuery学习笔录 简单的JQuery
2012/04/09 Javascript
js解决弹窗问题实现班级跳转DIV示例
2014/01/06 Javascript
浅谈javascript的分号的使用
2015/05/12 Javascript
js中函数声明与函数表达式
2015/06/03 Javascript
用JavaScript实现PHP的urlencode与urldecode函数
2015/08/13 Javascript
js实现表单多按钮提交action的处理方法
2015/10/24 Javascript
bootstrap模态框实现拖拽效果
2016/12/14 Javascript
jquery实现倒计时小应用
2017/09/19 jQuery
基于 Immutable.js 实现撤销重做功能的实例代码
2018/03/01 Javascript
Vue实现剪贴板复制功能
2019/12/31 Javascript
Pandas之groupby( )用法笔记小结
2019/07/23 Python
python分布式编程实现过程解析
2019/11/08 Python
使用tensorflow DataSet实现高效加载变长文本输入
2020/01/20 Python
python实现同一局域网下传输图片
2020/03/20 Python
PyQt5实现登录页面
2020/05/30 Python
基于selenium及python实现下拉选项定位select
2020/07/22 Python
客服端调用EJB对象的几个基本步骤
2012/01/15 面试题
学生爱国演讲稿
2014/01/14 职场文书
关于圣诞节的广播稿
2014/01/26 职场文书
材料工程专业毕业生求职信
2014/03/04 职场文书
2014民事授权委托书范本
2014/09/29 职场文书
小学运动会入场词
2015/07/18 职场文书
2015新教师教学工作总结
2015/07/22 职场文书
禁毒主题班会教案
2015/08/14 职场文书
Pytest allure 命令行参数的使用
2021/04/18 Python
Python使用random模块实现掷骰子游戏的示例代码
2021/04/29 Python
python中出现invalid syntax报错的几种原因分析
2022/02/12 Python