带你从0开始了解vue3核心(响应式)

由于自己工作主要的技术栈是vue,感觉最近自己每天都是代码搬运工,很无聊,就想着去系统学习vue3源码,提升自己对vue的理解能力。但是每次遇到自己不是很懂得知识后,就放弃思考,也就没了继续学习的想法了。

这次我TM一定要把这些内容啃下来,不然直接回家种地。(真的需要逼自己一把,找机会回杭州。)

vue3源码分析

命令式

详细描述做事的过程。关注过程的一种编程范式,他描述了完成一个功能的详细逻辑与步骤。

声明式

不关注过程,只关注结果的范式。 并不关心完成一个功能的详细逻辑与步骤。

  • 命令式的性能 > 声明式的性能

  • 命令式的可维护性 < 声明式的可维护性 (对于用户来说)

所以框架的设计过程其实是一个不断在可维护性和性能之间进行取舍的过程。在保证可维护性的基础上,尽可能的减少性能消耗。

Vue封装了命令式的逻辑,而对外暴露出了声明式的接口。

编译时 compiler

把template中的html编译成render函数。

<div id="app"></div>
  <script>
    const {compile, createApp} = Vue

    const html = `
      <p class="pp">编译时</p>
    `
    const render = compile(html)

    createApp({ 
      render
    }).mount("#app")
  </script>

运行时 runtime

运行时可以利用render把vnode渲染成真实的dom节点。

render函数就是挂载h函数生成的虚拟dom。提供的render属性函数是用来生成虚拟dom树的。(vue2的render option, vue3 setup返回一个函数, comple(template)编译器生成的)

<div id="app"></div>
  <script>
    
    const {render, h} = Vue

    
    const vnode = h("p", {class: "pp"}, "运行时")

    
    const container = document.getElementById("app")
    
    render(vnode, container)
  </script>

const vnode = {
  type: "p",
  props: {
    class: "pp"
  },
  children: "运行时"
}

function render(vnode) {
  const node = document.createElement(vnode.type)
  node.className = vnode.props.class
  node.innerHTML = vnode.children
  document.body.appendChild(node)
}

render(vnode)

对于dom渲染而言,可以分成两个部分

  • 初次渲染,即挂载。

  • 更新渲染,即打补丁。

可以查看vue官网的渲染机制

Vue为啥要使用运行时加编译时

1.针对于纯运行时 而言:因为不存在编译器,所以我们只能够提供一个复杂的 JS对象(难以编写)。即Vnode对象。

2.针对于 纯编译时 而言:因为缺少运行时,所以它只能把分析差异的操作,放到编译时进行,同样因为省略了运行时,所以速度可能会更快。但是这种方式这将损失灵活性。比如 svelte,它就是一个纯编译时的框架,但是它的实际运行速度可能达不到理论上的速度。

3.运行时+编译时:比如 vue 或 react 都是通过这种方式来进行构建的,使其可以在保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。

副作用

副作用指的是:当我们对数据进行 setter或 getter 操作时,所产生的一系列后果。

vue对ts支持友好

为了vue拥有良好的ts类型支持,vue内部其实做了很多事情。定义了很多类型,所以我们编写ts才会很容易。并不是因为vue是ts写的。

写测试用例,调试,看源码

  • 摒弃边缘情况。

  • 跟随一条主线。

在我们测试打断点时,有很多边缘判断,我们只跟着条件为true的逻辑走。

fork vue3仓库到自己的github仓库(可以将自己阅读源码的过程提交到自己的仓库下,方便以后查看),并克隆到本地。然后做一下操作

  • 安装依赖

  • 开启sourcemap。"build": "node scripts/build.js -s"

  • 打包构建

  • packages/vue/example/...中编写测试用例,断点调试看源码。

ts配置项

{
 
 "compilerOptions": {
  
  "rootDir": ".",
  
  "strict": true,
  
  "moduleResolution": "node",
  
  "esModuleInterop": true,
  
  "target": "es5",
  
  "noUnusedLocals": false,
  
  "noUnusedParameters": false,
  
  "resolveJsonModule": true,
  
  "downlevelIteration": true,
  
  "noImplicitAny": false,
  
  "module": "esnext",
  
  "removeComments": false,
  
  "sourceMap": false,
  
  "lib": ["esnext", "dom"],
  
  "baseUrl": ".",
                
  "paths": {
              "@vue/*": ["packages/*/src"]
            }
 },
 
 "include": [
           "packages/*/src"
 ]
}

vue3与vue2的优势

  • 性能更好

  • 体积更小

  • 更好的ts支持,vue提供了很多的接口

  • 更好的代码组织,更好的逻辑抽离。(hooks)

响应式

vue2 Object.defineProperty实现响应式

Object.defineProperty缺陷

  • vue2中对data对象做深度监听一次性递归,性能较差。

  • 由于js限制,无法监听新增、删除属性。需要使用Vue.set, Vue.delete

  • 无法原生监听数组,需要特殊处理。重写了数组的一些方法,让其可以做到响应式。(push()pop()shift()unshift()splice()sort()reverse()) 具体看这里[4]

vue3 Proxy实现响应式

Proxy[5]

handler操作中的方法的receiver参数表示当前代理对象本身。

只有使用当前代理对象操作才会触发handler中对应的拦截方法。

Reflect[6]

Reflect提供的方法(get, set)传入的receiver参数可以替换掉操作对象中的this值。

Reflect.set(target, propertyKey, value[, receiver]) 

receiver这个参数对于vue中的响应式实现具有非常重要的意义。因为只要是读取属性,我们就需要走代理对象,而不是原始对象。

WeakMap[7]

