手摸手教你实现Vue3 Reactivity

目录
  • 前言
  • 开始
    • 小小思考
    • 代码实现
  • 模仿
    • 实现track
    • 实现trigger
    • 实现observe
    • 实现computed

前言

Vue3的响应式基于Proxy,对比Vue2中使用的Object.definedProperty的方式,使用Proxy在新增的对象以及数组的拦截上都有很好的支持。

Vue3的响应式是一个独立的系统,可以抽离出来使用,那他到底是如何实现的呢?

都知道有Getter和Setter,那Getter和Setter中分别都进行了哪些主要操作才能实现响应式呢?

哼哼,带着这些问题一起来看看吧,文章会一步一步实现一个完整的响应式系统(误)~。

开始

observer-util这个库使用了与Vue3同样的思路编写,Vue3中的实现更加复杂,从一个更加纯粹的库开始(我不会承认是因为Vue3中有些未看懂的,不会)。

根据官网的例子:

import { observable, observe } from '@nx-js/observer-util';

const counter = observable({ num: 0 });
const countLogger = observe(() => console.log(counter.num));

// this calls countLogger and logs 1
counter.num++;

这两个类似Vue3里的reactive和普通的响应式。

observable之后的对象被添加了代理,之后observe中添加的响应函数会在依赖的属性改变时调用一次。

小小思考

这里粗略思考是一个订阅发布的模型,被observable代理之后的对象建立一个发布者仓库,observe这时候会订阅counter.num,之后订阅的内容改变时便会一一回调。
伪代码:

// 添加监听
xxx.addEventListener('counter.num', () => console.log(counter.num))
// 改变内容
counter.num++
// 发送通知
xxx.emit('counter.num', counter.num)

而响应式的核心也就是这个,添加监听与发送通知会经由observable和observe自动完成。

代码实现

经由上面的思考,在Getter里我们需要将observe传过来的回调添加到订阅仓库中。
具体的实现中observable会为这个观察的对象添加一个handler,在Getter的handler中有一个

registerRunningReactionForOperation({ target, key, receiver, type: 'get' })
const connectionStore = new WeakMap()
// reactions can call each other and form a call stack
const reactionStack = []

// register the currently running reaction to be queued again on obj.key mutations
export function registerRunningReactionForOperation (operation) {
  // get the current reaction from the top of the stack
  const runningReaction = reactionStack[reactionStack.length - 1]
  if (runningReaction) {
    debugOperation(runningReaction, operation)
    registerReactionForOperation(runningReaction, operation)
  }
}

这个函数会获取出一个reaction(也就是observe传过来的回调),并且通过registerReactionForOperation保存。

export function registerReactionForOperation (reaction, { target, key, type }) {
  if (type === 'iterate') {
    key = ITERATION_KEY
  }

  const reactionsForObj = connectionStore.get(target)
  let reactionsForKey = reactionsForObj.get(key)
  if (!reactionsForKey) {
    reactionsForKey = new Set()
    reactionsForObj.set(key, reactionsForKey)
  }
  // save the fact that the key is used by the reaction during its current run
  if (!reactionsForKey.has(reaction)) {
    reactionsForKey.add(reaction)
    reaction.cleaners.push(reactionsForKey)
  }
}

这里生成了一个Set,根据key,也就是实际业务中get时候的key,将这个reaction添加进Set中,整个的结构是这样的:

connectionStore<weakMap>: {
    // target eg: {num: 1}
    target: <Map>{
        num: (reaction1, reaction2...)
    }
}

注意这里的reaction,const runningReaction = reactionStack[reactionStack.length - 1] 通过全局变量reactionStack获取到的。

export function observe (fn, options = {}) {
  // wrap the passed function in a reaction, if it is not already one
  const reaction = fn[IS_REACTION]
    ? fn
    : function reaction () {
      return runAsReaction(reaction, fn, this, arguments)
    }
  // save the scheduler and debugger on the reaction
  reaction.scheduler = options.scheduler
  reaction.debugger = options.debugger
  // save the fact that this is a reaction
  reaction[IS_REACTION] = true
  // run the reaction once if it is not a lazy one
  if (!options.lazy) {
    reaction()
  }
  return reaction
}

