美文网首页vueKagashino的Vue.js提高班
【Vue.js】数据监听和3.0的Proxy API

【Vue.js】数据监听和3.0的Proxy API

作者: Kagashino | 来源:发表于2019-08-11 01:36 被阅读0次

从数据绑定开始

数据绑定是目前主流前端框架普及的一个重要原因,它们让开发者专注于处理数据而非DOM的实现。Angular是基于scope的脏检查机制,React是组件的state,Vue则是基于Object.defineProperty,今天我们将Vue的数据绑定原理和新API的特性和优势。

Vue是双向绑定吗?

不是,原则上Vue的子组件不能改变父组件传下来的数据(prop),但可以通过v-model这样的语法糖去实现,事实上,Vue和React十分类似,都是采用了单向数据流,这样做更有利于状态的追踪和管理。

Vue如何实现数据绑定

可以分成2部分:数据监听=>数据映射
映射很好理解,数据传入模板或者render函数编译成虚拟DOM,虚拟DOM保存了节点的标签(如div h3等)、绑定的数据(data、methods、props、computed等)以及子节点/关联节点,再根据虚拟DOM的信息映射成真实DOM

数据监听基于Object.defineProperty,下面开始着重介绍

Object.defineProperty(以下简称OD)

这是JavaScript定义对象属性的一个api,通过调用该方法,我们可以定义一个对象的属性及属性描述符,可以理解为属性的属性

Object.defineProperty(object, key, descriptor)

其中,descriptor可以定义如下内容:
详情参考mdn文档

interface PropertyDescriptor {
    configurable?: boolean; // 可以对该属性进行删改
    enumerable?: boolean; // 是否可以被for in 或者 Object.key迭代获取
    value?: any; // 属性值,默认为undefined
    writable?: boolean; //是否可以赋值
    get?(): any; // 如果定义了getter,当获取到这个属性后,无视默认值,读取getter的返回值
    set?(v: any): void; // 对这个属性赋值后触发的回调
}

既然能够通过劫持对象的获取与设置,那么这里边就可以做一些文章了 ,比如我想设计一个高温预警系统,当温度达到40度时发出警告:

const Temperature = {
    degree: 28
}

Object.defineProperty (Temperature, 'degree', {
   set (value) {
     if (value > 40) { alert('高温红色预警!') }
   }
})

Temperature.degree = 28 // 不会触发预警
Temperature.degree = 41 // 触发预警

Vue的监听机制同理,当一个data对象定义时,Vue会对data所有的属性设置setter和getter。假设我有一个组件:

export default {
    data () {
        foo: 1
    },
    template: `<h3>{{foo}}</h3>`
}

工作原理如下

无标题.png
先对Data定义属性'foo'并添加getter/setter和Dep(dependeny依赖)
当访问foo字段时,触发了getter(1.),getter函数中先收集Data.foo依赖(2.),再返回返回初始值value(3.)。当foo值改变后,触发了setter(4.),setter函数中通知Dep(5.)进行更新,通过更新调度后最终返回更新后的结果(6.)。

Object.defineProperty的问题

1.对每一个key都要添加描述符:

在我之前的文章提到过,data中的每一个属性都有监听,这样做比较浪费JavaScript的开销,无法监听到对象属性的添加和删除(需要通过Vue.set和Vue.delete处理)

2.无法响应对象的增删和数组的长度等方法:

如果直接往对象里添加一个属性(如往o = {a:1}中添加o.b = 2),或者改变数组长度,Vue无法触发监听

var a = [1,2,3,4,5]

a.forEach((v,k,a)=>{
    Object.defineProperty(a,k, {
      get: ()=>{console.log('你获取了a'); return v},
      set: (newVal)=>{ alert(`你设置了${newVal}`)}
    })
})

a.push(6) // 没有任何反应
a.length = 2 // 没有任何反应

对此,Vue对数组的方法如pop、push、sort等提供了响应补丁,还提供了Vue.set方法做兼容处理。

Proxy

既然OD方法存在这些方面的缺陷,那么使用Proxy无疑是很好的替代品:

