说一下vue3响应式原理?可不止只有proxy
凭借着之前的学习积累,用自己的方式叙述一下自己所学的知识点,笔者高中讨厌写作文,硕士期间不喜写论文,水平肯定有限,能详述之前所学的知识已是不易,若能给读者带来一点启发,将倍感荣幸,同时也虚心接受大佬、同仁指点。
Monorepo管理项目
- Monorepo是管理代码的一种方式,可以在一个项目仓库下,管理多个 子项目,Vue3注重模块的拆分,单个模块可以单独使用,不需要引入完整的vuejs包。因此,Vue3使用Monorepo管理项目,每个模块都单独放在packages目录下。
- 大佬的文章:Monorepo详解
Monorepo环境搭建
- pnpm是快速的,节省空间的包管理器,类似于npm、yarn。主要采用符号链接的方式管理模块。
- 全局安装
npm install pnpm -g # 全局安装pnpm
- 初始化:
pnpm init -y # 初始化配置文件
- 这里我们尝试安装一下vue3:
pnpm install vue@next
,我们发现在node_modules下的vue文件夹下,只有vue的集成文件,没有其各个模块的依赖文件,这是因为pnpm对依赖文件做了处理,全部隐藏在node_modules/.pnpm文件夹下。这样操作避免了幽灵依赖的问题。- 所谓幽灵依赖,是指当项目中引入了A包后,如果A包内部引用了B包,在npm A包的时候同时也会把B包给下载下来,这就导致了一个问题,项目中没有要求下载B包,package.json中也只有A包的依赖记录,但是自然而然的,代码中却可以引用B包。
- 我们的npm包管理方式,显然就是这种模式,要想按照npm的方式,把模块的依赖模块保留,只需要根目录创建.npmrc文件,将依赖提升即可:
shamefully-hoist = true
- 这样,重新安装vue3,发现已经能看到其所有的依赖
- vue3每个包都是一个独立的模块,并且可以单独引用,因此需要在项目的packages文件夹下,挨个创建vue3的模块,模块之间可以相互引用,新建pnpm-workspace.yaml文件将packages下的所有目录都标记为包进行管理,这样Monorepo的环境就搭建好了。
packages:
- 'packages/*'
- 此时,如果我们卸载安装的Vue,重新安装,控制台将会报错,原因就是,你的包得安装在packages目录下,我们可以使用
pnpm install vue -w
,来强制安装到外层的node_modules中
开发环境安装
- 我们写的源码需要打包,这里使用esbuild来打包我们的Vue源码,使用typescript来标注类型,使用minimist来监视控制台命令,因此需要全部安装:
pnpm install esbuild typescript minimist -D -w
- 使用ts的话,需要配置ts相关的命令:
pnpm tsc --init
,生成tsconfig.json文件,在文件中配置:
{
"compilerOptions": {
"outDir": "dist",
"sourceMap": true, // 采用sourcemap
"target": "es2016", // 目标语法
"module": "esnext", // 模块格式
"moduleResolution": "node", // 模块解析方式
"strict": false, // 严格模式
"resolveJsonModule": true, // 解析json模块
"esModuleInterop": true, // 允许通过es6语法引入commonjs模块
"jsx": "preserve", // jsx 不转义
"lib": ["esnext", "dom"], // 支持的类库 esnext及dom
"baseUrl": ".",
"paths": {
"@vue/*": ["packages/*/src"]
}
}
}
一个库必须要考虑的一步:打包源码
打包的格式
我们打开node_modules下的vue包的dist文件夹,发现vue打包的文件有多种格式,这是为了给用户在不同的使用场景下使用的,打包的格式不同,对用的使用规范也是不同的。
- 总体来说可以分为三种格式:
- node中使用的格式:commonjs(cjs)格式
- esmodule使用的格式:esm
- esm-bundler:将所有模块打包时集成到一起
- 浏览器直接通过script引入来使用的格式:iife自执行函数(global)
包与包之间的依赖
vue3的响应式是一个独立的包,通过包管理工具下载的vue3可以看出来,里面有一个reactivity包,就是vue的响应式模块,shared包是存放公共逻辑的模块。
- 模仿vue包在项目中创建packages文件夹:
- 分别在该文件夹下创建 reativity文件夹和shared文件夹
- 分别在两个文件夹通过 pnpm init 初始化项目模块,然后创建scr/index.ts作为该模块打包的入口文件
- 为方便模块的打包和模块间引用,两个模块的package.json分别配置如下:
{
"name": "@vue/reactivity",
"version": "1.0.0",
"description": "",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
"scripts": {},
"buildOptions": {
"name": "reactivity",
"formats": [
"esm-browser",
"esm-bundler",
"cjs",
"global"
]
},
"dependencies": {},
"devDependencies": {}
}
{
"name": "@vue/shared",
"version": "1.0.0",
"description": "",
"module": "dist/shared.esm-bundler.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"buildOptions": {
"name": "reactivity",
"formats": [
"esm-browser",
"esm-bundler",
"cjs",
"global"
]
},
"dependencies": {},
"devDependencies": {}
}
- 我们在shared/src/index.ts下简单导出一个函数
export function isObject(value:any){
return value !==null && typeof value == 'object'
}
- 而在reactivity中引入这个模块就很简单,只需要
import {isObject } from '@vue/shared'
即可。- 这里的路径@vue不会去node_modules下查找,原因是我们在tsconfig.json中配置了paths。
模块的打包流程
- 在根目录下新建script/dev.js脚本,通过运行该脚本实现模块的打包
- 在根目录package.json的script下,配置运行脚本的命令
"dev": "node scripts/dev.js reactivity -f esm",
- 使用npm run dev ,会运行dev.js,默认打包reactivity模块,默认格式为esm
- 至此,我们梳理一下打包的流程:
- 首先,用户输入
npm run dev **
,运行打包脚本,同时传入打包的参数 - dev.js运行,接收用户传入的参数
- 根据参数,确定打包的模块,打包的格式,打包的输出目录
- 调用esbuild模块,对模块进行打包。
- 首先,用户输入
- 了解了基本的流程,我们开始对dev脚本进行完善。
- 首先,接收用户的参数,我们知道,通过
process.argv
可以获取用户在命令台输入的命令,而minimist就是一个很好的解析命令的模块,将命令传给minmist,它可以解析成固定的格式传给我们,我们在控制台输入npm run dev
:
const args = require('minimist')(process.argv.slice(2)) console.log(args) // { _: [ 'reactivity' ], f: 'esm' }
- 了解了它的格式,我们就能解析,获取用户的命令,从而确定要打包的文件夹,打包格式,以及输出地址等。
const target = args._[0] || 'reactivity' const format = agrs.f || 'global' //查找打包模块下的package.json const pkg = require(path.resolve(__dirname,`../packages/${target}/package.json`)) //输出格式 const outputFormat = format.startsWith('global') ? 'iife' : format== 'cjs'?'cjs':'esm' //输出地址 const outFile = path.resolve(__dirname,`../packages/${target}/dist/${target}.${format}.js`)
- 然后调用esbuild中的build函数,打包即可
//输出地址 const outFile = path.resolve(__dirname,`../packages/${target}/dist/${target}.${format}.js`) build({ entryPoints: [path.resolve(__dirname, `../packages/${target}/src/index.ts`)], outfile: outFile, bundle: true, sourcemap: true, format: outputFormat, globalName: pkg.buildOptions?.name, watch: { onRebuild(error) { if (error) console.log('~~~') } }, platform: form
- 运行命令,可以看到reactivity文件夹下生成打包的文件
- 首先,接收用户的参数,我们知道,通过
万事俱备,响应式开造
最开始的两个函数
相信大家在最开始接触响应式的时候,必会接触一个函数
reactive
,函数的作用想必大家也都清楚:将一个对象变成响应式。还有一个函数叫effect
,他能监听一个函数,当响应式数据变化后,让这个函数重新执行。这个函数可能大家有点陌生,原因就是在使用Vue的时候,基本使用的都是html模板,数据改变,模板重新刷新
<body>
<div id="app"></div>
<script type="module">
import { reactive,effect } from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
const state = reactive({
name:'宫本',
age:18
})
//传入一个副作用函数
effect(()=>{
document.getElementById('app').innerHTML = state.name +":"+ state.age
})
setTimeout(()=>{
state.age=19
},1000)
</script>
</body>
可以看到,vue的响应式基本靠这个函数可以诠释,我们接下来就尝试实现这两个函数
- 在reactivity/src下新建effect.ts文件和reactive.ts文件,分别声明这两个函数,并在index.ts中集成导出
reactive函数实现
-
首先尝试实现一下reactive函数,我们可以模仿着源码里的原函数的功能去构思:
- 首先,reactive接收一个对象,不是对象那直接返回即可
- 其次,要对这个对象做一下代理
import { isObject } from "@vue/shared"; export function reactive(target){ if(!isObject(target)){ return target } const proxyObj = new Proxy(target,{ get(target,key,receiver){ return target[key] }, set(target,key,value,receiver){ target[key] = value return true } }) }
- 我们可以很容易的想到利用proxy对数据做代理,这样取数据和改变数据的时候都可以监测到,但是这里面有一个很大的问题:
const obj = { name:'小明', get getName(){ return this.name } } const proxy = new Proxy(obj,{ get(target,key,receiver){ console.log(key) return target[key] }, set(target,key,value,receiver){ return true } }) console.log('sx',proxy.getName)
我们可以梳理一下这个流程:访问proxy.getName,由于proxy是Porxy实例,所以访问属性的时候会触发get操作,返回对象的target[key],此时的target是obj,key是getName,控制台输出
getName
,相当于执行obj.getName。最关键的一步来了,getName里面会访问name属性,此时是会访问obj里面的name,还是会访问proxy里面的name?按照我们响应式的设想,访问了getName之后也会访问到name,既然有属性被访问,都应该被监测到,但是这里的name属性不会被监测,原因就是执行target[key]后,这里的this指向obj,所以会自然走到obj.name中,不会走proxy.name,既然不会代理,自然不会被监测到。
显然,这不符合我们响应式的预期,那么如何解决属性里面this指向的问题,让它一直指向proxy实例呢?Reflect对象可以将this指针绑定在传入的对象上,完美解决这个问题。Reflect介绍
const obj = { name:'小明', get getName(){ return this.name } } const proxy = new Proxy(obj,{ get(target,key,receiver){ console.log(key) return Reflect.get(target,key,receiver) }, set(target,key,value,receiver){ return Reflect.set(target,key,value,receiver) } }) console.log('sx',proxy.getName)
Reflect.get中最后一个参数receiver表示当前实例proxy,它会将操作的指针绑定在proxy上,这样访问任何属性,都会触发代理。
-
上述对数据实现简单的代理,目的是在获取属性和修改属性的时候能有感知和拦截(get,set),但是这个代理对象依然有很大的问题。我们来仔细探究一下:
- 假如有个老六写出下面这种代码:
const obj = { name:'宫本', age:18 } const state1 = reactive(obj) const state2 = reactive(obj)
对一个数据同时代理两次,很显然两个代理对象是不相等的,因为开辟了两个内存空间,这显然不符合我们的预期要求,因为对一个数据的代理对象只能有一个,不然后面使用代理对象,使用哪一个呢,数据都不同步了,不断创建空间,对性能也开销很大。
对此在对一次代理对象的时候,可以在reactive内部对对象做缓存,当再次代理同一个对象的时候,取出这个缓存即可。
import { isObject } from "@vue/shared"; //get,set操作抽离到baseHandles中 import { mutableHandlers } from './baseHandles' const reactiveMap =new WeakMap() export function reactive(target){ if(!isObject(target)){ return target } const existProxy = reactiveMap.get(target) if(existProxy){ return existProxy } const proxyObj = new Proxy(target,mutableHandlers) reactiveMap.set(target,proxyObj) return proxyObj }
- 假如老六又写出这种代码:
const obj = { name:'宫本', age:18 } const state1 = reactive(obj) const state2 = reactive(state2)
代理对象也是个对象,也能作为reactive的参数,但是对代理对象做代理,显然没有什么意义,vue中针对这种情况,是在内部判断传入的对象,如果已经是代理对象,则直接返回就行。而判断对象是代理对象的方式也很巧妙,它是通过尝试对对象进行取值,如果能触发get操作,则说明它是代理对象,直接返回该对象。
export enum ReactiveFlag {
IS_REACTIVE = '__is_Reactive'
}
const reactiveMap =new WeakMap()
export function reactive(target){
if(!isObject(target)){
return target
}
//尝试触发对象的get操作
if(target[ReactiveFlag.IS_REACTIVE]){
return target
}
const existProxy = reactiveMap.get(target)
if(existProxy){
return existProxy
}
const proxyObj = new Proxy(target,mutableHandlers)
reactiveMap.set(target,proxyObj)
return proxyObj
}
get(target,key,receiver){
if(key === ReactiveFlag.IS_REACTIVE){
return true
}
return target[key]
},
effect函数实现
reactive数据代理函数初步实现之后,需要完成effect函数,该函数传入一个副作用函数,实际上,这个副作用函数会在effect函数内部默认执行一次,如果副作用函数里面有代理的数据,那么数据就会记住你这个effect函数,然后某个时间段数据变化了,这个数据就会找到它记住的effect函数,依次让这些函数执行,那么对应的,页面开始刷新,刷新后的数据就是最新的数据。
首先,effect函数内部会创建一个类,将传入的副作用函数传入这个类中,生成一个实例,实例有一个run方法,会在内部执行这个副作用函数。由于effect函数传入副作用函数后,会默认执行一次副作用函数。因此,其详细的流程应该是,effect传入副作用函数,内部根据副作用函数传入一个类中声明一个实例,实例调用run方法,执行副作用函数。
class ReactiveEffect{
public active = true
public deps=[]
constructor(public fn){
}
run(){}
}
export function effect(fn){
const _effect = new ReactiveEffect(fn);
_effect.run()
}
ReactiveEffect是一个响应式类,意味着,通过这个类会创建一个响应式的实例,这个类在effect内部实例化。类中的active属性,是一个控制器,默认为true,意味着是否创建响应式 实例,因为有些场合不需要响应式(false),可以理解为是一个控制操作,后面的需求中会讲到。deps属性是一个收集装置,执行run函数会执行传入的副作用函数,副作用函数中会调用代理的数据,deps的作用就是记录当前的响应式实例所对应的代理数据。在里面调用了哪个数据,它就记录上哪个。
run函数内部判断active,若是为true,则执行副作用函数
run(){
if(this.active){
return this.fn()
}
}
下面就是最关键的一步,如何让effect与reactive联立起来,当effect内部调用副作用函数,让reactive感知到,并记录这个响应式函数以便下次更新再次执行这个函数,更新页面数据。
其实,我们可以根据js单线程的特点实现这个逻辑,假设我们创建一个全局指针,当创建响应式实例后,执行run函数前,将指针指向响应式实例,然后执行run函数,run函数内部执行副副作用函数,副作用函数内部访问代理属性,触发get操作,get内部收集相关数据对应的指针。然后某个时刻,数据改变,触发数据的set操作,更改数据,获取属性对应的指针集合,通过指针执行其run函数,再次执行副作用函数,访问数据,此时的数据是最新的,然后页面刷新。
run(){
if(!this.active){
//直接执行函数,不进行后面的依赖收集
return this.fn()
}
try {
activeEvent = this
return this.fn()
}finally{
//执行完之后,让指针置空
activeEvent = null
}
}
在代理操作中,get读取到某个属性,监测当时是否在_effect中执行的(activeEffect存在),如果不是,则不需要进行依赖收集,如果是,则进行依赖收集,收集的格式是target -> key -> activeEffect,数据改变后,set中,取出收集的activeEffect集合,依次执行其run函数
get(target,key,receiver){
if(key === ReactiveFlag.IS_REACTIVE){
return true
}
//收集依赖
if(activeEffect){
//从对象中寻找键值,纪录键值对应的指针集合
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)
}
return target[key]
},
set(target,key,value,receiver){
target[key] = value
//数据更改,找到对应的key收集的_effect实例
let depsMap = targetMap.get(target)
if(!depsMap){
//不存在,说明之前副作用函数中没有使用过这个属性,则这个属性改变不需要重现刷新页面
return ;
}
let effects = depsMap.get(key)
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
return true
}
我们尝试打包我们的代理,使用他们,发现已经可以实现简单的响应式
<script type="module">
import { reactive,effect } from './reactivity.esm.js'
const obj = {
name:'宫本',
age:18
}
const state = reactive(obj)
//传入一个副作用函数
effect(()=>{
document.getElementById('app').innerHTML = state.name +":"+ state.age
})
setTimeout(()=>{
state.age=19
},1000)
</script>
我们在代理对象中的set和get属性中设置了依赖收集和重新执行依赖的逻辑,对于这部分逻辑,我们可以抽离出来,使逻辑直接负责的任务更加纯粹一些
export const mutableHandlers = {
get(target,key,receiver){
if(key === ReactiveFlag.IS_REACTIVE){
return true
}
track(target,key)
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver){
let oldValue = target[key]
let r = Reflect.set(target,key,value,receiver)
if(oldValue !==value){
trigger(target,key,value)
}
return r
}
}
export function track(target,key){
//收集依赖
if(activeEffect){
//从对象中寻找键值,纪录键值对应的指针集合
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)
trackEffect(deps)
}
}
export function trackEffect(deps){
let shouldTrack = !deps.has(activeEffect)
if(shouldTrack){
deps.push(activeEffect)
}
//让_effect实例记住对应的key
activeEffect.deps.push(deps)
}
export function trigger(target,key,value){
//数据更改,找到对应的key收集的_effect实例
let depsMap = targetMap.get(target)
if(!depsMap){
//不存在,说明之前副作用函数中没有使用过这个属性,则这个属性改变不需要重现刷新页面
return ;
}
let effects = depsMap.get(key)
triggerEffects(effects)
}
export function triggerEffects(effects){
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
}
以上实现了一个简单的响应式模块,但是在实际使用中,依然会暴漏出很多问题,在Vue3源码中,做了很多hook的问题处理,这里我们只讲述其简单的逻辑,很一些比较常见的问题。
- 问题一: effect嵌套,导致指针丢失
根据我们的逻辑,假设老六写了以下代码:
import { reactive,effect } from './reactivity.esm.js'
const obj = {
name:'宫本',
age:18
}
const state = reactive(obj)
//传入一个副作用函数
effect(()=>{
effect(()=>{
console.log(state.name)
})
console.log(state.age)
})
setTimeout(()=>{
state.age=19
},1000)
老六写了个effect嵌套,这种在vue中很常见,我们很自然可以想到组件嵌套,后面state.age改变,却不输出任何东西,这是什么原因呢?
我们按照之前响应式的逻辑,逐步分析一下,将外层的eefect设置为e1,内层effect设置为e2,e1创建好,会执行内部的run方法,此时指针指向e1(实际是指向e1内部的响应式类实例,这里简短的说),然后执行传入的副作用函数,副作用函数内部按顺序执行,会先执行e2,e2创建后执行e2内部的run函数,指针指向e2,然后执行e2内部副作用函数,触发stata.name的依赖收集,name属性收集到e2,然后指针 置空。此时e2执行完后,在e1的副作用函数中继续往下执行,执行到state.age的触发,由于此时指针为空,无法进行依赖收集,所以后面修改state.age,不会进行响应式更新
很明显,这种情况属于内部的指针改掉了外部的指针,然后内部使用完成之后,外部指针没有复原。早期的vue解决方案是通过一个栈来存储指针,指针切换通过入栈出栈的方式来实现。还有一种方案,是在内部创建一个parent指针,记录其外层指针,结束之后将指针重新指向其parent
public active = true
public parent = undefined
public deps=[]
constructor(public fn){
}
run(){
if(!this.active){
//直接执行函数,不进行后面的依赖收集
return this.fn()
}
try {
this.parent = activeEffect
activeEffect = this
return this.fn()
}finally{
//执行完之后,让指针置空
activeEffect = this.parent
this.parent = undefined
}
}
- 问题二: 代理的数据是多层对象
对于proxy创建的实例,我们只是对传入的对象的第一层属性做了代理,但是如果属性值还是一个对象,则不会被代理,对此,在get操作中,应该对获取的数据再次判断,倘若是对象,则再次代理。这也是一个性能优化的表现,Vue3一开始并不是直接对传入的对象做深层代理,则是当用户访问到某个属性,触发get后,发现它是对象类型,才会对它做代理。换句话说,就是用到这个数据的时候才会处理它。
get(target,key,receiver){
if(key === ReactiveFlag.IS_REACTIVE){
return true
}
track(target,key)
let r = Reflect.get(target,key,receiver)
if(isObject(r)){
return reactive(r)
}
return r
},
- 问题三:vue的分支切换
分支切换关系到属性依赖收集的问题,对于后来不需要的属性,需要把这个属性收集的依赖给清空掉。
import { reactive,effect } from './reactivity.esm.js'
const state = reactive({
flag:true,
age:18,
name:'sx'
})
effect(()=>{
document.getElementById('app').innerHTML = state.flag? state.age:state.name
console.log('sx')
})
setTimeout(()=>{
state.flag = false
setTimeout(()=>{
state.age = 19
},1000)
},1000)
可以看到,当flag改变。age属性已经和页面没有任何关系,但是改变age,依然会刷新页面,这显然不合理。
分析上述代码属性收集依赖的过程,1.副作用函数fn第一次执行,flag ->effect,age->effect 2. flag改变fn再次执行,flag->effect,name->effect。此时页面数据和age没有关系,但是在第一次fn执行 时,age->effect,因此age改变,依然会执行fn。
xport function triggerEffects(effects){
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
}
可以发现,第2步出了问题,当 属性更新时,会找出属性收集的依赖
effect集合
,然后执行这个集合的run方法,重新调用fn,由于重新调用fn,所以会进行新的依赖收集,只收集有用到的数据。倘若在执行集合的run方法前,先把集合对应的旧的依赖删除掉(清除age属性影响),然后执行run方法,创建新的依赖(flag->effect;age->effect)不久可以了。
run(){
if(!this.active){
//直接执行函数,不进行后面的依赖收集
return this.fn()
}
try {
this.parent = activeEffect
activeEffect = this
return this.fn()
}finally{
//执行完之后,让指针置空
activeEffect = this.parent
this.parent = undefined
}
}
那么我们只需要在上面的代码里动一下手脚,执行run前,获取effect存储的deps,deps中存储了所有的属性收集的关于当前effect的依赖,依次清除各个属性对当前effect的依赖即可
export function cleanTrack(effect){
let { deps } = effect
for(let i = 0;i<deps.length;++i){
deps[i].delete(effect)
}
deps.length = 0
}
我们在run函数中执行fn前调用cleanTrack,清除当前依赖。但是,结果并不理想,页面直接死循环了。
问题还是出在这里
export function triggerEffects(effects){
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
}
effects是一个map类型的集合,effects找到对应的effect执行,effect调用cleanTrack找到对应的deps集合,deps中又找到当前effects,删除effect,然后执行fn,effects又添加这个effect,相当于下面这样。
let test = new Set([1,2])
test.forEach(t=>{
test.delete(1)
test.add(1)
})
Set类型的数据遍历时不能删除又添加自己,那么只需要拷贝一份数据,遍历拷贝的数据,操作原有数据即可
export function triggerEffects(deps){
const effects = [...deps]
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
}
- 问题四: 副作用函数内部自己调用自己
import { reactive,effect } from './reactivity.esm.js'
const state = reactive({
flag:true,
age:18,
name:'sx'
})
effect(()=>{
document.getElementById('app').innerHTML = state.age++
})
我们分析一下流程为什么会无限调用,首先
age++
可以拆分为age;age = age+1
两步,第一步访问age,age收集当前effect,第二步改变age,触发更新逻辑,执行fn,fn执行age改变,fn再次执行.....无线循环
明白了原因就好办了,我们的目的就是让数据更改一次就行了,后面的执行不需要,也就是避免自调用无限循环。那么我们需要思考一个问题,当fn调用之后,指针指向当前effect,然后触发再次更新,到代码这里.
export function triggerEffects(deps){
const effects = [...deps]
//依次执行
if(effects){
effects.forEach(effect=>effect.run())
}
}
此时是上一个fn再执行到这,下一个fn即将执行,那么此时的指针必然是effect,在下一个fn执行的时候
try {
this.parent = activeEffect
activeEffect = this
cleanTrack(this)
return this.fn()
}finally{
//执行完之后,让指针置空
activeEffect = this.parent
this.parent = undefined
}
这里的this还是这个effect,那么此时activeEffect == this,所以我们只需要添加一层判断,当内部指针和全局指针相同,说明是自调用情况,这种情况取消新一轮的fn执行即可
export function triggerEffects(deps){
const effects = [...deps]
//依次执行
if(effects){
effects.forEach(effect=>{
if(activeEffect !== effect){
effect.run()
}
})
}
}
自此,我们基本实现vue的响应式原理
转载自:https://juejin.cn/post/7159523608144904199