export function runAsReaction (reaction, fn, context, args) {
  // do not build reactive relations, if the reaction is unobserved
  if (reaction.unobserved) {
    return Reflect.apply(fn, context, args)
  }

  // only run the reaction if it is not already in the reaction stack
  // TODO: improve this to allow explicitly recursive reactions
  if (reactionStack.indexOf(reaction) === -1) {
    // release the (obj -> key -> reactions) connections
    // and reset the cleaner connections
    releaseReaction(reaction)

    try {
      // set the reaction as the currently running one
      // this is required so that we can create (observable.prop -> reaction) pairs in the get trap
      reactionStack.push(reaction)
      return Reflect.apply(fn, context, args)
    } finally {
      // always remove the currently running flag from the reaction when it stops execution
      reactionStack.pop()
    }
  }
}

在runAsReaction中,会将传入的reaction(也就是上面的const reaction = function() { runAsReaction(reaction) })执行自己的包裹函数压入栈中,并且执行fn,这里的fn即我们想自动响应的函数,执行这个函数自然会触发get,此时的reactionStack中则会存在这个reaction。这里注意fn如果里面有异步代码的情况,try finally的执行顺序是这样的:

// 执行try的内容,
// 如果有return执行return内容,但不会返回,执行finally后返回,这里面不会阻塞。

function test() {
    try {
        console.log(1);
        const s = () => { console.log(2); return 4; };
        return s();
    } finally {
        console.log(3)
    }
}

// 1 2 3 4
console.log(test())

所以如果异步代码阻塞并且先于Getter执行,那么就不会收集到这个依赖。

模仿

目标实现observable和observe以及衍生出来的Vue中的computed。
借用Vue3的思路,get时的操作称为track,set时的操作称为trigger,回调称为effect。

先来个导图:

function createObserve(obj)  {

    let handler = {
        get: function (target, key, receiver) {
            let result = Reflect.get(target, key, receiver)
            track(target, key, receiver)
            return result
        },
        set: function (target, key, value, receiver) {
            let result = Reflect.set(target, key, value, receiver)
            trigger(target, key, value, receiver)
            return result
        }
    }

    let proxyObj = new Proxy(obj, handler)

    return proxyObj
}

function observable(obj) {
    return createObserve(obj)
}

这里我们只作了一层Proxy封装,像Vue中应该会做一个递归的封装。

区别是只做一层封装的话只能检测到外层的=操作,内层的如Array.push,或者嵌套的替换等都是无法经过set和get的。

实现track

在track中我们会将当前触发的effect也就是observe的内容或者其他内容压入关系链中,以便trigger时可以调用到这个effect。

const targetMap = new WeakMap()
let activeEffectStack = []
let activeEffect

function track(target, key, receiver?) {
    let depMap = targetMap.get(target)

    if (!depMap) {
        targetMap.set(target, (depMap = new Map()))
    }

    let dep = depMap.get(key)

    if (!dep) {
        depMap.set(key, ( dep = new Set() ))
    }

    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
    }
}

targetMap是一个weakMap,使用weakMap的好处是当我们observable的对象不存在其他引用的时候会正确的被垃圾回收掉,这一条链是我们额外建立的内容,原对象不存在的情况下不应该在继续存在。

这里面最终会形成一个:

targetMap = {
    <Proxy 或者 Object>observeable: <Map>{
        <observeable中的某一个key>key: ( observe, observe, observe... )
    }
}

activeEffectStack和activeEffect是两个用于数据交换的全局变量,我们在get中会把当前的activeEffect添加到get的key的生成的Set中保存起来,让set操作可以拿到这个activeEffect然后再次调用,实现响应式。

实现trigger

function trigger(target, key, value, receiver?) {
    let depMap = targetMap.get(target)

    if (!depMap) {
        return
    }

    let dep = depMap.get(key)

    if (!dep) {
        return
    }

    dep.forEach((item) => item && item())
}

