专栏分享:vue2源码专栏vue3源码专栏vue router源码专栏玩具项目专栏,硬核推荐

欢迎各位ITer关注点赞收藏

Vue3中响应数据核心是 reactive , reactive 的实现是由 proxy 加 effect 组合,上一章节我们利用 proxy 实现了一个简易版的 reactive,# 【源码系列#01】Vue3响应式原理(Reactive)。接下来让我们一起手写下 effect 的源码

effect

effect 作为 reactive 的核心,主要负责收集依赖,更新依赖

在学习 effect之前,我们再来看下这张图

  • targetMap:存储了每个 "响应性对象属性" 关联的依赖;类型是 WeakMap
  • depsMap:存储了每个属性的依赖;类型是 Map
  • dep:存储了我们的 effects ,一个 effects 集,这些 effect 在值发生变化时重新运行;类型是 Set

编写effect函数

// 当前正在执行的effect
export let activeEffect = undefined export class ReactiveEffect {
// @issue2
// 这里表示在实例上新增了parent属性,记录父级effect
public parent = null
// 记录effect依赖的属性
public deps = []
// 这个effect默认是激活状态
public active = true // 用户传递的参数也会传递到this上 this.fn = fn
constructor(public fn, public scheduler) {} // run就是执行effect
run() {
// 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集
if (!this.active) {
return this.fn()
}
// 这里就要依赖收集了 核心就是将当前的effect 和 稍后渲染的属性关联在一起
try {
// 记录父级effect
this.parent = activeEffect
activeEffect = this
// 当稍后调用取值操作的时候 就可以获取到这个全局的activeEffect了
return this.fn()
} finally {
// 还原父级effect
activeEffect = this.parent
}
}
} export function effect(fn, options: any = {}) {
// 这里fn可以根据状态变化 重新执行, effect可以嵌套着写
const _effect = new ReactiveEffect(fn) // 创建响应式的effect
// issue1
_effect.run() // 默认先执行一次
}

@issue1 effect 默认会先执行一次

依赖收集

const targetMap = new WeakMap()
export function track(target, type, key) {
// @issue3
// 我们只想在我们有activeEffect时运行这段代码
if (!activeEffect) return let depsMap = targetMap.get(target) // 第一次没有
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // key -> name / age
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 单向指的是 属性记录了effect, 反向记录,应该让effect也记录他被哪些属性收集过,这样做的好处是为了可以清理
trackEffects(dep)
} export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect) // 去重了
if (shouldTrack) {
dep.add(activeEffect)
// @issue4
// 存放的是属性对应的set
activeEffect.deps.push(dep) // 让effect记录住对应的dep, 稍后清理的时候会用到
}
}
}

@issue3 当activeEffect有值时,即只在effect运行时执行track依赖收集

@issue4 双向记录 ,一个属性对应多个effect,一个effect对应多个属性

一个属性对应多个 effect: 在之前的 depsMap 图中,我们得知,一个属性映射一个 dep(即 effect 集合,类型为 Set)

一个effect对应多个属性: 在 effect 中,有一个 deps 属性,她记录了此 effect 依赖的每一个属性所对应的 dep。让 effect 记录对应的 dep, 目的是在稍后清理的时候会用到

触发更新

export function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return // 触发的值不在模板中使用 let effects = depsMap.get(key) // 找到了属性对应的effect // 永远在执行之前 先拷贝一份来执行, 不要关联引用
if (effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
effects.forEach(effect => {
// 我们在执行effect的时候 又要执行自己,那我们需要屏蔽掉,不要无限调用,【避免由activeEffect触发trigger,再次触发当前effect。 activeEffect -> fn -> set -> trigger -> 当前effect】
// @issue5
if (effect !== activeEffect) {
effect.run() // 否则默认刷新视图
}
})
}

@issue5 避免由run触发trigger,无限递归循环

我们在执行 effect 的时候,又要执行自己,那我们需要屏蔽掉,不要无限调用【避免由 activeEffect 触发 trigger,再次触发当前 effect。 activeEffect -> fn -> set -> trigger -> 当前effect】

举个栗子

const { effect, reactive } = VueReactivity
const data = { name: '柏成', age: 13, address: { num: 517 } }
const state = reactive(data)
// vue3中的代理都是用proxy来解决的 // 此effect函数默认会先执行一次, 对响应式数据取值(取值的过程中数据会依赖于当前的effect)
effect(() => {
state.age = Math.random()
document.getElementById('app').innerHTML = state.name + '今年' + state.age + '岁了'
}) // 稍后name和age变化会重新执行effect函数
setTimeout(() => {
state.age = 18
}, 1000)

