vue3响应式原理及简易实现

了解proxy

  • proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。具体可以前往mdn查看
  • proxy为一个构造函数使用new Proxy(obj, {})实例化,接收的参数obj为proxy包装对象,{}中主要是一些操作的处理函数
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
  • vue3的响应式原理主要用到了proxy中的get,set方法,get接收三个参数target,key, receiver,target参数为目标对象,key为获取的属性名,receiver为proxy对象,set参数和get参数一致但多了一个value值
  • proxy中使用Reflect方法,Reflect方法不是一个构造函数,可以将其当成函数调用,Reflect所有的方法和参数都是静态的,proxy上面的方法Reflect上面都有,接收的参数也一致
    export const reactive:Function = <T extends object>(target:T) => {
      return new Proxy(target, {
        get(target, key, receiver) {
          let result = Reflect.get(target, key, receiver)
          return result
        },
        set(target, key, value, receiver) {
          return Reflect.set(target, key, value, receiver)
        },
      })
    }

    了解vue3响应式原理与实现

  • vue3通过包装一个对象对对象的操作进行劫持,取值时会触发proxy的属性读取操作捕捉器 get,存值或对值进行改动时会触发属性设置操作捕捉器set
  • 在get 方法中当我们对一个对象进行取值操作时需要增加一个track方法收集依赖,set方法修改值赋值时需要增加一个trigger方法实现依赖
  • 在此之前需要先通过一个effect闭包方法来收集一个副作用函数,在这个副作用函数里面渲染dom节点,流程代码如下
    import { effect } from './effect.js'
        let obj = reactive({
          name: '张三',
          nickname: '法外狂徒',
          age: 18
        })
    effect(() => {
          let vDom = {
            tag: 'div',
            children: [
              {
                tag: 'h1',
                children: obj.nickname
              },
              {
                tag: 'h2',
                children: obj.name
              },
              {
                tag: 'h3',
                children: obj.age
              },
              {
                tag: 'button',
                props: {
                  onclick: () => {obj.age = 25}
                },
                children: '改变年龄'
              },
            ]
          }
          let app = document.querySelector('#app')
          app.innerHTML = ""
          rander(vDom, app)
        })

  • 在写track方法前我们先需要一个全局的weakMap用来存取数据,而恰好weakMap只能接收一个对象做键,而vue3中的reactive只能接收引用类型的数据,这个waekMap以传入的对象做键,一个Map对象做值,然后这个Map对象的键是weakMap对象的键的键,值是一个Set对象,Set里面存储的是每次监听到get操作保存的副作用函数effect,而effect真正调用的是传入的渲染节点的方法,在proxy监听到get操作时按照如下结构保存值,监听到set操作时取到对应的Set对象使用forEach遍历执行其中的effect方法,具体数据结构图和代码如下
    const targetMap:any = new WeakMap()
    export const track = <T>(target:object, key:T) => {
      let depsMap = targetMap.get(target)
      if(!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
      }
      let deps = depsMap.get(key)
      if(!deps) {
        deps = new Set()
        depsMap.set(key, deps)
      }
      deps.add(activeEffect)
    }
    
    export const trigger = <T>(target:object, key:T) => {
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        return
      }
      let deps = depsMap.get(key)
      deps.forEach((fn:Function) => fn());
    }
    import {track, trigger} from './effect'
    
    let isObj:Function = <T>(obj:T) => {
      return obj != null && typeof obj == 'object'
    }
    
    export const reactive:Function = <T extends object>(target:T) => {
      return new Proxy(target, {
        get(target, key, receiver) {
          track(target, key)
          let res = Reflect.get(target, key, receiver)
          if(isObj(res)) {
            return reactive(res)
          }
          return res
        },
        set(target, key, value, receiver) {
          let res = Reflect.set(target, key, value, receiver)
          trigger(target, key)
          return res
        },
      })
    }

    遍历渲染dom树

  • 上面的步骤其实已经实现了双向绑定的核心操作,接下来只需要把dom树渲染成真实dom即可完成双向绑定(博主的代码的dom更新是先注销之前根节点下的dom然后重新渲染一遍,以便演示双向绑定效果,在vue3中则是先根据模板编译出dom树(虚拟dom),对其中会动态变化的值打上标记,然后使用diff算法计算出最小性能消耗,只替换更改过的节点)
  • function isObj(obj) {
          let show = obj !== null && typeof obj === 'object'
          return show
        }
        // vnode是虚拟节点树,container是渲染的根节点
        function rander(vnode, container) {
          let el = document.createElement(vnode.tag)
          //存在方法或者属性
          if(vnode.props) {
            for (const key in vnode.props) {
              if (/^on/.test(key)) {
                //判断是否为方法
                el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
              } else {
                //属性
                el.setAttribute(key, vnode.props[key])
              }
            }
          }
          if(isObj(vnode.children)) {
            // 有子节点
            if(vnode.children instanceof Array) {
              // 为数组遍历
              vnode.children.forEach(item => {
                rander(item, el)
              });
            } else{
              rander(vnode.children, el)
            }
          } else {
            // 没有子节点
            el.innerText = vnode.children
          }
          container.appendChild(el)
        }

    完整代码

  • reactive.ts
    import {track, trigger} from './effect'
    
    let isObj:Function = <T>(obj:T) => {
      return obj != null && typeof obj == 'object'
    }
    
    export const reactive:Function = <T extends object>(target:T) => {
      return new Proxy(target, {
        get(target, key, receiver) {
          track(target, key)
          let res = Reflect.get(target, key, receiver)
          if(isObj(res)) {
            return reactive(res)
          }
          return res
        },
        set(target, key, value, receiver) {
          let res = Reflect.set(target, key, value, receiver)
          trigger(target, key)
          return res
        },
      })
    }

    effect.ts

    let activeEffect:Function;
    export const effect = (fn:Function):void => {
      const _effect = () => {
        activeEffect = _effect
        fn()
      }
      _effect()
    }
    
    const targetMap:any = new WeakMap()
    export const track = <T>(target:object, key:T) => {
      let depsMap = targetMap.get(target)
      if(!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
      }
      let deps = depsMap.get(key)
      if(!deps) {
        deps = new Set()
        depsMap.set(key, deps)
      }
      deps.add(activeEffect)
    }
    
    export const trigger = <T>(target:object, key:T) => {
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        return
      }
      let deps = depsMap.get(key)
      deps.forEach((fn:Function) => fn());
    }

    index.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>
      <div id="app">
      </div>
      <script type="module">
        import { reactive } from './reactive.js'
        import { effect } from './effect.js'
        let obj = reactive({
          name: '张三',
          nickname: '法外狂徒',
          age: 18
        })
        function isObj(obj) {
          let show = obj !== null && typeof obj === 'object'
          return show
        }
        // vnode是虚拟节点树,container是渲染的根节点
        function rander(vnode, container) {
          let el = document.createElement(vnode.tag)
          //存在方法或者属性
          if(vnode.props) {
            for (const key in vnode.props) {
              if (/^on/.test(key)) {
                //判断是否为方法
                el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
              } else {
                //属性
                el.setAttribute(key, vnode.props[key])
              }
            }
          }
          if(isObj(vnode.children)) {
            // 有子节点
            if(vnode.children instanceof Array) {
              // 为数组遍历
              vnode.children.forEach(item => {
                rander(item, el)
              });
            } else{
              rander(vnode.children, el)
            }
          } else {
            // 没有子节点
            el.innerText = vnode.children
          }
          container.appendChild(el)
        }
        effect(() => {
          let vDom = {
            tag: 'div',
            children: [
              {
                tag: 'h1',
                children: obj.nickname
              },
              {
                tag: 'h2',
                children: obj.name
              },
              {
                tag: 'h3',
                children: obj.age
              },
              {
                tag: 'button',
                props: {
                  onclick: () => {obj.age = 25}
                },
                children: '改变年龄'
              },
            ]
          }
          let app = document.querySelector('#app')
          app.innerHTML = ""
          rander(vDom, app)
        })
      </script>
    </body>
    </html>

    注:html记得引用ts编译后的js

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值