trigger这里按照思路实现一个最小的内容,只是将get中添加的effect逐个调用。

实现observe

根据导图,在observe中我们需要将传入的function压入activeEffectStack并调用一次function触发get。

function observe(fn:Function) {
    const wrapFn = () => {

        const reaction = () => {
            try {
                activeEffect = fn
                activeEffectStack.push(fn)
                return fn()
            } finally {
                activeEffectStack.pop()
                activeEffect = activeEffectStack[activeEffectStack.length-1]
            }
        }

        return reaction()
    }

    wrapFn()

    return wrapFn
}

function有可能出错,finally中的代码保证activeEffectStack中对应的那个会被正确删除。

测试

let p = observable({num: 0})
let j = observe(() => {console.log("i am observe:", p.num);)
let e = observe(() => {console.log("i am observe2:", p.num)})

// i am observe: 1
// i am observe2: 1
p.num++

实现computed

在Vue中一个很有用的东西是计算属性(computed),它是依赖于其他属性而生成的新值,会在它依赖的其他值更改时自动更改。
我们在实现了ovserve之后computed就实现了一大半。

class computedImpl {
    private _value
    private _setter
    private effect

    constructor(options) {
        this._value = undefined
        this._setter = undefined
        const { get, set } = options
        this._setter = set

        this.effect = observe(() => {
            this._value = get()
        })
    }

    get value() {
        return this._value
    }

    set value (val) {
        this._setter && this._setter(val)
    }
}

function computed(fnOrOptions) {

    let options = {
        get: null,
        set: null
    }

    if (fnOrOptions instanceof Function) {
        options.get = fnOrOptions
    } else {
        const { get, set } = fnOrOptions
        options.get= get
        options.set = set
    }

    return new computedImpl(options)
}

computed有两种方式,一种是computed(function)这样会当做get,另外还可以设置setter,setter更多的像是一个回调可以和依赖的其他属性完全没有关系。

let p = observable({num: 0})
let j = observe(() => {console.log("i am observe:", p.num); return `i am observe: ${p.num}`})
let e = observe(() => {console.log("i am observe2:", p.num)})
let w = computed(() => { return '我是computed 1:' + p.num })
let v = computed({
    get: () => {
        return 'test computed getter' + p.num
    },

    set: (val) => {
        p.num = `test computed setter${val}`
    }
})

p.num++
// i am observe: 0
// i am observe2: 0
// i am observe: 1
// i am observe2: 1
// 我是computed 1:1
console.log(w.value)
v.value = 3000
console.log(w.value)
// i am observe: test computed setter3000
// i am observe2: test computed setter3000
// 我是computed 1:test computed setter3000
w.value = 1000
// 并没有为w设置setter所以并没有生效
// 我是computed 1:test computed setter3000
console.log(w.value)

到此这篇关于手摸手教你实现Vue3 Reactivity的文章就介绍到这了,更多相关Vue3 Reactivity内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue3.0中setup使用(两种用法)

    一.setup函数的特性以及作用 可以确定的是 Vue3.0 是兼容 Vue2.x 版本的 也就是说我们再日常工作中 可以在 Vue3 中使用 Vue2.x 的相关语法 但是当你真正开始使用 Vue3 写项目时 你会发现他比 Vue2.x 方便的多 Vue3 的一大特性函数 ---- setup 1.setup函数是处于 生命周期函数 beforeCreate 和 Created 两个钩子函数之间的函数 也就说在 setup函数中是无法 使用 data 和 methods 中的数据和方法的 2.

  • 配置一个vue3.0项目的完整步骤

    说起来有点丢人,我已经使用vue好久了,但是怎么从0开始配置一个vue项目,每次还是要百度.这次决定写个博客,加强下记忆,如果再记不住就直播自己的女朋友洗澡. 以下以新建一个图书管理项目为例.我使用vue3新建项目,对于创建一个项目来说,vue3真的比vue2简单很多. 1.初始化项目 1.1全局安装vue-cli 创建vue项目,首先要确保全局安装了vue命令行工具. 我这边使用yarn而不用npm,因为yarn要比npm好用的多,强烈推荐使用.如果大家对yarn不熟悉,我这边免费赠送我的ya

  • 详解Vue3.0 前的 TypeScript 最佳入门实践

    前言 我个人对更严格类型限制没有积极的看法,毕竟各类转类型的骚写法写习惯了. 然鹅最近的一个项目中,是 TypeScript + Vue ,毛计喇,学之...-真香! 注意此篇标题的"前",本文旨在讲Ts混入框架的使用,不讲Class API 1. 使用官方脚手架构建 npm install -g @vue/cli # OR yarn global add @vue/cli 新的 Vue CLI 工具允许开发者 使用 TypeScript 集成环境 创建新项目. 只需运行 vue cr

  • 关于vue3.0中的this.$router.replace({ path: '/'})刷新无效果问题

    首先在store中定义所需要的变量可以进行初始化,再定义一个方法,登录成功后A页面,跳转到B页面之前,需要直接调用store中存储数据的方法,全局可以使用 诸如以上所示,该问题,百度了好久,多亏群里大佬. vue使用less报错的解决方法 安装less less-loader cnpm install less less-loader --save-dev app.vue是所有XXX.vue文件的根文件 所以webapp,的底部通常是在这里配置 h5的新增 <header>标题</hea

  • 如何搭建一个完整的Vue3.0+ts的项目步骤

    相信9月18日尤大大的关于Vue3.0的发表演讲大家一定有所关注,现在Vue3.0 也已经进入RC阶段(最终产品的候选版本,如果没有问题则可发布成为正式版本).所以Vue3.0的学习是我们必然的趋势,今天,主要分享一下Vue3.0的详细搭建过程,希望可以为初入Vue3的小伙伴有所帮助. 我们现在开始进入今天的主题啦~~ 一.安装 1. 安装nodejs 此处提供nodejs下载地址: https://nodejs.org/zh-cn/download/ 大家根据自己电脑的配置选择适配的 LTS(

  • 手摸手教你实现Vue3 Reactivity

    目录 前言 开始 小小思考 代码实现 模仿 实现track 实现trigger 实现observe 实现computed 前言 Vue3的响应式基于Proxy,对比Vue2中使用的Object.definedProperty的方式,使用Proxy在新增的对象以及数组的拦截上都有很好的支持. Vue3的响应式是一个独立的系统,可以抽离出来使用,那他到底是如何实现的呢? 都知道有Getter和Setter,那Getter和Setter中分别都进行了哪些主要操作才能实现响应式呢? 哼哼,带着这些问题一

  • Android开发事件处理的代码如何写手摸手教程

    目录 正文 剖析事件分发的过程 ACTION_DOWN ACTION_MOVE ACTION_UP ACTION_CANCEL 完成案例代码 ACTION_DOWN ACTION_MOVE ACTION_UP ACTION_CANCEL 截断ACTION_DOWN 结束 正文 经过事件分发之View事件处理和ViewGroup事件分发和处理源码分析这两篇的的理论知识分析,我们已经大致的了解了事件的分发处理机制,但是这并不代表你就一定能写好事件处理的代码. 既然我们有了基本功,那么本文就通过一个案

  • 手把手教你搭建vue3.0项目架构

    前言: GitHub上我开源了vue-cli.vue-cli3两个库,文章末尾会附上GitHub仓库地址.这次把2.0的重新写了一遍,优化了一下.然后按照2.0的功能和代码,按照vue3.0的语法,完全重写了一遍.虽然名字叫cli,其实两个库都是基于vue-cli创建的.做这个的目的是为了工作中快速启动项目,毕竟切片打包.less.axios.vuex.router.UI框架.基础文件目录.权限,这些都是基操,当然项目不同,还是要做些调整的.这两个项目的master分支都是最基础的东西,里面还包

  • 教你利用Vue3模仿Windows窗口

    目录 一.前言 二.功能分析 三.指令封装 四.通用组件封装 五.总结及其源代码参考 六.博文参考 一.前言 Vue3终于在2022年2月7日正式发布了,之前用vite+vue3搭了一个小demo,资料太少而我太菜了,所以一直不敢用Vue3搭新项目,现在随着Vue3正式版本的发布,而且相关配合的子项目库也已经完善,大量的翻译资料和文献都已经可以百度到了,再加上领导支持用Vue3新框架,所以我在新项目上着手用vue-cli(@vue/cli 4.5.9)脚手架搭建Vue3项目. 图1 拖拽窗体效果

  • 手把手教你用vue3开发一个打砖块小游戏

    前言 用vue3写了几个实例,感觉Vue3的composition Api设计得还是很不错,改变了一下习惯,但写多两个就好了. 这次写一个也是儿时很觉得很好玩的游戏-打砖块, 无聊的时候玩一下也觉得挺好玩,游戏性也挺高.这次我直接用vite+vue3打包尝试一下,vite也是开箱即用,特点是也是可以清除死代码,按需打包,所以打包速度也是非常快的.没用过的同学可以尝试用用. 游戏效果 游戏需求 创建一个场景 创建一个球,创建一堆被打击方块 创建一个可以移动方块并可控制左右移动 当球碰撞左右上边界及

  • 教你使用vue3写Json-Preview的示例代码

    引入后直接<json-preview v-model="jsonData"></json-preview>就可以使用了.近期比较忙,代码就不做调整了. 示例效果 index.vue 文件 <template> <div v-if="isObject" class="json-preview"> <span v-if="!!parentKey"><span cla

  • iOS动画教你编写Slack的Loading动画进阶篇

    前几天看了一篇关于动画的博客叫手摸手教你写 Slack 的 Loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇iOS版的,下面是我写这个动画的分解~ 老规矩先上图和demo地址: 刚看到这个动画的时候,脑海里出现了两个方案,一种是通过drawRect画出来,然后配合CADisplayLink不停的绘制线的样式:第二种是通过CAShapeLayer配合CAAnimation来实现动画效果.再三考虑觉得使用后者,因为前者需要计算很多,比较复杂,而且经过测试前者相比于后者消耗更多的C

  • Android自定义View实现支付宝支付成功-极速get花式Path炫酷动画

    本文手把手教你图片->SVG->Path的姿势.. 从此酷炫Path动画,如此简单. 效果先随便上几个图,以后你找到的图有多精彩,gif就有多精彩: 随便搜了一个铅笔画的图,丢进去 随手复制的二维码icon 来自大佬wing的铁塔 前文回顾 这里简单回顾一下前文,GIF如下图: PathAnimView接受的唯一数据源是Path(给我一个Path,还你一个动画View) 所以内置了几种将别的资源->Path的方法: 直接传string.(A-Z,0-9 "." &qu

  • 手把手带你封装一个vue component第三方库

    为什么选择自己封装第三方库 最近几个月我司把之前两三年的所有业务都用了 vue 重构了一遍,前台使用 vue+ssr,后台使用了 vue+element,在此过程中封装和自己写了很多 vue component.其实vue 写 component 相当简单和方便,github上有很多的 vue component 都只是简单的包装了一些 jquery 或者原生 js 的插件,但我个人是不太喜欢使用这些第三方封装的.理由如下: 很多第三方封装的组件参数配置项其实是有缺损的.如一些富文本或者图表组件

  • 使用位运算、值交换等方式反转java字符串的多种方法(四种方法)

    在本文中,我们将向您展示几种在Java中将String类型的字符串字母倒序的几种方法. StringBuilder(str).reverse() char[]循环与值交换 byte循环与值交换apache-commons-lang3 如果是为了进行开发,请选择StringBuilder(str).reverse()API.出于学习的目的,我们可以研究char[]和byte方法,其中涉及到值互换和移位运算技术,这些技术对于了解StringBuilder(str).reverse()API黑匣子背后

随机推荐