高级前端需要会的手写响应式原理(reactive, effect,依赖收集,依赖触发)

在这个大前端时代,要想从中级突破到高级,掌握框架底层原理是必须要经历的过程。本篇将以手写Vue3中的响应式原理来掌握Vue3 响应式的底层实现,突破技术瓶颈。

项目初始化和环境搭建

  1. 新建项目 vue-mini
  2. 初始化package.json, 执行 npm init -y
  3. 安装rollup, 执行 npm i rollup -D
  4. 创建项目目录(参考Vue3源码)

本篇的代码主要编写在packages/reactivity 目录下面 5. 安装ts,配置ts

js 复制代码
npm i typescript tslib -D
js 复制代码
{
  "compilerOptions": {
    "rootDir": ".",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "target": "es2021",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "resolveJsonModule": true,
    "downlevelIteration": true,
    "noImplicitAny": false,
    "module": "esnext",
    "removeComments": false,
    "sourceMap": true,
    "lib": ["esnext", "dom"],
    "baseUrl": ".",
    "paths": {
      "@vue/*": ["packages/*/src"]
    }
  },
  "include": ["packages/*/src"]
}
  1. 安装rollup相关插件
js 复制代码
npm i @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript -D
  1. rollup打包配置 rollup.config.js
js 复制代码
const typescript = require('@rollup/plugin-typescript')
const nodeResolve = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
module.exports = [
  {
    input: 'packages/vue/src/index.ts',
    output: {
      sourceMap: true,
      file: 'packages/vue/dist/vue.js',
      format: 'iife',
      name: 'Vue'
    },
    plugins: [
      typescript({
        sourceMap: true
      }),
      nodeResolve(),
      commonjs()
    ]
  }
]
  1. packages.json 里面配置打包命令
js 复制代码
"scripts": {
    "dev": "rollup -c -w",
    "build": "rollup -c"
  },
  1. packages/vue下面新建测试代码看是否能正常打包

packages/vue/src/index.ts

js 复制代码
export function add(a, b) {
  console.log(a, b);
  return a + b;
}

packages/vue/examples/test/test.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>test</title>
</head>
<body>
  <button id="btn">add</button>
  <p>
    <span>3</span>
    <span>+</span>
    <span>5</span>
    <span>=</span>
    <span id="result">?</span>
  </p>
  <script src="../../dist/vue.js"></script>
  <script>
    const { add } = Vue
    document.getElementById('btn').onclick = function () {
      document.getElementById('result').innerHTML = add(3, 5)
    }

  </script>
</body>
</html>

执行打包命令 npm run build, 这时候就能看到vue 目录下多了个dist目录,和dist目录下的vue.js

packages/vue/dist/vue.js

js 复制代码
var Vue = (function (exports) {
    'use strict';

    function add(a, b) {
        console.log(a, b);
        return a + b;
    }

    exports.add = add;

    return exports;

})({});

这就是我们打包的产物。现在来运行下packages/vue/examples/test/test.html, 推荐大家使用vscode 中的live server 插件进行运行。

点击add

可以看到结果符合预期,说明我们的打包配置没问题。到这里项目的初始化相关配置就完成了

reactive的初步实现

看过Vue3 源码的同学,相信大概知道reactive 的过程实际调用了很多函数,大概流程如下:

packages/reactivity 下新建src, 并新建三个文件

index.ts

ts 复制代码
import { reactive } from './reactive'

export { reactive }

响应式的出口文件,导出reactive方法,给用户使用

reactive.ts

ts 复制代码
import { mutableHandlers } from './baseHandlers'

const reactiveMap = new WeakMap<object, any>() // 用来缓存的

export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, reactiveMap)
}

function createReactiveObject(
  target: object,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
  const exitingProxy = proxyMap.get(target) // 通过传入的对象判断缓存里面有没有

  if (exitingProxy) {
    // 如果缓存里面已经有了传入对象所对应的代理对象,则直接返回代理过的对象
    return exitingProxy
  }
  const proxy = new Proxy(target, baseHandlers) // 缓存中不存在时用传入的对象创建一个Proxy代理对象
  proxyMap.set(target, proxy) // 存入缓存中
  return proxy // 返回代理对象给用户
}

在这个文件中:

  • 创建了reactive 方法, 并返回createReactiveObject 的执行
  • 创建了 createReactiveObject 函数,createReactiveObject 主要判断缓存中存不存在传入对象的代理,存在则直接返回,不能存则创建。
  • 创建Proxyhandler是从baseHandlers 中导入的mutableHandlers

baseHandlers.ts

