什么是数据劫持?

定义: 数据劫持,指的是在拜访或者批改对象的某个属性时,通过一段代码拦挡这个行为,进行额定的操作或者批改返回后果。

简略地说,就是当咱们 触发函数的时候 动一些手脚做点咱们本人想做的事件,也就是所谓的 "劫持"操作

数据劫持的两种计划:

  • Object.defineProperty
  • Proxy

1).Object.defineProperty

  • 语法:

Object.defineProperty(obj,prop,descriptor)

  • 参数:

    • obj:指标对象
    • prop:须要定义的属性或办法的名称
    • descriptor:指标属性所领有的个性
  • 可供定义的个性列表:

    • value:属性的值
    • writable:如果为false,属性的值就不能被重写。
    • get: 一旦指标属性被拜访就会调回此办法,并将此办法的运算后果返回用户。
    • set:一旦指标属性被赋值,就会调回此办法。
    • configurable:如果为false,则任何尝试删除指标属性或批改属性性以下个性(writable, configurable, enumerable)的行为将被有效化。
    • enumerable:是否能在for...in循环中遍历进去或在Object.keys中列举进去。

例子

在Vue中其实就是通过Object.defineProperty来劫持对象属性的settergetter操作,并“种下”一个监听器,当数据发生变化的时候发出通知,如下:

var data = {name:'test'}Object.keys(data).forEach(function(key){    Object.defineProperty(data,key,{        enumerable:true,        configurable:true,        get:function(){            console.log('get');        },        set:function(){            console.log('监听到数据产生了变动');        }    })});data.name //控制台会打印出 “get”data.name = 'hxx' //控制台会打印出 "监听到数据产生了变动"

下面的这个例子能够看出,咱们齐全能够管制对象属性的设置和读取。在Vue中,在很多中央都十分奇妙的使用了Object.defineProperty这个办法,具体用在哪里并且它又解决了哪些问题,上面就简略的说一下:

监听对象属性的变动

它通过observe每个对象的属性,增加到订阅器dep中,当数据发生变化的时候收回一个notice。 相干源代码如下:(作者采纳的是ES6+flow写的,代码在src/core/observer/index.js模块外面)

