• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

编写vue3响应式原理(万字)

武飞扬头像
溜达溜达哈哈哈
帮助1

区别介绍

  • 源码采用monorepo 方式进行管理,将模块拆分到package目录中

  • Vue3采用 ts 开发增强类型检测。Vue2 则采用 flow

  • Vue3的性能优化,支持tree-shaking,不使用就不会被打包

  • Vue2 后期引入RFC,使每个版本改动可控 rfcs

  • Vue3 劫持数据采用proxy Vue2 劫持数据采用 defineProperty 。defineProperty 有性能问题和缺陷

  • Vue3 中对模板编译进行了优化,编译时 生成了Block tree,可以对子节点的动态节点进行收集,可以减少比较,并且采用了 patchFlag 标记动态节点

  • Vue3 采用 compositionApi 进行组织功能,解决反复模跳,优化复用逻辑 (mixin带来的数据来源不清晰、命名冲突等),相比 optionsApi 类型推断更加方便

  • 增加了 Fragment ,TeleportSuspense 组件

一、 Vue3 架构分析

1.Monorepo 介绍

Monorepo 是管理项目代码的一个方式,指在一个项目仓库( repo )中管理多个模块/包(package)

  • 一个仓库可维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用,调用都非常方便

缺点:仓库体积会变大。

2.Vue3 项目结构

  • reactivity :响应式系统
  • runtime-core :与平台无关的运行时核心(可以创建针对特定台的运行时-自定义染器)
  • runtime-dom :针对浏览器的运行时。包括 DOM API,属性,事件处理等
  • runtime-test :用于测试
  • server-renderer ;用于服务器端渲染
  • compiler-core :与平台无关的编译器核心
  • compiler-dom :针对浏览器的编译模块
  • compiler-ssr :针对服务端渲染的编译模块
  • compiler-sfe :针对单文件解析
  • size-check :用来测试代码体积
  • template-explorer : 用于试编译器输出的开发工具
  • shared :多个包之间共享的内容
  • vue :完整版本包括运行时和编译器

学新通

3.安装依赖

yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa --save-dev

  • typescript --- 支持typescript
  • rollup --- 打包工具
  • rollup-plugin-typescript2 --- rollup 和 ts 的桥梁
  • @rollup/plugin-node-resolve --- 解析node第三方模块
  • @rollup/plugin-json --- 支持引入jison
  • execa --- 开启子进程方便执行命令

4.workspace配置

初始化项目package.json

yarn init -y

注意,需要使用yarn,npm不支持

{
  "private": true, // 私有的,npm不会发布它
  "workspaces": [
    "packages/*" // 需要执行workspace的入口文件地址
  ],
  "scripts": {
    "dev": "node scripts/dev.js", // 使用node运行script目录下的dev.js文件
    "build": "node scripts/build.js" // 使用node运行script目录下的build.js文件
  },
  "name": "my-vue3", // 包名
  "version": "1.0.0", // 版本号
  "main": "index.js", // 入口文件
  // ……
}
初始化功能模块package.json

根目录中创建文件夹 packages --- 用于管理所有功能模块

packages中创建文件夹 reactivity --- reactivity相关的功能 ,每个功能模块就是一个文件夹,后面会创建多个

reactivity中创建文件夹 src --- 用于存放功能代码

src中创建index.js --- 功能代码的统一出口,只负责导出

reactivity文件夹中 执行 yarn init -y,生成package.json并完善配置
{
  "name": "@vue/reactivity", // 单个功能模块名
  "version": "1.0.0", // 版本号
  "main": "index.js", // node会去找这个
  "license": "MIT",
  "module": "dist/reactivity.esm-bundler.js", // 打包输入目录
  "buildOptions":{ // 打包配置信息
    "name":"VueReactivity", // 全局变量名
    "formats":[ // 打包哪些格式
      "cjs", // commonjs 只能在node运行
      "esm-bundler", // es6 import……
      "global" // iife 立即执行 暴露变量
    ]
  }
}

二、环境搭建

1. script配置

创建script文件夹,分别编写dev.js和build.js,对应前面package.json中的脚本命令

build.js
const fs = require('fs'); // node文件模块
const execa = require('execa'); // 开启子进程 进行打包, 最终还是使用rollup来进行打包

// 同步读取packages下的所有文件夹
const targets = fs.readdirSync('packages').filter(f =>{
    return fs.statSync(`packages/${f}`).isDirectory() // 只找文件夹
})

// async 返回的是一个promise
async function build(target){ // rollup  -c --environment TARGET:shated
    // exece可以同时执行多个node命令,子进程打包的信息共享给父进程
    // -c 执行rollup environment环境信息 TARGET打包的文件对象……
    await execa('rollup',['-c','--environment',`TARGET:${target}`],{stdio:'inherit'}); 
}

// 执行build 打包功能模块
function runParallel(targets,iteratorFn){
    const res = [] // promise集合
    for(const item of targets){
        const p = iteratorFn(item) // iteratorFn就是build函数,build函数返回的是promise
        res.push(p); // 丢到promise集合里
    }
    return Promise.all(res) // 执行全部
}
runParallel(targets,build) // 根据target的配置执行不同的打包策略