分支切换与cleanup

// 每次执行effect的时候清理一遍依赖,再重新收集,双向清理
function cleanupEffect(effect) {
// deps 里面装的是name对应的effect, age对应的effect
const { deps } = effect
for (let i = 0; i < deps.length; i++) {
// 解除effect,重新依赖收集
deps[i].delete(effect)
}
effect.deps.length = 0
} export class ReactiveEffect {
// @issue3
// 这里表示在实例上新增了parent属性,记录父级effect
public parent = null
// 记录effect依赖的属性
public deps = []
// 这个effect默认是激活状态
public active = true // 用户传递的参数也会传递到this上 this.fn = fn
constructor(public fn, public scheduler) {} // @issue8 - scheduler // run就是执行effect
run() {
// 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集
if (!this.active) {
return this.fn()
}
// 这里就要依赖收集了 核心就是将当前的effect 和 稍后渲染的属性关联在一起
try {
// 记录父级effect
this.parent = activeEffect
activeEffect = this
// 这里我们需要在执行用户函数之前将之前收集的内容清空
cleanupEffect(this) // @issue6
// 当稍后调用取值操作的时候 就可以获取到这个全局的activeEffect了
return this.fn() // @issue1
} finally {
// 还原父级effect
activeEffect = this.parent
}
}
} export function triggerEffects(effects) {
// 先拷贝,防止死循环,new Set 后产生一个新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我们在执行effect的时候 又要执行自己,那我们需要屏蔽掉,不要无限调用,【避免由activeEffect触发trigger,再次触发当前effect。 activeEffect -> fn -> set -> trigger -> 当前effect】
if (effect !== activeEffect) {
effect.run() // 否则默认刷新视图
}
})
}

@issue6 分支切换 - cleanupEffect。我们需要在执行用户函数之前将之前收集的内容清空,双向清理,在渲染时我们要避免副作用函数产生的遗留,举个栗子,我们再次修改name,原则上不应更新页面

每次副作用函数执行时,可以先把它从所有与之关联的依赖集合中删除。当副作用函数执行完毕后,响应式数据会与副作用函数之间建立新的依赖关系,而分支切换后,与副作用函数没有依赖关系的响应式数据则不会再建立依赖,这样副作用函数遗留的问题就解决了;

const { effect, reactive } = VueReactivity
const state = reactive({ flag: true, name: '柏成', age: 24 }) effect(() => {
// 我们期望的是每次执行effect的时候都可以清理一遍依赖,重新收集
// 副作用函数 (effect执行渲染了页面)
console.log('render')
document.body.innerHTML = state.flag ? state.name : state.age
}) setTimeout(() => {
state.flag = false
setTimeout(() => {
// 修改name,原则上不更新页面
state.name = '李'
}, 1000)
}, 1000)

@issue7 分支切换 - 死循环。遍历 set 对象时,先 delete 再 add,会出现死循环

在调用循环遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除,并重新添加到集合,如果此时循环遍历没有结束,那该值会被重新访问

参考资料:ECMAScript Language Specification

提示:语言规范说的是forEach时是这样的,实测 for of 遍历Set会有同样的问题。

看一下 triggerEffects 方法,遍历了 effects

export function triggerEffects(effects) {
effects.forEach(effect => { effect.run() })
}

effect.run 方法中

  • 执行 cleanupEffect(effect),清理一遍依赖
deps[i].delete(effect)
  • 执行 this.fn(),重新执行函数,重新收集依赖
// track() 方法中
dep.add(activeEffect) // 将副作用函数activeEffect添加到响应式依赖中

解决方法:

let effect = () => {};
let deps = new Set([effect])
deps.forEach(item=>{
console.log('>>>')
deps.delete(effect);
deps.add(effect)
}); // 这样就导致死循环了 // 解决方案如下,先拷贝一份,遍历的Set对象 和 操作(delete、add)的Set对象不是同一个即可
let effect = () => {};
let deps = new Set([effect])
const newDeps = new Set(deps)
newDeps.forEach(item=>{
console.log('>>>')
deps.delete(effect);
deps.add(effect)
});

effect嵌套