Proxy(target, handler)

区别于OD,我们可以对整个对象进行监听操作,且看MDN文档示例代码

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 37;
    }
};

let p = new Proxy({}, handler);

p.a = 1;
p.b = undefined;

console.log(p.a, p.b);    // 1, undefined
console.log('c' in p, p.c);    // false, 37

而且对数组也能劫持:

var proxyArr = new Proxy(arr, {
  get (target, key) {
    alert(`你获取了${target}.${key}`)
    return target[key]
  },
  set (target, key, value) {
    alert(`你设置了${target}.${key}->${value}`)
  }
})

proxyArr[0] // 触发访问元素下标getter
proxyArr.sort //  触发访问数组方法的getter
proxyArr.length = 2 // 触发setter
proxyArr.push(1) // 触发setter和每个数组遍历的getter

另外,Proxy跟Reflect时相辅相成的,参见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

其中,Reflect.get(foo, 'key')等价于foo.keyReflect.set(foo,'key','value')等价于foo.key = 'value'Reflect.has(foo, 'key')等价于'key' in foo
一般来说,需要在Proxy的Hanlder中使用Reflect的API,那么上面的handler.get应该改为:

function(target, name){
  return Reflect.has(target, name) ? Reflect.get(target, name) : 37;
}

Vue3.0中的Proxy

上个月(19年10月),Vue3.0 - vue-next在github开放,根据上文介绍的Proxy和Reflect,我们来看看3.0如何使用Proxy做响应式数据的:
响应式的代码在packages/reactivity/src/reactive.ts中,我省略了边界判断的代码,直接上主线:
首先导入Proxy需要的Hanlder (这里先不讲,我们在后面解释)

import {
  mutableHandlers, // 可变代理Handlers
  /* 省略其他Handlers */
} from './baseHandlers'
import {
  mutableCollectionHandlers, // 专门针对Set/Map/WeakSet/WeakMap的Hanlers
} from './collectionHandlers'

创建2个Map,用来存储原始数据与响应式数据的相互映射

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始对应响应式
const reactiveToRaw = new WeakMap<any, any>() // 响应式对应原始

这样一来,原始与响应之间可以双向映射;
接下来,利用这2种映射表,创建一个入口函数,传入原始值、映射表和Hanlders

export function reactive(target: object) {
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
function createReactiveObject(
  target: unknown, // 原数据
  toProxy: WeakMap<any, any>, // rawToReactive
  toRaw: WeakMap<any, any>, // reactiveToRaw
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>, 
) {
  // 省略原数据边界检查代码
  //
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)  // 响应map中添加响应与原始映射
  toRaw.set(observed, target)  // 原始map中添加原始与响应映射
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

回过头来看Handlers代码

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

分析其中的getter和Setter

function createGetter(isReadonly: boolean, unwrap = true) {
  return function get(target: object, key: string | symbol, receiver: object) {
    let res = Reflect.get(target, key, receiver)
    if (unwrap && isRef(res)) {
      res = res.value
    } else {
      track(target, OperationTypes.GET, key)
    }
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

getter中先执行了track函数(对应了2.x的Dep.depend),再根据值的类型返回原始值/只读类型和递归响应式的值。

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  value = toRaw(value)
  const oldValue = (target as any)[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      trigger(target, OperationTypes.ADD, key)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, OperationTypes.SET, key)
    }
  }
  return result
}

setter中的trigger对应了2.xDep.notify,并且因为再setter里能够获取到目标对象,所以也自然能知道到底是添加还是修改了。

可以看到,Proxy对于对象劫持要灵活且有用得多,最主要的是相对于OD,Proxy额外生成的Getter和Setter更少,更节约内存(当然,嵌套的Object还得递归监听这点没变)。这也就是为什么Vue3.0会使用Proxy替代Object.defineProperty的原因了(同时也是我为什么在前文中说“仅限2.0”了)。

相关文章

网友评论

    本文标题:【Vue.js】数据监听和3.0的Proxy API

    本文链接:https://www.haomeiwen.com/subject/ippdjctx.html