前言
在使用vue时,每当我们需要响应式数据时,总是会用到reactive和ref。如果在面试过程中,面试官问你有关vue中reactive的知识,或者让你手搓一个reactive,这篇文章会对你有所帮助。欢迎交流补充。
reactive
我们都知道,reactive是来帮我们实现响应式 的,但是只能将引用类型代理成响应式。因为js提供的proxy方法只接收引用类型 ,而vue是基于 JavaScript 的 Object.defineProperty
方法或者 ES6 的 Proxy 对象来监听数据的变化。
-
Object.defineProperty
- 在早期版本的 Vue 中,Vue 使用了
Object.defineProperty
方法来拦截对象的属性,实现数据的响应式。 - 通过这种方式,当数据发生变化时,Vue 能够检测到并触发相应的视图更新。
- 在早期版本的 Vue 中,Vue 使用了
-
ES6 Proxy
- 在 Vue 3 中,为了解决
Object.defineProperty
的一些限制,比如无法监测数组下标的变化等,Vue 引入了 ES6 的 Proxy 对象作为数据监听的机制。
- 在 Vue 3 中,为了解决
第一点的版本有点古早,这里我们主要对Proxy展开解释(在es6的后续更新中,又加入了Reflect API)。
Proxy
Proxy用于拦截对目标对象的操作。这意味着你可以在目标对象上进行操作之前或之后,定义自定义的行为。我们来看看阮一峰es6对proxy的说明。来到目录第15条,可以发现,我们能将Proxy理解为一种"代理器",因此我们可以通过代理器来对被拦截下来的对象进行一些操作。
Proxy一共提供了13种拦截操作:
- get(target, propKey, receiver) :拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver) :拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey) :拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey) :拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target) :拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey) :拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc) :拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - preventExtensions(target) :拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - isExtensible(target) :拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - setPrototypeOf(target, proto) :拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
等会我们主要用到get 和 set。注意到这里有句话
这也就是为什么前面说Proxy只接受原始类型,vue用的就是ES6原生提供的Proxy,而打造之初规定如此。
在后续开发时,又打造了一个 Reflect。也就是目录第16。我们看看Reflect 又是何物。
其他内容大家感兴趣可以自己去看。根据第一点我们可以知道的是,reflect的打造就是把ES6之前的版本里,Object对象里的一些方法放到Reflect中(Object的方法依然可以使用),仅仅只是这样肯定是不够的,不然就没有打造的必要了。后续的一些新的方法也被放到了Reflect中。
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
是不是有点眼熟?这跟Proxy也太像了。
那我们就简单粗暴的将它理解为Object的克隆版,更新!更合理!
effect ------(副作用)函数收集和触发
- 依赖收集:当一个 Vue 组件的模板(或者计算属性、侦听器等)读取响应式数据时,Vue 会在内部进行依赖收集。它会将当前正在执行的 Watcher(观察者)实例添加到该数据的依赖列表中。
- 触发更新:当响应式数据发生变化时,它会通知依赖列表中的 Watcher 实例,以便它们可以重新计算并更新相关的视图。
函数的收集
在 Vue 中,函数的收集发生在模板编译的过程中,以及计算属性、侦听器的定义中。当 Vue 编译模板时,它会分析模板中的数据引用,以便知道哪些数据属性需要被监听,从而在数据变化时更新视图。
- 模板编译:Vue 的模板编译器会分析模板中的数据绑定和指令,并在内部生成相应的渲染函数。在这个过程中,它会收集模板中使用的数据属性,并在 Watcher 中建立响应式数据和视图之间的关联。
- 计算属性和侦听器:在组件中定义的计算属性和侦听器中,当访问响应式数据时,Vue 会自动进行依赖收集。这意味着当计算属性或侦听器中使用的数据发生变化时,它们会被自动重新计算或触发相应的回调函数。
为什么需要函数的收集?
函数的收集是 Vue 响应式系统的核心机制之一,它确保了当数据发生变化时,与之相关的视图能够自动更新。通过收集函数对数据的访问,Vue 可以建立起数据和视图之间的依赖关系,从而实现了响应式的数据绑定。
有了以上这些知识的铺垫,我们正式的开始手撕代码吧!
手撕代码
文件名:reject.js
javascript
import { mutableHandlers } from './baseHandlers.js'
// 保存被代理过的对象
export const reactiveMap = new WeakMap() // new Map() // new WeakMap 对内存的回收更加友好
export function reactive(target) { // 将target变成响应式
return createReactiveObject(target, reactiveMap, mutableHandlers)
}
export function createReactiveObject(target, proxyMap, proxyHandlers) { // 创建响应式的函数
// 判断target是不是一个引用类型
if (typeof target !== 'object' || target === null) { // 不是对象就不给操作
return target
}
// 该对象是否已经被代理过(已经是响应式对象)
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 执行代理操作(将target处理成响应式)
const proxy = new Proxy(target, proxyHandlers) // 第二个参数的作用:当target被读取值,设置值,判断值等等操作时会触发的函数
// 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
proxyMap.set(target, proxy)
return proxy
}
文件名:baseHandlers.js
vbnet
import { track, trigger } from './effect.js'
const get = createGetter()
const set = createSetter()
function createGetter() {
return function get(target, key, receiver) {
// console.log('target对象被读取值了');
const res = Reflect.get(target, key, receiver) // target[key]
// 这个属性究竟还有哪些地方用到了(副作用函数的收集, computed, watch,....) 依赖收集
track(target, key)
return res
}
}
function createSetter() {
return function set(target, key, value, receiver) {
// console.log('target对象被修改值了', key, value);
const res = Reflect.set(target, key, value, receiver) // target[key] = value;
// 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效, 更新浏览器的视图(响应式)
// 触发被修改的属性身上的副作用函数 触发依赖(被修改的key再哪些地方被用到了)发布订阅
trigger(target, key)
return res
}
}
export const mutableHandlers = {
get,
set,
}
-
tips
createGetter
函数用于创建获取器。当对象的属性被读取时,它会调用Reflect.get
方法获取属性的值,并通过track
函数来跟踪依赖关系,即记录哪些地方使用了这个属性的值。createSetter
函数用于创建设置器。当对象的属性被修改时,它会调用Reflect.set
方法设置属性的新值,并通过trigger
函数来触发副作用,即通知依赖这个属性的地方进行相应的更新。
文件名:effect.js
javascript
const targetMap = new WeakMap()
let activeEffect = null // 得是一个副作用函数
export function effect(fn, options={}) { // 也是watch,computed 的核心逻辑
const effectFn = () => {
try {
activeEffect = effectFn
return fn()
} finally {
activeEffect = null
}
}
if (!options.lazy) {
effectFn()
}
return effectFn
}
// 为某个属性添加 effect
export function track(target, key) {
// targetMap = { // 存成这样
// target: {
// key: [effect1, effect2, effect3,...]
// },
// target2: {
// key: [effect1, effect2, effect3,...]
// }
// }
let depsMap = targetMap.get(target)
if (!depsMap) { // 初次读取到值 收集effect
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) { // 该属性还未添加过effect
deps = new Set()
}
if (!deps.has(activeEffect) && activeEffect) {
// 存入一个effect函数
deps.add(activeEffect)
}
depsMap.set(key, deps)
}
// 触发某个属性 effect
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) { // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
return
}
const deps = depsMap.get(key)
if (!deps) { // 这个属性没有依赖
return
}
deps.forEach(effectFn => {
effectFn() // 将该属性上的所有的副作用函数全部触发
});
}
让我们来试试自己写的reactive有没有用。创建一个:text.html 文件
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive } from './reactive.js';
import { effect } from './effect.js'
const state = reactive({
name: '喜仔',
age: 18
})
effect(
() => {
console.log(`${state.name}今年${state.age}岁了`);
},
{lazy: false}
)
// console.log(state.name);
// console.log(state.age); // 18
setInterval(() => {
state.age = state.age + 1
}, 2000)
</script>
</body>
</html>
我们去控制台看看效果,发现确实实现了!你也快去试试吧!
看一遍不懂很正常,大家可以多多看几遍文档。一起加油吧!!!