Deng
Deng
Vue3源码解析 | odjBlog
    欢迎来到odjBlog的博客!

Vue3源码解析

问题: .vue 中的 HTML 是真实的 html 吗? 不是

中间做了什么事情, 让 假的 html 标签节点 被渲染成了 真实的 html 标签节点?

  • 编译时: compiler
  • 运行时: runtime

什么是运行时?

https://cn.vuejs.org/api/options-rendering.html#render
render: 用于编程式地创建组件虚拟 DOM 树的函数。
作用: 代替 template 模板来完成 DOM 的渲染

  • 以下是 运行时 的代码框架
    运行时可以利用 render 把 vnode 渲染成真实 dom 节点
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.js"></script>
</head>
<body>
  <div id="app">
<!--期望目标: <div class="test">hello render</div> -->
  </div>
</body>

<script>
  const {render,h}=Vue
  //生成 vnode
  const vnode=h('div',{
    class:'test'
  },'hello render')
  //拿到承载的容器
  const container= document.querySelector('#app')
  //渲染
  render(vnode,container)
</script>

</html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.js"></script>
</head>
<body>
  <div id="app">
  </div>
</body>

<script>
//根据如下数据渲染出 div
  const vnode={
    type:'div',
    props:{
      class:'test'
    },
    children:'hello render'
  }
  //创建一个 render 函数
  function render(vnode){
    const ele=document.createElement(vnode.type)
    ele.className=vnode.props.class
    ele.innerText=vnode.children
    document.body.appendChild(ele)
  }
  render(vnode)
</script>

</html>

什么是编译时?

如果只靠 运行时 , 那么是没法通过HTML 标签结构的方式,来进行渲染解析的,就需要借助 编译时

  • 主要作用:把 template 中的 html 编译成 render 函数。如何再利用 运行时 通过 render 挂载对应的 dom
  • 编译时可以把 html 的节点, 编译成 render 函数
 <script>
    const { compile, createApp } = Vue;
    //创建一个 html 结构
    const html = `<div class="test">hello compiler </div>`;
    //利用 compile 函数,生成 render 函数
    const renderFn = compile(html);
    console.log(renderFn);
    //创建实例
    const app = createApp({
      //利用 render 函数进行渲染
      render: renderFn,
    });
    //挂载
    app.mount("#app");
  </script>

Vue 是一个 运行时+编译时 的框架

  • vue 通过 compiler 解析 html 模板, 生成 render 函数, 然后通过 runtime 解析 render, 从而挂载真实 dom

为什么这样设计呢?

我们就需要知道 dom 渲染是如何进行的

  • 对于 dom 渲染而言, 可以被分为两部分:
    • 初次渲染,可以叫做 挂载
    • 更新渲染, 可以叫 打补丁

初次渲染

//当 div 的 innerHTML 为空时,
<div id="app"></div>
//渲染如下节点:
<ul>
<li>1</li>
<li>2</li>
</ul>

这样就是初始渲染

更新渲染

li -2 上升到了第一位, 那这样浏览器如何更新这次渲染呢?

两种方式:

  • 删除原有的所有节点, 重新渲染新的节点
    • 好处在于不需要任何比对,需要执行 6 次( 删除 3 次, 重新渲染 3 次) dom 处理即可 【会涉及到更多的 dom 操作】
  • 删除原位置的 li-2, 在新位置插入 li-2
    • 对比 旧节点新节点 之间的差异
    • 根据差异, 删除一个旧节点, 增加一个新节点
      【会涉及到 js 计算+ 少量的 dom 操作】
  <script>
    //相同数量的 DOM 操作和 JS 计算操作, 哪一个更快

    const length = 10000;
    //增加一万个 dom 节点, 查看耗时 3.992923...ms
    console.time("element");
    for (let i = 0; i < length; i++) {
      const div = document.createElement("div");
      document.body.appendChild(div);
    }
    console.timeEnd("element");
 0.
    //增加一万个 js 对象, 查看耗时 0.402099...ms
    console.time("js");
    const divList = [];
    for (let i = 0; i < length; i++) {
      const ele = {
        type: "div"
      };
      divList.push(ele);
    }

    console.timeEnd("js");
  </script>
  • dom 操作要比 js 的操作耗时多的多, 即 dom 操作比 js 更加耗费性能

为什么 vue 要设计成一个运行时+编译时 的框架呢? 答案:

查看文档 : https://cn.vuejs.org/guide/extras/rendering-mechanism.html

  • 1.针对于纯运行时而言:因为不存在编译器,所以我们只能够提供一个复杂的 JS 对象。
  • 2.针对于纯编译时而言:因为缺少运行时,所以它只能把分析差异的操作,放到编译时进行,同样因为省略了运行时,所以速度可能会更快。但是这种方式这将损失灵活性。比如 svelte,它就是一个纯编译时的框架,但是它的实际运行速度可能达不到理论上的速度。
  • 3.运行时+编译时:比如 vue 或 react 都是通过这种方式来进行构建的,使其可以在保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。

什么是副作用?

副作用会有多个吗? 会的

vue3 对 ts 支持友好, 并不是因为 vue3 是 ts 编写的

搭建框架雏形

为 vue 开启 sourceMap 用于 debugger

  • package.json 中 build 命令加-s
    "build": "node scripts/build.js -s ",

导入 ts, prettier

npm install -g typescript@4.7.4
npm install --save-dev prettier