WeakMap引用的对象,是弱引用,并不阻止js的垃圾回收机制。

  • 弱引用:不会影响垃圾回收机制。即:WeakMap的key不再存在任何引用时,会被直接回收。

  • 强引用:会影响垃圾回收机制。存在强引用的对象永远不会 被回收。

例如:在vue源码中使用在保存代理对象,如果当前对象以前被代理过,直接返回代理对象。

响应式依赖函数和对象以及对象属性映射的数据结构

响应式实现

reactive

setter 执行依赖函数, getter 收集依赖函数。

reactive.ts

reactive函数,返回createReactiveObj(target, mutableHandlers, reactiveMap)。

createReactiveObj返回一个Proxy实例。放在WeakMap中判断创建。

effect.ts

内部调用ReactiveEffect类的run方法首次触发,依赖收集。

ReactiveEffect类接收fn作为参数。run方法中将activeEffect全局变量赋值为this。并执行fn。如果有获取属性操作,就会触发Proxy的getter方法。

并触发track, 定义WeakMap数据结构保存依赖函数。(每个对象每个属性都保存依赖收集函数), trigger, 触发收集的依赖函数。建立了targetMap和activeEffect之间的联系。

baseHandlers.ts

定义Proxy操作方法,get(track), set(trigger)等。

dep.ts

createDep 创建一个set对象。用于存储依赖函数。

依赖收集和依赖触发数据结构

下面这个是比较直观的响应式基本代码。就是基于上面的数据结构构建的一套响应式流程。getter收集依赖,setter触发依赖。effect帮助getter收集依赖。

    class Dep {
      constructor() {
        this.subscribers = new Set();
      }

      depend() {
        if (activeEffect) {
          this.subscribers.add(activeEffect);
        }
      }

      notify() {
        this.subscribers.forEach(effect => {
          effect();
        })
      }
    }

    let activeEffect = null;
    function watchEffect(effect) {
      activeEffect = effect;
      effect();
      activeEffect = null;
    }


    
    
    const targetMap = new WeakMap();
    function getDep(target, key) {
      
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }

      
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      return dep;
    }


    
    function reactive(raw) {
      Object.keys(raw).forEach(key => {
        const dep = getDep(raw, key);
        let value = raw[key];

        Object.defineProperty(raw, key, {
          get() {
            
            dep.depend();
            return value;
          },
          set(newValue) {
            if (value !== newValue) {
              value = newValue;
              
              dep.notify();
            }
          }
        })
      })

      return raw;
    }

    
    function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const dep = getDep(target, key);
          dep.depend();
          return target[key];
        },
        set(target, key, newValue) {
          const dep = getDep(target, key);
          target[key] = newValue;
          dep.notify();
        }
      })
    }


断点跟踪vue源码

reactive

createReactiveObject

effect

ReactiveEffect

run

createGetter

track

createSetter

trigger

当我们修改代理对象中的属性,我们间接的在代理对象的set拦截器中修改了被代理对象的属性值。所以代理和被代理对象是同步的。

reactive, effect实现思路

调用reactive(返回proxy代理对象) > 在effect中创建ReactiveEffect实例 > 调用run方法(触发effect传入的回调,有代理对象的getter操作) > 触发代理对象的get方法(track函数收集依赖) > 收集对象对应的属性对应的activeEffect函数 > 触发代理对象的set方法(有代理对象的setter操作) > 触发对象对应的属性对应的activeEffect函数。

reactive局限性
  • 不能处理基本数据类型。因为Proxy代理的是一个对象。

  • 不能进行解构,结构后将失去响应性。因为响应性是通过代理对象进行处理的。结构后就不存在代理对象了,因此就不具备响应式了。

ref

测试用例
const { ref, effect } = Vue

const obj = ref({
  name: "zh"
})

effect(() => {
  
  
  document.getElementById("app").innerHTML = obj.value.name
})

setTimeout(() => {
  
  
  obj.value.name = "oop"
}, 1000)

断点跟踪vue源码
  • RefImpl类创建一个ref实例。

  • RefImpl中判断当传入的是否是一个对象,是则直接调用reactive做响应式。将其代理对象赋值给ref对象的_value属性保存。

  • RefImpl中提供get value,set value方法。在我们处理(读取value属性和为value属性赋值)ref对象时,就会调用对应的方法进行依赖收集和依赖触发。

然后obj.value.name又会触发代理对象name属性的依赖收集。

总结

obj.value就是一个reactive返回的代理对象 ,这里并没有触发set value。不管是对复杂数据类型赋值还是读值,他都值触发refImpl实例的get value。

但是对于简单数据类型就不一样了。 构建简单数据类型时,他并不是通过代理对象去触发依赖收集和依赖触发的。而是通过refImpl中的get value set value主动去收集依赖和触发依赖的,这就是为啥get value 中的trackValue将依赖收集到ref实例的dep中的原因。

ref复杂数据类型

  • 对于 ref 函数,会返回 RefImpl 类型的实例

  • 在该实例中,会根据传入的数据类型进行分开处理

    • 复杂数据类型:转化为 reactive 返回的 proxy 实例。在获取ref.value时返回的就是proxy实例。

    • 简单数据类型:不做处理

  • 无论我们执行 obj.value.name, 还是 obj.value.name=xxx, 本质上都是触发了 get value。

  • 响应性 是因为 obj.value 是一个reactive 函数生成的 proxy

ref简单数据类型

我们需要知道的最重要的一点是:简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。

只是因为 vue 通过了 set value()的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个“类似于”响应性的结果。

我们就知道了网上所说的,ref的响应性就是将其参数包裹到value中传入reactive实现的,了解了这些,我们就可以大胆的说扯淡了。

ref源码实现

xdm,一起学习vue核心思想吧,为了能突破现状,加油啊。天天对着表单表格看,人都傻掉了。

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值