vue 源码解析+手写 (vue3.x实现)

721 阅读5分钟

前言

vue3x版本即将迎来大面积使用,在此我们一起对vue3x版本的数据响应以及dom更新进行深入理解

vue核心类

  1. 实现渲染器扩展
  2. 模板初始化,数据更新

创建Vue

  1. createApp 用户使用创建一个app实例
  2. createRenderer 渲染器,里面的createApp 是实际的创建函数
  3. createRenderer -> mount 挂载节点,保存用户传入的 data setup 等数据源
  4. createRenderer -> proxy 这里的数据拦截只是为了进行 data 和 setup 的数据优先级问题,不是响应式触发的地点
  5. effect 副作用函数中是真正数据响应时被触发的回调。
  6. compile 在此只是粗暴的进行了dom的创建赋值,理解核心概念思路就行,vue3中使用的是 ast(抽象语法树) ,由template => ast => js(render => 得到虚拟dom)
  const Vue = {
    createApp(options) {
      // 获取渲染器 平台扩展性
      const renderer = Vue.createRenderer({
        querySelector(selector) { // 真正的平台特有操作,由平台传入, 目前实现的是web平台
          return document.querySelector(selector);
        },
        insert(child, parent, anchor) { // anchor 参考节点,如果未传入,insertBefore 等同于 appendChild
          parent.insertBefore(child, anchor || null)
        }
      })
      return renderer.createApp(options);
    },
    createRenderer({ querySelector, insert }) { // 扩展成高阶函数,平台特有操作,可以直接编写对应的渲染器直接进行多平台扩展使用
      // 获得渲染器
      return {
        createApp(options) {
          return {
            mount(selector) {
              // 宿主元素
              const parent = querySelector(selector); // 扩展的平台宿主获取函数
              this.setupState = {};
              this.data = {};
              console.log('selector', parent);
              
              // 获取渲染函数,编译结果
              if (!options.render) { // 如果选项中没有render,那么就获取
                options.render = this.compile(parent.innerHTML);
              }

              if (options.setup) { // 保存setup中的值
                this.setupState = options.setup();
              }

              if (options.data) { // 保存data中的值
                this.data = options.data();
              }

              // 监听数据 vue3中使用 proxy
              this.proxy = new Proxy(this, {
                get(target, key) { // 代理目标和访问的key
                  console.log('target', target);
                  
                  if (key in target.setupState) { // setup优先级高,如果访问的key在其中,直接返回
                    return target.setupState[key];
                  } else {
                    return target.data[key]
                  }
                },
                set(target, key, val) {
                  if (key in target.setupState) { // setup优先级高,如果访问的key在其中,直接更新
                    target.setupState[key] = val
                  } else {
                    target.data[key] = val
                  }
                }
              })

              // 更新函数设置为副作用回调函数即可,options.render中获取了响应式数据,那么effect中会自动收集依赖,当数据变化时,会执行这个回调,重新渲染dom
              this.update = effect(() => {
                // 渲染dom,并追加宿主元素
                const el = options.render.call(this.proxy); // 绑定上下文 能够获取实例中data或者setup中的值
                parent.innerHTML = ""; // vue3中是直接清空
                insert(el, parent); // 平台对应的插入函数
              })

            },

            compile(template) { // 返回render
              return function render() {
                // 描述视图
                const h1 = document.createElement("h1");
                h1.textContent = this.title;
                return h1;
              }
            }

          }
        }
      }
    }
  }

reactive 数据响应

reactive实现了数据响应,依赖收集,是vue3中数据响应的核心机制,比vue2中的更容易理解,且更强大。 使用 Proxy 进行数据监听,无需进行数组拦截额外处理,

  const isObject = v => typeof v === "object" && v !== null;

  // 响应式监听数据
  const reactive = function(obj) {
    if (!isObject(obj)) return obj; // 如果不是对象,不需要代理
    return new Proxy(obj, {
      get(target, key) {
        console.log("get: ", key);
        const res = Reflect.get(target, key); // 处理异常,可以被捕获并且一定会返回一个值
        track(target, key); // 依赖收集触发点 (订阅)
        return isObject(res) ? reactive(res) : res; // 懒处理,如果我访问的数据是一个对象,那么将这个对象也设置成响应式的,而不是一开始全部递归,节省了初始化时间,将深层对象的响应移动到了运行时
      },
      set(target, key, val) {
        console.log("set: ", key);
        const res = Reflect.set(target, key, val);
        trigger(target, key); // 取出依赖收集时放入的更新函数,进行调用更新dom (发布)
        return res;
      },
      deleteProperty(target, key) {
        console.log("delete: ", key);
        const res = Reflect.deleteProperty(target, key);
        trigger(target, key); 
        return res;
      }
    })
  }