export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: Function) {  const dep = new Dep()//创立订阅对象    const property = Object.getOwnPropertyDe述  //属性的形容个性外面如果configurable为false则属性的任何批改将有效    if (property && property.configurable === false) { return }scriptor(obj, key)//获取obj对象的key属性的描    // cater for pre-defined getter/setters      const getter = property && property.get      const setter = property && property.set  let childOb = observe(val)//创立一个观察者对象    Object.defineProperty(obj, key, {    enumerable: true,//可枚举        configurable: true,//可批改        get: function reactiveGetter () {      const value = getter ? getter.call(obj) : val//先调用默认的get办法取值      //这里就劫持了get办法,也是作者一个奇妙设计,在创立watcher实例的时候,通过调用对象的get办法往订阅器dep上增加这个创立的watcher实例      if (Dep.target) {        dep.depend()        if (childOb) {          childOb.dep.depend()        }        if (Array.isArray(value)) {          dependArray(value)        }      }      return value//返回属性值        },    set: function reactiveSetter (newVal) {      const value = getter ? getter.call(obj) : val//先取旧值            if (newVal === value) { return }      //这个是用来判断生产环境的,能够忽视            if (process.env.NODE_ENV !== 'production' && customSetter) {        customSetter()      }      if (setter) {        setter.call(obj, newVal)      } else {        val = newVal      }      childOb = observe(newVal)//持续监听新的属性值            dep.notify()//这个是真正劫持的目标,要对订阅者发告诉了        }  })}

以上是Vue监听对象属性的变动,那么问题来了,咱们常常在传递数据的时候往往不是一个对象,很有可能是一个数组,那是不是就没有方法了呢,答案显然是否则的。那么上面就看看作者是如何监听数组的变动:

监听数组的变动

看代码:

const arrayProto = Array.prototype//原生Array的原型export const arrayMethods = Object.create(arrayProto);[  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse'].forEach(function (method) {    const original = arrayProto[method]//缓存元素数组原型  //这里重写了数组的几个原型办法      def(arrayMethods, method, function mutator () {    //这里备份一份参数应该是从性能方面的思考        let i = arguments.length    const args = new Array(i)    while (i--) {      args[i] = arguments[i]    }    const result = original.apply(this, args)//原始办法求值    const ob = this.__ob__//这里this.__ob__指向的是数据的Observer        let inserted    switch (method) {        case 'push':            inserted = args            break              case 'unshift':            inserted = args            break              case 'splice':            inserted = args.slice(2)            break        }    if (inserted) ob.observeArray(inserted)    // notify change        ob.dep.notify()    return result  })})...//定义属性function def (obj, key, val, enumerable) {    Object.defineProperty(obj, key, {        value: val,        enumerable: !!enumerable,        writable: true,        configurable: true      });}

参考 前端进阶面试题具体解答

下面的代码次要是继承了Array自身的原型办法,而后又做了劫持批改,能够发出通知。Vue在observer数据阶段会判断如果是数组的话,则批改数组的原型,这样的话,前面对数组的任何操作都能够在劫持的过程中管制。联合Vue的思维,简略的写个小demo不便更好的了解:

let arrayMethod = Object.create(Array.prototype);['push','shift'].forEach(function(method){    Object.defineProperty(arrayMethod,method,{        value:function(){            let i = arguments.length            let args = new Array(i)            while (i--) {              args[i] = arguments[i]            }            let original = Array.prototype[method];            let result = original.apply(this,args);            console.log("曾经管制了,哈哈");            return result;        },        enumerable: true,        writable: true,        configurable: true        })})let bar = [1,2];bar.__proto__ = arrayMethod;bar.push(3);//控制台会打印出 “曾经管制了,哈哈”;并且bar外面曾经胜利的增加了成员 ‘3’

整个过程看起来如同没有什么问题,仿佛Vue曾经做到了完满,其实不然,Vue还是不能检测到数据项和数组长度扭转的变动,例如上面的调用:

vm.items[index] = "xxx";vm.items.length = 100;

所以咱们尽量避免这样的调用形式,如果的确须要,作者也帮咱们实现了一个$set操作,上来本人理解

实现对象属性代理

失常状况下咱们是这样实例化一个Vue对象:

var VM = new Vue({ data:{ name:'lhl' }, el:'#id'})

按理说咱们操作数据的时候应该是VM.data.name = ‘hxx’才对,然而作者感觉这样不够简洁,所以又通过代理的形式实现了VM.name = ‘hxx’的可能。 相干代码如下:

function proxy (vm, key) {  if (!isReserved(key)) {    Object.defineProperty(vm, key, {      configurable: true,      enumerable: true,      get: function proxyGetter () {        return vm._data[key]      },      set: function proxySetter (val) {        vm._data[key] = val;      }    });  }}

外表上看起来咱们是在操作VM.name,实际上还是通过Object.defineProperty()中的getset办法劫持实现的。

Object.defineProperty()的毛病

1).不能监听数组的变动
let arr = [1,2,3]let obj = {}Object.defineProperty(obj, 'arr', {  get () {    console.log('get arr')    return arr  },  set (newVal) {    console.log('set', newVal)    arr = newVal  }})obj.arr.push(4) // 只会打印 get arr, 不会打印 setobj.arr = [1,2,3,4] // 这个能失常 set

数组的以下几个办法不会触发 set: pushpopshiftunshiftsplicesortreverse

Vue 把这些办法定义为变异办法 (mutation method),指的是会批改原来数组的办法。与之对应则是非变异办法 (non-mutating method),例如 filter, concat, slice 等,它们都不会批改原始数组,而会返回一个新的数组。

2).必须遍历对象的每个属性

应用 Object.defineProperty() 少数要配合 Object.keys() 和遍历,于是多了一层嵌套。如:

