Vue2 缺陷
-
通过Object.defineProperty对对象的每一个属性进行数据拦截(getter,setter)
-
Object.defineProperty 只拦截其他操作,对象/数组操作方法不提供
- 所以vue给出了vue.get/vue.delete 方法
-
vue不对每个数组元素单独拦截get/set,而是直接重写了数组的原型方法以检测数组变化。原因如下:
- 考虑到使用索引直接访问数组的情况比较少
- 而且操作数组的方法更为常用,所以直接放弃了第一个方法
- 对于对象则是直接操作的更多,所以对对象遍历执行defineProperty
通过observer() 递归 生成响应式内容
let obj = {
a:1,
b:2,
c :{
n:3
},
d:['1','2','3']
}
function observer(target){
for(let key in target){
defineReactive(target,key,target[key])
}
}
function defineReactive(target,key,value) {
if(typeof value === 'object' && value !==null){
observer(value)
}
Object.defineProperty(target,key,{
get(){ // 取值
return value;
},
set(newVal){ // 设置值
if(newVal !== value){
value = newVal
updateView () //触发虚拟DOM到DIFF,渲染的流程
}
}
})
}
observer(obj)
Proxy 代理
代替了 Vue2 的 Object.defineProperty
proxy 简介
包装一个对象,拦截诸如读写和其他的操作,自行处理他们 let proxy = new Proxy(target,handler )
类似于中间件,对proxy的操作会先经有handler替换
| [[Get]] | get | 读取属性 |
|---|---|---|
| [[Set]] | set | 写入属性 |
| [[HasProperty]] | has | in 操作符 |
| [[Delete]] | deleteProperty | delete 操作符 |
| [[Call]] | apply | 函数调用 |
| [[Construct]] | construct | new 操作符 |
| [[GetPrototypeOf]] | getPrototypeOf | Object.getPrototypeOf |
| [[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
| [[IsExtensible]] | isExtensible | Object.isExtensible |
| [[PreventExtensions]] | preventExtensions | Object.preventExtensions |
| [[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
| [[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entr… |
| [[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entr… |
- 上述方法受到JS不变量限制:内部方法和捕捉器必须返回一些规定的值Invariant是强制执行的,否则触发 TypeError
- 可以使用可撤销的代理
let {proxy, revoke} = Proxy.revocable(target, hander) - 上述多种操作拦截器可以实现对对象操作方法/直接访问的拦截解决vue2的问题
Reflect
Reflect 是一个内建方法,用于简化 Proxy 的创建以及传递正确的接收方(receiver)
- Reflect 和 Proxy 的 handler 有一样的函数名
解决了如下的问题:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = { __proto__ : userProxy, _name : "Admin" };
// 期望输出:Adminalert(admin.name); // 输出:Guest (?!?)
target[prop]不传递正确的this,在admin中查找name失败,在原型(userProxy) 上get name, 根据handler,return target[prop],此处的target是user,所以访问 user.get , 访问this._name,此时this是user,所以返回Guest
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect . get (target, prop, receiver); // (*)
// 可以写成 return Reflect.get(...arguments)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert (admin. name ); // Admin
使用Reflect 修复。在userProxy return 处传递了receiver,调用user.get的时候,this就是 userProxy 的get 方法入参receiver,也即 admin,所以获取到admin._name = "Admin"
简而言之,Reflect
- 提供一种方便的将捕捉器转发给对象的方法
Reflect.<method> - 在转发给原对象的时候保留调用方正确的this (Receiver)
创建响应式的对象
reactive -- 对象
ref -- 基本类型/对象
readonly -- 只读的对象,创建过程
对于基本类型 Ref
- 通过RefImpl定义属性拦截器,创建响应式对象
- 依赖存储在RefImpl主动的一个set中
所以是包裹了一层RefImpl来实现响应式,所以用ref需要写.value。如果传入的是对象,第一层有value,后面有observer递归调用reative(创建Proxy),不用再加.value了
对于对象 Ref / reactive
-
创建Proxy
-
Proxy保存在weakmap中
- 方便查找
- 方便释放内存
- 防止重复创建
-
weakMap中还有各个属性的Map
-
map下保存各个属性的effect set
- 通过target标记:target.__v_reactive = observed 来标记已经是响应式的对象,以防止重复代理
- 在定义的时候没有递归创建Proxy,而是在getter中创建,也即访问的时候才会去创建嵌套对象的Proxy,这是一种性能优化
依赖收集和触发
依赖收集
Proxy的 setter 被重写,访问 setter 时回会触发 tarck(),tarck() 会将读取该值的副作用函数添加到 targetMap 中
如何获取当前的副作用函数?全局变量 activeEffect 保存当前effect ,track() 中闭包访问 activeEffect 。
- activeEffect 存入 set : deps中
- shouldTrack 判断当前是否需要收集依赖
- deps 存入关于 key 的 weakMap targetMap 中
targetMap:
state -> {
count -> [effect1,effect2],
name -> [effect1,effect2]
}
实际上收集的是副作用函数的effect 方法生成的 ReactiveEffect 类的实例 _effect
依赖 cleanup
执行renderEffect的时候需要先清除之前收集的rendereffect,防止执行没有渲染的rendereffect
effect 函数
一个用来注册副作用函数的函数
- 接收一个回调函数,这个回调函数就是被注册的副作用函数,他会在合适的时候被调用(vue挂载,state更新)
- 一个对象
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延迟触发 effect
computed?: boolean // 是否为计算属性
scheduler?: (job: ReactiveEffect) => void // 调度函数
onTrack?: (event: DebuggerEvent) => void // 追踪时触发
onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
onStop?: () => void // 停止监听时触发
}
依赖触发
Proxy的 setter 被重写, 访问getter的时候会触发 trigger() ,trigger() 会读取副作用函数set,触发依赖身上的所有依赖函数
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (!deps) return
deps.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect()
}
})
}
计算属性API
computedAPI 也是一个依赖收集的过程
为什么不直接使用函数呢?
基本流程
- 传入的参数作为一个getter
- 判断 dirty 是否需要重新计算,默认为true,首次访问会执行 getter
- 执行runner() 获取计算结果 value
- 将dirty设置为false
- 使用track收集依赖
- 返回 value 每次计算的结果
当依赖被触发
- ref/reactive setter trigger到 scheduler
- scheduler 将 dirty 设置为 true ,然后再去触发 runner (computed)
- 延时计算:只有当我们去访问computed计算属性的时候,coputed getter函数才真正计算
- 缓存:当dirty为false,会使用上次的value,这就是为什么使用computed比直接使用function会更好,因为缓存
优先级更高
相比普通函数,computed runner 的执行优先级相比 ref reactive 会更高
Watch
和effect很像,他们有什么区别吗?
watch(count,(count,prevCount)=>{})
watch(()=>count,(count,prevCount)=>{})
watch([count1,count2],([count1,count2],[prevCount1,prevCount2])=>{})
基本流程
-
标准化 source
- ref:创建一个访问ref.value 的getter
- reactive :创建一个访问reactive的getter并deep设置为true
- 如果是一个函数:当做getter。监听基本类型需要用函数形式
- 否则报错
- deep 会递归监听
-
构造 applyCb 回调函数
- newValue,oldValue,onInvalidate
-
创建 scheduler 时序执行函数
- 执行方式 flush 属性
- 同步sync watch cb同步执行
- 异步pre 通过queueJob在组件更新之前执行,如果组件未挂载则同步执行,保证是在组件挂载之前执行(preview)
- 未设置 通过queuePostRenderEffect在组件更新之后执行
-
创建 effect 副作用函数
- 是一个computed Effect 所以是优先执行的
- 配置immediate则会立刻执行
-
返回监听器销毁函数
- 执行stop方法使失活
- 清空依赖
回调函数调度方式
flush:sync
同步执行
flush:pre/null
watcher回调函数异步执行。维护一些内部的队列帮助调度
- queueJob
- queuePostRenderEffect
- queue 异步任务队列
- queuePostFlushCb 异步任务执行完的回调队列
及时多次执行queueFlush,也可以通过标志位防止flushJobs重复执行
- isFlushPending
- isFlushing
Vue3 中 nextTick 的实现
Promise.resolve().then( 在此执行flushJobs )
排序
queueJob 执行前,需要按升序排序
- 在queue中,组件更新id是父组件小于子组件,为了保证父组件先于子组件更新,需要在queueJob执行之前将其按升序排序
- 当父组件在更新过程中被卸载,其子组件的更新也应该停止
WatchEffect
于watch的不同
- 监听的是一个普通函数,内部访问了响应式对象,不需要返回响应式对象
- 没有回调函数,副作用函数中的响应式变量发生变化后重新执行副作用函数
- 发生变化的时候马上执行watchEffect
provide inject
创建组件的时候组件会将当前provides引用指向父级提供的provides
创建的时候,他将provides内容添加到父级提供的provides上
因此会将上一级的同名内容被覆盖
笔者才疏学浅,各位读者多多担待,不吝赐教。部分插图来自网络,侵删。