dev.js --- 这个就比较简单,没有那么多策略 直接打包就好

const fs = require('fs');
const execa = require('execa'); // 开启子进程 进行打包, 最终还是使用rollup来进行打包
const target = 'reactivity' // 这里假设只打包reactivity功能模块
async function build(target){ // rollup  -c --environment TARGET:shated
    await execa('rollup',['-cw','--environment',`TARGET:${target}`],{stdio:'inherit'}); // 当子进程打包的信息共享给父进程
}
build(target)

2. rollup配置

import path from 'path';
import json from '@rollup/plugin-json';
import resolvePlugin from '@rollup/plugin-node-resolve'
import ts from 'rollup-plugin-typescript2'

const packagesDir = path.resolve(__dirname,'packages'); // 找到packages目录
// 前面的build.js中使用exece运行了node命令,取出环境遍历中的打包文件对象名
const packageDir = path.resolve(packagesDir,process.env.TARGET)  // 找到要打包的某个包

const resolve = (p)=>path.resolve(packageDir,p) // 从packageDir中取出某个文件
const pkg = require(resolve('package.json')); // 取出package.json文件
const name = path.basename(packageDir); // 取出文件名

// 对打包类型 先做一个映射表,根据你提供的formats 来格式化需要打包的内容
const outputConfig = { // 自定义的
    'esm-bundler':{
        file: resolve(`dist/${name}.esm-bundler.js`), // 打包后输出目录
        format:'es' // 采用es模式  es6  import ……
    },
    'cjs':{
        file:resolve(`dist/${name}.cjs.js`),
        format:'cjs' // commonjs  运行在node
    },
    'global':{
        file:resolve(`dist/${name}.global.js`),
        format:'iife' // 立即执行函数 全局变量
    }
}
const options = pkg.buildOptions; // 拿到自己在每个功能包package.json中定义的选项
function createConfig(format,output) {
    output.name = options.name; // 打包名就是配置的名称
    output.sourcemap = true; // 生成sourcemap
    return { // 生成rollup配置
        input: resolve(`src/index.ts`), // 入口文件
        output, // 出口配置
        plugins:[
            json(), // 需要使用json插件
            ts({ // ts 插件 
                tsconfig:path.resolve(__dirname,'tsconfig.json') // 读取你的ts配置文件
            }),
            resolvePlugin() // 解析第三方模块插件
        ]
    }

}
// rollup 最终需要到出配置
// 遍历功能包中的package.json配置的打包格式,对不同的格式采取不同的策略
export default options.formats.map(format => createConfig(format, outputConfig[format]))

3. tsconfig.json

{
  "compilerOptions": {
    "target": "ESNEXT", /* 指定es版本 */
    "module": "ESNEXT", /* 指定代码生成类型 */
    "sourceMap": true, /* 产生map文件 用于源码调试 */
    "strict": false, /* 严格模式 关闭 可写any */
    "moduleResolution": "node", // 模块解析策略 采用node
    "baseUrl": ".", // 基础路径
    "paths": {
      "@vue/*": [
        "packages/*/src" // @Vue的 功能包的 类型检测规则映射
      ]
    }
  }
}

4. 执行命令

做完这些后,执行yarn install,会自动把自己的功能模块打包,并放入node_modules中,包名就是自己在功能模块package.js中配置的

学新通

三、shared

公共的工具函数集合,源码做了整合,这里只用一些关键功能,就都放到index里面了

index.js

// 检测对象
export const isObject = (value) => typeof value == "object" && value !== null;
// 合并对象
export const extend = Object.assign;
// 检测数组
export const isArray = Array.isArray;
// 检测函数
export const isFunction = (value) => typeof value == "function";
// 检测number
export const isNumber = (value) => typeof value == "number";
// 检测字符串
export const isString = (value) => typeof value === "string";
// 检测整数
export const isIntegerKey = (key) => parseInt(key)   "" === key;
// 检测是属性是否属于目标对象
let hasOwnpRroperty = Object.prototype.hasOwnProperty;
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key);
// 浅比较是否相等
export const hasChanged = (oldValue, value) => oldValue !== value;
// 源码中还有很多

四、reactivity

响应式系统,主要功能处理数据,收集依赖

reactive:reactive,readonly,isReactive,isReadonly,isShallow,isProxy,shallowReactive,shallowReadonly,markRaw,toRaw

effect:effect,stop,trigger,track,enableTracking,pauseTracking,resetTracking

ref:ref, shallowRef, isRef, toRef,toValue,toRefs,unref, proxyRefs,customRef,triggerRef

computed:computed

deferredComputed:deferredComputed

effectScope:effectScope,getCurrentScope,onScopeDispose

1.reactive.js

数据代理,变成响应式,包含reactive、shallowReactive、readonly、shallowReadonly

核心都是通过createReactiveObject方法创建一个proxy,并收录进reactiveMap和readonlyMap中

1)reactive、shallowReactive、readonly、shallowReadonly

