2020年前端大事件之一,Vue 3.0终于正式发布了。作为一个大的版本更新,Vue 3 与 Vue 2相比,实现原理,使用方式等均有着不小的改动。本文主要会介绍讲述二块内容,分别是Vue 3.0 的简要介绍,Vue 3.0 数据侦测源码分析。小伙伴们可以根据自己的需求,查看对应的内容,也欢迎各位一起探讨,一起学习。
2016年,Vue 2.0 正式发布,时至今日,已经过去了四年的时光。诚然,在这四年中,Vue 2的社区建设一直呈现出一副蓬勃向上的态势,各种第三方包,工具层出不重。但是,在这四年中,前端技术飞速发展,typescript,lerna等新技术新理念变的越来越流行,越来越普及,如何接入并使用这些,成为了vue开发者的一个问题。尤大和他的小伙伴们自然也发现了这个问题。同时,伴随着ES6的标准化,主流浏览器普遍提供了新的JavaScript功能支持,而伴随着时间的迁移,前端应用的处境越来越复杂,要求也不断发生变化,之前 Vue 2 代码库中设计和体系结构问题逐渐暴露了出来,加上其他的种种期望和要求,Vue 3的研发发布被提上了日程。整个研发阶段,大致可以分为以下四个阶段:
作为一个大的版本更新,Vue 3 相比于 Vue 2,自然有着不小的变化;简单点来说呢,主要可以分为以下几个新的特性:
image20201212163402159.png
各个packages的功能作用如下:
image20201212175212041.png
而在 Vue 3中,我们则可以这样写:
image20201212175349672.png
其中, setup 相当于 Vue 2 中的beforeCreate 与 created。除此之外,组合式 API 还新增了生命周期钩子函数,如onBeforeMount, onMounted等。在简单了解了写法之后,我们再来聊聊为什么说组合式 API 要优于之前的选项式 API。在使用选项式 API 时,当我们组件的功能越来越复杂,我们同一代码逻辑将会分散在 components,props,data,computed,methods 和生命周期函数中,因此降低了代码的可阅读性和可维护性。想想吧,当我们开发复杂需求的时候,从如山一般的代码中找到自己想要修改的变量后,又要回头去 methods 中找对应的函数,再去生命周期函数中看调用的场景,是不是很痛苦?但是使用组合式 API 就可以很好的解决这个痛点。除此之外,使用组合式 API 创建的组件,复用时更加方便,相比于Mixins和Scoped插槽更灵活。具体的使用等,由于本文篇幅限制,在此不多加以赘述,推荐各位去官网查看文档食用。
image20201213174813321.png
很明显,只有 message 所在的 div 是动态的,靶向更新该节点即可。问题是如何从整个 DOM 节点树中,定位到这个节点呢?这个时候,就轮到patchFlags出场了。Vue 3 在 compiler 时,分析模板并提取有效信息,Vue 3 根据这些信息,在创建 VNode 的时候,打上标记,PatchFlags = 1,也就是上图中下发红框处。通过 PatchFlags,Vue 3就可以在 VNode 创建阶段,将所有的动态节点提取出来,并统一存放在一个数组内,也就是 dynamicChildren。说到这里,就不得不提到 Block 块的概念。其实简单理解,一个 Block 就是一个拥有特殊属性的 VNode,其中,就包括 dynamicChildren。上文中的最外层 div,就是一个 Block,它的 dynamicChildren 数组中,会包含其所有子代的动态节点。当我们更新这一个块中的节点时,就不需要再递归遍历整个虚拟 Dom 节点树,跟踪这个块中的动态节点即可。Block Tree 也是以此为基础。当然,一个 Block 是无法组成 Block Tree 的,一个虚拟 DOM 节点树中会有多个 VNode 作为 Block构成Block Tree 。这里,就不得不提到 v-if 条件渲染和 v-for 循环渲染了。
首先说一下 v-if 条件渲染。由于 dynamicChildren 的 diff 判断是忽略了 VDom 树的层级的,如果不做任何处理的话,其只会返回并告知其子树中变化的节点,而忽略其他的。说起来可能有点晦涩,还是看下面的代码能更好的理解:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
// 不做处理时,无论真假,此时收集到的动态节点为
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
从这里我们可以看出,尽管 p 标签的父标签已经发生了变化,但是并没有被收集到,这样就会导致 DOM 结构不稳定的问题。Vue 3是如何解决这个问题的呢?Vue 3 将 v-if , v-else 对应的标签都当做一个 Block,从而构成 Block Tree,dynamicChildren 在收集动态节点的时候,也会收集 Block,实际的结果如下:
image20201213213508710.png
说完 v-if,再来聊聊 v-for,其实思路是一致的,解决方法就是是 v-for 的元素作为一个 Block 即可。
image20201213213947501.png
具体的优化策略,可以参考这篇文章:Vue3 Compiler 优化细节,如何手写高性能渲染函数,讲的很详细。
image20201213224031851.png
但是仅仅启动服务还是远远不够的,在平时的项目开发过程中,我们都会安装使用各种依赖。在使用中,我们往往只需要写 import xxx from xxx
这种代码,就可以引入并使用,剩下的工作,也是找到这些文件,并将之打包进产物中,都有 webpack 帮我们做好了。但是由于 Vite 使用的是浏览器的 ESM,浏览器并不知道我们这些依赖会安装在 node-modules 里面,它只会根据相对/绝对定位来找这些文件,这自然是无法找到的。所以,为了避免找不到模块导致的问题,vite 需要自己去拦截浏览器对模块的请求并且返回处理后的结果。Vite 写了一个 koa 中间件,拿到 import 资源的信息后,判断其请求的是不是 npm 模块,如果是,则会对 import xxx from xxx
这种语法进行处理,给它加上一个 @modules
的路径前缀,然后走后续的逻辑。同样,也是先拿到资源路径,判断是否已 @modules
开头,如果是,则会拿出包名,并且去 node_modules 中去处理返回对应内容。到这里,vite 的大部分工作就已经完成了,剩下的还有诸如 vue,css,ts 等文件的处理,就留给诸位自行探索了,这里给各位安利一个其他大佬写的 vite 源码解析,希望对大家有所帮助。如果小伙伴们对于浏览器解析有兴趣的,推荐大家了解一下 snowpack 这款首次提出浏览器 ESM 的工具。
零零总总说了这么多,第一部分 Vue 3.0 的简单介绍总算说完了,其实总结起来就是一句话:Vue 3 相比于 Vue 2,性能更好,理念更先进,开发更友好,十分推荐大家使用。
在了解 Vue 3.0 的数据侦测之前,我们最好先了解一下 Vue 2 的数据侦测和有关的前置知识。
Vue 2 面世已经足够久了,相信看这边文章的小伙伴们都对其的数据绑定逻辑十分的熟悉了解了。不管怎么样,我们这里还是简要介绍一下。为了收集数据依赖,监听数据变化并更新视图,vue2.0 通过 Object.defineProperty 这个方法,对 对象的 set 与 get 属性访问描述符进行了劫持;多层对象,则是通过递归的方式,来添加劫持;针对数组,则是通过对数组总共有 8 个原型方法进行了改写;就总体的设计模式而言,是通过实现观察者模式实现数据监听,并在此之上,实现的MVVM模式;下图就是对应的代码:
然而,这种实现方式存在一定的局限性,其中,最老生常谈的,就是对于对象和数组动态添加的属性,无法进行监听,如Array[1] = 111这种修改,这也是为什么我们在vue开发中,遇见这种情况,常使用vue.$set与slice等方法的原因;除此之外,由于对于对象需要做递归遍历的操作,导致性能并没有达到最优;无法对 Map,WeakMap 等数据结构进行数据绑定等。
而说到 Vue 3 的数据侦测的实现,无论如何都绕不过 Reflect 和 Proxy。这两者都是 ES 6 新增的。其中,Reflect 作为一个内置的对象,负责提供拦截 JavaScript 操作的方法,这些方法和 proxy handler 的方法相同;而 proxy 的作用则是在目标对象的外层,设置一层拦截,外界对于这个对象的访问,都会过这个拦截,从而可以实现对外界访问,响应的过滤和改写。Proxy 有两个入参,分别是 target 目标对象,handler 拦截处理函数。Proxy 和 Reflect 搭配使用,由 Proxy 负责对访问进行拦截,Reflect 则负责对数据进行操作。在能够收集到数据变化后,就可以根据变化去通知视图进行相应的更新,这也就是 ”发布-订阅者“最简单的实现。数据是发布者,视图是订阅者,视图得知数据变化后,就会进行更新。
Vue 3 的数据响应式 API 都是在 reactivity 里实现并暴露出来的,所以我们这次主要看的是这个 package 的代码。
image20201216230119426.png
reactivity 向外暴露出的 API 主要是可以划分为四类,分别是 computed,effect,reactive,ref,其中,我们这次主要了解的是 reactive 基于 Proxy 实现的响应式数据。
简单点来理解呢,reactive 和 ref 都可以说是返回数据的响应式副本,供我们在页面上进行使用,视图会随着其数值的变化而更新,以下是一个简单的使用示例:
image20201216231358009.png
在此之后,我们就可以在模板中直接使用 list 或者 user 变量,而不需要像使用 ref 那样通过 list.value 来使用。ref 实际上是一个数据容器,实现的方法和 reactive 还是有区别的。
首先呢,我们来看一下 reactive.ts 文件中暴露出来的 reactive 方法。
image20201216231735453.png
我们可以看到,reactive 方法接受一个函数,也就是我们的目标对象。方法内首先会对传入的目标对象做一个检测,检测其是否是一个 readonly 只读类型的数据,对于只读类型数据,自然没有什么必要实现数据响应,毕竟不会发生变化嘛;如果非只读类型数据,reactive 方法就会调用一个名为 createReactiveObject 的方法来给目标对象创建一个响应式的对象副本。这个副本对象中自动接入了ref,所以我们不需要在通过 .value 拿到并使用其属性,直接通过 key 就可以使用。
那么,重头戏来了,让我们看看 createReactiveObject 方法是如何创建响应式副本的。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
首先, createReactiveObject 方法接收 4 个参数,分别是 target,isReadonly,baseHandlers,collectionHandlers。
好了,在说完入参之后,我们就可以来看一下主要的代码逻辑了。
creativeReactiveObject 方法首先会对传入的目标对象进行分析:如果传入的值并非对象,或传入的对象本身已经是一个响应式的对象,又或该值的数据类型并不在支持数据侦测的白名单上时,会直接返回该值;在不满足上述条件的情况下,才会利用 new Proxy 给目标对象生成一个 Proxy 实例。在生成 Proxy 实例时,会先对 target 的数据类型进行判断,判断其是否为 Map,Set,WeakMap,WeakSet,如果是,则使用 collectionHandlers,不是则使用 baseHandlers。那么,问题来了,collectionHandlers 与 baseHandlers 的区别在哪里呢?
还是看回 reactive 方法,我们可以看到 baseHandlers 和 collectionHandlers 实际的值分别是 mutableHandlers 与 mutableCollectionHandlers。下面分别看一下这二者的代码。
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(false, false)
}
让我们先不管 createInstrumentationGetter 这个方法,先对比一下 mutableHandlers 和 mutableCollectionHandlers,我们会发现mutableCollectionHandlers 只有 get,这个是给不需要派发更新的变量使用的;而 mutableHandlers 则有 get,set 等,就是我们真正需要使用的 handler。接下来,让我们看一下 get,set等方法。
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
其中,createGetter负责依赖收集,createSetter负责变化通知。
createGetter 方法返回一个 get 方法,这个方法接收3个入参,分别是 target,key,receiver。get 方法首先会对 key 值进行校验,根据 key 值和 ReactiveFlags 返回特殊值;如果没有匹配到,才会继续判断 target 是否是数组,并且对数组类型的 target 的 get 操作进行设置;如果是对象类型的数据,且非只读,则会递归调用 reactive 方法去创建响应式的副本,相较于 Vue 2 在应用初始化时就递归劫持所有的 get 方法而言,这种处理方式有着很明显的优化。整个逻辑并不复杂,这里我们需要注意的是一个名为 track 的方法,这个方法将 activeEffect 添加到数据属性的依赖列表中,完成依赖的收集工作(依赖实质上是一个个的 effect 方法,通过 WeakMap 进行存储)。
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key) // 依赖收集
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
createSetter方法负责通知变化,也就是通过调用 trigger 更新数据,并通知依赖进行处理。需要注意的是,在进行处理时,会区分是此时的操作是动态添加属性,还是对属性进行更新。trigger 方法是在修改值时,通过 target 对象,从全局 weak map 对象中取出对应的depMap 对象,然后再根据修改的 key 取出对应的 dep 依赖集合,并遍历并执行该集合中所有的 effect,从而实现的数据更新与通知,具体的代码我就不放出来了,有兴趣的小伙伴可以自行了解一下。
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: 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, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 属性更新
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
到了这里,我们就已经很简单的把 Vue 3 数据侦测的代码过了一遍了。
The process: Making Vue 3
https://increment.com/frontend/making-vue-3/
Vue3 Compiler 优化细节,如何手写高性能渲染函数
https://zhuanlan.zhihu.com/p/150732926
vite design
https://github.com/zhangyuang/vite-design
有了 vite,还需要 webpack 么?
https://zhuanlan.zhihu.com/p/150083887