10 月 5 日,尤雨溪在 GitHub 开放了 vue 3.0 处于 pre-alpha 状态的源码,这次 Vue 3.0 Updates 版本的更新,将带来五项重大改进:
截止目前,Vue 3.0 主要的架构改进、优化和新功能均已完成,剩下的主要任务是完成一些 Vue 2 现有功能的移植。
结合目前的 RFCs 和已经完成的改进,可以窥探到 Vue 3.0 将带来:
看了这么多的改进和新功能的介绍,新版本到底会给性能带来多大的提升,真的很值得期待。
由于 Vue 3 的变更检测是基于 Proxy 代理的,所以在理解 Vue 3 的响应系统之前,有必要先熟知 Proxy 具有哪些特性和它能解决什么问题。
JavaScript 运行环境包含了一些不可枚举、不可写入的对象属性,然而在 ES5 之前开发者无法定义他们自己的不可枚举属性或不可写入属性。ES5 引入 Object.defineProperty() 方法以便开发者在这方面能够像 JS 引擎那样做。
ES6 为了让开发者能进一步接近 JS 引擎的能力,推出了 Proxy,代理是一种封装,能够拦截并改变 JS 引擎的底层操作。简单的说,就是在目标对象上架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截,提供了一种改变 JS 引擎过滤和改写的能力。
let target = {};
let proxy = new Proxy(target, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
通过调用 new Proxy() 来创建一个代理时,需要传递两个参数:目标对象 target 以及一个处理器 handler,handler 是一个对象,可以定义一个或多个陷阱函数 (能够响应特定操作的函数),来定制拦截行为。
如果未提供陷阱函数,代理会对所有操作采取默认行为。
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
我们已经知道,通过调用 new Proxy() 可以创建一个代理用来替代目标对象 target。这个代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。
Reflect 是 ES6 提供的一个内置的对象,它提供拦截 JavaScript 操作的方法。被 Reflect 对象所代表的反射接口,是给底层操作提供默认行为的方法的集合。
每个陷阱函数都可以重写 JS 对象的一个特定内置行为,允许你拦截并修改它。如果你仍然需要使用原先的内置行为,则可使用对应的 Reflect 方法。
简单的来讲,Proxy 是拦截默认行为,Reflect 是恢复默认行。被 Proxy 拦截、过滤了一些默认行为之后,可以使用 Reflect 恢复未被拦截的默认行为。通常它们两个会结合在一起使用。
到这里不明白没关系,在下文会介绍的陷阱函数中,应该就会明白了。
let target = {};
let proxy = new Proxy(target, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
proxy.name = 'proxy';
delete proxy.name;
name in proxy;
上面代码中,Proxy 对象设置了一些拦截操作(get、delete、has),并且内部都调用了对应的 Reflect 方法,保证原生行为能够正常执行。
每个陷阱函数都有一个对应的 Reflect 方法,每个方法都与对应的陷阱函数同名,并且接收的参数也与之一致。
下表中列出了所以陷阱函数和 Reflect 方法对应的默认行为,在这里只介绍其中几个陷阱函数的用法,因为它们在 Vue 3 源码中有所涉及。
陷阱函数 | 被重写的行为 | 默认行为 |
---|---|---|
get | 读取一个属性的值 | Reflect.get() |
set | 写入一个属性 | Reflect.set() |
has | in 运算符 | Reflect.has() |
deleteProperty | delete 运算符 | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty |
ownKeys | Object.keys、Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 使用 new 调用一个函数 | Reflect.construct() |
下文介绍到的陷阱函数,都会在 Vue 3 源码中出现,提前进行了解。
假设你想要创建一个对象,并要求其属性值只能是数值,并且在属性值不为数值类型时应当抛出错误。
可以使用 set() 陷阱函数来重写设置属性值时的默认行为,该陷阱函数能接受四个参数:
let target = {
name: "target"
};
let handler = {
set(target, key, value, receiver) {
// 拦截,忽略已有属性,避免影响它们
if (!target.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 满足条件的进行写入 等价于 target[key] = value;
return Reflect.set(target, key, value, receiver);
}
}
let proxy = new Proxy(target, handler);
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误
proxy.anotherName = "proxy";
set 陷阱函数允许你在写入属性值的时候进行拦截,而 get() 代理陷阱则允许你在读取属性值的时候进行拦截。
我们知道,JavaScript 在读取对象不存在的属性时并不会抛出错误,而会把 undefined 当作该属性的值,例如:
let target = {};
console.log(target.name); // undefined
JS 的这种行为在非常大型的项目中,可能会导致严重的问题,尤其是当属性名称存在书写错误时。我们可以使用代理对访问不存在的属性时,抛出错误。
由于该属性验证只须在读取属性时被触发,因此只要使用 get() 陷阱函数。该陷阱函数会在读取属性时被调用,即使该属性在对象中并不存在,它能接受三个参数:
Reflect.get() 方法同样接收这三个参数,并且默认会返回属性的值。
使用 get() 陷阱函数与 Reflect.get() 方法在目标属性不存在时抛出错误:
let proxy = new Proxy({}, {
get(target, key, receiver) {
// 读取属性时进行拦截
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
// 保持默认的读取行为
return Reflect.get(target, key, receiver);
}
})
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // 抛出错误
in 运算符用于判断指定对象中是否存在某个属性,如果对象的属性名与指定的字符串或符号值相匹配,那么 in 运算符应当返回 true,无论该属性是对象自身的属性还是其原型的属性。例如:
let target = {
value: 42
}
console.log("value" in target); // true
console.log("toString" in target); // true
value 是对象自身的属性,而 toString 则是原型属性,可以使用代理的 has() 陷阱函数来拦截这个操作,从而在使用 in 运算符时返回不同的结果。
has() 陷阱函数会在使用 in 运算符的情况下被调用,并且会被传入两个参数:
Reflect.has() 方法接受与之相同的参数,并向 in 运算符返回默认响应结果。
使用 has() 陷阱函数以及 Reflect.has() 方法,允许你修改部分属性在接受 in 检测时的行为,但保留其他属性的默认行为。
let target = {
name: "target",
value: 42
}
let proxy = new Proxy(target, {
has(target, key) {
// 拦截操作
if (key === "value") {
return false;
} else {
// 保持默认行为
return Reflect.has(target, key);
}
}
})
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
delete 运算符能够从指定对象上删除一个属性,在删除成功时返回 true ,否则返回 false。如果试图用 delete 运算符去删除一个不可配置的属性,在严格模式下将会抛出错误;而非严格模式下只是单纯返回 false 。这里有个例子:
let target = {
name: "target",
value: 42
}
Object.defineProperty(target, "name", {configurable: false});
console.log("value" in target); // true
delete target.value; // true
console.log("value" in target); // false
delete target.name; // 非严格模式下返回false(在严格模式下会抛出错误)
console.log("name" in target); // true
name 属性是不可配置的,因此对其使用 delete 操作符只会返回 false(如果代码运行在严格模式下,则会抛出错误)。可以在代理对象中使用 deleteProperty() 陷阱函数以改变这种行为。
deleteProperty 陷阱函数会在使用 delete 运算符去删除对象属性时下被调用,并且会被传入两个参数:
Reflect.deleteProperty() 方法也接受这两个参数,并提供了 deleteProperty() 陷阱函数的默认实现。
可以结合 Reflect.deleteProperty() 方法以及 deleteProperty() 陷阱函数,来修改 delete 运算符的行为。例如,能确保 value 属性不被删除:
let target = {
name: "target",
value: 42
}
let proxy = new Proxy(target, {
deleteProperty(target, key) {
// 拦截行为
if (key === "value") {
return false;
} else {
// 恢复行为
return Reflect.deleteProperty(target, key);
}
}
})
console.log("value" in proxy); // true
// 尝试删除 proxy.value
delete proxy.value; // false // 不能删除,因为这个默认行为被拦截了
console.log("value" in proxy); // true
console.log("name" in proxy); // true
// 尝试删除 proxy.name
delete proxy.name; // true
console.log("name" in proxy); // false
value 属性是不能被删除的,因为该操作被 proxy 对象拦截。这么做允许你在严格模式下保护属性避免其被删除,并且不会抛出错误。
ownKeys() 代理陷阱拦截了内部方法 [[OwnPropertyKeys]],并允许你返回一个数组用于重写该行为。
可以使用 ownKeys() 陷阱函数去过滤特定的属性,以避免这些属性被 Object.keys()、 Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 或 Object.assign() 方法使用。
ownKeys() 陷阱函数的默认行为由 Reflect.ownKeys() 方法实现,会返回一个由全部自有属性的键构成的数组,无论键的类型是字符串还是符号。
ownKeys() 陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象,不合要求的返回值会导致错误。
假设你不想在结果中包含任何以下划线打头的属性(在 JS 的编码惯例中,这代表该字段是私有的),那么可以使用 ownKeys() 陷阱函数来将它们过滤掉,就像下面这样:
let proxy = new Proxy({}, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => {
// 过滤掉一些特定属性
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private"; // 被过滤掉
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy);
let keys = Object.keys(proxy);
let symbols = Object.getOwnPropertySymbols(proxy);
console.log(names); // ["name"]
console.log(names[0]); // "name"
console.log(keys); // ["name"]
console.log(keys[0]); // "name"
console.log(symbols); // [Symbol(name)]
console.log(symbols[0]); // Symbol(name)
这个例子使用了一个 ownKeys 陷阱函数,做了如下操作:
因此在输出结果中 _name 属性则始终没有出现在结果里,因为它被过滤了。
ownKeys 陷阱函数也能影响 for-in 循环,因为这种循环调用了陷阱函数来决定哪些值能够被用在循环内。(Vue 源码会涉及这里)
到这里陷阱函数的介绍就告一段落了,下面我们回到正题,一起来看下 Vue 3 是如何使用 Proxy 代理打造全新的响应系统的吧。
Vue 2 中响应系统是基于 Object.defineProperty 的,递归遍历 data 对象上的所有属性,将其转换为 getter/setter,当 setter 触发时,通知 watcher,来进行变更检测的。
...
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
...
for (const key in propsOptions) {
...
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
这种变更检测机制存在一个限制,那就是 Vue 无法检测到对象属性的添加或删除。为此我们需要使用 Vue.set 和 Vue.delete 来保证响应系统的运行符合预期。
// vue 2
Vue.set(vm.state, 'name', 'vue 2');
// vue 3
this.state.name = 'vue 3';
Vue 3 进行了全新改进,使用 Proxy 代理的作为全新的变更检测,不再使用 Object.defineProperty。
使用代理的好处是,对目标对象 target 架设了一层拦截,可以对外界的访问进行过滤和改写,不用再递归遍历对象的所有属性并进行 getter/setter 转换操作,这使得组件更快的初始化,运行时的性能上将得到极大的改进,据测试新版本的 Vue 比之前 速度快了 2 倍(非常夸张)。
Vue 3.0 创建响应式数据可以有三种方法:
// 根组件
<template>
<div id="app">
<div>{{ name }}</div>
</div>
</template>
<script>
import { createApp } from Vue;
export default {
const App = {
data: {
name: 'Vue 3',
// count: ref(0)
}
}
createApp().mount(App, '#app')
</script>
data 选项定义的数据,最终也会被 reactive 转换为响应式的 Proxy 代理。
// runtime-core > src > apiOptions.ts
instance.data = reactive(data)
返回原始对象的响应式 Proxy 代理( 同 2.x 的 Vue.observate() )。
<template>
<div>{{ state.name }}</div>
</template>
<script>
import { reactive } from Vue;
export default {
setup() {
const state = reactive({
name: "Vue 3"
})
return {
state
}
}
}
</script>
reactive() 函数最终返回一个可观察的响应式 Proxy 代理。
// reactivity > src > reactive.ts
reactive(target) => observed => new Proxy(target, handlers)
获取一个内部值并返回一个响应式的可变 ref 对象。
<template>
<div>{{ name }}</div>
</template>
<script>
import { ref } from Vue;
export default {
setup() {
return {
name: ref('Vue 3')
}
}
}
</script>
ref 对象有一个指向内部值的单个属性 .value。如果将一个值分配为 ref 对象,则 reactive() 方法会使该对象具有高度的响应性。
...
const r = {
_isRef: true,
get value() {
track(r, "get" /* GET */, 'value');
return raw;
},
set value(newVal) {
raw = convert(newVal);
// trigger 方法扮演通信员的角色,贯穿整个响应系统,使得 ref 具有高度的响应性
trigger(r, "set" /* SET */, 'value', { newValue: newVal } );
}
};
return r
...
因此,无需在模版中追加 .value。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
在 Vue 3 中,将 Vue 的核心功能(例如创建和观察响应状态)公开为独立功能,例如使用 reactive() 创建一个响应状态:
import { reactive } from 'vue'
// reactive state
const state = reactive({
name: "vue 3.0",
count: ref(42)
})
我们向 reactive() 函数传入了一个 {name: "Vue 3.x", count: {…}},对象,reactive() 函数会将传入的对象进行 Proxy 封装,将其转换为"可观测"的对象。
//reactive f => createReactiveObject()
function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
...
// 设置拦截器
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
observed = new Proxy(target, handlers);
...
return observed;
}
传入的目标对象 target 最终会变成这样:
从打印的结果我们可以得知,被代理的目标对象 target 设置了 get()、set()、deleteProperty()、has()、ownKeys(),这几个陷阱函数,结合我们上文介绍的内容,一起来看下它们都做了什么。
get() 会自动读取使用 ref 对象创建的响应数据,并进行 track 调用。
// get() => createGetter(false)
function createGetter(isReadonly: boolean, unwrap: boolean = true) {
return function get(target: object, key: string | symbol, receiver: object) {
// 恢复默认行为
let res = Reflect.get(target, key, receiver)
// 根据目标对象 key 类型进行的一些处理
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 如果目标对象存在使用 ref 创建的数据,直接获取内部值
if (unwrap && isRef(res)) {
res = res.value // 案例中 这里是 42
} else {
// 调用 track() 方法
track(target, OperationTypes.GET, key)
}
return isObject(res)
? isReadonly
? readonly(res)
: reactive(res)
: res
}
}
set() 陷阱函数,对目标对象上不存在的属性设置值时,进行 “添加” 操作,并且会触发 trigger() 来通知响应系统的更新。解决了 Vue 2.x 中无法检测到对象属性的添加的问题。
function set(target, key, value, receiver) {
value = toRaw(value);
// 获取修改之前的值,进行一些处理
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
const hadKey = hasOwn(target, key);
// 恢复默认行为
const result = Reflect.set(target, key, value, receiver);
// //如果目标对象在原型链上,不要 trigger
if (target === toRaw(receiver)) {
/* istanbul ignore else */
{
const extraInfo = {
oldValue,
newValue: value
};
// 如果设置的属性不在目标对象上 就进行 Add
// 这就解决了 Vue 2.x 中无法检测到对象属性的添加或删除的问题
if (!hadKey) {
trigger(target, "add" /* ADD */ , key, extraInfo);
} else if (hasChanged(value, oldValue)) {
// trigger 方法进行一系列的调度工作,贯穿着整个响应系统,是变更检测的“通讯员”
trigger(target, "set" /* SET */ , key, extraInfo);
}
}
}
return result;
}
deleteProperty() 陷阱函数关联 delete 操作,当目标对象上的属性被删除时,会触发 trigger() 来通知响应系统的更新。这也解决了 Vue 2.x 中无法检测到对象属性的删除的问题。
// 这里就没什么好说的
function deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const oldValue = target[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
/* istanbul ignore else */
{
发布通知
trigger(target, "delete" /* DELETE */ , key, {
oldValue
});
}
}
return result;
}
function has(target, key) {
const result = Reflect.has(target, key);
track(target, "has" /* HAS */ , key);
return result;
}
function ownKeys(target) {
track(target, "iterate" /* ITERATE */ );
return Reflect.ownKeys(target);
}
从源码可以看出,这个两个陷阱函数并没有修改默认行为,但是它们都调用 track(...) 函数,回顾上文我们可知,has()会对应 in 操作的默认行为,ownKeys() 也会影响 for...in 循环。
梳理一下:
最后,本文就不详细介绍 track() 和 trigger() 两个函数的内部细节的实现了,但是从上图我们可以得知,track 是依赖收集阶段的核心函数,trigger 会对 getter 、effect 进行计算,贯穿 Vue 的整个响应系统,起到 调度、协调的作用。
原文:https://segmentfault.com/a/1190000021003414
安装 vue-cli3,在使用任何 @vue/composition-api 提供的能力前,必须先通过 Vue.use() 进行安装,安装插件后,您就可以使用新的 Composition API 来开发组件了。
Vue3 就是基于 Proxy 对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 --> 依赖收集阶段 --> 数据响应阶段
在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。新版Vue 3.0计划并已实现的主要架构改进和新功能:
有关即将发布的 Vue.js 的第 3 个主要版本的信息越来越多。通过下面的讨论,虽然还不能完全确定其所有内容,但是我们可以放心地认为,它将是对当前版本(已经非常出色)的巨大改进。 Vue 团队在改进框架 API 方面做得非常出色
用新的 Vue 3 编写的程序效果会很好,但性能并不是最重要的部分。对开发人员而言,最重要的是新版本将会怎样影响我们编写代码的方式。如你所料,Vue 3 带来了许多令人兴奋的新功能。值得庆幸的是
emmm 用半天时间捋顺了 vue3 的源码,再用半天时间写了个 mini 版……我觉得我也是没谁了,vue3 的源码未来一定会烂大街的,我们越早的去复现它,就……emm可以越早的装逼hhh
从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。
Vue 开发团队终于在今天发布了 3.0-beta.1 版本,也就是测试版。通常来说,从测试版到正式版,只会修复 bug,不会引入新功能,或者删改老功能。所以,如果你对新版本非常感兴趣,或者有新项目即将上马,不妨尝试一下新版本
对于大多数单页应用程序而言,管理路由是一项必不可少的功能。随着新版本的Vue Router处于Alpha阶段,我们已经可以开始查看下一个版本的Vue中它是如何工作的。
对于大多数组件,Vue2和Vue3中的代码即使不完全相同,也是非常相似的。但是,Vue3支持片段,这意味着组件可以有多个根节点。这在呈现列表中组件以删除不必要的包装器div元素时特别有用。但是,在本例中,表单组件的两个版本都将只保留一个根节点
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!