模块打包器: rollup

  • rollup 是一个模块打包器,和 webpack 一样可以将 JavaScript 打包为指定的模块。
  • 但是不同的是,对于 webpack 而言,它在打包的时候会产生许多冗余的代码,这样的一种情况在我们开发大型项目的时候没有什么影响,但是如果我们是开发一个的时候,那么这些冗余的代码就会大大增加库体积,这就不好美好了。
  • 所以说我们需要一个小而美的模块打包器,这就是 rollup:
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
  • 安装插件:
    npm i -D @rollup/plugin-commonjs@22.0.1 @rollup/plugin-node-resolve@13.3.0 @rollup/plugin-typescript@8.3.4

    npm i --save-dev tslib@2.4.0 typescript@4.7.4

配置路径映射

  • tsconfig.json
 // 设置快捷导入
    "baseUrl": ".",
    "paths": {
      "@vue/*": ["packages/*/src"]
    }

响应系统

响应系统的核心设计原则

  • 会影响视图变化的数据称为响应数据,当响应式数据发生变化时, 视图理应发生变化
  • 提出问题:
    • 那么 Vue 中这样的响应性数据是如何进行实现的呢?
    • Vue2 和 Vue3 之间响应性的设计有什么变化吗? 为什么会 产生这种变化呢?

1.JS 的程序性

  • JS 的程序性是一套固定的, 不会发生变化的执行流程,
  • 在这样的一个程序性之下, 我们不可以拿到想要的 50, 想要程序变聪明就需要具备响应性
  <script>
      //定义一个商品对象
      let product = {
        price: 10,
        quantity: 2,
      };
      //总价格
      let total = product.price * product.quantity;
      console.log("总价格:", total);//20
      //修改商品的数量
      product.quantity = 5;
      console.log("总价格:", total);//20? 为啥不是 50
    </script>

2.初步响应性

  • 存在问题: 必须主动在数量发生变化之后, 重新主动执行 effect 才可以得到我们想要的结果, 那么这样太麻烦了
    <script>
      //定义一个商品对象
      let product = {
        price: 10,
        quantity: 2,
      };
      //总价格
      let total = 0;
      //计算总价格
      let effect = () => {
        total = product.price * product.quantity;
      };
      effect();
      console.log("总价格:", total); //20
      //修改商品的数量
      product.quantity = 5;
      effect();
      console.log("总价格:", total); //20
    </script>

3.使用 Vue2 的 Object.defineProperty

  • 会直接在一个对象定义一个新属性, 或者修改一个对象的现有属性, 并返回此对象
 <script>
      //定义一个商品对象
      let quantity = 2;
      let product = {
        price: 10,
        quantity: quantity,
      };
      //总价格
      let total = 0;
      //计算总价格
      let effect = () => {
        total = product.price * product.quantity;
      };
      //第一次打印
      effect();
      console.log("总价格:", total); //20

      Object.defineProperty(product, "quantity", {
        set(newVal) {
          console.log("setter");
          quantity=newVal
          effect()
        },
        get() {
          console.log("getter");
          return quantity
        },
      });
    </script>

存在一个致命的缺陷

vue2 官网存在这样一段描述

由于 JavaScript 的限制,Vue2 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

vue2 有以下响应性的限制:

  • 当为 对象 新增一个没有在 data 中声明的属性时,新增的属性 不是响应性的
  • 当为 数组 通过下标的形式新增一个元素时,新增的元素不是响应性的

想要搞明白这个原因,那就需要明白官网所说的由于 JavaScript 的限制指的是什么意思。

我们知道:

  • 1.vue2 是以 Object.defineProperty 作为核心 API 实现的响应性
  • 2.Object.defineProperty 只可以监听 指定对象 的指定属性的 getter 和 setter
  • 3.被监听了 getter 和 setter 的属性, 就被叫做 该属性具备了响应性

那么这就意味着:我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。

但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty 来监听 getter 和 setter,所以新增的属性将失去响应性
(那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现)

Vue3 的响应性核心 API: proxy

Proxy 和 Object.defineProperty 存在一个非常大的区别, 那就是:

proxy

  • 1.proxy 将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
  • 2.当想要修改对象的指定属性时,我们应该使用代理对象进行修改
  • 3.代理对象的任何一个属性都可以触发 handler 的 getter 和 setter

Object.defineProperty

  • 1.Object.defineProperty 为 指定对象的指定属性 设置属性描述符
  • 2.当想要修改对象的指定属性时, 可以使用原对象进行修改
  • 3.通过属性描述符, 只有 被监听 的指定属性, 才可以触发 getter 和 setter

所以当 Vue3 通过 Proxy 实现响应性核心 API 之后, vue3 将 不会 再存在新增属性时失去响应性的问题

   <script>
      //定义一个商品对象
      let product = {
        price: 10,
        quantity: 2,
      };
      //product: 被代理对象
      //proxyProduct: 代理对象 (只有代理对象才会触发 setter, getter)
      const proxyProduct = new Proxy(product, {
        set(target ,key, newVal,receiver) {
          // console.log(target ,key, newVal,receiver);
          // console.log("setter");
          target[key]=newVal
          effect()
          return true
        },
        get(target ,key, receiver) {
          // console.log(target ,key, receiver,);
          // console.log("getter");
          return target[key]
        },
      });

      //总价格
      let total = 0;
      //计算总价格
      let effect = () => {
        total = proxyProduct.price * proxyProduct.quantity;
      };
      effect();
      console.log("总价格:", total); //20
    </script>

proxy 的最佳拍档:Reflect---拦截 js 对象操作

