在10月05日凌晨Vue3的源代码正式发布了,来自官方的消息:
目前的版本是 Pre-Alpha , 仓库地址:Vue-next,可以通过Composition API了解更多新版本的信息,目前版本单元测试相关情况vue-next-coverage。
文章大纲:
Vue 的核心之一就是响应式系统,通过侦测数据的变化,来驱动更新视图。
实现可响应对象的方式
通过可响应对象,实现对数据的侦测,从而告知外界数据变化。实现可响应对象的方式:
- getter 和 setter
- defineProperty
- Proxy
关于前两个 API 的使用方式不多赘述,单一的访问器 getter/setter 功能相对简单,而作为 Vue2.x 实现可响应对象的 API -defineProperty ,API 本身存在较多问题。
Vue2.x 中,实现数据的可响应,需要对 Object 和 Array 两种类型采用不同的处理方式。
Object 类型通过 Object.defineProperty 将属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key,来实现深度的侦测。
为了感知 Array 的变化,对 Array 原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题,或者说不够方便的情况。同时,defineProperty 通过递归实现 getter/setter 也存在一定的性能问题。
更好的实现方式是通过 ES6 提供的 Proxy API。
Proxy API 的一些细节
Proxy API 具有更加强大的功能,相比旧的 defineProperty API ,Proxy 可以代理数组,并且 API 提供了多个 traps ,可以实现诸多功能。
这里主要说两个trap: get 、 set , 以及其中的一些比较容易被忽略的细节。
细节一:trap 默认行为
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 // "htmlcode">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 就方便得多。
细节二:多次触发 set / get
从前面的例子中可以看出,当代理对象是数组时,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 。
我们后面会讲述如何解决相应的这个问题,继续。
细节三:proxy 只能代理一层
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 中的细节问题
前面提到了使用 Proxy 来侦测数据变化,有几个细节问题,包括:
- 使用 Reflect 来返回 trap 默认行为
- 对于 set 操作,可能会引发代理对象的属性更改,导致 set 执行多次
- proxy 只能代理对象中的一层,对于对象内部的操作 set 未能感知,但是 get 会被执行
接下来,我们将先自己尝试解决这些问题,后面再分析 Vue3 是如何解决这些细节的。
setTimeout 解决重复 trigger
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 "text-align: center">可以看到深度代理后的对象,都携带 proxy 的标志。
到这里,我们解决了使用 proxy 实现侦测的系列细节问题,虽然这些处理方式可以解决问题,但似乎并不够优雅,尤其是递归 proxy 是一个性能隐患,当数据对象比较大时,递归的 proxy 会消耗比较大的性能,并且有些数据并非需要侦测,我们需要对数据侦测做更细的控制。
接下来我们就看下 Vue3 是如何使用 Proxy 实现数据侦测的。
Vue3 中的 reactivity
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) "htmlcode">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) "text-align: center">可以看到这个代理后的对象内层并没有代理的标志,这里仅仅是代理外层对象。
输出其中一个存储代理数据的 rawToReactive :
对于内层 ary: [1, 2] 的代理,已经被存储在了 rawToReactive 中。
由此实现了深度的数据侦测。
问题二:如何避免多次 trigger ?
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 做了很大程度的简化,实际上要处理的细节要复杂得多。更多的细节还是需要查看源码获得。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]