响应式、浅的响应式、只读、浅的只读,都是通过createReactiveObject函数创建的

import { isObject } from "@vue/shared"; // 判断是否对象
import {
  mutableHandlers, // 可变数据 --- proxy代理配置
  shallowReactiveHandlers, // 浅的可变数据 --- proxy代理配置
  readonlyHandlers, // 只读数据 --- proxy代理配置
  shallowReadonlyHandlers, // 浅的只读数据 --- proxy代理配置
} from "./baseHandlers";

// 响应式代理
export function reactive(target) {
  // 典型的柯里化函数   策略模式,执行不同的代理策略
  return createReactiveObject(target, false, mutableHandlers); 
}
// 响应式浅代理
export function shallowReactive(target) {
  return createReactiveObject(target, false, shallowReactiveHandlers);
}
// 只读代理
export function readonly(target) {
  return createReactiveObject(target, true, readonlyHandlers);
}
// 只读浅代理
export function shallowReadonly(target) {
  return createReactiveObject(target, true, shallowReadonlyHandlers);
}
2)createReactiveObject --- 创建proxy代理target,并存入WeakMap数据中

将传入的对象作为key,proxy代理后的对象作为值,存入到弱引用数据中

/*
    weekmap会自动垃圾回收,不会造成内存泄漏, 存储的key只能是对象
    mep的key可以是非对象,但如果是对象的话会浪费一次引用,
    如果对象被清空,map可能还在引用这个对象,造成内存泄漏
*/ 
const reactiveMap = new WeakMap(); // 存放代理过的响应式对象
const readonlyMap = new WeakMap(); // 存放代理过的只读对象

// 需要考虑的:是不是仅读 是不是深度,new Proxy() 核心 需要拦截 数据的读取和数据的修改get set
export function createReactiveObject(target, isReadonly, baseHandlers) {
  // 如果目标不是对象 没法拦截了,reactive这个api只能拦截对象类型
  if (!isObject(target)) {
    return target; // 如果不是对象 无法代理 直接返回
  }
  // 如果某个对象已经被代理过了 就不要再次代理了;可能一个对象 被深度代理 又被仅读代理了
  const proxyMap = isReadonly ? readonlyMap : reactiveMap;
  const existProxy = proxyMap.get(target); // 去重
  if (existProxy) {
    return existProxy; // 如果已经被代理了 直接返回即可
  }
  const proxy = new Proxy(target, baseHandlers); // 代理对象
  proxyMap.set(target, proxy); // 将要代理的对象 和对应代理结果缓存起来
  return proxy;
}
3)handlers --- mutableHandlers、shallowReactiveHandlers、readonlyHandlers、shallowReadonlyHandlers

baseHandler.js --- proxy的代理策略,核心是get和set

通过createGetter和createSetter两个工厂函数分别创建不同的get和set --- 只读/非只读 浅的/非浅的

返回4个代理策略,作为4个响应式函数的proxy的options配置

import { extend, isObject } from "@vue/shared/src";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { reactive, readonly } from "./reactive";
// get 
const get = createGetter(); // 非只读 非浅 reactive
const shallowGet = createGetter(false, true); // 非只读 浅代理 shallowReactive
const readonlyGet = createGetter(true); // 只读 非浅 readonly
const showllowReadonlyGet = createGetter(true, true); // 只读 浅代理 shallowReadonly
// set 两个只读的没有set
const set = createSetter(); // 深度代理
const shallowSet = createSetter(true); // 浅代理,set只有两个,只读没有set

// reactive 的 代理策略
export const mutableHandlers = { get, set };

// shallowReactive 的 代理策略
export const shallowReactiveHandlers = {
  get: shallowGet,
  set: shallowSet,
};

// 只读的set公用策略,直接提示错误信息
let readonlyObj = {
  set: (target, key) => {
    console.warn(`set on key ${key} falied`);
  },
};

// readonly 的 代理策略 readonly
export const readonlyHandlers = extend(
  { get: readonlyGet }, readonlyObj
);

// shallowReadonly 的 代理策略
export const shallowReadonlyHandlers = extend(
  { get: showllowReadonlyGet }, readonlyObj
);
4)createGetter
// get工厂函数,两个参数:是不是仅读的,是不是浅的
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    /*
      Reflect说明:
        proxy   reflect  搭配使用,具备proxy所有同名方法
        后续Object上的方法 会被迁移到Reflect 例如 Reflect.getProptypeof()
        以前target[key] = value 方式设置值可能会失败,并不会报异常 ,也没有返回值标识
        receiver修复上下文this指向
        Reflect 方法具备返回值
    */
    const res = Reflect.get(target, key, receiver);
    if (!isReadonly) {
        track(target, TrackOpTypes.GET, key) // 取值时,调用effect中的track,关联effect
    }
    if (shallow) {
      return res; // 浅的不再继续代理,到这里就结束了
    }
    if (isObject(res)) { // 深度代理,判断是深度只读还是深度可读可写
      // vue2 是一上来就递归,vue3 是当取值时会进行代理 。 vue3的代理模式是懒代理
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}
5)createSetter
// set工厂函数,1个参数:是不是浅的
function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]; // 获取老的值 如果新的和老大比对没有变化,就不处理
    let hadKey =
      isArray(target) && isIntegerKey(key) // 如果是数组,且key是整数---数组的索引
        ? Number(key) < target.length // 如果key小于数组长度,说明是数组,否则是增加数组
        : hasOwn(target, key); // 非数组就是对象,看key在target中是否存在,存在就是修改,反之新增
    if (!hadKey) { // 新增
      // 调用effect.js中的trigger方法   触发更新
      trigger(target, TriggerOrTypes.ADD, key, value);
    } else if (hasChanged(oldValue, value)) { // 修改,同时判断新旧value是否有变化
      // 调用effect.js中的trigger方法   触发更新
      trigger(target, TriggerOrTypes.SET, key, value, oldValue); 
    }
    const result = Reflect.set(target, key, value, receiver); // target[key] = value
    return result;
  };
}