Reflect 属性, 多数时候会与 proxy 配合进行使用在 MDN Proxy 的例子中, Reflect 也有对此出现。

      const odj = {
        name: "张",
      };
      console.log(obj.name)//张三
      console.log(Reflect.get(obj,'name'))//张三

由以上代码可以发现,两次打印的结果是相同的。这其实也就说明了
Reflect.get(obj,'name') 本质上 和 obj.name 的作用相同。

那为啥还需要 Reflect 呢? 其还有第三个参数 receiver, 官网介绍:如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。(Reflect 可以修改 this 指向)

    <script>
      const p1 = {
        lastName: "张",
        firstName: "三",
        //通过 get 标识符标记, 可以让方法的调用像属性的调用一样
        get fullName() {
          return this.lastName + this.firstName;
        },
      };

      const p2 = {
        lastName: "李",
        firstName: "四",
        get fullName() {
          return this.lastName + this.firstName; //  ↓---触发 2 次
        },
      };
      /**
       * 第三个参数 receiver 在对象指定了 getter 时表示为 this
       * 利用 p2 作为第三个参数 receiver, 以此来修改 fullName 的打印结果。
       * 即:此时触发的 fullName 不是 p1 的而是 p2 的
       * */
      console.log(Reflect.get(p1, "name", p2)); //李四

      const proxy = new Proxy(p1, {
        //target:被代理对象    receiver:代理对象
        get(target, key, receiver) {
          console.log("getter 行为被触发");
          // return target[key];//1 次
          return Reflect.get(target, key, receiver); //触发 3 次
        },
      });
      console.log(proxy.fullName); //张三  ---↑触发 1 次
      //思考:使用 return target[key]; getter 行为应该被触发几次? 3 次, 实际触发一次
    </script>

总结

当我们期望监听代理对象的 getter 和 setter 时, 不应该使用 target[key], 因为它在某些时刻(比如 fullName) 下是不可靠的。应该使用 Reflect, 借助它的 get 和 set 方法, 使用 receiver ( proxy 实例 ) 作为 this, 已达到期望的结果(触发三次 getter)

初见 reactivity 模块

阅读源码????

构建 reactive 函数, 获取 proxy 实例

  • 整个 reactive 函数,本质上是返回了一个 proxy 实例
    1.创建 packages/reactivity/src/reactive.ts 模块
import { mutableHandlers } from './baseHandlers'

/**
 * 响应性 Map 缓存对象
 * key:target
 * val:proxy
 */
export const reactiveMap = new WeakMap<object, any>()

/**
 * 为复杂数据类型, 创建响应式对象
 * @param target 被代理的对象
 * @returns 代理对象
 */
export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, reactiveMap)
}

/**
 * 创建响应式对象
 * @param target 被代理对象
 * @param baseHandlers handler
 * @param proxyMap
 */
function createReactiveObject(
  target: object,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
  //如果该实例已经被代理, 则直接读取即可
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  //未被代理则生成 proxy 实例
  const proxy = new Proxy(target, baseHandlers)

  //缓存代理对象
  proxyMap.set(target, proxy)
  return proxy
}

2.创建 packages/reactivity/src/baseHandlers.ts

/**
 * 响应性的 handler
 */
export const mutableHandlers: ProxyHandler<object> = {}

3.创建 packages/reactivity/src/index.ts ,作为 reactivity 的入口模块

export { reactive } from './reactive'

4.创建 packages/vue/src/index.ts ,导入 reactivity 模块

export { reactive } from '@vue/reactivity'

5.执行 build 打包, 生成 vue.js
6.创建测试用例

WeakMap 和 Map 有什么区别?

核心共同点:都是{key,value}的结构对象

对于 WeakMap 而言,

  • key 必须是对象
  • key 是弱引用的

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

创建 createGettter 和 createSetter

packages/reactivity/src/baseHandlers.ts

/**
 * 响应性的 handler
 */
import { track, trigger } from './effect'

const get = createGetter()
function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)
    track(target, key)
    return res
  }
}

const set = createSetter()
function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, key, value)
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set
}

设置热更新

"dev": "rollup -c -w",

构建 effect 函数, 生成 ReactiveEffect

export function effect<T = any>(fn: () => T) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}
  run() {
   //当前被激活的 effect 实例
    activeEffect = this
    return this.fn() //完成第一次 getter 行为的触发
  }
}
  • 成功渲染了数据到 html 中
<body>
<div id="app"></div>
</body>
<script>
    const { reactive,effect } = Vue
    const obj=reactive({
        name:'张三'
    })
    effect(()=>{
        document.querySelector('#app').innerText=obj.name
    })
</script>

track 和 trigger

由 baseHandlers.ts 中代码可知: 当触发 getter 行为时, 其实我们会触发 track 方法, 进行依赖收集, 当触发 setter 行为时, 会触发 trigger 方法, 来触发依赖

  • 响应性指的是: 当响应性数据触发 setter 时执行 fn 函数,
  • 想要达到这样的目的, 就必须在getter 时能够收集当前的 fn 函数, 以便 setter 的时候可以执行对应的 fn 函数

但是对于收集而言, 如果仅仅是把 fn 存起来还是不够的, 我们还需要知道, 当前的这个 fn 是哪个响应式数据对象哪个属性对应的, 只有这样, 我们才可以在 该属性触发 setter 的时候, 准确的执行响应性

如何进行依赖收集

WeakMap :
key: 响应性对象
value: Map 对象
key: 响应性对象的指定属性
value:指定对象的指定属性的执行函数