effect 副作用函数

接收一个使用了响应式数据的回调,并调用(初始化),当对应回调中访问了响应式数据时,就会进行依赖收集过程(在reactive get 中)

  // 临时保存响应式函数,也就是传入的fn
  const effectStack = [];

  // 副作用函数,如果副作用函数中用了响应式数据,那么尝试建立它们之间的关系
  const effect = function(fn) {
    // fn可能有异常 进行封装捕获 防止程序卡死
    const eff = () => {
      try {
        effectStack.push(fn); // 传入响应式副作用函数。使用数组是因为假如 effect 有嵌套的情况,那么js执行栈就会往深层执行,此时就会出现多个fn需要存储
        fn(); // 执行触发依赖收集过程
      } finally {
        effectStack.pop(); // 收集完成后出栈
      }
    }
    eff(); // 初始化
    return eff;
  }

track 依赖收集

  // 存储依赖关系的map,使用weakmap,好处,可以使用对象当做key,且是弱引用,如内部对象消失或删除,不用担心内存泄露垃圾释放不了
  const targetMap = new WeakMap();

  // 依赖收集
  const track = function(target, key) {
    // 当effect接受的回调中,有响应式数据,那么执行该函数时,会立即触发proxy中的get,get中会立即调用track,这就是建立联系的关键时刻
    const eff = effectStack[effectStack.length - 1]; // 拿到最新的副作用函数
    if (eff) {

      // 获取响应式数据对象 对应的 map (target是一个对象)
      let depMap = targetMap.get(target);
      if (!depMap) { // 不存在则创建
        depMap = new Map();
        targetMap.set(target, depMap); // 存入
      }

      // 获取key 对应的 set(key是target对象中的某个key)
      let deps = depMap.get(key);
      if (!deps) { // 不存在则创建
        deps = new Set();  // set结构可以去重,相同的回调函数只需添加一次
        depMap.set(key, deps);
      }

      // 建立 target key 与 eff 之间的关系,建立之后就可以在需要的地方取出来使用
      deps.add(eff);
    }
  }

trigger 依赖触发 调用对应更新函数

  // 依赖触发
  const trigger = function(target, key) {
    // 获取target对应的map
    const depMap = targetMap.get(target);
    if (depMap) {
      // 通过key获取对应的set
      const deps = depMap.get(key);
      if (deps) { // 如果有,那么遍历执行其中所有的副作用函数(有响应式数据的更新函数)
        console.log(`依赖触发 target: `,target)
        console.log(`依赖触发 key:`, key)
        console.log(`依赖触发 deps: `, deps)
        deps.forEach(dep => dep());
      }
    }
  }

html示例

将以上代码导入,使用createApp创建app实例,定义响应式数据,并挂载。

<!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">
    <h1>{{ title }}</h1>
  </div>
</body>
<script src="./vue3x.js"></script>
<script>
  const { createApp } = Vue;
  
  const app = createApp({
    data () {
      return {
        title: "hello vue3!"
      }
    },
    setup() {
      const state = reactive({
        title: "hello vue3 setup"
      })
      setTimeout(() => {
        state.title = " hello vue3 setup change"
      }, 2000)
      return state;
    }
  })

  app.mount("#app");

</script>
</html>

结语

vue3x对比vue2x,进行了大量的优化更新,如:响应式机制变更(初始化更快,对任何类型都可监听,如Array,Set, Map),函数式(去掉大量的上下文this.xx调用,更好的支持TS,可针对性打包,摇树优化),复用性(compositions api,可以提出大量通用逻辑,更好的复用,对比mixins中不知到底是那个mixin的数据,可以更明确的知道来源,不会命名冲突)等等。 还有更多的好处请前往官方文档仔细阅读体验,并推荐看 vue conf 2021 视频。