Vue3学习有感(仅供自己查阅)

159 阅读7分钟
  1. vue3中的h函数是辅助创建虚拟DOM的工具函数,h函数的返回值就是一个对象。

image.png

  1. vue3中的render函数最终结果是虚拟DOM,而用h函数可以更简单的传参创建,当然也可以直接用对象描述出来。

image.png

  1. vue3中的组件渲染只有根据render函数的返回值,也就是虚拟DOM,才能渲染页面。

  2. 渲染器:渲染函数,将虚拟DOM渲染成真实DOM

/**
 * 渲染函数,将虚拟DOM渲染成真实DOM
 * @param {*} vnode 虚拟DOM对象
 * @param {*} container 一个真实DOM,渲染器会把虚拟DOM渲染到该挂载点下
 */
function renderer(vnode, container) {
    const el = document.createElement(vnode.tag)

    for (const key in vnode.props) {
        if (/^on/.test(key)) {
            el.addEventListener(key.substring(2).toLowerCase(), vnode.props[key])
        }
    }

    if (typeof vnode.children === 'string') {
        el.appendChild(document.createTextNode(vnode.children))
    } else {
        vnode.children.forEach(child => renderer(child, el))
    }

    container.appendChild(el)
}

  1. 组件本质上就是一组虚拟DOM的集合,他的返回值依旧是虚拟DOM。他是一个对象,有一个render方法,调用render方法的返回值就是虚拟DOM
const myComponent = {
        render() {
          return {
            tag: "div",
            props: {
              onClick() {
                console.log("324234234");
              },
            },
            children: "555",
          };
        },
      };
  1. 编译器的本质就是将写在template里面的声明式UI,编译成渲染函数如下。:
<template>
    <div @click="handler">
        我是div
    </div>
</tamplate>


上述编译成渲染函数后就是:

<script>
    export default {
        methods:{
            handler(){}
        },
        render(){
            return h('div',{onClick:handler},'我是div')
        }
    }
</script>
  1. 所以一个组件需要展示的内容就是通过渲染函数产生的,渲染函数产生的虚拟DOM再由渲染器转化成真实DOM。

  2. Vue3的响应式系统通过proxy来做。当读取一个代理对象时,触发get。设置一个代理对象时,触发set。Vue3中的响应式系统在渲染函数被调用时,就会完成每一个组件的每一个属性的依赖收集。Vue3设计了weakMap -> Map -> Set的数据结构,分别对应了Vue实例 -> 属性 -> 读取了这个属性对应的值的副作用函数(简称依赖),副作用函数就是render函数内的一些函数,因为render函数的返回值是虚拟DOM,虚拟DOM转化成真实DOM时读取了响应式数据。

  3. 当渲染函数调用时,所有的依赖就会被收集,并且以上述的数据结构存储。每一个依赖都是被包装过后的副作用函数,他存在deps属性,用于记录这个依赖都被收集到哪一些依赖集合中,在属性对应的依赖集合收集依赖时,依赖也会收集依赖集合。他们是相互记录的关系。

  4. 当属性对应的值发生变化,那么这些依赖就会被重新触发实现页面的更新。在触发更新前,会将依赖被收集到的所有依赖集合中,删除自己,达到依赖更新的目的,因为可能存在当前的依赖被2个key所对应,但是当一些值变化后,只有1个key对应该依赖,那就就需要依赖更新。

const bucket = new WeakMap();

const data = {
  text: "hello world",
};
let activeEffect;

function effect(fn) {
  const effectFn = () => {
    cleanUp(effectFn);
    activeEffect = effectFn;
    fn();
  };

  effectFn.deps = [];
  effectFn();
}

function cleanUp(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

function track(target, key) {
  if (!activeEffect) {
    return target[key];
  }
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }

  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);

  activeEffect.deps.push(deps);
}

function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectsToRun = new Set(effects)
  effectsToRun && effectsToRun.forEach((fn) => fn());
}

const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
    return true;
  },
});

// console.log(obj);

effect(() => {
  console.log(obj.text, "我被触发了");
});

setTimeout(() => {
  obj.noExist = "big";
}, 1000);

  1. Vue3的render渲染函数实际上被包含在effect函数内执行,所以当render渲染函数执行时,对响应式数据的读取,就完成了依赖收集的过程,依赖就是render渲染函数。如果响应式数据发生变化,会重新调用render函数,生成虚拟DOM树,新旧两个树对比仅将差异的部分渲染到页面上。