//effect.ts
type KeyToDepMap = Map<any, ReactiveEffect>
/**
 * 收集所有依赖的 WeakMap 实例
 * key: 响应性对象
 * value: Map 对象
 *    key: 响应性对象的指定属性
 *      value:指定对象的指定属性的执行函数
 */
const targetMap = new WeakMap<any, KeyToDepMap>()

/**
 * 收集依赖
 * @param target WeakMap 的 key
 * @param key key 代理对象的 key, 当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  if (!activeEffect) return
  //尝试从 targetMap 中, 根据 target 获取 map
  let depsMap = targetMap.get(target)
  //如果获取到的 map 不存在,则生成新的 map 对象, 并把该对象赋值给对应的 value
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  //为指定 map, 指定 key, 设置回调函数
  depsMap.set(key, activeEffect)
  console.log(targetMap)
}

trigger 触发依赖

/**
 * 触发依赖
 * @param target WeakMap 的 key
 * @param key key 代理对象的 key, 当依赖被触发时,需要根据该 key 获取
 */
export function trigger(target: object, key: unknown, newValue: unknown) {
  //依据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  //依据 key, 从 depsMap 中取出 value, 该 value 是一个 ReactiveEffect 类型的数据
  const effect = depsMap.get(key) as ReactiveEffect
  if (!effect) {
    return
  }
  //执行 effect 中保存的 fn 函数
  effect.fn()
}
  • 这样就可以触发 setter 时, 执行保存的 fn 函数了

构建 Dep 模块, 处理一对多的依赖关系

 <div id="app">
    <p id="p1"></p>
    <p id="p2"></p>
</div>
 effect(()=>{
    document.querySelector('#p1').innerText=obj.name
    })
    effect(()=>{
    document.querySelector('#p2').innerText=obj.name
    })

name 属性对应两个 DOM 的变化, 但是 p1 的更新渲染是无效的
我们期望: 一个 key 可以对应多个有效的 effect 函数的话, 应该可以构建一个 Set 类型的对象, 作为 Map 的 value

reactive('张三')不能接收简单数据类型

  • 对于我们 reactive 的 effect 而言, 最终生成一个 proxy 实例, 其中第一个参数必须是一个对象(被代理对象), 如果是简单数据类型, 是没法代理的

对 reactive 的属性进行解构, 不会具备响应性

<script>
    const { reactive,effect } = Vue
    const obj=reactive({
        name:'张三'
    })
    let {name}=obj
    console.log('name',name);

    effect(()=>{
        document.querySelector('#app').innerText=name
    })
    setTimeout(()=>{
        obj.name='李四'

    console.log('obj',obj);
    },2000)
</script>

reactive 总结

对于 reactive 的响应性函数而言,

  • 1.是通过 proxy 的 setter 和 getter 来实现的数据监听
  • 2.需要配合 effect 函数进行使用
  • 3.基于 WeakMap 完成的依赖收集和处理
  • 4.可以存在一对多的依赖关系

同时我们也了解了 reactive 函数的不足:

  • 1.reactive 只能对复杂数据类型进行使用
  • 2.reactive 的响应性数据,不可以进行解构

因为 reactive 的不足,所以 vue3 又为我们提供了 ref 函数构建响应性,那么:

  • 1.ref 函数的内容是如何进行实现的呢?
  • 2.ref 可以构建简单数据类型的响应性吗?
  • 3.为什么 ref 类型的数据,必须要通过.value 访问值呢?

ref 的响应性

如果 ref 传递过来的 value 是一个对象, 那么当前 ref 的响应性本质上是通过 reactive 实现的; 如果不是对象, 则原样将 value 返回

源码阅读


构建 ref 简单数据类型响应性

详情看代码实现

class RefImpl<T> {
  private _value: T
  private _rawValue: T
  ...
  constructor(
    value: T,
    public readonly __v_isShallow: boolean
  ) {
  ...
    //原始数据
    this._rawValue = value
  }
...
  /**
   * newValue 新数据
   * this._rawValue 为旧数据(原始数据)
   * 对比两个数据是否发生改变
   */
  set value(newValue) {
    if (hasChanged(newValue, this._rawValue)) {
      //更新原始数据
      this._rawValue = newValue
      //更新 .value 的值
      this._value = toReactive(newValue)
      //触发依赖
      triggerRefValue(this)
    }
  }
}
//为 ref 的 value 进行触发依赖工作
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
  • 2.在 packages/shared/src/index.ts 中,新增 hasChanged 方法
/**
 * 对比两个数据是否发生改变, 如果发生改变则返回 true
 */
export const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)

ref 总结

  • 简单数据类型, 不具备数据监听的概念, 即本身并不是响应性的。只是因为 vue 通过了 set value()的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数, 来完成了一个“类似于”响应性的结果。

  • 1.ref 函数的内容是如何进行实现的呢?
    答:ref 函数本质上是生成了一个 RefImpl 类型的实例对象,通过 get 和 set 标记处理了 value 函数

  • 2.ref 可以构建简单数据类型的响应性吗?
    答:是的。ref 可以构建简单数据类型的响应性

  • 3.为什么 ref 类型的数据,必须要通过.value 访问值呢?
    答:1.因为 ref 需要处理简单数据类型的响应性, 但是对于简单数据类型而言,它无法通过 proxy 建立代理。
    2.所以 vue 通过 get value()和 set value()定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集触发依赖
    3.所以我们必须通过.value 来保证响应性

