对于vue3而言,核心分为响应性,运行时,编译器三大块。运行时可以利用render将虚拟dom转化为真实dom。编译器是把html节点编译成render函数
响应性
vue2响应性
vue2使用的是Object.defineProperty来听对象指定属性的getter和setter行为,来实现响应性,但有一种致命缺陷:由于javascript限制,vue不能检测到数组和对象的变化。
1.当对象新增一个没有在data中声明的属性时,新增的属性没有响应性。(可以通过Vue.set设置新属性的响应性)
2.当数组通过下标的形式新增元素时,新增的元素不是响应性(可以通过vue.set,也可以使用vue2内部重写的7个数组api:push(),pop(),shift(),unshift(),splice(),sort(),reverse())
vue3响应性
vue3使用Proxy实现响应性,Proxy可以代理整个对象,监听所有属性变化,包括数组和动态添加的属性,proxy与reflect一般是一起配合使用
<script>
const p1 = {
lastName: '张',
firstName: '三',
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName
}
}
const proxy = new Proxy(p1, {
// target:被代理对象
// receiver:代理对象
get(target, key, receiver) {
console.log('触发了 getter');
return target[key]
}
})
console.log(proxy.fullName);
</script>
此时proxy.fullName只打印一次,因为通过target[key]触发的fullName函数中this是p1中的this,需要将其修改为proxy代理对象的this,故需要改为
const proxy = new Proxy(p1, {
// target:被代理对象
// receiver:代理对象
get(target, key, receiver) {
console.log('触发了 getter');
+ // return target[key]
+ return Reflect.get(target, key, receiver)
}
})
reactivity模块
使用weakMap缓存代理对象:weakMap与map的不同在于,key必须是对象且key是弱引用的(WeakMap中的key不存在任何引用时,会被直接回收)。
reactive主要是通过proxy实现响应性,核心是监听复杂数据类型的getter和setter行为,监听到getter时收集当前依赖(effect),触发setter时触发收集到的依赖,一次完成响应性。
收集依赖的WeakMap实例:
1.key:响应性对象 2.value:map对象 2.1 key:响应性对象的指定属性 2.2 map:指定属性的fn函数的set数组
ref模块
ref接收复杂数据类型时,通过toReactive方法,把复杂数据类型交给reactive处理。监听简单数据类型是监听value属性,触发get value()时收集依赖存储到RefImpl中的dep中,触发set value()时触发依赖
computed模块
核心代码
import { isFunction } from '@vue/shared'
import { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { trackRefValue, triggerRefValue } from './ref'
/**
* 计算属性类
*/
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public _dirty = true
constructor(getter: () => T) {
this.effect = new ReactiveEffect(getter, () => {
/**
* 脏:为false时,表示需要触发依赖,为true时表示需要重新执行run方法,获取数据。也就是数据脏了
*/
if (!this._dirty) {
this._dirty = true
//触发依赖,触发的是effect函数的依赖
triggerRefValue(this)
}
})
this.effect.computed = this
}
get value() {
//收集依赖,收集的是effect的依赖
trackRefValue(this)
//判断当前脏状态,如果为true,则需要重新执行run,获取最新数据
if (this._dirty) {
this._dirty = false
//执行run函数,此时执行的是run函数
this._value = this.effect.run()!
}
return this._value
}
}
/**
* 计算属性
*/
export function computed(getterOrOptions) {
let getter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
}
const cRef = new ComputedRefImpl(getter)
return cRef as any
}
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { reactive, computed, effect } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
})
setTimeout(() => {
//此时触发依赖
obj.name = '李四'
}, 2000);
</script>
</html>
运行第一个effect时会触发计算属性对get value()的监听,收集测试实例中effect的,并判断是否为脏状态,如果是脏状态则要修改脏状态并运行this.effect.run()获取最新数据(通过脏状态设置缓存,当连续设置值时,因为没有通过依赖项的值改变触发依赖,就不会触发调度器函数,进而不会重新触发计算函数),在运行run方法时修改activeEffect的值,将其指向ComputedRefImpl中的effect。然后触发computed内部的函数,此时obj.name时会触发依赖收集,收集的activeEffect是指向ComputedRefImpl中的effect。当依赖项被触发,此时触发的是reactive的set value,由于调度器的存在会先触发调度器函数(也就是ComputedRefImpl中的effect函数中第二个参数),此时如果不为脏状态就更改为脏状态并触发测试实例中effect依赖。
watch模块
//apiwatch.ts
import { effect } from '@vue/reactivity'
import { EMPTY_OBJ, hasChanged, isObject } from '@vue/shared'
import { queuePreFlushCb } from './scheduler'
import { ReactiveEffect } from 'packages/reactivity/src/effect'
import { isReactive } from 'packages/reactivity/src/reactive'
/**
* watch配置项属性
*/
export interface WatchOption<Immediate = boolean> {
immediate?: Immediate
deep?: boolean
}
/**
* 指定的watch函数
* @param source监听的响应性数据
* @param cb 回调函数
* @param options 配置对象
* @returns
*/
export function watch(source, cb: Function, options?: WatchOption) {
return doWatch(source as any, cb, options)
}
function doWatch(
source,
cb: Function,
{ immediate, deep }: WatchOption = EMPTY_OBJ
) {
//触发getter的指定函数
let getter: () => any
//判断source的数据类型
if (isReactive(source)) {
getter = () => source
//深度
deep = true
} else {
getter = () => {}
}
//存在回调函数和deep属性
if(cb&&deep){
const baseGetter = getter
getter=()=>traverse(baseGetter())
}
//旧值
let oldValue={}
//job执行方法
const job=()=>{
if(cb){
const newValue=effect.run()
if(deep||hasChanged(newValue,oldValue)){
cb(newValue,oldValue)
oldValue=newValue
}
}
}
//调度器
let scheduler=()=>queuePreFlushCb(job)
const effect=new ReactiveEffect(getter,scheduler)
if(cb){
if(immediate){
job()
}else{
oldValue=effect.run()
}
}else{
effect.run()
}
return () => {
effect.stop()
}
}
/**
*
* 一次执行getter,从而触发收集依赖
*/
export function traverse(value:unknown){
if(!isObject(value)){
return value
}
for(const key in value as object){
traverse((value as any)[key])
}
return value
}
//scheduler.ts
//对应promise的pending状态
let isFlushPending = false
/**
* promise.resolve
*/
const resolvedPromise = Promise.resolve() as Promise<any>
/**
* 当前的执行任务
*/
let currentFlushPromise: Promise<void> | null = null
/**
* 待执行的任务队列
*/
const pendingPreFlushCbs: Function[] = []
/**
* 队列预处理函数
*/
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs)
}
/**
* 队列预处理函数
*/
function queueCb(cb: Function, pendingQueue: Function[]) {
pendingQueue.push(cb)
queueFlush()
}
/**
* 依次执行队列中的执行函数
*/
function queueFlush() {
if (!isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
/**
* 处理队列
*/
function flushJobs() {
isFlushPending = false
flushPreFlushCbs()
}
/**
* 一次处理队列中的任务
*/
export function flushPreFlushCbs() {
if (pendingPreFlushCbs.length) {
console.log('pendingPreFlushCbs: ', pendingPreFlushCbs);
let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
console.log('activePreFlushCbs: ', activePreFlushCbs);
pendingPreFlushCbs.length = 0
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]()
}
}
}
//watch.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { reactive, watch } = Vue
const obj = reactive({
name: '张三'
})
watch(obj, (value, oldValue) => {
console.log('watch 监听被触发')
console.log('value', value)
})
//依赖触发时是同一个job,后面new set时会合并相同的,所以只打印王五
setTimeout(() => {
obj.name = '李四'
obj.name = '王五'
}, 2000)
</script>
</html>
watch的实现本质还是收集依赖和触发依赖,在doWatch方法里面会生成一个带有调度器的ReactiveEffect函数,而 getter 行为的触发是依赖于内部的 traverse 方法进行的。traverse 方法就是依次遍历数据,分别触发 getter 行为。此时activityEffect指向的是带有调度器的ReactiveEffect,z settimeout中触发依赖,此时因为有调度器,会执行调度器内部的job函数,通过effect.run()获取新值,当新值发生改变时触发watch里面的回调以此实现监听效果。
调度器的实现: scheduler=()=>queuePreFlushCb(job),每次触发schedule并不会直接执行job方法,而是被push到set集合中,promise.then之后再一起执行。从而避免了每次已修改依赖项,就触发一次watch函数的情况
immediate:immediate的实现是当这个参数存在时,直接执行job函数,通过effect.run()获取新值,当新值发生改变时触发watch里面的回调以此实现监听效果。