一篇文章,带你读懂vue中的reactive是如何打造的,用js手写一个vue中的reactive
哈喽哈喽,大家好。我是你们的金樽清酒。我们知道,vue最核心的就是响应式了。什么是响应式呢?在Vue.js中,"响应式"通常指的是Vue实例中的数据变化会自动触发相关联的DOM更新的特性。Vue通过其响应式系统实现了这一功能。当Vue实例的数据发生变化时,与之相关联的DOM将会自动更新以反映这些变化,而不需要手动操作DOM。
Vue的响应式系统是通过使用ES5的Object.defineProperty
或者ES6的Proxy
来实现的,它会劫持数据的变化,从而能够追踪到数据的变化,并且在数据发生变化时触发相关联的更新。那么我们今天要做的事情就是手写一个reactive响应式数据源。
reactive设计理念
将对象代理
我们知道ref和reactive的区别在于reactive是将引用类型转化为响应式对象。而ref是将原始类型转化为响应式对象,当然ref也可以将引用类型转化为响应式对象。为什么reactive会有这样的局限呢?从源码的角度出发,因为reactiv是通过ES6的Proxy实现的。而Proxy只能接受引用数据类型(复杂数据类型)(不知道Proxy的小伙伴可以可以去看ES6的官方文档)
知道这个之后,那我们的第一步就是抛出reactive函数,并通过reactive函数里面调用Proxy将数据代理。
js
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
}
代码分析:14-16行代码是为了保证我们的Proxy代理的数据target是一个对象,因为Proxy只支持对象作为参数。第18到22行是确定该对象是否被代理过,因为被代理过的数据已经是响应式数据了,再执行一遍代理会浪费性能。在这两步之后,我们就可以new一个Proxy对象,Proxy对象有两个参数,一个是被代理的对象target,另一个就是当target被读取值后触发的各种函数,而我们的响应式reactive就是通过这些函数完成的。第二个参数有很多种函数,我们就把它写在另一个文件里面用一个函数作为它的参数。也就是第一行引用的函数。
js
import { track, trigger } from './effect.js'
const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数
function createGetter() {
return function get(target, key, receiver) {
console.log('target被读取值');
const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
// 这个属性究竟还有哪些地方用到了,(副作用函数的收集,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,
}
在这个文件里面。我们抛出了mutableHandlers()函数。函数里面有两个方法createGetter(),createSetter()。一个用来获取我们需要代理的对象,一个用来重新设置值。这样我们简易的reactive的大致功能就完成了。
我们来验证一下
html
<!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';
const state = reactive({
name: 'li',
age: 18
})
setInterval(() => {
state.age = state.age + 1
console.log(state.age)
}, 2000)
</script>
</body>
</html>
这是一个html文件,我们用引入自己写的reactive函数。在代码13到16行写一个reactive响应式数据源。 在代码17-20行写了一个定时器,每两秒修改state.age的值。再将state.age打印一下。
你看,我们写的reactive就起作用了。你以为就完成了嘛。不,这还只是一个丐版的reactive,甚至都不能称之为响应式。你想想,我们的reactive是不是有时候会用在watch和computed计算属性里面。那reactive发生改变,watch和computed里面的相应的响应式数据源不是也得发生改变嘛,那怎么再次让watch和computed函数再次执行呢?也就是说,响应式数据是一个地方改变,存在该响应式数据源的地方都得改变,watch和computed里面的逻辑也得重新执行。
副作用收集
什么是副作用收集呢?也叫做转发订阅。就是说谁里面存在这个响应式数据源呢?这个属性还要运用到哪些地方呢?这些东西我们要在get获取到响应式对象的时候执行,注意到我上面的get里面有个外部引入的track()那个就是收集函数。统计一下,比如computed,watch等。为什么要收集呢?因为待会要触发。哈哈哈哈哈哈哈。
副作用触发
既然我们收集了副作用函数之后,我们就要在修改值的时候触发。因为响应式对象值被修改之后,相应的副作用函数也要再次触发,也就是watch,computed函数在该依赖的数据源发生变化时要再次被调用。这些都写在了另一个函数effect.js当中。
js
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() // 将该属性上的所有的副作用函数全部触发
});
}
代码分析:在21到46行是track()函数,也就是我们的副作用收集。我们用set数据结构来存储我们的副作用函数。49行到63行就是触发我们的副作用函数。两个if,一个当该副作用函数数组为空则返回不执行,当里面没有依赖也不执行。然后循环执行收集作用函数数组里面的函数,使它们全部触发。
这样子呢,我们整个的reactive就完成啦。但是,还没完,我们难道不要用watch和computed来验证一下嘛。诶,别急,看到上面还有一个抛出的effect函数嘛。在代码4-17抛出一个effect函数。里面两个参数fn(),和options = {},当options.lazy为true时,函数不调用。不然最后的返回值为fn,也就是我们的功能函数。其实这个effect就是watch和computed的核心逻辑。你看watch和computed是不是也接受一个这样的方法来执行呢。那我们就可以把effct暂时当成watch和computed来使用。
测试效果
总文件路径
text.html
<!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: 'li',
age: 18
})
effect(
() => {
console.log(`${state.name}今年${state.age}岁了`);
},
{ lazy: false }
)
setInterval(() => {
state.age = state.age + 1
}, 2000)
</script>
</body>
</html>
reactive.js
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
import { track, trigger } from './effect.js'
const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数
function createGetter() {
return function get(target, key, receiver) {
console.log('target被读取值');
const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
// 这个属性究竟还有哪些地方用到了,(副作用函数的收集,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,
}
effect.js
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() // 将该属性上的所有的副作用函数全部触发
});
}
成功在effect里面执行,说明我们的副作用函数也生效了。成功的完成了响应式。
总结
其实用js写一个reactive就三个设计理念。
- 1.对象代理。(基于Proxy)。
- 2.收集副作用函数。在get的时候触发。
- 3.触发副作用函数。在set的时候触发。 好啦,vue的源码确实有点难度。vue作业的代码确实很优雅,也值得我们去细细的剖析。那我们下次再见,一起再手写ref。欢迎友友们提建议哦。
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!"