watch 和 computed 【复杂!!!】

计算属性 computed

  • 基于其响应式依赖被缓存,并且在依赖的响应式数据发生变化时 重新计算
<script>
  const { reactive,effect,computed } = Vue
  const obj=reactive({
    name:'张三'
  })
  const computedObj= computed(()=>{
    return '姓名:'+obj.name
  })
  effect(()=>{
    document.querySelector('#app').innerText=computedObj.value
  })
  setTimeout(()=>{
    obj.name='李四'
  },2000)
</script>

在以上测试实例中,程序主要执行了 5 个步骤:

  • 1.使用 reactive 创建响应性数据
  • 2.通过 computed 创建计算属性 computedObj,并且触发了 obj 的 getter
  • 3.通过 effect 方法创建 fn 函数
  • 4.在 fn 函数中, 触发了 computed 的 getter
  • 5.延迟触发了 obj 的 setter

computed 的代码在 packages/reactivity/src/computed.ts 中, 我们可以在这里为 computed 函数增加断点.

  • 1.代码进入 computed 函数

  • 2.执行 const onlyGetter=isFunction(getterOrOptions)方法:

    • getterOrOptions 为传入的第一个参数, 因为我们传入的为函数, 所以 onlyGetter=true
  • 3.执行: getter=getterOrOptions, 即: getter 为我们传入的函数

  • 4.执行: setter=NOOP, NOOP 为()=>{}

  • 5.执行: new ComputedRefImpl,创建 ComputedRefImpl 实例。 那么这里的 ComputedRefImpl 是什么呢?

  • 6.进入 ComputedRefImpl

    • a.在构造函数中,可以看到: 创建了 ReactiveEffect 实例,并且传入了两个参数:

      //1.getter:触发 computed 函数时,传入的第一个参数
      //2.匿名函数: 当 this._dirty 为 false 时,会触发 triggerRefValue,我们知道 triggerRefValue 会 **依次触发依赖**
      ()=>{
      //_dirty 表示"脏"的意思, 这里可以理解为: 依赖的响应性数据发生了变化, 计算属性需要重新计算了
      if(!this._dirty){
          this._dirty=true
          triggerRefValue(this)
      }
      }
    • b.而对于 ReactiveEffect 而言,

      • 1.它位于 packages/reactivity/src/effect.ts 文件中
      • 2.提供了一个 run 方法和一个 stop 方法:
        run 方法: 触发 fn, 即传入的第一个参数
        stop 方法: 语义上为停止的意思
      • 3.生成的实例, 我们一般叫做 effect
    • c.执行 this.effect.computed=this, 即effect 实例 被挂载了一个新的属性 computed 为当前的 ComputedRefImpl 的实例.

    • d.ReactiveEffect构造函数执行完成

  • 7.在 computed 中返回了 ComputedRefImpl 实例

由以上代码可知, 当我们在执行 computed 函数时:

  • 1.定义变量 getter 为我们传入的回调函数
  • 2.生成了 ComputedRefImpl 实例, 作为 computed 函数的返回值
  • 3.ComputedRefImpl 内部, 利用了 ReactiveEffect 函数, 并且传入了 第二个参数

computed 的 getter

当 computed 代码执行完成之后, 我们在 effect 中触发了 computed 的 getter:

computedObj.value

根据我们之前在学习 ref 的时候可知,.value 属性的调用本质上是一个get value 的函数调用,而 computedObj 作为 computed 的返回值,本质上是 ComputedRefImpl 的实例, 所以此时会触发 ComputedRefImpl 下的 get value 函数。

  • 1.进入 ComputedRefImpl 下的 get value 函数

  • 2.执行 trackRefValue(self),该方法我们是有过了解的,知道它的作用是: 收集依赖,它接收个 ref 作为参数,该 ref 本质上就是 ComputedRefImpl 的实例:

  • 3.执行 self._dirty=false,我们知道 _dirty 是 的意思,如果 _dirty = true 则会触发执行依赖。在当前(标记为 false 之前),self._dirty=true

  • 4.所以接下来执行 self.effect.run()!,执行了 run 方法,我们知道 run 方法内部其实会触发 fn 函数,即: computed 接收的第一个参数

  • 5.接下来把 self._value = self.effect.run()!, 此时 self._value 的值为 computed 第一个参数(fn 函数)的返回值,即为: 计算属性计算之后的值

  • 6.最后执行 return self._value,返回计算的值

由以上代码可知:

  • 1.ComputedRefImpl 实例本身就没有 代理监听 , 它本质上是一个 get value 和 set value 的触发
  • 2.在每一次 get value 被触发时, 都会主动触发一次 依赖收集
  • 3.根据 _dirty 和 _cacheable 的状态判断, 是否需要触发 run 函数
  • 4.computed 的返回值, 其实是 run 函数执行之后的返回值

ReactiveEffect 的 scheduler

初步分析完成了 computed 的源码执行逻辑, 还存在一些问题。
我们知道对于计算属性而言, 当它依赖的响应式数据发生变化时, 它将重新计算。换句话说:当响应性数据触发 setter 时, 计算属性需要触发依赖.

在上面的代码中,我们知道,当《每一次 getvalue 被触发时,都会主动触发一次依赖收集》,但是触发依赖的地方在哪呢?

根据以上代码可知: 在 ComputedRefImpl 的构造函数中,我们创建了 ReactiveEffect 实例,并且传递了第二个参数,该参数为一个回调函数,在这个回调函数中: 我们会根据的状态来执行 triggerRefValue,即触发依赖,重新计算。