2.effect

effect接收的函数中用到的所有响应式属性,都会收集_effect,当这个属性变化的时候,重新执行_effect

effect相当于是vue2的watcher,和数据的关系也是多对多,一个effect可能对应多个数据,一个数据也可能对应多个effect

1)思考:
  1. 我如何让这个effect变成响应的effect,可以做到数据变化重新执行?

    1. 属性取值时让属性记住effect
    2. 属性修改时找到自己的effect并执行
  2. 取值时如何关联effect?

    1. 取值前一定会先调用effect,因为数据在effect函数中使用,把effec放入一个栈中 --- effectStack
    2. 取值时调用track函数,找到effectStack中的effect --- activeEffect,让target的每个属性都记住activeEffect
    3. 建立好关联后,为了方便查找,用WeakMap存放关联数据 --- targetMap;
  3. 属性修改如何找到对应的effect并执行?

    1. 修改调用trigger函数,帮我们找出targeMap中和target对应的depsMap;(修改和新增有不同的逻辑)
    2. 找出depsMap中的所有effect,放入一个数组中---effects,随后遍历该数组,一次性执行所有effect
2)源码中的实现原理详解:
  • 调用effect的时候,会将传入的函数fn 通过new ReactiveEffect得到一个_effect函数,_effect其实就是一个切片高阶函数,帮我们执行传入的fn,同时做了一些其它事情,例如依赖收集,(我这里用createReactiveEffect高阶函数代替)

  • 生成的_effect默认会执行一次,_effect函数中首先会将自身放入effectStack中,并通过索引找到activeEffect,后面数据变更就调用该数据所关联的_effect,所以要保证activeEffect就是当前属性关联的_effect

  • 在reactive等响应式代理函数的get方法中,调用tarck方法,传入target、type、key等关键信息,该方法会将响应式对象和调用它的effect简历依赖关系(targetMap)

    • targetMap也是一个WeakMap数据,target响应式对象作为键,值是所收集的depsMap
    • depsMap是一个Map数据,以key为健(响应式对象target的属性),其值是一个Set,Set中存放的是_effect
  • 属性变化的时候触发set,调用tirgger函数传入target、type、key、value等数据,根据target找出targetMap中的depsMap,遍历depsMap取出每一个dep所关联的effect,然后执行该effect

  • 总结上面的关系:

    • effectStack:存放_effect,是一个调用栈,执行完就会出栈,先进后出
    • activeEffect:当前调用栈的effect,是effectStack中的最后一个
    • targetMap:存放的是响应式对象和其对应的depsMap的集合 (WeakMap弱引用)
    • depsMap:存放的是每个响应式属性所对应的dep集合 (Map数据)
    • dep:存放的是每个响应式属性所关联的_effect集合 (Set数据)
3)operators.js --- 存放操作枚举类型
// 追踪类型
export const enum TrackOpTypes { // 后续会添加更多
    GET 
}
// 触发类型
export const enum TriggerOrTypes { // 后续会添加更多
    ADD,
    SET
}
// 源码中还有很多……
4)effect方法 --- effect.js中

源码通过new ReactiveEffect生成一个_effect,并默认执行一次

用数组push和pop模仿调用栈,解决执行顺序问题

import { isArray, isIntegerKey } from "@vue/shared/src";

export function effect(fn, options: any = {}) {
    // 我需要让这个effect变成响应的effect,可以做到数据变化重新执行 
    // 通过高阶函数,创建effect,源码中是通过new ReactiveEffect实现
    const _effect = createReactiveEffect(fn, options);
    if (!options.lazy) { // 默认的effect会先执行
        _effect(); // 响应式的effect默认会先执行一次
    }
    return _effect;
}

