编写vue3响应式原理(万字)
区别介绍
-
源码采用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)思考:
-
我如何让这个effect变成响应的effect,可以做到数据变化重新执行?
-
- 属性取值时让属性记住effect
- 属性修改时找到自己的effect并执行
-
取值时如何关联effect?
-
- 取值前一定会先调用effect,因为数据在effect函数中使用,把effec放入一个栈中 --- effectStack
- 取值时调用track函数,找到effectStack中的effect --- activeEffect,让target的每个属性都记住activeEffect
- 建立好关联后,为了方便查找,用WeakMap存放关联数据 --- targetMap;
-
属性修改如何找到对应的effect并执行?
-
- 修改调用trigger函数,帮我们找出targeMap中和target对应的depsMap;(修改和新增有不同的逻辑)
- 找出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
这玩意有什么用?
-
假如我们将某个响应式对象中的属性 进行解构后,访问该属性是直接访问的变量名,所以该属性失去了target,于是不再是响应式
-
- const { a,b } = state 此时修改a ,是不会触发proxy的,至于为什么,自己想想
-
如果不解构,那么我们在{{}}中渲染数据的时候,都需要{{ state.xxx state.xxx state.xxx }}……非常冗余
-
- {{ 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
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13