那么这个 ReactiveEffect 第二个参数是什么呢?它会在什么时候被触发,以触发依赖呢?

我们来看一下:

  • 1.进入 packages/reactivity/src/effect.ts 中
  • 2.查看 ReactiveEffect 的构造函数,可以第二个参数为 scheduler
  • 3.scheduler 表示 调度器 的意思, 我们查看 packages/reactivity/src/effect.ts 中 triggerEffect 方法, 可以发现这里进行了调度器的判定:
    function triggerEffect(...){
    ...
    if(effect.scheduler){
    effect.scheduler()
    }
    }

跟踪代码

我们知道延迟两秒之后,会触发 obj.name
即 reactive 的 setter 行为.
所以我们可以在
packaaes/reactivity/src/baseHandlers.ts 中为 set 增加一个断点:

  • 1.进入 reactive 的 setter (注意: 这里是延迟两秒之后 setter 行为)
  • 2.跳过之前的相同逻辑之后,可知,最后会触发:trigger(target,TriggerOpTypes.SET,key,
    value,oldValue)方法
  • 3.进入 trigger 方法:
  • 4.同样跳过之前相同逻辑,可知,最后会触发:triggerEffects(deps[0],eventInfo)方法
  • 5.进入 triggerEffects 方法:
  • 6.



构建 ComputedRefImpl,读取计算属性

packages/reactivity/src/computed.ts

  • 构建 ComputedRefImpl 类, 创建出 computed 方法, 并且能够读取值

computed 的响应性: 初见调度器, 处理脏的状态

如果想要实现响应性, 必须具备两个条件:

  • 1.收集依赖: 已在 get value 中进行
  • 2.触发依赖:
调度器
  • 调度器 scheduler 是一个相对比较复杂的概念,它在 computed 和 watch 中都有涉及, 但是在当前的 computed 实现中, 它的作用还算比较清晰。
  • 所以根据我们秉承的 没有使用就当做不存在 的理念,我们只需要搞清楚,它在当前的作用即可。
  • 此时的 scheduler 就相当于一个回调函数.
  • 在 triggerEffect 只要 effect 存在 scheduler,则就会执行该函数。
_dirty 脏

它只是一个变量, 我们只需要知道: 它为 false 时, 表示需要触发依赖。为 true 时表示需要重新执行 run 方法, 获取数据 即可

computed 的缓存性

computed 区别于 function 最大的地方就是: computed 具备缓存,当多次触发计算实行时,那么计算属性只会计算一次

死循环 的问题
<script>
  const { reactive,effect,computed } = Vue
  const obj=reactive({
    name:'张三'
  })
  const computedObj= computed(()=>{
    console.log('计算属性执行')
    return '姓名:'+obj.name
  })
  effect(()=>{
    document.querySelector('#app').innerText=computedObj.value
    document.querySelector('#app').innerText=computedObj.value
  })
  setTimeout(()=>{
    obj.name='李四'
  },2000)
</script>

运行到浏览器,出现了 死循环 的问题

这个死循环是在 延迟两秒后 出现的,而延迟两秒之后是 obj.name 的调用, 即: reactive 的 getter 行为被触发, 也就是 trigger 方法触发时:

1.为 packages/reactivity/src/effect.ts 中的 trigger 方法增加断点, 延迟两秒之后,进入断点:
2.此时执行的代码是 obj.name='李四', 所以在 target 为{name:'李四'}
3.但是要注意, 此时 targetMap 中, 已经在 收集过 effect 了, 此时的 dep 中包含一个计算属性的 effect:


4.进入到 triggerEffects(dep)方法

死循环 的问题---解决方法
  • packages/reactivity/src/effect.ts
/**
 * 触发 dep 中保存的依赖
 * @param dep
 */
export function triggerEffects(dep: Dep) {
  //把 dep 构建为一个数组
  const effects = isArray(dep) ? dep : [...dep]
  //不再依次触发依赖
  // for (const effect of effects) {
  //     triggerEffect(effect)
  // }
  //而是先触发所有的计算属性依赖, 再触发所有的非计算属性依赖
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect)
    }
  }
}

总结---计算属性 computed

1.计算属性的实例,本质上是一个 ComputedRefImpl 的实例
2.ComputedRefImpl 中通过 dirty 变量来控制 run 的执行和 triggerRefValue 的触发
3.想要访问计算属性的值,必须通过.value,因为它内部和 ref 一样是通过 get value 来进行实现的
4.每次.value 时都会触发 trackRefValue 即:收集依赖
5.在依赖触发时,需要谨记,先触发 computed 的 effect,再触发非 computed 的 effect

响应性的数据监听器 watch

vue 官方文档
watch 可以监听响应式数据的变化, 从而触发指定的函数

  • 源码阅读
<script>
  const { reactive,watch} =Vue
  const obj=reactive({
    name:'张三'
  })

  watch(obj,(value,oldValue)=>{
    document.querySelector('#app').innerHTML=obj.name
  })
  setTimeout(()=>{
    obj.name='李四'
  },2000)
</script>

异步的微任务

Promise

  • 同时因为接下来 同步任务已经执行完成, 所以 异步的微任务 马上就要开始执行, 即接下来我们将会进入 flushJobs 中

  • watch 整体分为了四大块:

    • watch 函数本身
    • reactive 的 setter
    • flushJobs
    • job
  • 整个 watch 还是比较复杂的,主要是因为 vue 在内部进行了很多的 兼容性处理,使代码的复杂度上升了好几个台阶,我们自己去实现的时候会简单很多的。