let uid = 0; // 每个effect的唯一id
let activeEffect; // 存储当前的effect
const effectStack = [] // effect栈
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        // 如果已经关联过,就不要再关联,否则可能会死循环,例如effect(()=>{ state.num   })
        if (!effectStack.includes(effect)) { // 保证effect没有加入到effectStack中
            try {
                effectStack.push(effect); // 进栈
                activeEffect = effect; // 当前effect
                return fn(); // 函数执行时会取值  会执行get方法 然后调用track方法
            } finally {
                effectStack.pop(); // 出栈
                activeEffect = effectStack[effectStack.length - 1]; 
            }
        }
    }
    effect.id = uid  ; // 制作一个effect标识 用于区分effect
    effect._isEffect = true; // 用于标识这个是响应式effect
    effect.raw = fn; // 保留effect对应的原函数
    effect.options = options; // 在effect上保存用户的属性
    return effect;
}
5)track方法

get取值时会调用此方法,用于依赖关系收集

1.拿到activeEffect

2.targetMap [ target,depsMap ] depsMap = new Map

3.depsMap [ key,dep ] dep = new Set

4.dep [ activeEffect,activeEffect…… ]

const targetMap = new WeakMap(); // 收集的对象数据 --- 在effect函数中使用过的响应式对象
export function track(target, type, key) { // 可以拿到当前的effect
    // 当前正在运行的effect,如果没有,就说明reactive代理过的对象没在effect中使用
    // 如果在effect中使用,就一定会调上面的effect方法,就会记录当前effect
    if (activeEffect === undefined) { 
        return; // 此属性不用收集依赖,因为没在effect中使用
    }
    let depsMap = targetMap.get(target); // 先看当前对象有没有被收集过
    if (!depsMap) { // 如果没被收集过
        // 收集过了就不用再收集了,例如连续调用10000次effect,执行内容也一样,那收集一万个重复的?
        // 下面为什么用map 也和上面这句话一个意思
        // depsMap就是所有出现在effect函数中的,响应式对象target,存放在WeakMap中的映射
        targetMap.set(target, (depsMap = new Map)); // 1:depsMap赋值,2:targetMap中添加数据
    }
    let dep = depsMap.get(key); // dep就是target对象中的每个属性,
    if (!dep) { // 如果没有关联过
        // 为什么是set?因为要去重,假如effect(()=>{ a,a,a,a…… })使用了n次a属性,岂不是收集了4个dep?
        depsMap.set(key, (dep = new Set)) // 赋值   关联,
    }
    if (!dep.has(activeEffect)) { // 如果dep中没有关联effect
        dep.add(activeEffect); // 让dep记住effect,后面属性变化,就执行dep的effect函数
    }
}
6)trigger方法

set时调用此方法,触发属性对应的effect函数执行

1.targetMap中通过target找到depsMap

2.遍历depsMap,取出dep中的所有_effect,放入effects

3.对数组进行特殊处理,修改数组长度时判断是否小于数组长度(如果大于原长度说明是无效操作,不更新视图),如果直接操作数组索引也判断索引是否有效()

4.遍历执行effects

// 找属性对应的effect 让其执行
export function trigger(target, type, key?, newValue?, oldValue?) {
  // 如果这个属性没有收集过effect,那不需要做任何操作
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  // 找出target中所有属性关联的effect,最终一起遍历执行effect
  const effects = new Set(); //  这里对effect去重了
  const add = (deps) => {
    if (deps) { // 遍历前面track收集的所有属性的effec集合 --- Set类型的数据
      deps.forEach((effect) => effects.add(effect));
    }
  };
  // 看修改的是不是数组的长度 因为改长度影响比较大
  if (key === "length" && isArray(target)) { // 如果操作的数组的length
    depsMap.forEach((dep, key) => { // dep是effect,key是在effect中用到的属性的key
      /*
        场景1:
          app.innerHTML = state.arr.length // 取值收集依赖 key是length
          state.arr.length = 4 // 修改length,newValue=4
        场景2: 
          app.innerHTML = state.arr // 取值收集依赖 key是数组
          state.arr.length = 2 // 修改length, newValue=2 小于原数组,需要重新执行
      */
      if (key === "length" || newValue < target.length) add(dep);
    });
  } else { // 可能是对象
    if (key !== undefined) { // 这里肯定是修改,不能是新增
      add(depsMap.get(key)); // 如果是新增
    }
    // 如果修改数组中的 某一个索引 怎么办?
    switch (
      type // 如果添加了一个索引就触发长度的更新
    ) {
      case TriggerOrTypes.ADD:
        if (isArray(target) && isIntegerKey(key)) { // 是数组,且是有效索引
          // 触发依赖中长度的更新,例如取值使用了length,那么deps中就一定有length对应的effect
          add(depsMap.get("length")); 
        }
    }
  }
  // 遍历执行全部effect
   effects.forEach((effect: any) => {
        if(effect.options.scheduler){ // 如果传入的该函数,就调用该函数
            effect.options.scheduler(effect); // 计算属性会用到这个
        }else{
            effect(); // 默认执行自身的effect
        }
   })
}

3.ref

reactive原理是proxy,而ref是用的class get set,最终babel会编译成defineProperty

ref一般用于将原始值数据(number、string、boolean、null、undefined、bigint)转为响应式

原理就是把传入的原始值变成一个对象,劫持对象的get和set方法

同时它也可以传入对象,如果是对象,就直接调用reactive……

