vite 原理
webpack开发阶段实现
- 根据入口文件分析依赖模块,构建依赖图后根据配置打包文件,返回产物
- 在一次启动的情况下会将入口模块所有依赖的文件全部读取并打包,在模块较多时比较影响构建性能
vite开发阶段实现
-
通过启动本地服务,默认返回 index.html ,使其通过esm的形式引入,然后发送网络请求访问入口的js文件
- 本地服务通过node的http模块创建Websocket和httpserver
-
入口文件会引入其他文件,也就实现了只会加载入口文件所引用的文件
-
页面和服务交互流程
- 入口html引入入口js发起网络请求到本地服务器
- 本地服务器读取对应的js文件,进行编译后返回给页面,如果该js文件还存在依赖还会继续发起请求
- 再次进行上面的过程
-
对不同文件的处理
-
js
- 通过编译进行降级或者引用处理等,
-
css
- 将css代码转换为字符串返回
-
vue
-
@vitejs/plugin-vue -
使用该编译器转换成纯js返回
-
-
-
特殊注意
- vite由于是直接使用esm实现,在代码里面无法使用commonjs
- 且只能支持一些比较新的现代浏览器
vite如何实现生产环境打包
- 使用vite自带的打包命令,本质上vite使用的 rollup实现
- 使用webpack指定src目录实现打包
vue更新内容
静态提升
-
标记静态阶段,不再渲染函数执行,提出静态节点,在渲染函数外执行创建,保证静态节点只创建一次
-
静态属性也会被提升,在render函数外创建
-
// vue2 的静态节点 render() { createVNode('h1',null,'Hello World') } // vue3 的静态节点 const hoisted = createVNode('h1',null,'Hello World') function render() { // 直接使用hoisted }
预字符串化
-
当编译器遇到大量连续的静态内容,会直接将其编译成一个普通字符串节点
-
要求必须是连续的20个及以上静态节点
-
<ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>0</li> </ul> // 编译后 _createStaticVNode("<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>0</li></ul>", 1))
缓存事件处理函数
-
在创建模版时会缓存绑定的事件
-
<button @click='count++'> plus </button>// vue2 处理 render(ctx) { return createVNode('button',{ onClick:function($event) { ctx.count++ } }) } // vue3 处理 render(ctx,_cache) { return createVNode('button',{ onClick:cache[0] || (cache[0] = ($event) => (ctx.count++)) }) }
BlockTree
- vue2在对比新旧树的时候,并不知道那些节点是静态的,哪些是动态的,因此只能一层一层的比较,这就浪费了部分时间在对比静态节点上
- vue3会标记静态节点和动态节点,会在根节点记录所有的动态节点,后续对比只对比block根节点,只对比动态节点数组
- vue3为每个组件创建了一个名为block的根节点,用于后续对比,跳过不需要对比的节点
PatchFlag
-
vue2在对比每个节点时,并不知道这个节点那些信息会发生变化,因此只能将所有信息依次对比
-
vue3在编译过程中会记录每一个节点是动态节点,并且会标记哪一个内容是动态的(样式,内容这些)
-
_createTextVNode( " " + _toDisplayString(_ctx.data1), 1 /* TEXT */ )
去掉了Vue构造函数
-
vue2中对多应用开发会出现多次new Vue的操作,对于一些mixin或者全局插件会出现冲突的问题
-
调用构造函数的静态方法,会对所有vue应用生效,不利于隔离不同应用
-
vue2的构造函数继承了太多功能,不利于tree shaking
-
vue2没有把组件实例和vue应用两个概念区分开,在vue2中,通过new Vue创建 的对象,既是一个vue应用,同时又是一个特殊的vue组件
-
new Vue({}).$mount("#root") new Vue({}).$mount("#root1")
-
-
vue3去除了Vue构造函数的暴露,使用了一个Api保留,后续操作仅是对实例进行操作
-
vue3使用导出函数的形式可以充分利用tree shaking优化打包体积
-
通过createApp创建的对象是一个vue应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件
-
import {createApp} from 'vue' createApp().use().mixin().component().mount('#app1') createApp().mount('#app1')
-
组件实例的Api和属性
vue3中实例现在是一个proxy,通过this访问拿到就是proxy返回的属性,能够调用的属性就是vue对外暴露的属性,
属性
- $data
- $props
- $el
- $options
- $parent
- $root
- $slots
- $refs
- $attrs
Api
- $watch
- $emit
- $forceUpdate
- $nextTick
vue3数据响应式理解
- vue2和vue3均在相同的声明周期完成数据响应式,但是做法不一样
- 对数据的响应式发生在 beforeCreate和 created 之间,这个时候会进行注入和响应式代理
- vue2的响应式实现
- 递归遍历组件里面的data,对每一个属性 使用
Object.defineProperty,拦截get和set - 所以对于新增属性是无法监听变化的,必须使用$set完成
- 数组由于内容比较多,一个个监听比较耗费性能,所以代理的是整个属性,必须出现地址变化才会触发更新
- 递归遍历组件里面的data,对每一个属性 使用
- vue3的响应式实现
- 使用proxy完成对整个data的代理, 操作data的属性就会操作代理对象,就会准确的定位到对应的属性,
- vue3中对数据的访问是动态的,当访问某个属性的时候,在动态的获取和设置,极大的提升了在组件初始阶段的效率
- 对于索引类型的访问会返回一个新的proxy,后续操作就是对这个属性的新的proxy进行操作
- 同时proxy可以监控到成员的新增和删除,因此在vue3中新增成员和删除成员以及索引访问都是可以触发重新渲染的
v-model更新
-
vue2提供了两种双向绑定
v-model和.sync在vue3中,去掉了.sync 修饰符,只需要使用 v-model 进行双向绑定-
<ChildComponent :value='pageTitle' @input='pageTitle = $event' /> // 简写 <ChildComponent v-model='pageTitle' /> -
<ChildComponent :value='pageTitle' @input='pageTitle = $event' /> // <ChildComponent :value.sync='pageTitle' />
-
-
vue3实现
-
<ChildComponent :modelValue='pageTitle' @update:modelValue='pageTitle = $event' /> // 简写 <ChildComponent :v-model='pageTitle' /> -
<ChildComponent :title='pageTitle' @update:input='pageTitle = $event' /> // 使用v-model指定绑定的属性 <ChildComponent v-model:title='pageTitle' /> -
<script lang="ts" setup> import { ref, defineComponent, watchEffect } from 'vue'; import CheckEditor from './components/CheckEditor.vue'; defineComponent({ CheckEditor }) const data1 = ref(false) watchEffect(() => { console.log(data1.value) }) </script> <template> <CheckEditor v-model:checked="data1" /> </template> // 子组件 写法1 使用emit <template> <input type="checkbox" @change="handleChecked" :value="props.checked" /> </template> <script lang="ts" setup> import { defineProps, defineEmits, watchEffect } from 'vue' const props = defineProps({ checked: Boolean, }) const emit = defineEmits() const handleChecked = () => { emit('update:checked', !props.checked) // 通过emit实现 } </script> // 写法2 使用defineModel <template> <input type="checkbox" v-model="checked" /> </template> <script lang="ts" setup> import { defineModel } from 'vue' const checked = defineModel('checked') // 绑定对应的父组件的model </script> -
vue3支持 v-modle 绑定多个属性
-
<ChildComponent v-model:title='pageTitle' />
-
-
允许自定义 v-model 修饰符
-
在使用v-model的修饰符的情况,会在组件内部创建一个属性记录使用了那些v-model修饰符
-
<ChildComponent v-model.cap='pageTitle' /> // modelModifyers:{ cap:true } <ChildComponent v-model:title.cap='pageTitle' /> // titleModifyers:{ cap:true } -
<template> <input type="text" v-model="checked" /> </template> <script lang="ts" setup> import { defineModel } from 'vue' const [checked] = defineModel('checked', { set(val) { return (val as string).trim() } }) // 绑定对应的父组件的model </script>
-
-
v-for和v-if优先级
- 在vue中会出现v-for和v-if一起使用的情况
- vue2中v-for的优先级是高于v-if的,也就是在循环体里面我可以通过v-if控制元素的显示
- 但是这样写会存在效率问题,每次都要重新比较
- 在vue3中这个优先级被调整了,而是 v-if 高于 v-for
v-for里面的key处理
- 当使用
<template>进行v-for循环时,需要吧key值放到<template>中,而不是它的子元素中 - 当使用
v-if v-else-if v-else分支时,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key- 即便需要手动给予 key 值,也必须给予每个分支唯一的key, 不能因为要重用分支而给予相同的key
// vue2处理 没有key的情况会出现输入的元素没更新
<div v-if="flag" key='checked'>
显示<input type="text" />
</div>
<div v-else key='noChecked'>
隐藏 <input type="text" />
</div>
<button @click="changeFlag">切换显示/隐藏</button>
Fragment
vue3选择允许组件出现多个根节点
异步组件
-
vue3使用vue暴露的函数创建异步组件,
-
<script lang="ts" setup> import { defineAsyncComponent, defineComponent, h } from 'vue' import Loading from '../components/Loading.vue'; import Error from '../components/Error.vue'; import { delay } from '../utils'; const Block3 = defineAsyncComponent({ loader: async () => { await delay(2000) throw new TypeError() return import('../components/Block3.vue') as any }, loadingComponent: Loading, errorComponent: { render() { return h(Error, { text: '报错了' }) } } }) const Block5 = defineAsyncComponent(() => import('../components/Block5.vue')) defineComponent({ Block3, Block5 }) </script> <template> <div>home</div> <Block3 /> <Block5 /> </template> <style scoped lang="scss"></style> -
defineAsyncComponent 有两个重载
- 接收一个函数返回一个promise,promise可以是组件
- 接收一个对象
- loader: 第一种写法的参数
- loadingComponent?: Component; 加载时的组件
- errorComponent?: Component; 出现错误时显示的组件
-
修改组件真实节点位置
<Teleport to="#app"></Teleport>
-
在我们封装modal组件的时候,会出现层级问题,将modal写在使用的地方,导致后面的蒙层无法覆盖其它元素
-
vue3提供了一个内置组件
Teleport,能够将其真实视图和逻辑视图分离,使其真实dom挂载到目标节点<Teleport to="#app"> <Modal v-if="modalVisible"> <button @click='modalVisible = false'>关闭蒙层</button> </Modal> </Teleport>
reactivity Api
- vue3提供的将数据变为响应式函数的能力
- 在vue3里面是对外暴露了响应式相关能力的api,可以使用该api将目标数据变成响应式
- 而且该api可以脱离vue实例和组件在js使用
- 我们只知道composition Api,但是其中部分能力是由reactivityApi 完成,然后统一暴露给使用者
- ```watchEffect,ref `` 等`
- 在vue2里面是vue内部自己处理的响应式数据,没有对外暴露该能力
获取响应式数据
-
reactive
-
只能操作对象格式
-
将普通对象转换成响应式对象(调用之后返回一个proxy)
-
import { reactive } from "vue"; reactive({ name: 1 })
-
-
readonly
-
创建一个只读的代理对象
-
接收 proxy 或者 普通对象
-
console.log(readonly(reactive({ name: 1 })));
-
-
ref
-
可以代理任何类型的数据
-
如果是原始值类型,将原始值封装到一个对象来实现proxy,通访问器属性操作对象的value属性
-
const a = ref(0) a.value = 2 // 实际上 a => { _value: 0, get value() { return this._value }, get value(val) { this._value = val } }
-
-
如果是引用值类型就使用reactive,然后代理结果
-
const a = ref({a:1}) // 实际上 if(typeof result === 'object') { return reactive(result) }
-
-
如果已经是代理的数据,则直接返回该代理
-
const data = reactive({ name: 1 }) const data1 = ref(data) data1 == data
-
-
-
computed
-
根据目标响应式数据返回一个新的响应式数据
- 返回的结果跟ref的结果一样
-
读取computed的返回值时,根据情况返回( 根据情况触发函数执行 )
- 创建时不会触发,只有当目标被使用才会触发
- 多次使用返回值,computed绑定的函数只会触发一次,只有函数内依赖的数据发生变化才会触发
- 实现原理
- computed由于返回的是一个ref格式响应式数据,当我们调用 .value时候就会触发访问器
- 访问器会执行computed绑定的函数,执行函数后得到结果进行缓存,并且将使用的响应式数据加入依赖
- Vue 的响应式系统会在访问响应式属性时记录下“是谁”访问了这些属性
- 当目标依赖发生变化时又会触发computed的set访问器
-
const data1 = reactive({ a: 1, b: 2 }); const data = computed(() => { console.log("computed"); return data1.a + data1.b; }); console.log(data.value); //调用这个代码才会打印 console.log("computed");
-
监听数据变化
-
watchEffect
-
创建一个响应式监听函数,返回监听移除函数
-
监听函数中用到响应式数据,响应式数据变化会再次执行
-
初始化时用到了多个数据只会触发一次函数执行
-
在初始化后修改响应式数据多次函数也只会触发一次
- 会等待修改结果完成,将执行操作放入微队列,
-
const data1 = reactive({ a: 1, b: 2 }); watchEffect(() => { console.log("触发watch"); return data1.a + data1.b; }); // console.log("触发watch"); data1.a++; data1.a++; // console.log("触发watch");
-
-
watch
-
类似于vue2里面的watch操作,可以手动指定需要监听的目标,可以得到新旧属性值
-
初始化时不会运行,如果需要直接运行需要使用配置
immediate -
观察的目标只能是响应式数据
-
可以使用函数处理响应式数据里面的数据,直接使用的话得到的是表达式的结果
-
watch( () => data1.a, (newVal,oldVal) => { console.log("触发watch"); } );
-
-
监控多个数据的变化
-
watch( [() => data1.a,data2], ([newVal,oldVal],[newVal1,oldVal1]) => { console.log("触发watch"); } );
-
-
判断
- isProxy
- 判断某个数据是否是由 reactive 或者 readonly 创建
- isReactive
- 判断某个对象是否通过 reactive 创建
- isReadOnly
- 判断某个对象是否通过 readonly 创建
- isRef
- 判断某个对象是否是一个ref对象
转换
-
unref
-
用于处理ref数据,实现等同于使用 isRef进行三目运算符
-
isRef(val) ? val.value : val
-
-
toRef
-
得到某一个响应式数据的某个属性的ref格式数据
-
对 readonly 格式的数据操作后也无法修改
-
const data1 = reactive({ a: 1, b: 2 }); const footRef = toRef(data1,'a') footRef.value++ // 会影响原来的数据 // data.a => 2 //========= const data2 = readonly(data1); const footRef1 = toRef(data2,'a') footRef1.value++ // data2.a => 1
-
-
toRefs
-
把一个响应式对象的所有属性转换为ref格式,然后包装成一个普通对象返回,对象的每个属性都是ref
-
一般用于setup返回数据的进行数据合并
-
setup() { const data1 = reactive({a:1}) const data2 = reactive({b:2}) return { //...data1, // 这样写会让数据失去响应式的能力 //...data2 ...toRefs(data1), ...toRefs(data2), } }
-
降低心智负担
所有的composition function均以ref 的结果返回,以保证setup函数的返回结果不包含reactive和readonly直接产生的数据
composition Api
- 不同于reactivity Api, composition Api提供的函数很多是与组件深度绑定的,不能脱离组件而存在
setup
- 该函数在组件属性被复制后立即执行,早于所有声明周期钩子
- 此时访问this是undefined
- 组件被使用一次就会执行一次,同生命周期一样
- props 是一个对象,包含了所以得组件属性值
- context 是一个对象,提供了组件所需的上下文信息
- attrs 同 vue2的this.$attrs
- slots 同 vue2的this.$slots
- emit 同 vue2的this.$emit
export default {
setup(props,content) {
}
}
生命周期函数
- beforeCreate
- 同vue2 的 beforeCreate,不需要使用,直接在setup内部书写
- created
- 同vue2 的 created,不需要使用,直接在setup内部书写
- onBeforeMount
- 同vue2 的 beforeMount
- onMounted
- 同vue2 的 mounted
- onBeforeUpdate
- 同vue2 的 beforeUpdate
- onUpdated
- 同vue2 的 updated
- onBeforeUnmount
- 同vue2 的 beforeDestroy,改名了,option Api 也改成了beforeUnmount
- onUnmounted
- 同vue2的 destroy,option Api 也改成了unmounted
- onErrorCaptured
- 同vue2的 errorCaptured
- onRenderTracked
- 新增api
- 当前模板被预编译成虚拟节点时,有收集依赖的过程,每当收集了一个依赖时就会触发该函数
- 通过 函数的
e .target就能拿到对应的属性
- onRenderTriggered
- 新增api
- 同上面的api一样,可以追踪目标的重新渲染,可以获取导致模板重新渲染的响应式属性
composition api 相比 option api (vue2的写法)有哪些优势
- 为了更好的逻辑复用和代码组织
- 有了composition api配个reactivity api可以在组件内部进行更加细颗粒度的控制,使得组件不同的功能高度聚合,提升了代码的可维护性,对于不同组件的相同功能,也能更好的复用
- 更好的类型推导
- 相比于option api,composition api 中没有了指向奇怪的this,所以得api变得更加函数式,这有利于和ts深度配合
数据共享
vuex方案
-
版本vuex@4.x
-
两个重要变动
- 去掉了构造函数Vuex,二十一createStore创建仓库
- 为了配合 composition api 新增了 useStore 函数获得仓库对象
-
// store.ts 创建命名空间的store import { createStore } from "vuex"; import loginUser from "./loginUser"; export default createStore({ modules: { loginUser }, }); // loginUser.ts import { delay } from "../utils"; export default { namespaced: true, state: { user: null, loading: false, }, mutations: { setUser(state: any, payload: any) { state.user = payload; }, setLoading(state: any, payload: any) { state.loading = payload; }, }, actions: { async login({ commit }: any) { commit("setLoading", true); await delay(1000); const user = await Promise.resolve({ name: "123" }); commit("setUser", user); commit("setLoading", false); return user; }, async loginOut({ commit }: any) { commit("setLoading", true); await delay(1000); await Promise.resolve(true); commit("setUser", null); commit("setLoading", false); }, async whoAmi({ commit }: any) { commit("setLoading", true); await delay(1000); const user = await Promise.resolve({ name: "123" }); commit("setUser", user); commit("setLoading", false); }, }, }; -
<script lang="ts" setup> import { computed } from 'vue'; import { useStore } from 'vuex' const store = useStore() const userInfo = computed(() => store.state.loginUser.user) const loading = computed(() => store.state.loginUser.loading) function userLogin() { store.dispatch('loginUser/login') } function userLoginOut() { store.dispatch('loginUser/loginOut') } </script> <template> {{ userInfo?.name }} <button @click="userLogin"> 点击登录</button> <button @click="userLoginOut"> 退出登录</button> <div v-if="loading"> 正在加载中 </div> </template>
global state
- 由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,可以制作多个全局响应式数据
- 通过响应式api创建第一个单例的全局变量,通过readonly只读元数据,通过暴露api修改原始数据
import { readonly, ref } from "vue";
export function loginHook() {
const isLoginRef = ref<boolean>(false);
const setLoginStatus = (val: boolean) => {
isLoginRef.value = val;
};
return { isLoginRef: readonly(isLoginRef), setLoginStatus };
}
Provide & Inject
-
在vue2中提供了这两个配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用
-
vue3中对这两个配置进行了函数化,对外保留两个方法
- 也可以在构造vue应用的时候注入整个应用都需要的数据,写法跟插件注册一样
-
// 父子组件使用 provide('foo', ref(1)) // const foo = inject('foo') -
// 应用使用 createApp(App).provide('foo1',ref(1)).mount(#app)// 独立创建全局 // concatProvide.ts import { createProvider as provideLoginUserStore } from "./provider"; export default function provideStore(app: any) { provideLoginUserStore(app); return app; } // main.ts 挂载 createApp(App).use(provideStore).mount("#app"); // provider.ts 创建 import { inject, reactive, readonly } from "vue"; import { delay } from "../utils"; const key = Symbol("foo"); export function createProvider(app: { provide: (arg0: any, arg1: any) => void; }) { const state = reactive({ user: null, loading: false, }); const login = async (user: any) => { state.loading = true; await delay(1000); state.user = user; state.loading = false; }; const loginOut = async () => { state.loading = true; await delay(1000); state.user = null; state.loading = false; }; app.provide(key, { state: readonly(state), login, loginOut, }); } export function useProvide(defaultValue?: any) { return inject(key, defaultValue); } // 使用 const provide = useProvide() function userLogin() { provide.login({ name: 1 }) } function userLoginOut() { provide.loginOut() } const user = computed(() => provide.state.user)
对比
| vuex | global state | provide & inject | |
|---|---|---|---|
| 组件数据共享 | √ | √ | √ |
| 调试工具 | √ | √ | × |
| 可否脱离组件使用 | √ | × | √ |
| 状态树 | √ | 自行决定 | 自行决定 |
| 量级 | 重 | 轻 | 轻 |
script setup
-
是vue3里面提供的语法糖,会将setup简化在当前script执行
-
以往在setup返回的属性和方法不需要再手动返回,在setup标签内会自动暴露
-
对于一些部分还是存在差异化
-
props定义使用 defineProps
-
const props = defineProps({ name:String, })
-
-
emit 触发 使用 defineEmits
-
const emit = defineEmits() emit('update',11)
-
-
组件属性暴露使用 defineExpose
-
const expose = defineExpose() expose({name:'aaa'}) // 不传值表示不暴露任何属性
-
-
-
使用ref操作子组件
-
需要子组件使用 defineExpose 暴露属性
-
const childRef = ref() setTimeout(() => { console.log(childRef.value) }, 2000); </script> <template> <Block5 ref="childRef" /> </template>
-
源码解析
reactive
- packages/reactivity/src/reactive.ts
// 对外暴露的api
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 判断是否已经是readonly对象 对象属性上有没有这个属性 __v_isReadonly
if (isReadonly(target)) {
return target
}
// 不是的话 开始创建reactObject
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
function createReactiveObject(
target: Target, // 目标对象
isReadonly: boolean, // 是否是readonly对象
baseHandlers: ProxyHandler<any>, // 根据不同的api传入的不同的proxyhandler
collectionHandlers: ProxyHandler<any>, // set 和 map这种集合类型使用的handler
proxyMap: WeakMap<Target, any>, // 不同api收集的proxy对象集合
) {
// 判断是不是对象 reactive只支持对象
if (!isObject(target)) {
if (__DEV__) {
warn(
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
target,
)}`,
)
}
return target
}
// 如果已经是 proxy,判断对象是否有该属性 __v_raw
// 如果是readonly对象 需要进行下一步操作
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果proxymap 存在该对象,直接获取后返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 读取对象类型,判断对象是否是Object
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 生成proxy
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
// 收集对应的对象的proxy
proxyMap.set(target, proxy)
return proxy
}
mutableHandlers
export const mutableHandlers: ProxyHandler<object> =
/*@__PURE__*/ new MutableReactiveHandler()
BaseReactiveHandler
// hanlder基础类
class BaseReactiveHandler implements ProxyHandler<Target> {
// 初始化属性 readonly(只读) 和 shallow(还是reactive对象)
constructor(
protected readonly _isReadonly = false,
protected readonly _isShallow = false,
) {}
// 定义get方法 给其它的handler 使用
get(target: Target, key: string | symbol, receiver: object): any {
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
const isReadonly = this._isReadonly,
isShallow = this._isShallow
//
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return isShallow
} else if (key === ReactiveFlags.RAW) {
if (
receiver ===
(isReadonly
? isShallow
? shallowReadonlyMap
: readonlyMap
: isShallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
// 获取原始对象
return target
}
// early return undefined
return
}
const targetIsArray = isArray(target)
if (!isReadonly) {
let fn: Function | undefined
// 调用arrayInstrumentations 处理数组方法
if (targetIsArray && (fn = arrayInstrumentations[key])) {
return fn
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 获取值
const res = Reflect.get(
target,
key,
isRef(target) ? target : receiver,
)
// 判断symbol类型
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// 记录操作的属性值
track(target, TrackOpTypes.GET, key)
}
if (isShallow) {
return res
}
if (isRef(res)) {
// 判断需要返回value还是res
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// 返回包装类型
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
MutableReactiveHandler
- 和shallow的区别是在于二级对象不再是proxy,使用shallowReactive包装的数据只有第一层是响应式数据
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(isShallow = false) {
super(false, isShallow)
}
set(
target: Record<string | symbol, unknown>,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
// 获取原始值
let oldValue = target[key]
// 不是reactive值
if (!this._isShallow) {
// 原来的值是readonly
const isOldValueReadonly = isReadonly(oldValue)
// 不是reactive值也不是readonly 可以修改 使用toRaw获取原始值
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// 不是数组原来的数据是ref ,新的数据不是ref
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
// 修改ref的value
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 如果是数组 要判断数组中有没有这个值
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(
target,
key,
value,
isRef(target) ? target : receiver,
)
// 判断代理对象是原始对象原型上的属性
if (target === toRaw(receiver)) {
if (!hadKey) {
// 触发track属性
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
deleteProperty(
target: Record<string | symbol, unknown>,
key: string | symbol,
): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
has(target: Record<string | symbol, unknown>, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// 不是symbol 或者没有收集这个symbol
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// 收集track
track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY,
)
return Reflect.ownKeys(target)
}
}
mutableCollectionHandlers
- 处理set 和 map 集合类型的handler
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*@__PURE__*/ createInstrumentationGetter(false, false),
}
createInstrumentationGetter
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// 创建一个新的instrumentations
const instrumentations = createInstrumentations(isReadonly, shallow)
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes,
) => {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
// 判断该属性是否在instrumentations中存在,存在读取instrumentations的属性
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver,
)
}
}
createInstrumentations
function createInstrumentations(
readonly: boolean,
shallow: boolean,
): Instrumentations {
// 预置一个 instrumentations 对象
const instrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
// 读取原始对象
const target = this[ReactiveFlags.RAW]
// 再转化一次
const rawTarget = toRaw(target)
// map的key可以是对象,需要进行转换
const rawKey = toRaw(key)
if (!readonly) {
// 判断转换后的key是否一致 使用track收集调用触发的key
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
// 始终需要收集转换后的key
track(rawTarget, TrackOpTypes.GET, rawKey)
}
// 获取原始对象的has函数
const { has } = getProto(rawTarget)
// 确定包装函数
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
// 返回包装后的值
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
},
get size() {
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// track一些属性
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
},
forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
!readonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
},
}
// 判断是否是readonly ,需要使用不同的add set等赋值操作
extend(
instrumentations,
readonly
? {
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
}
: {
add(this: SetTypes, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
},
// ts 声明this类型,并不是参数
set(this: MapTypes, key: unknown, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
},
delete(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined,
undefined,
oldTarget,
)
}
return result
},
},
)
const iteratorMethods = [
'keys',
'values',
'entries',
Symbol.iterator,
] as const
// 针对这几个属性 重写,改为迭代器读取,读一次改一次属性为reactive
iteratorMethods.forEach(method => {
instrumentations[method] = createIterableMethod(method, readonly, shallow)
})
return instrumentations
}
ref
- ref创建后本身就是一个对象,调用属性就会触发proxy的get和set,拿到内部保存的_value
- packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
export function isRef(r: any): r is Ref {
return r ? r[ReactiveFlags.IS_REF] === true : false
}
class RefImpl<T = any> {
_value: T
private _rawValue: T
// 构建发布订阅对象
dep: Dep = new Dep()
// 标记是ref
public readonly [ReactiveFlags.IS_REF] = true
// 初始化标记不是shallow
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
//
constructor(value: T, isShallow: boolean) {
// 记录原始的value
this._rawValue = isShallow ? value : toRaw(value)
// 如果不是shallow 需要使用Reactive包装
this._value = isShallow ? value : toReactive(value)
// 修改shallow标记
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
// 增加订阅
this.dep.track()
}
return this._value
}
set value(newValue) {
const oldValue = this._rawValue
// 获取包装后的值 需要判断shallow 和 readonly
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||
isReadonly(newValue)
// 拿到raw的值
newValue = useDirectValue ? newValue : toRaw(newValue)
// 判断新旧属性是一致
if (hasChanged(newValue, oldValue)) {
// 修改raw的值和 proxy的值
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
// 触发订阅
this.dep.trigger()
}
}
}
}
computed
- packages/reactivity/src/computed.ts
- computed可以接受一个函数或者对象,对象里面会有getter 或者 setter
- 返回的是一个ref,有一个value属性,value属性有get和set方法,只有触发get方法时才会调用传入的函数
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
// 如果是函数的话 就没有setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建computedRef
const cRef = new ComputedRefImpl(getter, setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
export class ComputedRefImpl<T = any> implements Subscriber {
_value: any = undefined
readonly dep: Dep = new Dep(this)
readonly __v_isRef = true
readonly __v_isReadonly: boolean
deps?: Link = undefined
depsTail?: Link = undefined
flags: EffectFlags = EffectFlags.DIRTY
globalVersion: number = globalVersion - 1
isSSR: boolean
next?: Subscriber = undefined
effect: this = this
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
/**
* Dev only
* @internal
*/
_warnRecursive?: boolean
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn
}
}
get value(): T {
// 增加track
const link = __DEV__
? this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
// 在这个函数获取_value的最新值
refreshComputed(this)
// sync version after evaluation
if (link) {
link.version = this.dep.version
}
return this._value
}
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else if (__DEV__) {
warn('Write operation failed: computed value is readonly')
}
}
}
// 比较computed是否dirty 和 version,决定是否需要修改_value 以及是否需要执行函数
export function refreshComputed(computed: ComputedRefImpl): undefined {
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
) {
return
}
computed.flags &= ~EffectFlags.DIRTY
// Global version fast path when no reactive changes has happened since
// last refresh.
if (computed.globalVersion === globalVersion) {
return
}
computed.globalVersion = globalVersion
const dep = computed.dep
computed.flags |= EffectFlags.RUNNING
// In SSR there will be no render effect, so the computed has no subscriber
// and therefore tracks no deps, thus we cannot rely on the dirty check.
// Instead, computed always re-evaluate and relies on the globalVersion
// fast path above for caching.
if (
dep.version > 0 &&
!computed.isSSR &&
computed.deps &&
!isDirty(computed)
) {
computed.flags &= ~EffectFlags.RUNNING
return
}
const prevSub = activeSub
const prevShouldTrack = shouldTrack
activeSub = computed
shouldTrack = true
try {
prepareDeps(computed)
// 执行函数得到结果 修改_value 等到调用value属性才会触发track
const value = computed.fn(computed._value)
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value
dep.version++
}
} catch (err) {
dep.version++
throw err
} finally {
activeSub = prevSub
shouldTrack = prevShouldTrack
cleanupDeps(computed)
computed.flags &= ~EffectFlags.RUNNING
}
}
watchEffect
- packages/runtime-core/src/apiWatch.ts
export function watchEffect(
effect: WatchEffect,
options?: WatchEffectOptions,
): WatchHandle {
return doWatch(effect, null, options)
}