侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue3源码学习-3-实现reactivity

蜗牛
2022-05-27 / 0 评论 / 0 点赞 / 6 阅读 / 10664 字 / 正在检测是否收录...

前言

经过前面的环境搭建以及项目构建,完成了基础的项目框架,下面学习实现vue3的reactivity。

观察官方如何使用

首先修改.npmrc文件

# 解决一个问题 例如vue中有个依赖abc ,那么我们安装了vue就可以直接用abd,有一天vue不依赖abc了,那么你用abc就出错了,未来让这种幽灵依赖以后不出错,就在这里配置羞耻提升
shamefully-hoist = true

我们在vue3-plan上安装vue3

pnpm install vue -w

这个时候发现node_moules中vue的依赖被展开了放在根目录上,在packages/reactivity/index.html上引入vue官方的reactivity

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- <script src="./dist/reactivity.global.js"></script> -->
  <div id="app"></div>

  <!-- 官方的 -->
  <script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
  
  <!-- 自己实现的 -->
  <!-- <script src="./dist/reactivity.global.js"></script> -->
  <script>
    // effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
    // reactive 将数据变成响应式 相当于proxy
    // shallowRactive,readonly,shallowReadonly
    const {effect,reactive} = VueReactivity;

    const state = reactive({name:"david",age:12,address:{num:567}})

    //set 和 map 也可以劫持

    effect(() => {
      document.getElementById("app").innerHTML = `${state.name}今年${state.age}`
    })
    setTimeout(() =>{
      state.age = 13
    },1000)
  </script>
</body>
</html>

通过上面的实验观察发现通过reactive包裹之后的对象,能被监听到变化,然后effect通过监听到变化而触发回调函数,从而打印出上面到语句。并且reactive是能深层检测到对象的改变,当你修改了address里面的num值时也能被监听到变化,这得益于vue3采用到proxyshallowRactiveshallowReadonly如名字,只能监听到表层,以为深处到属性并未做包装。

vue3对比vue2的变化

  • 在Vue2的时候采用defineProperty来进行数据的劫持,需要对属性进行重写gettersetter性能差。
  • 当新增属性和删除属性式就无法监听变化,需要通过$set$delete实现。
  • 数组不采用defineProperty来进行劫持(浪费性能,对所有索引进行劫持会造成性能的浪费)需要对数组单独进行处理。

编写自己的响应式

首先引入的JS文件的html,从官方的引入链接改成引入自己的链接

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- <script src="./dist/reactivity.global.js"></script> -->
  <div id="app"></div>

  <!-- 官方的 -->
  <!-- <script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->
  
  <!-- 自己实现的 -->
  <script src="./dist/reactivity.global.js"></script>
  <script>
    // effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
    // reactive 将数据变成响应式 相当于proxy
    // shallowRactive,readonly,shallowReadonly
    const {effect,reactive} = VueReactivity;

    const state = reactive({name:"david",age:12,address:{num:567}})

    //set 和 map 也可以劫持

    effect(() => {
      document.getElementById("app").innerHTML = `${state.name}今年${state.age}`
    })
    setTimeout(() =>{
      state.age = 13
    },1000)
  </script>
</body>
</html>

为功能划分文件

reactivity/src/下新建effect.ts和reactive.ts文件,对应上面html的2个功能。

# reactive.ts
export function reactive() {}
# effect.ts
export function effect() {}

同时在index.ts中抛出这2个函数

import { effect } from './effect'
import { reactive } from './reactive'

export { effect, reactive }

