响应式原理
- 响应式是
Vue.js
组件化更新渲染的一个核心机制
Vue2.x响应式实现
-
Object.defineProperty API
劫持数据的变化
- 在数据被访问的时候收集依赖
- 然后在数据被修改的时候通知依赖更新
- 在
Vue.js 2.x
中,Watcher
就是依赖,
- 首先是依赖收集流程,组件在
render
的时候会访问模板中的数据,触发 getter
把 render watcher
作为依赖收集,并和数据建立联系
- 然后是派发通知流程,当我对这些数据修改的时候,会触发
setter
,通知 render watcher
更新,进而触发了组件的重新渲染
-
Object.defineProperty API
的一些缺点:
- 不能监听对象属性新增和删除
- 初始化阶段递归执行
Object.defineProperty
带来的性能负担
响应式对象的实现差异
- 在
Vue.js 2.x
中构建组件时,只要我们在 data
、props
、computed
中定义数据,那么它就是响应式的
- 到了
Vue.js 3.0
构建组件时,你可以不依赖于 Options API
,而使用 Composition API
去编写
-
Composition API
更推荐用户主动定义响应式对象,而非内部的黑盒处理
Reactive API
function reactive (target) {
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (target && target.__v_isReadonly) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
-
reactive
内部通过 createReactiveObject
函数把 target
变成了一个响应式对象
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
}
- 1、 函数首先判断
target
是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target
必须是对象或者数组。
- 2、通过
target.__v_raw
属性,和__v_isReactive
属性来判断 target
是否已经是一个响应式对象,如果是,直接返回该对象
- 3、使用
canObserve
函数对 target
对象做一进步限制
- 带有
__v_skip
属性的对象、被冻结的对象,以及不在白名单内的对象如 Date
类型的对象实例是不能变成响应式的
const canObserve = (value) => {
return (!value.__v_skip &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value))
}
const isObservableType = /*#__PURE__*/
makeMap('Object,Array,Map,Set,WeakMap,WeakSet')
- 4、通过
Proxy API
劫持 target
对象,把它变成响应式
- 5、给原始数据打个标识,
target.__v_reactive = observed
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
Proxy 处理器对象 mutableHandlers
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}
- 劫持了我们对 observed 对象的一些操作
- 1)访问对象属性会触发
get
函数;
- 2)设置对象属性会触发
set
函数;
- 3)删除对象属性会触发
deleteProperty
函数;
- 4)in 操作符会触发
has
函数;
- 5)通过
Object.getOwnPropertyNames
访问对象属性名会触发 ownKeys
函数
依赖收集:get 函数
- 依赖收集发生在数据访问的阶段,get执行
createGetter
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)
// 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
}
}
-
get
函数主要做了四件事情
- 1)首先对特殊的
key
做了代理
- 2)通过
Reflect.get
方法求值,如果 target
是数组且 key
命中了 arrayInstrumentations
,则执行对应的函数
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function (...args) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(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
}
}
})
- 3)通过
Reflect.get
求值,然后会执行 track
函数收集依赖
- 4)对计算的值
res
进行判断,如果它也是数组或对象,则递归执行 reactive
把 res
变成响应式对象,因为Proxy
劫持的是对象本身,并不能劫持子对象的变化
track函数收集依赖
- 整个
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)
}
}
- 收集的依赖就是数据变化后执行的副作用函数
- 创建了全局的
targetMap
作为原始数据对象的 Map,它的键是 target
,值是 depsMap
,作为依赖的 Map
-
depsMap
的键是 target
的 key
,值是 dep
集合
-
dep
集合中存储的是依赖的副作用函数
派发通知:set 函数
- 派发通知发生在数据更新的阶段,
set
函数的实现,它是执行 createSetter
函数的返回值
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
value = toRaw(value)
const hadKey = hasOwn(target, key)
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
}
}
- 主要做两件事情:
- 1)通过
Reflect.set
求值
- 2)通过
trigger
函数派发通知,并依据 key
是否存在于 target
上来确定通知类型,即新增还是修改
trigger 函数派发通知
// 原始数据对象 map
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)
}
- 主要做了四件事情
- 1)通过
targetMap
拿到 target
对应的依赖集合 depsMap
- 2)创建运行的
effects
集合
- 3)根据
key
从 depsMap
中找到对应的 effects
添加到 effects
集合;
- 4)遍历
effects
执行相关的副作用函数
副作用函数
// 全局 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
}
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 = uid++
// 标识是一个 effect 函数
effect._isEffect = true
// effect 自身的状态
effect.active = true
// 包装的原始函数
effect.raw = fn
// effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
effect.deps = []
// effect 的相关配置
effect.options = options
return effect
}
-
effect
内部通过执行 createReactiveEffect
函数去创建一个新的 effect
函数,为了和外部的 effect
函数区分,我们把它称作 reactiveEffect
函数,并且还给它添加了一些额外属性
- 这个
reactiveEffect
函数就是响应式的副作用函数,当执行 trigger
过程派发通知的时候,执行的 effect
就是它
-
reactiveEffect
函数只需要做两件事情
- 1)把全局的
activeEffect
指向它
- 2)然后执行被包装的原始函数
fn
-
effectStack
维护一个栈,解决嵌套场景,activeEffect
指向问题,activeEffect
指向 effectStack
最后一个元素
- 入栈前会执行
cleanup
函数清空 reactiveEffect
函数对应的依赖
readonly API
- 创建只读对象,不能修改它的属性,也不能给这个对象添加和删除属性
function readonly(target) {
return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers)
}
-
readonly
和 reactive
函数的主要区别,就是执行 createReactiveObject
函数时的参数 isReadonly
不同。
- 首先
isReadonly
变量为 true,所以在创建过程中会给原始对象 target
打上一个 __v_readonly
的标识
- 另外还有一个特殊情况,如果
target
已经是一个 reactive
对象,就会把它继续变成一个 readonly
响应式对象
- 创建代理是,传入
readonlyHandlers
const readonlyHandlers = {
get: readonlyGet,
has,
ownKeys,
set(target, key) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target)
}
return true
},
deleteProperty(target, key) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target)
}
return true
}
}
-
readonlyHandlers
和 mutableHandlers
的区别主要在 get
、set
和 deleteProperty
三个函数上
- 在非生产环境下
set
和 deleteProperty
函数的实现都会报警告,提示用户 target
是 readonly
的
-
readonlyGet
的实现,即createGetter(true)
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
// ...
// isReadonly 为 true 则不需要依赖收集
!isReadonly && track(target, "get" /* GET */, key)
return isObject(res)
? isReadonly
?
// 如果 res 是个对象或者数组类型,则递归执行 readonly 函数把 res readonly
readonly(res)
: reactive(res)
: res
}
}
- 和
reactive API
最大的区别就是不做依赖收集
ref API
-
reactive API
对传入的 target
类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 String
、Number
、Boolean
)是不支持的,因此有了ref API
- 使用
const msg = ref('Hello World')
msg.value = 'Hello Vue'
function ref(value) {
return createRef(value)
}
const convert = (val) => isObject(val) ? reactive(val) : val
function createRef(rawValue) {
if (isRef(rawValue)) {
// 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
return rawValue
}
// 如果是对象或者数组类型,则转换一个 reactive 对象。
let value = convert(rawValue)
const r = {
__v_isRef: true,
get value() {
// getter
// 依赖收集,key 为固定的 value
track(r, "get" /* GET */, 'value')
return value
},
set value(newVal) {
// setter,只处理 value 属性的修改
if (hasChanged(toRaw(newVal), rawValue)) {
// 判断有变化后更新值
rawValue = newVal
value = convert(newVal)
// 派发通知
trigger(r, "set" /* SET */, 'value', void 0)
}
}
}
return r
}
- 首先处理嵌套
ref
,如果传入的 rawValue
也是 ref
,那么直接返回
- 然后对
rawValue
做了一层转换,如果 rawValue
是对象或者数组类型,那么把它转换成一个 reactive
对象。
- 最后定义一个对
value
属性做 getter
和 setter
劫持的对象并返回
-
get
部分就是执行 track
函数做依赖收集然后返回它的值
-
set
部分就是设置新值并且执行 trigger
函数派发通知
- 区别于
vue2.x
- 1)劫持数据的方式改成用
Proxy
实现
- 2)收集的依赖由
watcher
实例变成了组件副作用渲染函数