effect(()=>{
    Foo.render() // 组件的render渲染函数
})
  1. Vue3多次对同一个属性值的修改,只会引起页面一次更新,这是因为响应式数据实现了调度器,调度器决定了副作用函数调用的方式与时机。模版读取了响应式数据,依赖被记录。重新对响应式数据修改,会触发render函数重新执行,多次对响应式数据的修改,会多次触发render函数重新调用。但由于调度器的存在,每一次触发的render函数会被缓冲到同一个队列中,并且多次开启调用该队列的微任务,通过是否开启过第一次调用的标识,限流调用该队列函数只能开启一次。由于微任务的特性,等到多次同步的对响应式数据的修改完成,然后此时微任务才会触发,一次性调用被缓冲的render函数,达到高性能的更新页面。
const effectStack = [];

const jobQueue = new Set();

const p = Promise.resolve();

// 是否正在刷新队列
// 利用微任务队列,做到了同一个时刻是有一个flushJob任务开启了,并且当同步任务都add进来了,微任务队伍就开始一次性执行
let isFlushing = false;

function flushJob() {
  if (isFlushing) return;
  isFlushing = true;
  p.then(() => {
    jobQueue.forEach((job) => job());
  }).finally(() => {
    isFlushing = false;
  });
}


effect(
  () => {
    console.log(obj.foo);
  },
  {
    scheduler(fn) { // 调度
      jobQueue.add(fn);
      flushJob();
    },
  }
);

  1. 计算属性如何实现,原理是什么? 定义一个计算属性时,实际上是创建了一个特殊的函数,我们称之为“响应式getter函数”,这个函数的作用时追踪函数内所依赖的响应式数据,并在这些数据发生变化时自动重新计算。
  • getter函数会被包装成reactive effect函数,这样就能够追踪它所依赖的响应式数据,并在这些数据发生变化时自动触发重新计算。
  • 计算属性本质是一个函数,但是可以被用属性的方式读取,是因为计算属性返回一个带有value属性的对象,这个value属性具有访问器属性的特性。被读取时,会执行该函数getter函数,依赖的响应式数据与getter包装的reactive effect函数对应,返回的对象的value属性与模版读取时的渲染函数对应,然后Vue会完成各自的依赖收集的过程,他的内部有一个属性标识了是否需要重新计算。
  • 当被依赖的响应式数据发生变化,会将computed函数内部的标识改为需要重新计算,触发value的副作用函数,告诉vue需要重新调用render函数生成新的虚拟DOM树,此时会对computed重新读值。就会重新执行被包装成reactive effect的getter函数,取得最新的值。否则读取的值永远都是缓存值。这利用了闭包的原理,确保只在需要重新计算时才执行计算逻辑。
function computed(getter) {
  let value;
  let dirty = true;
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true;
      trigger(obj, "value");
    },
  });
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      track(obj, "value");
      return value;
    },
  };
  return obj;
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanUp(effectFn);
    activeEffect = effectFn;

    effectStack.push(effectFn);
    const res = fn();

    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    return res;
  };
  effectFn.options = options;
  effectFn.deps = [];
  if (!options.lazy) {
    effectFn();
  }
  return effectFn;
}

  1. watch的实现原理,watch是当要监控的数据发生变化,执行回调函数,其实这里跟调度器的作用很相似,调度器就是当响应式数据发生变化后,控制依赖的执行时机,那么可以利用该机制,去实现watch。首先就是读取响应式数据,只有响应式数据才有依赖,当响应式数据发生变化,然后在调度器内去执行回调函数cb。这样就实现了最简单的watch。监控的数据可能是确切的,也可能是一个大对象,就需要traverse递归函数,递归的将对象的属性都加到Set中,统一读取一遍,这样无论哪个属性变化了,都可以触发回调函数。新旧值的获取,就需要lazy属性控制,利用闭包的方式,将新旧值都保留起来,初次手动执行被包装的副作用函数,可以得到旧值,新值的获取需要在调度器内的回调函数内,然后把旧值改为新值。
function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === "function") {
    getter = source;
  } else {
    getter = () => traverse(source);
  }

  let oldValue, newValue;

  const job = () => {
    newValue = effectFn();
    cb(newValue, oldValue);
    oldValue = newValue;
  };

  const effectFn = effect(() => getter(), {
    scheduler() {
      if (options.flush === "post") {
        const p = Promise.resolve();
        p.then(job);
      } else {
        job();
      }
    },
    lazy: true,
  });

  if (options.immediate) {
    job();
  } else {
    oldValue = effectFn();
  }
}