赞
踩
响应式,就是一个变量依赖了其他变量,当被依赖的其他变量更新后该变量也要响应式的更新。
响应式的思路一般都是这样一个模型
- 定义某个数据为响应式数据,它会拥有收集访问它的函数的能力
- 定义观察函数,在这个函数内部去访问响应式数据,访问到响应式数据的某个key的时候,会建立一个依赖关系key -> reaction观察函数
- 检测到响应式数据的key的值更新的时候,会去重新执行一遍它所收集的所有reaction观察函数
我们看个例子:
// 响应式数据
const state = reactive({
count: 0,
age: 18
})
// 观察函数
const effect = effect(() => {
console.log('effect: ' + state.count)
})
reactive({ count: 0, age: 18 }),会让{ count: 0, age: 18 }这个普通的对象变成一个proxy,而后续对于这个proxy所有的get、set等操作都会被我们内部拦截下来。
effect函数会先开启一个开始观察的开关,然后去执行
console.log('effect: ’ + state.count),执行到state.count的时候,proxy的get拦截到了对于state.count的访问,这时候就可以知道访问者是const effect = effect(() => { console.log('effect: ’ + state.count) })这个函数,那么就把这个函数作为count这个key值的观察函数收集在一个地方。
下次对于state.count修改时,会去找count这个key下所有的观察函数,轮流执行一遍。
这样就实现了响应式模型。
我们通过一个例子看下:
<template>
{{ obj.b }}
</template>
<script>
export default {
data: {
obj: { a: 0 },
},
mounted() {
this.obj.b = 5
}
}
</script>
Object.defineProperty必须对于确定的key值进行响应式的定义。这就导致了如果data在初始化的时候没有b属性,那么后续对于b属性的赋值都不会触发Object.defineProperty中对于set的劫持。只能用一个额外的api Vue.set来解决。
const raw = {}
const data = new Proxy(raw, {
get(target, key) { },
set(target, key, value) { }
})
从例子中可以看出来:Proxy在定义的时候并不用关心key值,只要定义了get方法,那么后续对于data上任何属性的访问(哪怕是不存在的),都会触发get的劫持,set也是同理。这样Vue3中,对于需要定义响应式的值,初始化时的要求就没那么高了,只要保证它是个可以被Proxy接受的对象或者数组类型即可。
具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
是dependence的缩写,也就是依赖
指因某种原因导致产生结果,着重持续稳定的影响
指追踪、踪迹
指触发
我们先来看一张经典的Vue3响应式原理图,该图清晰的描述了vue3响应式原理的具体实现过程。
下面将围绕这个图仔细讲解
我们先看来个例子:
首先clone Vue3源码,在 packages/reactivity 模块下调试,在项目根目录运行yarn dev reactivity,然后进入packages/reactivity目录找到产出的 dist/reactivity.global.js 文件,创建index.html文件,内容如下:
<html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>响应式demo测试</title> </head> <body> <script src="./dist/reactivity.global.js"></script> <script> const { reactive, effect } = VueReactivity const origin = { count: 0 } const state = reactive(origin) const fn = () => { const count = state.count console.log(`set count to ${count}`) } effect(fn) </script> </body> </html>
通过浏览器打开,在控制台我们输入state.count++,会输出set count to 1,即实现了响应式。
从上述代码中可以总结下初始化做了什么:
Vue 3.0使用Proxy来代替之前的Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。reactive() 函数主要实现逻辑如下:
import { handler } from './handlers.js'
export function reactive(target) {
// handler包括get和set
const observed = new Proxy(target, handler)
return observed
}
handler中主要包括get和set。
get实现代码如下:
/** 劫持get访问 收集依赖 */ /** * * @param {*} target 原始对象 * @param {*} key 当前赋值属性 * @param {*} receiver 可以简单理解为响应式proxy本身 * @returns */ function createGetter (target, key, receiver) { const res = Reflect.get(target, key, receiver) // 求值 track(target, 'get', key) // 深层数据的劫持 // 即在深层访问的时候,若访问的数据是个对象,就把这个对象也用reactive包装成proxy再返回,这样在最里面属性进行赋值操作的时候,也可以是响应式的 return isObject(res) ? reactive(res) : res }
set赋值操作的时候,本质上就是去检查这个key收集到了哪些reaction观察函数,然后依次触发。
set实现代码如下:
/** 劫持set访问 触发收集到的观察函数 */
function createSetter (target, key, value, receiver) {
const hadKey = hasOwn(target, key) // 先检查一下这个key是不是新增的
const oldValue = target[key] // 拿到旧值
const result = Reflect.set(target, key, value, receiver) // 设置新值
if (!hadKey) {
// 新增key值时触发观察函数
trigger(target, 'add', key)
} else if (value !== oldValue) {
// 已存在的key的值发生变化时触发观察函数
trigger(target, 'set', key)
}
return result
}
effect.js实现如下:
// 接受用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖 /** * 观察函数 * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖 * 下次访问的key发生变化的时候 就会重新运行这个函数 * @param {*} fn * @returns */ export function effect (fn) { // 构造一个effect // effect是包装了原始函数之后的观察函数 // 在run的上下文中执行原始函数 可以收集到依赖 const effect = function effect(...args) { return run(effect, fn, args) } // 立即执行一次 effect() // 返回出去 让外部也可以手动调用 return effect } // 收集响应依赖的的函数,在get操作的时候要调用 /** 把函数包裹为观察函数 */ export function run(effect, fn, args) { if (effectStack.indexOf(effect) === -1) { try { // 往栈里放入当前effect // 把当前的观察函数推入栈内 开始观察响应式proxy effectStack.push(effect) // 立即执行一遍fn() // fn()执行过程会完成依赖收集,会用到effect // 运行用户传入的函数 这个函数里访问proxy就会收集effect函数作为依赖了 return fn(...args) } finally { // 完成依赖收集后从栈中扔掉这个effect // 运行完了永远要出栈 effectStack.pop() } } }
到这里,整个初始化过程就结束了。
先看下依赖收集阶段做了什么?
就是在effect被立即执行,其内部的fn() 触发了Proxy对象的getter的时候。简单说,只要执行到类似state.count时,就会触发state的getter。
建立一份”依赖收集表“,即图中的"targetMap"。targetMap是一个 WeakMap,其key值是当前的Proxy对象state代理前的对象origin,而 value则是该对象所对应的depsMap。
depsMap是一个Map,key值为触发getter时的属性值(这里是 count),而value则是触发过该属性值所对应的各个 effect函数。
通过一段伪代码看下:
const state = reactive({
count: 0,
age: 18
})
const effect1 = effect(() => {
console.log('effect1: ' + state.count)
})
const effect2 = effect(() => {
console.log('effect2: ' + state.age)
})
const effect3 = effect(() => {
console.log('effect3: ' + state.count, state.age)
})
在这段代码中targetMap表示如下:
即,{ target -> key -> dep } 的对应关系就建立起来了,依赖收集也就完成了。
targetMap、depsMap、dep 之间的关系可以通过一张经典的图来看下:
具体代码实现如下:
export const targetMap = new WeakMap() export const effectStack = [] // 栈结构,依赖收集栈,供后续依赖收集的时候使用 export function track (target, operationType, key) { /** 从栈的末尾取到正在运行的observe包裹的函数(即观察函数) */ const effect = effectStack[effectStack.length - 1] if (effect) { let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (dep === void 0) { // 如果这个key之前没有收集过观察函数 就新建一个, 然后set到整个value的存储里去 depsMap.set(key, (dep = new Set())) } if (!dep.has(effect)) { // 把当前key对应的观察函数收集起来 dep.add(effect) } } }
该函数接受一个用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖。
代码实现如下:
// 接受用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖 /** * 观察函数 * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖 * 下次访问的key发生变化的时候 就会重新运行这个函数 * @param {*} fn * @returns */ export function effect (fn) { // 构造一个effect // effect是包装了原始函数之后的观察函数 // 在run的上下文中执行原始函数 可以收集到依赖 const effect = function effect(...args) { return run(effect, fn, args) } // 立即执行一次 effect() // 返回出去 让外部也可以手动调用 return effect }
核心逻辑在run函数中:
// 收集响应依赖的的函数,在get操作的时候要调用 /** 把函数包裹为观察函数 */ export function run(effect, fn, args) { if (effectStack.indexOf(effect) === -1) { try { // 往栈里放入当前effect // 把当前的观察函数推入栈内 开始观察响应式proxy effectStack.push(effect) // 立即执行一遍fn() // fn()执行过程会完成依赖收集,会用到effect // 运行用户传入的函数 这个函数里访问proxy就会收集effect函数作为依赖了 return fn(...args) } finally { // 完成依赖收集后从栈中扔掉这个effect // 运行完了永远要出栈 effectStack.pop() } } }
const { reactive, effect } = VueReactivity
const origin = {
count: 0
}
const state = reactive(origin)
const fn = () => {
const count = state.count
console.log(`set count to ${count}`)
}
effect(fn)
上述代码中,effect内部对于state的key值count的访问,会收集fn作为count的依赖。state.count++的操作,会触发对于state的set劫持,此时就会从key值的依赖收集里面找到fn,再重新执行一遍。
先通过一张图看下响应阶段做了什么?
export function trigger (target, operationType, key) { // 取得对应的depsMap const depsMap = targetMap.get(target) if (depsMap === void 0) { return } // 取得对应的各个 dep const effects = new Set() if (key !== void 0) { const dep = depsMap.get(key) dep && dep.forEach(effect => { effects.add(effect) }) } // 触发某些由循环触发的观察函数收集 if (operationType === 'add' || operationType === 'set') { const iterationKey = Array.isArray(target) ? 'length' : Symbol('iterate') const dep = depsMap.get(iterationKey) dep && dep.forEach(effect => { effects.add(effect) }) } // 简化版scheduleRun,挨个执行effect, 值更新时触发观察函数 effects.forEach(effect => { effect() }) }
参考网址:
https://juejin.cn/post/6938702983014121485#heading-7
https://juejin.cn/post/6844903959660855309#heading-2(精简非常详细)
https://juejin.cn/post/6844904050014552072#heading-14
https://juejin.cn/post/6844904050912133133
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。