因为Vue 3.0刚推出,本身打算只是简单了解下,但了解Vue 3.0的新特性后,如monorepo代码管理,源码偏向函数性编程,还有Composition Api设计,拍手叫绝,决定认真学习下,刚好前段时间拉勾教育搞特惠,用便宜的价钱买了Vue 3.0源码解析,结合最近在拉勾前端训练营学到手撕Reactive Api,整理一篇Vue 3.0响应式原理笔记。
代理
深入了解Vue 3.0前,必须先了解Javascript的代理 (Proxy),因为Vue 3.0的响应式数据是基于代理实现的。在Vue 2.0时代,响应式数据是基于 Object.defineProperty
实现,可以用下图概括它的实现:
Object.defineProperty
的好处是兼容性好,可以操控对象属性的细节,然而也有相应的问题出现,一是直接修改对象本身,二是在数据劫持上性能劣于Proxy,三是由于是针对属性,所以如果属性上有任何变动,无法处理,四是无法处理数组。
上述的问题在Vue 3.0中通过 Proxy解决。
代理模式
代理不是JS独有的对象,JS的Proxy是基于代理模式所设计,所以了解代理模式,有助我们更好理解代理。 代理模式指的是间接操控对象的设计,用一张图概括说明:
我们平时在桌面上的捷径其实就是代理模式的实现,用户不是直接打开应用软件,而是通过桌面的捷径打开。
Proxy
Javascript的Proxy就是基于上述的代理模式设计的,可以通过Proxy间接操控对象或数组。
下面用代码示例简单介绍它的用法:
const target = {
foo: 'bar'
};
const handler = {
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
console.log (proxy.foo) // handler override
console.log (target.foo) // bar
Proxy接收两个参数,第一个是需要代理的对象,第二个是捕获器 (handler),它是一个对象,里面有Proxy指定的捕获方法,如 get, set, delete等,用于操作代理对象时,触发不同的捕获器。注意,与Object.defineProperty不同的是,它是以整个对象为单位,而不是属性。用户可以通过操作 Proxy创建的实例去间接操作对象本身。 上述的示例中,添加get处理器,重载对象的获取。
get接收3个参数: trapTarget, property, receiver。trapTarget是捕获对象,property是属性,receiver是代理对象本身。有了这些参数,就可以重建被捕获方法的原始行为:
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
console.log (receiver === proxy) // true
return trapTarget[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // true bar
console.log(target.foo); // bar
处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射API 也可以 像下面这样定义出空代理对象:
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
Proxy的简单介绍到此为止,详细可查阅 MSDN或 Javascript高级程序设计 (第四版)。
Reactive 简单实现
Vue 2.0 的响应式数据创建是 在"黑盒" 进行,即创建Vue实例时,根据传入的参数来创建响应式数据。而在 Vue 3.0 则是可以显式创建响应式数据:
<template>
<div>
<p>{{ state.msg }}</p>
<button @click="random">Random msg</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
msg: 'msg reactive'
})
const random = function() {
state.msg = Math.random()
}
return {
random,
state
}
}
}
</script>
上述的例子导入reactive函数,通过reactive显式创建响应式数据。
在阅读reactive源码前,先简单实现一个reactive来理解它。
先看下官方文档reactive是干什麽的:
简单来说,就是reactive接收一个对象,返回一个响应式副本,它是一个Proxy实例,里面的属性,包括嵌套对象都是响应式的。
根据上述得知,函数首先需要判定参数是否为对象,返回一个Proxy实例:
// 因为null的typeof也是object,所以要另外增加对它的判定
const isObject = val => val !== null && typeof val === 'object'
function reactive (target) {
if (!isObject (target)) {
return target
}
...
return new Proxy(target, handler)
}
现在可以实现捕获器,需要注意要对嵌套情况的处理:
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
function reactive (target) {
if (!isObject(target)) return target
const handler = {
get (target, key, receiver) {
const result = Reflect.get(target, key, receiver)
return convert(result)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
}
return result
},
deleteProperty (target, key) {
const result = Reflect.deleteProperty(target, key)
return result
}
return new Proxy(target, handler)
}
Vue 2.0对嵌套对象情况,是创建实例时直接递归转变为响应式数据,而在Vue 3.0则是当获取相应属性时处理,判断是否是嵌套对象,是则递归创建响应式数据,从而优化性能。
现在有一个大概的实现,不过最关键的响应式部分还没有实现。Vue 3.0的响应式设计与Vue 2.0 类似,也是使用观察者模式,所以可以参考 Vue 2.0的简单实现,有助于理解Vue 3.0的实现。
Vue 3.0会存有一个全局的TargetMap,用来存放收集依赖,它的键为被依赖的对象,值也是一个Map,键为被依赖的属性,值是属性发生改变时,需要调用的函数。因此我们需要track和trigger函数,前者收集依赖,后者当属性发生改变,调用函数。
let targetMap = new WeakMap() // 全局变量,存放依赖
function track (target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target) // 获取被依赖对象的Map,没有则创建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // 根据对象属性,获取需调用的函数,没有则创建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
剩下最关键的effect还没有实现。实现之前,先看一个使用例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = 0
effect(() => {
total = product.price * product.count
})
console.log(total) // 15000
product.price = 4000
console.log(total) // 12000
product.count = 1
console.log(total) // 4000
</script>
</body>
</html>
上述的例子可得知,当调用effect函数时,它会执行一次传入的函数,如果之后传入函数里的值发生变更,会调用之前effect里传入的函数。问题是Vue是怎样知道要调用的函数?答案是当调用effect函数时,Vue已经在获取相应的值时,收集依赖。先看effect的实现:
let activeEffect = null // 全局指针指向最近传入effect的函数
function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象属性,去收集依赖
activeEffect = null
}
通过上述的例子解释effect的执行过程。我们先命名例子传入的函数为 totalSum,当调用effect时,activeEffect指向totalSum,然后调用totalSum,它会分别获取 product.price 和 product.count,就在此时,触发了代理对象的get捕获器,因此触发了track函数,收集依赖。再看一次track吧:
let targetMap = new WeakMap() // 全局变量,存放依赖
function track (target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target) // 获取被依赖对象的Map,没有则创建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // 根据对象属性,获取需调用的函数,没有则创建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect) // 收集依赖
}
最后把track和trigger放进代理对象proxy里就完成reactive函数了:
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
function reactive (target) {
if (!isObject(target)) return target
const handler = {
get (target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
return convert(result)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty (target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 触发更新
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
用网上找到一个图概括Vue 3.0的响应式原理:
源码阅读
有了简单实现的基础后,可以阅读源码了。
Reactive函数的源代码位于源码路径 packages/reactivity/src/reactive.ts。
function reactive (target) {
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (target && target.__v_isReadonly) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
// isReadonly指定target是否唯读,baseHandlers是基本数据类型的代理捕获器,collectionHandlers则是集合的
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!isObject(target)) {
// 目标必须是对象或数组类型
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
return target
}
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
// target 已经有对应的 Proxy 了
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有在白名单里的数据类型才能变成响应式
if (!canObserve(target)) {
return target
}
// 利用 Proxy 创建响应式
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
// 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
return observed
}
以上是reactive基本构建过程,与之前实现差不多,只是源码考虑更多。isReadonly是一个布尔值,用于表示代理对象是否唯读。Proxy比Object.defineProperty好的一点是,它可以处理数组。
canObserve函数针对target作进一步限制:
const canObserve = (value) => {
return (!value.__v_skip &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value))
}
const isObservableType = /*#__PURE__*/ makeMap('Object,Array,Map,Set,WeakMap,WeakSet')
带有 __v_skip 属性的对象、被冻结的对象,以及不在白名单内的对象是不能变成响应式的。
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
创建代理对象,根据target的构造函数,如果是基本数据类型,则返回baseHandlers,它的值是mutableHandlers。
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}
无论命中哪个处理器函数,它都会做依赖收集和派发通知这两件事其中的一个。
依赖收集:get 函数
看下创造get捕获器的源码:
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
// 根据不同属性返回不同结果
if (key === "__v_isReactive" /* isReactive */) {
// 代理 observed.__v_isReactive
return !isReadonly
}
else if (key === "__v_isReadonly" /* isReadonly */) {
// 代理 observed.__v_isReadonly
return isReadonly;
}
else if (key === "__v_raw" /* raw */) {
// 代理 observed.__v_raw
return target
}
const targetIsArray = isArray(target)
// 如果target是数组而且属性包含于arrayInstrumentations
// arrayInstrumentations 包含对数组一些方法修改的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 求值
const res = Reflect.get(target, key, receiver)
// 内置 Symbol key 不需要依赖收集
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// 依赖收集
!isReadonly && track(target, "get" /* GET */, key)
return isObject(res)
? isReadonly
?
readonly(res)
// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
: reactive(res)
: res
}
}
看一下arrayInstrumentations,它是对代理数组的一些方法,调用所包含的方法时,收集依赖:
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function (...args) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(this) // this是数组本身
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, "get" /* GET */, i + '')
}
// 先尝试用参数本身,可能是响应式数据
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果失败,再尝试把参数转成原始数据
return arr[key](...args.map(toRaw))
}
else {
return res
}
}
})
为什么要修改这几个方法?因为当修改数组数据时,这几个方法获得的值可能不同,所以每次调用它们,都需要重新收集依赖。
看track函数之前,看一下get的最后,最后根据结果作出不同行动,如果是基本数据类型,直接返回值,否则递归变成响应式数据,这是与Vue 2.0不同之处。vue 2.0是创建时直接递归处理,3则是当获取属性时才判断是否处理,延时定义子对象响应式的实现,在性能上会有较大的提升。
最后看get的最核心函数track:
// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每个 target 对应一个 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
基本实现与之前的简单版相同,只是现在激活的effect也要收集dep作为依赖。
派发通知:set 函数
派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现,它是执行 createSetter 函数的返回值:
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
value = toRaw(value)
const hadKey = hasOwn(target, key)
// 使用Reflect修改
const result = Reflect.set(target, key, value, receiver)
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
if (target === toRaw(receiver)) {
if (!hadKey) {
没有属性而增加则触发增加类型
trigger(target, "add" /* ADD */, key, value)
}
else if (hasChanged(value, oldValue)) {
有属性而修改则触发修改类型
trigger(target, "set" /* SET */, key, value, oldValue)
}
}
return result
}
}
set的逻辑很简单,重点是trigger函数,它的作用是派发通知。
// 原始数据对象 map WeakMap的特点是键为引用
const targetMap = new WeakMap()
function trigger(target, type, key, newValue) {
// 通过 targetMap 拿到 target 对应的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}
// 创建运行的 effects 集合
const effects = new Set()
// 添加 effects 的函数
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// SET | ADD | DELETE 操作之一,添加对应的 effects
if (key !== void 0) {
add(depsMap.get(key))
}
const run = (effect) => {
// 调度执行
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// 直接运行
effect()
}
}
// 遍历执行 effects
effects.forEach(run)
}
派发通知与之前的实现相似,就是获取相应代理对象属性收集的依赖,然后派发通知。
副作用函数: effect
重点需要关注的是 activeEffect (当前激活副作用函数),这部分的实现比之前的简易版複杂多了,它是整个Vue 3.0响应式的重点。
// 全局 effect 栈
const effectStack = []
// 当前激活的 effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
// 如果 fn 已经是一个 effect 函数了,则指向原始函数
fn = fn.raw
}
// 创建一个 wrapper,把传入的函数封装,它是一个响应式的副作用的函数
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// lazy 配置,计算属性会用到,非 lazy 则直接执行一次
effect()
}
return effect
}
与之前简单让activeEffect指向最近使用的effect函数不同,源码还封装effect函数,接下来看它是怎样封装:
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(...args) {
if (!effect.active) {
// 非激活状态,则判断如果非调度执行,则直接执行原始函数。
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
// 清空 effect 引用的依赖
cleanup(effect)
try {
// 开启全局 shouldTrack,允许依赖收集
enableTracking()
// 压栈
effectStack.push(effect)
activeEffect = effect
// 执行原始函数
return fn(...args)
}
finally {
// 出栈
effectStack.pop()
// 恢复 shouldTrack 开启之前的状态
resetTracking()
// 指向栈最后一个 effect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
// 给effect一个id
effect.id = uid++
// 标识是一个 effect 函数
effect._isEffect = true
// effect 自身的状态
effect.active = true
// 包装的原始函数
effect.raw = fn
// effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
effect.deps = []
// effect 的相关配置
effect.options = options
return effect
}
createReactiveEffect最后返回一个带属性的effect函数。封装effect函数的过程中,做了两件事:
- 设定activeEffect的指向
- 负责effectStack的出入栈
第1点在之前的简易版了解,为什么要有 effectStack?因为要处理嵌套场景,考虑以下场景:
import { reactive} from 'vue'
import { effect } from '@vue/reactivity'
const counter = reactive({
num: 0,
num2: 0
})
function logCount() {
effect(logCount2)
console.log('num:', counter.num)
}
function count() {
counter.num++
}
function logCount2() {
console.log('num2:', counter.num2)
}
effect(logCount)
count()
我们希望的是当 counter.num发生变化时,触发logCount函数,但如果没有栈,只有activeEffect指针,当调用 effect(logCount),activeEffect指向的是 logCount2,而不是logCount,所以最后结果是:
num2: 0
num: 0
num2: 0
而不是:
num2: 0
num: 0
num2: 0
num: 1
因此我们需要一个栈来存下外层的effect函数,以让activeEffect指针之后指向外层effect。不妨重看源码相关部分:
function createReactiveEffect(fn, options) {
...
try {
...
// 压栈
effectStack.push(effect)
activeEffect = effect
// 执行原始函数
return fn(...args)
}
finally {
// 执行完结后,出栈
effectStack.pop()
// 恢复 shouldTrack 开启之前的状态
resetTracking()
// 指向栈最后一个 effect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
...
return effect
}
当effect函数调用logCount时,把logCount压入effectStack栈中,然后在logCount里,又有一个effect函数调用logCount2,把logCount2压入effectStack栈中。logCount2获取counter.num2的值,这时activeEffect指向logCount2,counter.num2收集logCount2 (activeEffect)为依赖,然后effect执行finally区域的代码,把logCount2出栈,activeEffect指向栈的尾部,即logCount,现在logCount继续执行,获取counter.num,counter.num把logCount收集为依赖,因为activeEffect指向logCount。
counter.num发生变化,则会执行logCount。
最后还有一个cleanUp函数没有解释,它会把effect函数的依赖删除:
function cleanup(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
在执行 track 函数的时候,除了收集当前激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了,然后把 effect 从这些 dep 中删除。
为什么需要 cleanup 呢?因为要考虑组件的渲染函数也是副作用函数,以下面的场景为例:
<template>
<div v-if="state.showMsg">
{{ state.msg }}
</div>
<div v-else>
{{ Math.random()}}
</div>
<button @click="toggle">Toggle Msg</button>
<button @click="switchView">Switch View</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
msg: 'Hello World',
showMsg: true
})
function toggle() {
state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'
}
function switchView() {
state.showMsg = !state.showMsg
}
return {
toggle,
switchView,
state
}
}
}
</script>
这是黄轶老师在Vue 3.0举的例子,他的解释如下:
有点複杂,不过读多几次就明白cleanUp的作用。
以上就是reactive的主要内容。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!