// 当前正在执行的effect
export let activeEffect = undefined export class ReactiveEffect {
// @issue2
// 这里表示在实例上新增了parent属性,记录父级effect
public parent = null
// 记录effect依赖的属性
public deps = []
// 这个effect默认是激活状态
public active = true // 用户传递的参数也会传递到this上 this.fn = fn
constructor(public fn, public scheduler) {} // run就是执行effect
run() {
// 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集
if (!this.active) {
return this.fn()
}
// 这里就要依赖收集了 核心就是将当前的effect 和 稍后渲染的属性关联在一起
try {
// 记录父级effect
this.parent = activeEffect
activeEffect = this
// 当稍后调用取值操作的时候 就可以获取到这个全局的activeEffect了
return this.fn()
} finally {
// 还原父级effect
activeEffect = this.parent
}
}
} export function effect(fn, options: any = {}) {
// 这里fn可以根据状态变化 重新执行, effect可以嵌套着写
const _effect = new ReactiveEffect(fn) // 创建响应式的effect
// issue1
_effect.run() // 默认先执行一次
}

@issue2 利用 parent 解决effect嵌套问题,effect 嵌套的场景在 Vue.js 中常常出现,如:Vue中的渲染函数(render)就是在一个effect中执行的,嵌套组件就会伴随着嵌套 effect

  1. 解决effect嵌套问题----栈方式------------------------vue2/vue3.0初始版本
// 运行effect,此effect入栈,运行完毕,最后一个effect出栈,属性关联栈中的最后一个effect
[e1] -> [e1,e2] -> [e1]
effect(() => { // activeEffect = e1
state.name // name -> e1
effect(() => { // activeEffect = e2
state.age // age -> e2
})
// activeEffect = e1
state.address // address = e1
})
  1. 解决effect嵌套问题----树形结构方式----------------vue3后续版本
// 这个执行流程 就类似于一个树形结构
effect(()=>{ // parent = null activeEffect = e1
state.name // name -> e1
effect(()=>{ // parent = e1 activeEffect = e2
state.age // age -> e2
effect(()=> { // parent = e2 activeEffect = e3
state.sex // sex -> e3
}) // activeEffect = e2
}) // activeEffect = e1 state.address // address -> e1 effect(()=>{ // parent = e1 activeEffect = e4
state.age // age -> e4
})
})

停止effect和调度执行


export class ReactiveEffect {
// @issue8 - stop
stop() {
if (this.active) {
this.active = false
cleanupEffect(this) // 停止effect的收集
}
}
} export function effect(fn, options: any = {}) {
// 这里fn可以根据状态变化 重新执行, effect可以嵌套着写
const _effect = new ReactiveEffect(fn, options.scheduler) // 创建响应式的effect @issue8 - scheduler
_effect.run() // 默认先执行一次 // @issue8 - stop
// 绑定this,run方法内的this指向_effect,若不绑定,这样调用run方法时,runner(),则指向undefined
const runner = _effect.run.bind(_effect)
// 将effect挂载到runner函数上,调用stop方式时可以这样调用 runner.effect.stop()
runner.effect = _effect
return runner
} export function triggerEffects(effects) {
// 先拷贝,防止死循环,new Set 后产生一个新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我们在执行effect的时候 又要执行自己,那我们需要屏蔽掉,不要无限调用,【避免由activeEffect触发trigger,再次触发当前effect。 activeEffect -> fn -> set -> trigger -> 当前effect】
if (effect !== activeEffect) {
// @issue8 - scheduler
if (effect.scheduler) {
effect.scheduler() // 如果用户传入了调度函数,则执行调度函数
} else {
effect.run() // 否则默认刷新视图
}
}
})
}

如何使用 stop 和 scheduler ?举个小栗子

  • 当我们调用 runner.effect.stop() 时,就双向清理了 effect 的所有依赖,后续 state.age 发生变化后,将不再重新更新页面
  • 基于 scheduler 调度器,我们可以控制页面更新的周期,下面例子中,会在1秒后,页面由 30 变为 5000
let waiting = false
const { effect, reactive } = VueReactivity
const state = reactive({ flag: true, name: 'jw', age: 30, address: { num: 10 } })
let runner = effect(
() => {
// 副作用函数 (effect执行渲染了页面)
document.body.innerHTML = state.age
},
{
scheduler() {
// 调度 如何更新自己决定
console.log('run')
if (!waiting) {
waiting = true
setTimeout(() => {
runner()
waiting = false
}, 1000)
}
},
},
) // 清理 effect 所有依赖,state.age 发生变化后,将不再重新更新页面
// runner.effect.stop() state.age = 1000
state.age = 2000
state.age = 3000
state.age = 4000
state.age = 5000

effect.ts

完整代码如下