1)ref、shallowRef

都是通过createRef函数创建一个ref实例,然后return

import { hasChanged, isArray, isObject } from "@vue/shared/src";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { reactive } from "./reactive";

export function ref(value) {
    // ref 和 reactive的区别 reactive内部采用proxy  ref中内部使用的是defineProperty
    // 将普通类型 变成一个对象 , 可以是对象 但是一般情况下是对象直接用reactive更合理
    return createRef(value) // 源码中随处可见这种高阶函数或纯函数减少冗余代码
}
export function shallowRef(value) {
    return createRef(value, true) // true表示浅的
}
2)toReactive

创建ref时如果传入的是对象,直接通过该函数调用reacitve,并把其代理过的数据挂载到ref实例的value上

const toReactive = (val) => isObject(val) ? reactive(val) : val // 如果是对象就返回reactive代理后的数据
3)RefImpl

ref的实例类,基于class的get和set实现,babel最终会把这个class编译成defineProperty

class RefImpl {
    public _value; //表示 声明了一个_value属性 但是没有赋值
    public __v_isRef = true; // 产生的实例会被添加 __v_isRef 表示是一个ref属性
    // 参数中前面增加修public饰符,标识此属性放到了实例上
    constructor(public rawValue, public shallow) { 
        // 如果不是浅的,并且传入的可能是对象,那么需要通过toReactive函数使用reactive进行代理
        // 将数据放到_value上,随后用.value取值时拿出
        this._value = shallow ? rawValue : toReactive(rawValue) 
    }
    // 类的属性访问器
    get value() { // 代理 取值取value 会帮我们代理到 _value上
        track(this, TrackOpTypes.GET, 'value'); // 取值时 进行effect依赖关系收集
        return this._value // 返回_value
    }
    set value(newValue) {
        if (hasChanged(newValue, this.rawValue)) { // 判断老值和新值是否有变化
            this.rawValue = newValue; // 新值会作为老值
            // 修改的时候,修改的是_value,同时如果不是浅的,有可能新值是对象,也要使用reactive代理
            this._value = this.shallow ? newValue : toReactive(newValue);
            // 触发器,取出属性关联的所有effect并执行该函数
            trigger(this, TriggerOrTypes.SET, 'value', newValue); 
        }
    }
}
4)createRef

创建ref的工厂函数,其核心是new RefImpl

function createRef(rawValue, shallow = false) {
    return new RefImpl(rawValue, shallow) // 创建ref实例
}
5)ObjectRefImpl

它是toRef实例的构造类,toRef就是new了这个构造类实现;其原理就是做了一层代理,将数据放到value上,并标记__v_isRef属性

注意:该方法只能对已经是ref的数据使用,源码中如果不是ref是会被拦截的,我这里写其核心原理,没有做拦截

// 假设在调用此构造类之前,已经对数据做了响应式判断
class ObjectRefImpl {
    // ref数据在后面渲染dom取值的时候,会直接取其value,所以{{}}中不用写xxx.value
    public __v_isRef = true;  // 标记这是一个ref数据
    constructor(public target, public key) { } // 将target换个key都放到了this上
    get value() { // 代理  
        // 原对象已经是响应式,直接访问原对象属性即可
        return this.target[this.key] 
    }
    set value(newValue) {
        // 修改也是直接修改的原对象,原对象会触发trigger
        this.target[this.key] = newValue;
    }
}
6)toRef

把对象中的某个值 转换成ref

这玩意有什么用?

  1. 假如我们将某个响应式对象中的属性 进行解构后,访问该属性是直接访问的变量名,所以该属性失去了target,于是不再是响应式

    1. const { a,b } = state 此时修改a ,是不会触发proxy的,至于为什么,自己想想
  2. 如果不解构,那么我们在{{}}中渲染数据的时候,都需要{{ state.xxx state.xxx state.xxx }}……非常冗余

    1. {{ state.a state.b }} vs {{ a b }}
export function toRef(target, key) { // 可以把一个响应式对象的值转化成 ref类型
    // 通过ObjectRefImpl对原数据做了一个代理
    // const a = toRef(state,'a')  => a成了ref,不必担心其的代理丢失,
    // 且改a.value的时候,修改的是state.a
    return new ObjectRefImpl(target, key) 
}
7)toRefs

把整个对象转换成ref,这玩意其实就是循环调用toRef。

export function toRefs(object) { // object 可能传递的是一个数组 或者对象
    const ret = isArray(object) ? new Array(object.length) : {}
    for (let key in object) { // for in循环 能同时遍历数组和对象 不用再多解释了吧
        ret[key] = toRef(object, key); // 循环调用toRef
    }
    return ret;
}

4.computed

计算属性,effect(lazy) scheduler 缓存标识实现

将传入的函数或者get传递给effect,options中标记laze=true,并传入scheduler函数(trigger时调用该函数执行自己的逻辑)

取值时判断_dirty,如果是脏的,那么需要重新调用effect计算_value,反之就用缓存的_value

和v2不一样的是,v3的computed会收集属性依赖,v2没有收集功能

