最后简单介绍下ref与computed,首先先看ref的主要代码:
export function ref(raw?: unknown) {
if (isRef(raw)) {
return raw
}
raw = convert(raw)
const r = {
_isRef: true,
get value() {
track(r, OperationTypes.GET, 'value')
return raw
},
set value(newVal) {
raw = convert(newVal)
trigger(
r,
OperationTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
return r as Ref
}
Vue.reactive用于对象,Vue.ref则用于基础类型,例如:
const observed = Vue.ref('test')
Vue.effect(() => {
console.log(observed.value) // test,test2
})
observed.value = 'test2'
通过源码其实能知道,ref其实就是通过将基础类型外面包了一层value属性,通过get set来进行发布与订阅,ref也可以与reactive搭配使用,当在reactive中使用时,会自动识别ref的value属性,例如:
const observed = Vue.reactive({name:'test',age:Vue.ref(20)})
Vue.effect(() => {
console.log(observed.age) // 20
})
age属性被包装成了对象,这样的好处是对象指针指向的是内存地址,将age属性赋值给一个变量,修改变量值值,由于指针指向相同内存空间,原值也会有变化,例如:
const age = Vue.ref(37)
const obj = Vue.reactive({
age: age,
person: {
name:'test',
age:age
}
})
Vue.effect(() => {
let age = obj.age
let personAge = obj.person.age
})
obj.age = 40
console.log(obj.age, obj.person.age) //40,40
如果不使用ref的话,运行结果为40,37
又例如:
const ref = Vue.ref(18)
const observed = Vue.reactive({name:'test',age:ref})
Vue.effect(() => {
let age = ref
let personAge = observed.age
})
observed.age = 30
console.log(ref.value) // 30
最后再讲一下computed,源码如下:
export function computed(
getterOrOptions: ComputedGetter | WritableComputedOptions
) {
let getter: ComputedGetter
let setter: ComputedSetter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value: T
const runner = effect(getter, {
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// When computed effects are accessed in a parent effect, the parent
// should track all the dependencies the computed property has tracked.
// This should also apply for chained computed properties.
trackChildRun(runner)
return value
},
set value(newValue: T) {
setter(newValue)
}
}
}
首先判断如果传入的为函数,将其作为getter,setting为空(只读),如果传入的是对象,将其get属性赋值给getter,set属性赋值给setter,这些和Vue2一样。
接下来是核心代码:
let dirty = true
let value: T
const runner = effect(getter, {
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// When computed effects are accessed in a parent effect, the parent
// should track all the dependencies the computed property has tracked.
// This should also apply for chained computed properties.
trackChildRun(runner)
return value
},
set value(newValue: T) {
setter(newValue)
}
}
能看出返回的是一个对象,有value属性,同样value属性具有get和set函数,其实和ref一样,包装了成了一个具有value属性的对象。其中get作用于依赖收集,当dirty为true时,触发runner函数依赖收集,然后将dirty置为false,其中trackChildRun(runner)作用也很简单,在依赖收集时将父副作用函数添加到dep依赖,这样当发布时触发不仅会触发计算属性的副作用函数,也会触发父副作用函数,例如:
const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
return `年龄是:${observed.age}`
})
Vue.effect(() => {
console.log('我被触发了') // 打印了2次
console.log(message.value) // 18,30
})
observed.age = 30
显然,如果不绑定父副作用,observed.age更改后Vue.effect并不会触发。
dirty主要的作用其实也很简单,用过redux的应该知道reselect,dirty的作用类似,用来判断是否要重新计算计算属性。具体用处下面会说到,接下来创建一个副作用函数,函数体就是getter,三个参数,lazy之前说过,副作用函数默认为初始执行一次,当lazy为true时初始不执行,scheduler之前也说过,设置了scheduler后,发布触发的不是effect函数而是scheduler函数,至于最后computed属性主要是作为标注符,主要用于发布trigger函数中:
export function trigger(
target: object,
type: OperationTypes,
key?: unknown,
extraInfo?: DebuggerEventExtraInfo
) {
// .....
computedRunners.forEach(run)
effects.forEach(run)
}
通过判断effect是否具有computed参数,筛选出计算属性的副作用函数,必须要先执行计算属性副作用函数再执行普通副作用函数,原因其实也在于dirty,只有当trigger发布时执行计算属性scheduler后,将dirty置为true,然后执行父副作用,父副作用执行过程中又会触发计算属性的get(通过trackChildRun绑定父副作用的依赖),如果普通副作用在前面的话就有可能先执行父副作用,触发计算属性的get,这个时候dirty并没有通过scheduler置为ture,显然并不会重新执行计算属性并依赖收集,直接返回之前的值,不信的话,可以将副作用执行顺序换下,然后运行代码如下:
const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
return `年龄是:${observed.age}`
})
Vue.effect(() => {
console.log('我被触发了') // 打印了2次
console.log(message.value) // 18,18
})
observed.age = 30
至于为什么使用dirty,一开始有说过,可以提高运行效率,防止不必要的运算,例如:
const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
console.log('依赖收集,返回结果') // 打印了一次
return `年龄是:${observed.age}`
})
testEffect = Vue.effect(() => {
const value = observed.name
console.log(message.value) // 年龄是:18,年龄是:18
})
observed.name = 'test2'
计算属性的依赖并没有name属性,故name变化不会触发计算属性的scheduler方法将dirty置为true,name变化触发testEffect副作用,当需要获取计算属性message.value时触发其get发现dirty为false,这时直接返回之前的值18。
至此,vue3响应式原理基本讲完了。