/**
* @issue1 effect默认会先执行一次
* @issue2 activeEffect 只在effect运行时执行track保存
* @issue3 parent 解决effect嵌套问题
* @issue4 双向记录 一个属性对应多个effect,一个effect对应多个属性 √
* @issue5 避免由run触发trigger,递归循环
* @issue6 分支切换 cleanupEffect
* @issue7 分支切换 死循环,set循环中,先delete再add,会出现死循环
* @issue8 自定义调度器 类似Vue3中的effectScope stop 和 scheduler
*/ // 当前正在执行的effect
export let activeEffect = undefined // @issue6
// 每次执行effect的时候清理一遍依赖,再重新收集,双向清理
function cleanupEffect(effect) {
// deps 里面装的是name对应的effect, age对应的effect
const { deps } = effect
for (let i = 0; i < deps.length; i++) {
// 解除effect,重新依赖收集
deps[i].delete(effect)
}
effect.deps.length = 0
} export class ReactiveEffect {
// @issue3
// 这里表示在实例上新增了parent属性,记录父级effect
public parent = null
// 记录effect依赖的属性
public deps = []
// 这个effect默认是激活状态
public active = true // 用户传递的参数也会传递到this上 this.fn = fn
constructor(public fn, public scheduler) {} // @issue8 - scheduler // run就是执行effect
run() {
// 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集
if (!this.active) {
return this.fn()
}
// 这里就要依赖收集了 核心就是将当前的effect 和 稍后渲染的属性关联在一起
try {
// 记录父级effect
this.parent = activeEffect
activeEffect = this
// 这里我们需要在执行用户函数之前将之前收集的内容清空
cleanupEffect(this) // @issue6
// 当稍后调用取值操作的时候 就可以获取到这个全局的activeEffect了
return this.fn() // @issue1
} finally {
// 还原父级effect
activeEffect = this.parent
}
}
// @issue8 - stop
stop() {
if (this.active) {
this.active = false
cleanupEffect(this) // 停止effect的收集
}
}
} export function effect(fn, options: any = {}) {
// 这里fn可以根据状态变化 重新执行, effect可以嵌套着写
const _effect = new ReactiveEffect(fn, options.scheduler) // 创建响应式的effect @issue8 - scheduler
_effect.run() // 默认先执行一次 // @issue8 - stop
// 绑定this,run方法内的this指向_effect,若不绑定,这样调用run方法时,runner(),则指向undefined
const runner = _effect.run.bind(_effect)
// 将effect挂载到runner函数上,调用stop方式时可以这样调用 runner.effect.stop()
runner.effect = _effect
return runner
} // 对象 某个属性 -》 多个effect
// WeakMap = {对象:Map{name:Set-》effect}}
// {对象:{name:[]}}
// 多对多 一个effect对应多个属性, 一个属性对应多个effect
const targetMap = new WeakMap()
export function track(target, type, key) {
// 我们只想在我们有activeEffect时运行这段代码
if (!activeEffect) return // @issue2
let depsMap = targetMap.get(target) // 第一次没有
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // key -> name / age
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 单向指的是 属性记录了effect, 反向记录,应该让effect也记录他被哪些属性收集过,这样做的好处是为了可以清理
trackEffects(dep)
} export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect) // 去重了
if (shouldTrack) {
dep.add(activeEffect)
// @issue4
// 存放的是属性对应的set
activeEffect.deps.push(dep) // 让effect记录住对应的dep, 稍后清理的时候会用到
}
}
} export function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return // 触发的值不在模板中使用 let effects = depsMap.get(key) // 找到了属性对应的effect // 永远在执行之前 先拷贝一份来执行, 不要关联引用
if (effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
// 先拷贝,防止死循环,new Set 后产生一个新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我们在执行effect的时候,有时候会改变属性,那我们需要屏蔽掉,不要无限调用,【避免由activeEffect触发trigger,再次触发当前effect。 activeEffect -> fn -> set -> trigger -> 当前effect】
// @issue5
if (effect !== activeEffect) {
// @issue8 - scheduler
if (effect.scheduler) {
effect.scheduler() // 如果用户传入了调度函数,则执行调度函数
} else {
effect.run() // 否则默认刷新视图
}
}
})
}

参考资料

Vue3响应式系统实现原理(二) - CherishTheYouth - 博客园