这样html中引入编译好的JS文件就能获取这2个函数了。

  1. 编写reactive功能

    import { isObject } from '@vue/shared'
    
    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
      if (!isObject(target)) {
        return
      }
    
      // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
      const proxy = new Proxy(target, {
        get(target, key, recevier) {
          return target[key]
        },
        set(target, value, key, recevier) {
          target[key] = value
          return true
        },
      })
      return proxy
    }
    

    上面定义个了proxy代理对象,但是为啥不能如上图编写。看下面的解析

    # 代码省略,和上面的一致
    let target = {
        name : "java",
        get alias(){ //属性访问器写法 es5
            console.log(this); // this  { name: 'java', alias: [Getter] }
            return this.name;
        }
    }
    const proxy = new Proxy(target, {
        get(target, key, recevier) {
        //   return target[key] ;
    
        // 这里控制台会打印出 alias  name
            console.log(key)
            return Reflect.get(target,key,recevier)
        },
        set(target, value, key, recevier) {
          target[key] = value
          return true
        },
      })
      proxy.alias
    //   通过proxy.alias,触发了get,然后return target[key],这个时候访问的是原对象target,traget又访问alias,alias访问了name,但是这个this是源对象,监控不到name,所以引入Reflect(反射)对象,这样访问alias,就回去代理对象上取值,这个时候this就变成了代理对象,那么this.name就又走一次get,这样name就被监控到。 recevier的作用是改变this指向
    

    经过上面的修改,初步得到了一个代理对象的方法。此时如果用户在使用上面的代码的时候,他是这么写的

    const {effect,reactive} = VueReactivity;
     let target = {name:"david",age:13,address:{num:134}};
     let p1 = reactive(target)
     let p2 = reactive(target)
    console.log(p1 === p2) //打印出false,因为每次都new了一个新的Porxy();
    

    那么实际上这2个应该使用一个对象的,为此我们修改一下上面的代码,增加缓存设置,这里用上了WeakMap。弱链接Map,好处在于key为null自动清空对应映射关系,其二是key只能为对象。修改上面的代码为

    import { isObject } from '@vue/shared'
    
    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
    import { isObject } from '@vue/shared'
    const reactiveMap = new WeakMap() // key只能是对象
    // 将数据转化成响应式数据,只能做对象的代理
    export function reactive(target: object) {
      if (!isObject(target)) {
        return
      }
    
      let existingProxy = reactiveMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
      const proxy = new Proxy(target, {
        get(target, key, recevier) {
          // return target[key]
          console.log(key)
          return Reflect.get(target, key, recevier)
        },
        set(target, value, key, recevier) {
          // target[key] = value
          // return true
          return Reflect.set(target, key, value, recevier)
        },
      })
      reactiveMap.set(target, proxy)
      return proxy
    }
    

    这个时候reactive就有了同一个对象代理多次,返回同一个代理。现在又有个新需求,如果代理再一次被代理,那应该返回代理,而不是代理的代理对象。

    let target = {name:"david",age:12,address:{num:567}}
    const state = reactive(target)
    const state2 = reactive(state)
    console.log(state === state2) //false
    

    那么怎么让判断为true呢,早期的处理方式是,WeakMap,正方向存一次,反方向存一次就像
    target -> proxy
    proxy -> target
    最新的处理方法是定一个枚举变量。当你你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE,那么就表示这个是被代理过的,就直接返回 target。

    import { isObject } from '@vue/shared'
    const reactiveMap = new WeakMap() // key只能是对象
    const enum ReactiveFlags {
      IS_RECEIVE = `__v_isReactive`,
    }
    // 将数据转化成响应式数据,只能做对象的代理
    // 同一个对象被代理多次返回同一个代理
    // 代理再次被代理,返回原代理
    export function reactive(target: object) {
      if (!isObject(target)) {
        return
      }
      //
      if (target[ReactiveFlags.IS_RECEIVE]) {
        return target
      }
    
      let existingProxy = reactiveMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
      const proxy = new Proxy(target, {
        // 第一次是普通对象,只是代理,在取值的时候会调用get
        // 下一次你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE,
        // 那么就表示这个是被代理过的,就直接返回 target
        get(target, key, recevier) {
          // return target[key]
          if (key == ReactiveFlags.IS_RECEIVE) {
            return true
          }
          console.log(key)
          return Reflect.get(target, key, recevier)
        },
        set(target, value, key, recevier) {
          // target[key] = value
          // return true
          return Reflect.set(target, key, value, recevier)
        },
      })
      reactiveMap.set(target, proxy)
      return proxy
    }
    
git:[@github/MicroMatrixOrg/vue3-plan/tree/watch]
0

评论区