深入 scheduler 调度系统实现机制

整个调度系统其实包含两部分实现:

  • 1.lazy: 懒执行
//packages/reactivity/src/effect.ts

export interface ReactiveEffectOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
}

export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
  const _effect = new ReactiveEffect(fn)
  if (!options || !options.lazy) {
    _effect.run()
  }
}
  • 2.scheduler: 调度器 [稍微复杂一些]
    • 控制执行顺序
<script>
    const { reactive,effect } = Vue
    const obj=reactive({
       count:1
    })

    effect(()=>{
        console.log(obj.count)
    },{
        scheduler(){
            setTimeout(()=>{
                console.log(obj.count)
            })
        }
    })
    obj.count=2

    console.log('代码运行结束');
</script>
//1  
//代码结束..
//2
  • 控制执行规则
    • packages/runtime-core/src/scheduler.ts
let isFlushPending = false

const resolvedPromise = Promise.resolve() as Promise<any>

let currentFlushPromise: Promise<void> | null = null

const pendingPreFlushCbs: Function[] = []
export function queuePreFlushCb(cb: Function) {
  queueCb(cb, pendingPreFlushCbs)
}
function queueCb(cb: Function, pendingQueue: Function[]) {
  pendingQueue.push(cb)
  queueFlush()
}

function queueFlush() {
  if (!isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
function flushJobs() {
  isFlushPending = false
  flushPreFlushCbs()
}
export function flushPreFlushCbs() {
  if (pendingPreFlushCbs.length) {
    //拷贝去重,类似深拷贝
    let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    pendingPreFlushCbs.length = 0
    for (let i = 0; i < activePreFlushCbs.length; i++) {
      activePreFlushCbs[i]()
    }
  }
}
<script>
    const { reactive,effect,queuePreFlushCb } = Vue
    const obj=reactive({
        count:1
    })

    effect(()=>{
        console.log(obj.count)
    },{
        scheduler(){
            queuePreFlushCb(()=>console.log(obj.count));

        }
    })
    obj.count=2
    obj.count=3
</script>

watch 数据监听器

packages/runtime-core/src/apiWatch.ts

watch 数据监听器的依赖收集

  • 解决问题---"响应式数据的改变并不会引起 watch 的触发"
/**
 * 本质上是拿 value, 出发一次 getter 行为
 * @param value
 */
export function traverse(value: unknown) {
  if (!isObject(value)) {
    return value
  }
  for (const key in value as object) {
    traverse((value as object)[key])
  }
  return value
}

runtime 运行时

  • 运行时: 即把 VNode 渲染到页面中。

运行时核心设计原则

HTML DOM 节点树与虚拟 DOM 树

DOM 树

虚拟 DOM

  • 虚拟 DOM 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓, 随后在许多不同的框架中都有不同的实现, 当然也包括 Vue
  • 在运行时 runtime, 渲染器renderer会遍历整个虚拟 DOM 树, 并据此构建真实的 DOM 树, 这个过程可以叫做 挂载 mount
  • 当这个 VNode 对象发生变化时, 那么我们会对比 旧的 VNode 和新的 VNode 之间的区别, 找出它们之间的区别, 并应用这其中的变化到真实的 DOM 上. 这个过程被称为更新 patch

挂载和更新

  • 挂载案例
<body>
  <div id="app"></div>
</body>
<script>
  // <div>hello render</div>
  const vnode = {
    type: 'div',
    children: 'hello render'
  }
  function render(oldVNode, newVNode, container) {
    if (!oldVNode) {
      mount(newVNode, container)
    }
  }
  //挂载函数
  function mount(vnode, container) {
    //根据 type 生成 element
    const ele = document.createElement(vnode.type)
    //把 children 赋值给 ele 的 innerText
    ele.innerText = vnode.children
    //把 ele 作为子节点插入 body 中
    container.appendChild(ele)
  }
  render(null, vnode, document.querySelector('#app'))
</script>
  • 更新案例
  • 在 patch 函数中, 我们先 删除了旧的 VNode, 然后创建了一个新的 vnode,
<script>
  // <div>hello render</div>
  const vnode = {
    type: 'div',
    children: 'hello render'
  }
  // <div>patch render</div>
  const vnode2 = {
    type: 'div',
    children: 'patch render'
  }
  function render(oldVNode, newVNode, container) {
    if (!oldVNode) {
      mount(newVNode, container)
    } else {
      patch(oldVNode, newVNode, container)
    }
  }
  function mount(vnode, container) {
    const ele = document.createElement(vnode.type)
    ele.innerText = vnode.children
    container.appendChild(ele)
  }
  //将之前内容清空
  function unmount(container) {
    container.innerHTML = ''
  }
  function patch(oldVNode, newVNode, container) {
    unmount(container)
    const ele = document.createElement(newVNode.type)
    ele.innerText = newVNode.children
    container.appendChild(ele)
  }
  render(null, vnode, document.querySelector('#app'))

  setTimeout(() => {
    render(vnode, vnode2, document.querySelector('#app'))
  }, 2000);
</script>

总结

虚拟 DOM

  • 挂载: 运行时渲染器调用渲染函数, 遍历返回的虚拟 DOM 树, 并基于它创建实际的 DOM 节点.
  • 更新: 当一个依赖发生变化后, 副作用会重新运行, 这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树, 将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

h 函数与 render 函数

h 函数

h 函数

  • h 函数本质上就是一个 用来生成 VNode 的函数
    • type: string | Component : 既可以是一个字符串, 也可以是一个 vue 组件定义
    • props?:object | null: 要传递的 prop
    • children?: Children | Slot | Slots: 子节点
<script>
  const { render, h } = Vue
  //生成 vnode
  const vnode = h('div', {
    class: 'test'
  }, 'hello render')
  //拿到承载的容器
  const container = document.querySelector('#app')
  //渲染函数
  render(vnode, container)
</script>
  • 打印 vnode 后, 精简的 vnode

render 函数

render 函数

  • 通过 render 函数, 我们可以: 使用编程式地方式,创建虚拟 DOM 树对应的真实 DOM 树,到指定位置。

运行时核心设计原则---解释

1.runtime-core 与 runtime-dom 的关系, 为什么要这么设计?

在 vue 源码中, 关于运行时的包主要有两个:

  • 1.packages/runtime-core: 运行时的核心代码
  • 2.packages/runtime-dom : 运行时关于浏览器渲染的代码

2.渲染时, 挂载和更新的逻辑处理

构建 h 函数生成 vnode

源码阅读

<body>
  <div id="app">
  </div>
</body>

<script>
  const { h } = Vue
  const vnode = h('div', {
    class: 'test',
  }, 'hello world')
  console.log(vnode);

</script>
  • 在 packages/runtime-core/src/h.ts 为 208 行 const l = arguments.length 增加 debugger

构建 h 函数,处理 ELEMENT+TEXT_CHILDREN 场景

shapeFlag 为 9
创建 packages/shared/src/shapeFlags.ts 写入所有的对应类型

export const enum ShapeFlags {
  /**
   * type=Element
   */
  ELEMENT = 1,
  /**
   * 函数组件
   */
  FUNCTIONAL_COMPONENT = 1 << 1,
  /**
   * 有状态(响应数据)组件
   */
  STATEFUL_COMPONENT = 1 << 2,
  /**
   * children=Text
   */
  TEXT_CHILDREN = 1 << 3,
  /**
   * children=Array
   */
  ARRAY_CHILDREN = 1 << 4,
  /**
   * children=slot
   */
  SLOTS__CHILDREN = 1 << 5,
  /**
   * 组件: 有状态(响应数据)组件 | 函数组件
   */
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
  • 创建 packages/runtime-core/src/h.ts
  • 创建 packages/runtime-core/src/vnode.ts

处理 ELEMENT+ARRAY_CHILDREN 场景

shapeFlag 为 17

源码阅读: h 函数, 组件的本质与对应的 VNode

  • 在 vue 中, 组件本质上是 一个对象或一个函数(少见)
  • 可以直接利用 h 函数+render 函数渲染出一个基本的组件:
    • 创建 h-component.html
<script>
    const { h, render } = Vue
    const component = {
        render() {
            // const vnode1=h('div','这是一个 component')
            // console.log(vnode1);
            // return vnode1

            //直接利用当前打印的 vnode , 绕过 h 的渲染
            return {
                "__v_isVNode": true,
                "type": 'div',
                "children": '这是一个 component',
                "shapeFlag": 9
            }
        }
    }
    // const vnode2=h(component)
    // console.log(vnode2);

    const vnode2 = {
        "__v_isVNode": true,
        "shapeFlag": 4,
        "type": component
    }
    render(vnode2, document.querySelector('#app'))
</script>

实现: 处理组件的 VNode

  • packages/runtime-core/src/vnode.ts 加上 isObject(type)的判定
export function createVNode(type, props, children): VNode {
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : isObject(type)
      ? ShapeFlags.STATEFUL_COMPONENT
      : 0
  return createBaseVNode(type, props, children, shapeFlag)
}

源码阅读: h 函数, 跟踪 Text、Comment、Fragment 场景

  <script>
    //Fragment : 片段  vue3 一个模板中, 可以有多个根节点,就是利用这片段完成的
    const { h, render, Text,Comment,Fragment } = Vue
    // const vnodeText = h(Text, '这是一个 Text')
    // console.log(vnodeText)

    // const vnodeComment=h(Comment,'这是一个 comment')
    // console.log(vnodeComment);
    // render(vnodeComment,document.querySelector('#app'))

    const vnodeFragment=h(Fragment)

    console.log(vnodeFragment);
    render(vnodeFragment,document.querySelector('#app'))
  </script>

实现: Text、Comment、Fragment

  • packages/runtime-core/src/vnode.ts
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
export const Comment = Symbol('Comment')

源码阅读: 对 class 和 style 绑定

Class 与 Style 绑定

完成虚拟节点下的 class 和 style 的增强

packages/shared/src/normalizeProp.ts

import { isArray, isObject, isString } from '@vue/shared'

export function normalizeClass(value: unknown): string {
  let res = ''

  if (isString(value)) {
    res = value
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i])
      if (normalized) {
        res += normalized + ' '
      }
    }
  } else if (isObject(value)) {
    for (const name in value as object) {
      if ((value as object)[name]) {
        res += name + ' '
      }
    }
  }

  return res.trim()
}
  • 案例
<script>
 const {h} =Vue
 const vnode=h('div',{
   class:[
     {
       'red':true,
     },
     {
       'pink':true,
     },
     {
       'blue':false
     }
   ]
 },'增强 class')
 console.log(vnode)
</script>

构建 renderer 渲染器

喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
已稳定运行:3年326天5小时11分