【源码系列#02】Vue3响应式原理(Effect)的更多相关文章

  1. vue 源码自问自答-响应式原理

    vue 源码自问自答-响应式原理 最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然. 所以打算把自己掌握的知识,试着组织成自己的语言表达出来 不打算平铺直叙的写清 ...

  2. Vue 源码解析:深入响应式原理(上)

    原文链接:http://www.imooc.com/article/14466 Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScri ...

  3. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  4. vue3剖析:响应式原理——effect

    响应式原理 源码目录:https://github.com/vuejs/vue-next/tree/master/packages/reactivity 模块 ref: reactive: compu ...

  5. vue3响应式原理以及ref和reactive区别还有vue2/3生命周期的对比,第二天

    前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...

  6. 大白话Vue源码系列(02):编译器初探

    阅读目录 编译器代码藏在哪 Vue.prototype.$mount 构建 AST 的一般过程 Vue 构建的 AST 题接上文,上回书说到,Vue 的编译器模块相对独立且简单,那咱们就从这块入手,先 ...

  7. 【Vue2.x源码系列07】监听器watch原理

    上一章 Vue2计算属性原理,我们介绍了计算属性是如何实现的?计算属性缓存原理?以及洋葱模型是如何应用的? 本章目标 监听器是如何实现的? 监听器选项 - immediate.deep 内部实现 初始 ...

  8. java官网门户源码 SSM框架 自适应-响应式 freemarker 静态模版引擎

    来源:http://www.fhadmin.org/webnewsdetail3.html 前台:支持(5+1[时尚单页风格])六套模版,可以在后台切换 官网:www.fhadmin.org 系统介绍 ...

  9. Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)

    刚开始使用Mybaits的同学有没有这样的疑惑,为什么我们没有编写Mapper的实现类,却能调用Mapper的方法呢?本篇文章我带大家一起来解决这个疑问 上一篇文章我们获取到了DefaultSqlSe ...

  10. vue2响应式原理与vue3响应式原理对比

    VUE2.0 核心 对象:通过Object.defineProtytype()对对象的已有属性值的读取和修改进行劫持 数组:通过重写数组更新数组一系列更新元素的方法来实现元素的修改的劫持 Object ...

随机推荐

  1. CTF比赛中Web的php伪协议类型题小结

    php协议类型 file:// - 访问本地文件系统 http:// - 访问 HTTP(s) 网址 ftp:// - 访问 FTP(s) URLs php:// - 访问各个输入/输出流(I/O s ...

  2. 论文解读(Moka‑ADA)《Moka‑ADA: adversarial domain adaptation with model‑oriented knowledge adaptation for cross‑domain sentiment analysis》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:Moka‑ADA: adversarial domain adaptation  with model‑o ...

  3. Acwing 周赛88 题解

    比赛链接 ·A题 题目描述 给定一个整数\(x\),请你找到严格大于\(x\)且各位数字均不相同的最小整数\(y\). \(1000 \le x \le 9000\) 做法分析 发现数据范围很小,那么 ...

  4. windows和linux键值表

    windows系统下对应键值 {8,KEY_BACKSPACE}, {9,KEY_TAB}, {13,KEY_ENTER}, {16,KEY_LEFTSHIFT}, {17,KEY_LEFTCTRL} ...

  5. 15种实时uv实现方案系列(附源码)之一:Flink基于set实时uv统计

    UVStatMultiPlans(GitHub)项目持续收集各种高性能实时uv实现方案并对各种实现方案的优缺点进行对比分析! 需求描述 统计每分钟用户每个页面的uv访问量. Kafka数据格式 {&q ...

  6. 小知识:OCI实例的私钥文件权限

    在OCI上创建新的实例时,会提示你保存私钥用于连接,而且该界面不会再次显示,所以一定要保存好这个私钥. 实例创建完成后,当使用保存的私钥进行连接时,却发现由于私钥文件的权限问题无法连接,查看当前私钥文 ...

  7. 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(5) -- 树列表TreeView的使用

    在我们展示一些参考信息的时候,有所会用树形列表来展示结构信息,如对于有父子关系的多层级部门机构,以及一些常用如字典大类节点,也都可以利用树形列表的方式进行展示,本篇随笔介绍基于WPF的方式,使用Tre ...

  8. Vue 搭配 Spring MVC 创建一个 web 项目

    Vue 搭配 Spring MVC 创建一个 web 项目 想要写一个登录的web应用程序.页面使用Vue,后端使用Spring MVC,最终打成war包,放在tomcat下启动. 1.创建Sprin ...

  9. Abp vNext 模块加载机制

    文章目录 生命周期 PreConfigureServices 添加依赖注入或者其它配置之前 ConfigureServices 添加依赖注入或者其它配置 PostConfigureServices 添 ...

  10. DESTOON做中英双语言(多语言)切换版本具体详解

    第一次发原创好激动,该注意点什么? 在开发过程中用户有许多要求,比如这个多语言切换就是一个需求. 首先讲解一下DESTOON(DT)后台系统如何做这个中英.甚至多语言切换的这个功能. DT本身不自带多 ...