在10月05日凌晨Vue3的源代码正式发布了,来自官方的消息:vue
目前的版本是 Pre-Alpha
, 仓库地址: Vue-next, 能够经过 Composition API了解更多新版本的信息, 目前版本单元测试相关状况 vue-next-coverage。react
文章大纲:git
Vue 的核心之一就是响应式系统,经过侦测数据的变化,来驱动更新视图。github
经过可响应对象,实现对数据的侦测,从而告知外界数据变化。实现可响应对象的方式:typescript
关于前两个 API 的使用方式很少赘述,单一的访问器 getter/setter
功能相对简单,而做为 Vue2.x 实现可响应对象的 API - defineProperty
, API 自己存在较多问题。api
Vue2.x 中,实现数据的可响应,须要对 Object
和 Array
两种类型采用不一样的处理方式。 Object
类型经过 Object.defineProperty
将属性转换成 getter/setter
,这个过程须要递归侦测全部的对象 key
,来实现深度的侦测。数组
为了感知 Array
的变化,对 Array
原型上几个改变数组自身的内容的方法作了拦截,虽然实现了对数组的可响应,但一样存在一些问题,或者说不够方便的状况。 同时,defineProperty
经过递归实现 getter/setter
也存在必定的性能问题。markdown
更好的实现方式是经过 ES6
提供的 Proxy API
。函数
Proxy API 具备更增强大的功能, 相比旧的 defineProperty
API ,Proxy
能够代理数组,而且 API 提供了多个 traps
,能够实现诸多功能。oop
这里主要说两个trap: get
、 set
, 以及其中的一些比较容易被忽略的细节。
let data = { foo: 'foo' } let p = new Proxy(data, { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { console.log('set value') target[key] = value // ? } }) p.foo = 123 // set value 复制代码
经过 proxy
返回的对象 p
代理了对原始数据的操做,当对 p
设置时,即可以侦测到变化。可是这么写其实是有问题, 当代理的对象数据是数组时,会报错。
let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { console.log('set value') target[key] = value } }) p.push(4) // VM438:12 Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3' 复制代码
将代码更改成:
let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { console.log('set value') target[key] = value return true } }) p.push(4) // set value // 打印2次 复制代码
实际上,当代理对象是数组,经过 push
操做,并不仅是操做当前数据,push
操做还触发数组自己其余属性更改。
let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return target[key] }, set(target, key, value, receiver) { console.log('set value:', key, value) target[key] = value return true } }) p.push(1) // get value: push // get value: length // set value: 3 1 // set value: length 4 复制代码
先看 set
操做,从打印输出能够看出,push
操做除了给数组的第 3
位下标设置值 1
,还给数组的 length
值更改成 4
。 同时这个操做还触发了 get 去获取 push
和 length
两个属性。
咱们能够经过 Reflect
来返回 trap 相应的默认行为,对于 set 操做相对简单,可是一些比较复杂的默认行为处理起来相对繁琐得多,Reflect
的做用就显现出来了。
let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.push(1) // get value: push // get value: length // set value: 3 1 // set value: length 4 复制代码
相比本身处理 set
的默认行为,Reflect
就方便得多。
从前面的例子中能够看出,当代理对象是数组时,push
操做会触发屡次 set
执行,同时,也引起 get
操做,这点很是重要,vue3 就很好的使用了这点。 咱们能够从另外一个例子来看这个操做:
let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.unshift('a') // get value: unshift // get value: length // get value: 2 // set value: 3 3 // get value: 1 // set value: 2 2 // get value: 0 // set value: 1 1 // set value: 0 a // set value: length 4 复制代码
能够看到,在对数组作 unshift
操做时,会屡次触发 get
和 set
。 仔细观察输出,不难看出,get
先拿数组最末位下标,开辟新的下标 3
存放原有的末位数值,而后再将原数值都日后挪,将 0
下标设置为了 unshift
的值 a
,由此引起了屡次 set
操做。
而这对于 通知外部操做 显然是不利,咱们假设 set
中的 console
是触发外界渲染的 render
函数,那么这个 unshift
操做会引起 屡次 render
。
咱们后面会讲述如何解决相应的这个问题,继续。
let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] } let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.bar.key = 2 // get value: bar 复制代码
执行代码,能够看到并无触发 set
的输出,反而是触发了 get
,由于 set
的过程当中访问了 bar
这个属性。 因而可知,proxy
代理的对象只能代理到第一层,而对象内部的深度侦测,是须要开发者本身实现的。一样的,对于对象内部的数组也是同样。
p.ary.push('c') // get value: ary 复制代码
一样只走了 get
操做,set
并不能感知到。
咱们注意到 get/set
还有一个参数:receiver
,对于 receiver
,其实接收的是一个代理对象:
let data = { a: {b: {c: 1 } } } let p = new Proxy(data, { get(target, key, receiver) { console.log(receiver) const res = Reflect.get(target, key, receiver) return res }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } }) // Proxy {a: {…}} 复制代码
这里 receiver
输出的是当前代理对象,注意,这是一个已经代理后的对象。
let data = { a: {b: {c: 1 } } } let p = new Proxy(data, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) console.log(res) return res }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } }) // {b: {c: 1} } 复制代码
当咱们尝试输出 Reflect.get
返回的值,会发现,当代理的对象是多层结构时,Reflect.get
会返回对象的内层结构。
记住这一点,Vue3 实现深度的proxy ,即是很好的使用了这点。
前面提到了使用 Proxy
来侦测数据变化,有几个细节问题,包括:
Reflect
来返回 trap
默认行为set
操做,可能会引起代理对象的属性更改,致使 set
执行屡次proxy
只能代理对象中的一层,对于对象内部的操做 set
未能感知,可是 get
会被执行接下来,咱们将先本身尝试解决这些问题,后面再分析 Vue3 是如何解决这些细节的。
function reactive(data, cb) { let timer = null return new Proxy(data, { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { clearTimeout(timer) timer = setTimeout(() => { cb && cb() }, 0); return Reflect.set(target, key, value, receiver) } }) } let ary = [1, 2] let p = reactive(ary, () => { console.log('trigger') }) p.push(3) // trigger 复制代码
程序输出结果为一个: trigger
这里实现了 reactive
函数,接收两个参数,第一个是被代理的数据 data
,还有一个回调函数 cb
, 咱们这里先简单的在 cb
中打印 trigger 操做,来模拟通知外部数据的变化。
解决重复的 cb
调用有不少中方式,比方经过标志,来决定是否调用。而这里是使用了定时器 setTimeout
, 每次调用 cb
以前,都清除定时器,来实现相似于 debounce
的操做,一样能够解决重复的 callback
问题。
目前还有一个问题,那即是深度的数据侦测,咱们可使用递归代理的方式来实现:
function reactive(data, cb) { let res = null let timer = null res = data instanceof Array ? []: {} for (let key in data) { if (typeof data[key] === 'object') { res[key] = reactive(data[key], cb) } else { res[key] = data[key] } } return new Proxy(res, { get(target, key) { return Reflect.get(target, key) }, set(target, key, val) { let res = Reflect.set(target, key, val) clearTimeout(timer) timer = setTimeout(() => { cb && cb() }, 0) return res } }) } let data = { foo: 'foo', bar: [1, 2] } let p = reactive(data, () => { console.log('trigger') }) p.bar.push(3) // trigger 复制代码
对代理的对象进行遍历,对每一个 key
都作一次 proxy
,这是递归实现的方式。 同时,结合前面提到的 timer
避免重复 set 的问题。
这里咱们能够输出代理后的对象 p
:
能够看到深度代理后的对象,都携带 proxy
的标志。
到这里,咱们解决了使用 proxy
实现侦测的系列细节问题,虽然这些处理方式能够解决问题,但彷佛并不够优雅,尤为是递归 proxy
是一个性能隐患, 当数据对象比较大时,递归的 proxy 会消耗比较大的性能,而且有些数据并不是须要侦测,咱们须要对数据侦测作更细的控制。
接下来咱们就看下 Vue3 是如何使用 Proxy
实现数据侦测的。
Vue3 项目结构采用了 lerna
作 monorepo
风格的代码管理,目前比较多的开源项目切换到了 monorepo 的模式, 比较显著的特征是项目中会有个 packages/
的文件夹。
Vue3 对功能作了很好的模块划分,同时使用 TS 。咱们直接在 packages 中找到响应式数据的模块:
其中,reactive.ts
文件提供了 reactive
函数,该函数是实现响应式的核心。 同时这个函数也挂载在了全局的 Vue 对象上。
这里对源代码作一点程度的简化:
const rawToReactive = new WeakMap() const reactiveToRaw = new WeakMap() // utils function isObject(val) { return typeof val === 'object' } function hasOwn(val, key) { const hasOwnProperty = Object.prototype.hasOwnProperty return hasOwnProperty.call(val, key) } // traps function createGetter() { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver) return isObject(res) ? reactive(res) : res } } function set(target, key, val, receiver) { const hadKey = hasOwn(target, key) const oldValue = target[key] val = reactiveToRaw.get(val) || val const result = Reflect.set(target, key, val, receiver) if (!hadKey) { console.log('trigger ...') } else if(val !== oldValue) { console.log('trigger ...') } return result } // handler const mutableHandlers = { get: createGetter(), set: set, } // entry function reactive(target) { return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, ) } function createReactiveObject(target, toProxy, toRaw, baseHandlers) { let observed = toProxy.get(target) // 原数据已经有相应的可响应数据, 返回可响应数据 if (observed !== void 0) { return observed } // 原数据已是可响应数据 if (toRaw.has(target)) { return target } observed = new Proxy(target, baseHandlers) toProxy.set(target, observed) toRaw.set(observed, target) return observed } 复制代码
rawToReactive
和 reactiveToRaw
是两个弱引用的 Map
结构,这两个 Map
用来保存 原始数据
和 可响应数据
,在函数 createReactiveObject
中,toProxy
和 toRaw
传入的即是这两个 Map
。
咱们能够经过它们,找到任何代理过的数据是否存在,以及经过代理数据找到原始的数据。
除了保存了代理的数据和原始数据,createReactiveObject
函数仅仅是返回了 new Proxy
代理后的对象。 重点在 new Proxy
中传入的handler参数 baseHandlers
。
还记得前面提到的 Proxy
实现数据侦测的细节问题吧,咱们尝试输入:
let data = { foo: 'foo', ary: [1, 2] } let r = reactive(data) r.ary.push(3) 复制代码
打印结果:
能够看到打印输出了一次 trigger ...
深度侦测数据是经过 createGetter
函数实现的,前面提到,当对多层级的对象操做时,set
并不能感知到,可是 get 会触发, 于此同时,利用 Reflect.get()
返回的“多层级对象中内层” ,再对“内层数据”作一次代理。
function createGetter() { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver) return isObject(res) ? reactive(res) : res } } 复制代码
能够看到这里判断了 Reflect 返回的数据是否仍是对象,若是是对象,则再走一次 proxy
,从而得到了对对象内部的侦测。
而且,每一次的 proxy
数据,都会保存在 Map
中,访问时会直接从中查找,从而提升性能。
当咱们打印代理后的对象时:
能够看到这个代理后的对象内层并无代理的标志,这里仅仅是代理外层对象。
输出其中一个存储代理数据的 rawToReactive
:
对于内层 ary: [1, 2]
的代理,已经被存储在了 rawToReactive
中。
由此实现了深度的数据侦测。
function hasOwn(val, key) { const hasOwnProperty = Object.prototype.hasOwnProperty return hasOwnProperty.call(val, key) } function set(target, key, val, receiver) { console.log(target, key, val) const hadKey = hasOwn(target, key) const oldValue = target[key] val = reactiveToRaw.get(val) || val const result = Reflect.set(target, key, val, receiver) if (!hadKey) { console.log('trigger ... is a add OperationType') } else if(val !== oldValue) { console.log('trigger ... is a set OperationType') } return result } 复制代码
关于屡次 trigger
的问题,vue 处理得很巧妙。
在 set
函数中 hasOwn
前打印 console.log(target, key, val)
。
输入:
let data = ['a', 'b'] let r = reactive(data) r.push('c') 复制代码
输出结果:
r.push('c')
会触发 set
执行两次,一次是值自己 'c'
,一次是 length
属性设置。
设置值 'c'
时,传入的新增索引 key
为 2
,target
是原始的代理对象 ['a', 'c']
,hasOwn(target, key)
显然返回 false
,这是一个新增的操做,此时能够执行 trigger ... is a add OperationType
。
当传入 key
为 length
时,hasOwn(target, key)
,length
是自身属性,返回 true
,此时判断 val !== oldValue
, val
是 3
, 而 oldValue
即为 target['length']
也是 3
,此时不执行 trigger
输出语句。
因此经过 判断 key 是否为 target 自身属性,以及设置val是否跟target[key]相等 能够肯定 trigger
的类型,而且避免多余的 trigger
。
实际上本文主要集中讲解 Vue3 中是如何使用 Proxy
来侦测数据的。 而在分析源码以前,须要讲清楚 Proxy
自己的一些特性,因此讲了不少 Proxy
的前置知识。同时,咱们也经过本身的方式来解决这些问题。
最后,咱们对比了 Vue3 中, 是如何处理这些细节的。能够看出,Vue3 并不是简单的经过 Proxy
来递归侦测数据, 而是经过 get
操做来实现内部数据的代理,而且结合 WeakMap
来对数据保存,这将大大提升响应式数据的性能。
有兴趣的小伙伴能够针对 递归Proxy 和 这种Vue3的这种实现方式作相应的 benchmark , 这二者的性能差距比较大。
文章仍是对 reactive
作了很大程度的简化,实际上要处理的细节要复杂得多。 更多的细节仍是须要查看源码得到。