js 复制代码
class BaseReactiveHandler {
  get(target: object, key: string) {
    console.log('getter')
    return Reflect.get(target, key)
  }
}
class MutableReactiveHandler extends BaseReactiveHandler {
  set(target: object, key: string, newVal: any) {
    console.log('setter')
    const result = Reflect.set(target, key, newVal)
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> =
  new MutableReactiveHandler()

在这个文件中:

  • 看到mutableHandlers 实际上是MutableReactiveHandler的一个实例
  • MutableReactiveHandler 中处理有个set方法,在set中后面写完effect之后,会在这里会执行依赖的回调函数
  • MutableReactiveHandler 还继承了BaseReactiveHandler
  • BaseReactiveHandler 中有个get 方法,get 方法会在effect 写完之后,在这里调用依赖收集的方法

在packages/vue/ 新建reactive的测试用例

index.ts

ts 复制代码
export { reactive } from '@vue/reactivity'

reactive.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>reactive</title>
</head>
<body>
  <script src="../../dist/vue.js"></script>
  <script>
    const {reactive} = Vue
    const obj = {
      a: 1
    }
    const proxy = reactive(obj)
    console.log(proxy)
    console.log(proxy.a)
    proxy.a = 5
  </script>
</body>
</html>

执行打包命令:npm run build 可以看到dist/vue.js 内容更新了

js 复制代码
var Vue = (function (exports) {
    'use strict';

    class BaseReactiveHandler {
        get(target, key) {
            console.log('getter');
            return Reflect.get(target, key);
        }
    }
    class MutableReactiveHandler extends BaseReactiveHandler {
        set(target, key, newVal) {
            console.log('setter');
            const result = Reflect.set(target, key, newVal);
            return result;
        }
    }
    const mutableHandlers = new MutableReactiveHandler();

    const reactiveMap = new WeakMap(); // 用来缓存的
    function reactive(target) {
        return createReactiveObject(target, mutableHandlers, reactiveMap);
    }
    function createReactiveObject(target, baseHandlers, proxyMap) {
        const exitingProxy = proxyMap.get(target); // 通过传入的对象判断缓存里面有没有
        if (exitingProxy) {
            // 如果缓存里面已经有了传入对象所对应的代理对象,则直接返回代理过的对象
            return exitingProxy;
        }
        const proxy = new Proxy(target, baseHandlers); // 缓存中不存在时用传入的对象创建一个Proxy代理对象
        proxyMap.set(target, proxy); // 存入缓存中
        return proxy; // 返回代理对象给用户
    }

    exports.reactive = reactive;

    return exports;

})({});

来运行下packages/vue/examples/reactivity/reactive.html

可以看到现在已经能成功的得到一个Proxy 代理对象,并且触发getter和setter

effect 实现

大概流程如下

packages/reactivity/src 新增effect.ts,代码编写如下

ts 复制代码
export let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
  constructor(public fn) {}

  run() {
    activeEffect = this
    return this.fn()
  }
}

function effect<T = any>(fn: () => T) {
  const e = new ReactiveEffect(fn)
  e.run()
}

effect执行会得到一个ReactiveEffect的实例,并且赋值给activeEffect 作为当前激活变量存储

packages/reactivity/src/index.ts 出口文件新增effect 的导入导出

packages/vue/src/index.ts 出口文件新增effect 的导入导出

依赖收集实现

依赖收集主要是在getter 时,将副作用函数收集起来,方便在修改数据的时候重新执行。依赖收集之后会得到如下的数据结构:

packages/reactivity/src 下面新建dep.ts, 代码实现如下:

js 复制代码
import { activeEffect } from './effect'
let targetMap = new WeakMap<object, any>()
export function track(target: object, key: unknown) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
  console.log(targetMap, '依赖收集的结构')
}

修改baseHandlers.ts中的BaseReactiveHandler 的get方法,添加track 的执行

修改测试的reactive.html代码

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>reactive</title>
</head>
<body>
  <p id="text"></p>
  <script src="../../dist/vue.js"></script>
  <script>
    const {reactive, effect } = Vue
    const obj = {
      a: 1
    }
    const proxy = reactive(obj)
    effect(() => {
      document.getElementById('text').innerHTML = proxy.a
    })
    console.log(proxy)
    console.log(proxy.a)
    proxy.a = 5
  </script>
</body>
</html>

运行reactive.html页面,就会看到控制台打印,前面途中我们看到的数据结构

依赖触发

dep.ts 新增trigger 方法

js 复制代码
export function trigger(target: object, key: string, newVal: any) {
  const desMap = targetMap.get(target)
  if (!desMap) {
    return
  }
  const dep = desMap.get(key)

  for (let effect of dep) {
    effect.fn()
  }
}

修改 baseHandler.ts 中的MutableReactiveHandlerset方法, 新增trigger方法的调用:

修改测试的reactive.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>reactive</title>
</head>
<body>
  <p id="text"></p>
  <script src="../../dist/vue.js"></script>
  <script>
    const {reactive, effect } = Vue
    const obj = {
      a: 1
    }
    const proxy = reactive(obj)
    effect(() => {
      document.getElementById('text').innerHTML = proxy.a
    })
    console.log(proxy)
    console.log(proxy.a)
    proxy.a = 5
  </script>
</body>
</html>

可以看到数据更新后,视图也跟着更新了。

到这里我们的响应式功能就开发完成了。来总结下吧。

总结

  • reactive 本身主要返回一个Proxy,并且会缓存,每次调用reactive方法时优先取缓存里面的
  • effect 主要是创建一个ReactiveEffect 实例
  • 依赖收集在get 的时候,调用track 方法将把副作用函数收集起来
  • 依赖触发在set的时候触发将键对应的所有副作用函数全部执行一遍
相关推荐
用户87612829073744 分钟前
前端ai对话框架semi-design-vue
前端·人工智能
干就完了17 分钟前
项目中遇到浏览器跨域前端和后端解决方案以及大概过程
前端
我是福福大王9 分钟前
前后端SM2加密交互问题解析与解决方案
前端·后端
实习生小黄12 分钟前
echarts 实现环形渐变
前端·echarts
_未知_开摆20 分钟前
uniapp APP端在线升级(简版)
开发语言·前端·javascript·vue.js·uni-app
喝拿铁写前端29 分钟前
不同命名风格在 Vue 中后台项目中的使用分析
javascript·vue.js
sen_shan32 分钟前
Vue3+Vite+TypeScript+Element Plus开发-02.Element Plus安装与配置
前端·javascript·typescript·vue3·element·element plus
疾风铸境44 分钟前
Qt5.14.2+mingw64编译OpenCV3.4.14一次成功记录
前端·webpack·node.js
晓风伴月1 小时前
Css:overflow: hidden截断条件‌及如何避免截断
前端·css·overflow截断条件
最新资讯动态1 小时前
使用“一次开发,多端部署”,实现Pura X阔折叠的全新设计
前端