import { isFunction } from "@vue/shared"
import { effect, track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";

class ComputedRefImpl {
    public _dirty = true // 是否脏的,用于控制是否需要重新计算
    public _value; // 返回的数据
    public effect; // 依赖的effect
    constructor(getter, public setter) { // getter只在constructor中用,不需要放到this上
        // effect执行的函数,就是传入的getter函数,后面属性变化时,就会调用该函数重新计算返回值
        this.effect = effect(getter, {
            lazy: true, // 默认不会自动执行
            scheduler: () => { // 这个函数是用于扩展你自己的逻辑,属性变化时,effect会调用这个函数
                if (!this._dirty) { // 属性变化时,发现有缓存
                    this._dirty = true // 改为脏的,让它重新计算
                    trigger(this, TriggerOrTypes.SET, 'value') // 重新执行effect 也就是传入的getter,然后又会取值,又走到下面的get
                }
            }
        })
    }
    get value() {
        if (this._dirty) { // 如果是脏的 (没被缓存或者需要重新计算),则需要重新计算取值
            this._value = this.effect() // effect函数会返回执行后的结果
            this._dirty = false // 取完值有缓存了,就不是脏的,下次如果属性没变化,就不会重新取值
        }
        track(this, TrackOpTypes.GET, 'value') // 收集value的依赖,属性变化的时候,让上面的trigger执行effect
        return this.value
    }
    set value(newValue) {
        this.setter(newValue) // 调用传入的setter
    }
}

export function computed(getterOrOptions) {
    let getter;
    let setter;
    if (isFunction(getterOrOptions)) { // 如果传入的是函数,就用该函数作为getter
        getter = getterOrOptions
        setter = () => { // 没有传set时,计算属性是只读的
            console.warn('计算属性只读')
        }
    } else {
        getter = getterOrOptions.get // 取传入的get
        setter = getterOrOptions.set // 传入的set
    }
    return new ComputedRefImpl(getter, setter) // 通过构造类实现
}

五、源码特性解析

1.reactive

1)重复调用,返回值一样。

已经是响应式的对象 不会再代理

已经被代理过的普通对象 也不会再代理

let obj = { name: 'zf' };
let proxy1 = readonly(obj);
let proxy2 = reactive(proxy1); // 已经是响应式的对象 不会再代理
let proxy3 = reactive(obj); // 已经被代理过的普通对象 也不会再代理

// 解析
export function reactive(target: object) {
  // 如果这个对象已经被 readonly代理过了,则直接返回。 
  // 被readonly代理过就会添加proxy,取值时会走get方法  reactive(readonly(obj))
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(  )
}

function createReactiveObject(  ) {
  // ……
  // 已经是响应式对象,就直接返回,readonly可以包裹reactive,但reactive不能包裹readonly
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 原对象 已经被代理过,就直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // ……
}
2)toRaw & markRaw

toRaw::返回响应式对象的 原对象,取代理对象的raw属性,get时判断如果取raw属性就直接返回taeget

markRaw:标记不被代理,劫持对象添加SKIP属性让其不可配置,创建代理时判断有SKIP属性就直接返回

toRaw
let obj = { name: 'zf' };
let proxy = reactive(obj); 
console.log(obj === toRaw(proxy)); // true,toRaw返回原对象

// toRaw源码
export function toRaw<T>(observed: T): T {
  // 尝试去target上面取枚举属性RAW(__v_raw),取值会走get,
  // get会进行判断,如果有该属性,就返回target
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

// get中
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ……
    if (
      key === ReactiveFlags.RAW && // 取值的属性是RAW 
      // ……
    ) {
      return target  返回原目标对象 而不是代理对象
    }
    // ……
  }
}
markRaw
let obj2 = { name: 'zf' };
let proxy2 = reactive(markRaw(obj2)) // 标记过的不会被代理

// markRaw源码
export function markRaw<T extends object>(value: T): Raw<T> {
  def(value, ReactiveFlags.SKIP, true)  // 劫持对象 添加SKIP属性 让其不可配置
  return value
}

// 劫持对象
export const def = (obj: object, key: string | symbol, value: any) => {
  Object.defineProperty(obj, key, {
    configurable: true, // 不可配置
    enumerable: false, // 可以枚举
    value // 返回布尔值
  })
}

// 创建代理时 判断
function createReactiveObject() {
  // 通过getTargetType方法查看是否可以被代理
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // ……
}

// 查看对象是否可被代理
function getTargetType(value: Target) {
  // 如果目标身上有skip属性,或者该目标不可扩展,那么就不能被代理
  // 否则返回targetTypeMap中的代理策略
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}
3)数组处理
增加依赖收集

数组用于查找属性的原型方法,需要增加依赖收集,includes、indexOf、lastIndexOf,这些方法都能返回查询到的数据;

假如result依赖于这几个方法,那么数组中的值变化的时候,也要重新计算result

例如 const x = proxy([a,b,c]).lastIndexOf(a=1); x也需要收集依赖,数组变化时,重新渲染;

切片重写上面的几个方法,在调用原方法前,先遍历原数组,调用track收集依赖