Object.keys(obj).forEach(key => {  Object.defineProperty(obj, key, {    // ...  })})
3).必须深层遍历嵌套的对象

所谓的嵌套对象,是指相似

let obj = {  info: {    name: 'eason'  }}

如果是这一类嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。

给出完整版的数据劫持代码:

const arrayProto = Array.prototype;// 失去原型上的办法const proto = Object.create(arrayProto)  // 复制一份原型上的办法    ;['push', 'shift', 'pop', 'splice'].forEach(method => {        // console.log(method)        // 重写'push','shift','pop','splice',当然也能够多加几个办法,想加什么就加什么        proto[method] = function (...args) {            // console.log(this)  // [ 1, 2, 3, { age: [Getter/Setter] } ]            updateView();            arrayProto[method].call(this, ...args)        }    })function updateView() {    console.log("更新视图胜利了...")}function observer(obj) {    if (typeof obj !== "object" || obj == null) {        return obj    }    if (Array.isArray(obj)) {        // 如果是一个数组要重写数组上原型上的办法         Object.setPrototypeOf(obj, proto)        for (let i = 0; i < obj.length; i++) {            let item = obj[i];            observer(item)        }    } else {        for (let key in obj) {            definedReactive(obj, key, obj[key])        }    }}function definedReactive(obj, key, value) {    observer(value)    Object.defineProperty(obj, key, {        get() {            console.log("获取数据胜利了...")            return value;        },        set(newValue) {            if (value !== newValue) {                observer(newValue)                value = newValue;                updateView();            }        }    })}let data = { name: [1, 2, 3, { age: 888 }] }observer(data)// 数据扭转了// data.name[3].age = 666;  // push shift unshift pop 也能扭转数组中的数组data.name.push({ address: "xxx" })  // 目标是:更新视图// 思路:重写Push办法  这些办法在Array的原型上//      不要把Array原型上的办法间接重写了//      先把原型上的办法copy一份,去重写(加上视图更新的操作)//      再去调用最原始的push办法

接下来说一下Object.defineProperty()的升级版 Proxy

2).Proxy数据代理

在数据劫持这个问题上,Proxy 能够被认为是 Object.defineProperty() 的升级版。外界对某个对象的拜访,都必须通过这层拦挡。因而它是针对 整个对象,而不是 对象的某个属性。

proxy即代理的意思。集体了解,建设一个proxy代理对象(Proxy的实例),承受你要监听的对象和监听它的handle两个参数。当你要监听的对象产生任何扭转,都会被proxy代理拦挡来满足需要。

var arr = [1,2,3]var handle = {    //target指标对象 key属性名 receiver理论承受的对象    get(target,key,receiver) {        console.log(`get ${key}`)        // Reflect相当于映射到指标对象上        return Reflect.get(target,key,receiver)    },    set(target,key,value,receiver) {        console.log(`set ${key}`)        return Reflect.set(target,key,value,receiver)    }}//arr要拦挡的对象,handle定义拦挡行为var proxy = new Proxy(arr,handle)proxy.push(4) //能够翻到控制台测试一下会打印出什么

长处:
1.应用proxy能够解决defineProperty不能监听数组的问题,防止重写数组办法;
2.不须要再遍历key
3.Proxy handle的拦挡处理器除了getset外还反对多种拦挡形式。
4.嵌套查问。实际上proxy get()也是不反对嵌套查问的。解决办法:

let handler = {  get (target, key, receiver) {    // 递归创立并返回    if (typeof target[key] === 'object' && target[key] !== null) {      return new Proxy(target[key], handler)    }    return Reflect.get(target, key, receiver)  }}

依赖治理计划

说完了下面的,简略说一下 依赖治理计划

Object.defineProperty 只是解决了状态变更后,如何触发告诉的问题,那要告诉谁呢?谁会关怀那些属性产生了变动呢?在 Vue 中,应用 Dep 解耦了依赖者与被依赖者之间关系的确定过程。简略来说:

  • 第一步,通过 Observer 提供的接口,遍历状态对象,给对象的每个属性、子属性都绑定了一个专用的 Dep 对象。这里的状态对象次要指组件当中的data属性。
  • 第二步,创立三中类型的watcher
    1.调用 initComputedcomputed 属性转化为 watcher 实例
    2.调用 initWatch 办法,将 watch 配置转化为 watcher 实例
    3.调用 mountComponent 办法,为 render 函数绑定 watcher 实例
  • 第三步,状态变更后,触发 dep.notify() 函数,该函数再进一步触发 Watcher 对象 update 函数,执行watcher的从新计算。

对应下图:

留神,Vue 组件中的 render 函数,咱们能够单纯将其视为一种非凡的 computed 函数,在它所对应的 Watcher 对象发生变化时,触发执行render,生成新的 virutal-dom 构造,再交由 Vue 做diff,更新视图。

OK 本章就到此了