vue3笔记

114 阅读18分钟

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完成
    • 数组由于内容比较多,一个个监听比较耗费性能,所以代理的是整个属性,必须出现地址变化才会触发更新
  • 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)
    

对比

vuexglobal stateprovide & 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)
}

watch