// values  VUE2 方法劫持, 重写原有方法 并且调用原有方法, 可以增加自己的逻辑
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
  const method = Array.prototype[key] as any // 复制原数组方法
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    const arr = toRaw(this) // 获取原数据
    for (let i = 0, l = this.length; i < l; i  ) {
      track(arr, TrackOpTypes.GET, i   '') // 属性收集依赖
    }
    // we run the method using the original args first (which may be reactive)
    const res = method.apply(arr, args) // 调用原数据的 原方法
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return method.apply(arr, args.map(toRaw)) // 调用响应式数据的 原方法
    } else {
      return res
    }
  }
})
停止依赖收集

能直接改变数组的方法,需要停止依赖收集,push、pop、shift、unshift、splice

这些方法,可能会造成死循环,例如 watchEffect(()=>{ arr.push(1) }) watchEffect(()=>{ arr.push(2) }) ……

切片重写原方法,使用pauseTracking方法让该数组停止收集依赖,再调完数组方法

调用resetTracking方法让数组恢复收集依赖

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  const method = Array.prototype[key] as any // 切片原方法
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    pauseTracking() // 暂停收集依赖
    const res = method.apply(this, args) // 调用原方法
    resetTracking() // 恢复依赖收集
    return res
  }
})
4)ref处理

响应式对象的属性,可能是ref,如果是普通ref就做了拆包处理

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (isRef(res)) {
      // 如果属性是ref,且不是数组,在访问该属性时,不用.value,直接返回的是value
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
  }
}
5)symbol和原型链处理

如果属性是symbol,或者原型链上的,就不做处理

if (
  // 是内置symbol  或者是原型链 查找到的,直接返回, 不需要收集symbol 和 __proto__的依赖
  isSymbol(key)
    ? builtInSymbols.has(key as symbol)
    : isNonTrackableKeys(key)
) {
  return res
}
6) receiver的处理

这个玩意有点绕,简单来说,就是你修改的属性,如果会去修改它原型链上依赖的属性,是不被允许的

源码通过target === toRaw(receiver) 进行判断的

// 示例代码
let obj = {};
let proto = {a:1}
let proxyProto = new Proxy(proto, {
    get(target,key,receiver) {
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver){
        console.log(proxyProto , receiver == myProxy)
        return Reflect.set(target,key,value,receiver)// 不要考虑原型链的set
    }
})
Object.setPrototypeOf(obj,proxyProto); // obj.__proto__ = proxyProto
let myProxy = new Proxy(obj,{  // proxy(obj)
    get(target,key,receiver) {
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver){
        console.log(receiver === myProxy)
        return Reflect.set(target,key,value,receiver); // 调用reflect.set 会触发原型链的set,调用上面的set
    }
})
myProxy.a = 100; // 修改a的时候 会触发proxyProto的set,这样是不允许的

// 源码 don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) { // 如果修改的不是原型链才会走下面的trigger
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}

2.effect

1)重复调用effect

重复被effect,返回的函数是不同的函数,但执行的是同一个函数

// 示例
let reactiveEffect = effect(() => { console.log(1) });
let reactiveEffect2 = effect(reactiveEffect); // 重复调用
console.log(reactiveEffect === reactiveEffect2); // false

// 源码
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    // 如果已经有fn,说明已经被effect调用过,直接复用原来的fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  const _effect = new ReactiveEffect(fn) // 执行的还是原来的fn
  // ……
}
2)cleanupEffect

属性每次修改时,effec都需要重新收集deps,如果不重新收集,可能会出现不必要的渲染

const state = reactive({ name: 'aa', age: 11 })
effect(() => {
    // 打印3次render:默认执行一次、修改age执行一次、修改name执行一次,name变了后不再打印age,所以再改age不会触发!
    console.log('rerender')
    if (state.name === 'aa') { // name是aa的时候,才会收集age
        console.log(state.age); // name变了后就不再收集依赖,所以每次修改,都要全部重新确认依赖关系
    }
}) 
state.age = 100; // effect使用了age 有依赖关系,会触发
state.name = 'bb'; // effect中使用了name 有依赖关系,会触发;但是!age依赖name,name变了,age就没有依赖了
state.age = 200; // 不会触发

// 源码
if (!effectStack.includes(effect)) {
    cleanup(effect);
    // ……
}
function cleanup(effect) {
    const { deps } = effect;
    if (deps.length) {
        for (let i = 0; i < deps.length; i  ) {
            deps[i].delete(effect);
        }
        deps.length = 0;
    }
}
3)scheduler

调度程序,自定义effect更新函数,扩展的外挂功能

options中传了该属性,就不会执行effect.run,而是执行你自己的

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler() // 执行你传入的
    } else {
      effect.run() // 否则执行run,也就是3.0base版本的_effect
    }
  }
}

六、渲染核心流程图

下一章的铺垫

学新通

这篇好文章是转载于:编程之路

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 编程之路
  • 本文地址: /boutique/detail/tanhgbaige
系列文章
更多 icon
同类精品
更多 icon
继续加载