Vue 原理篇

100 阅读6分钟

Vue3 响应式原理

1.vue3响应式原理的实现思路:

(1)创建一个依赖管理对象用于添加依赖和触发依赖(dep)

(2)创建一个reactive函数用于代理传入的对象或数组

(3)创建一个全局的Map,用于存储被代理对象和依赖函数的对应关系

(4)设置代理对象属性描述符的get函数,当触发get时,给全局Map的对应对象添加函数依赖

(5)设置代理对象属性描述符的set函数,当触发set时,触发全局Map的对应对象的依赖

(6)创建watchEffect函数,用于自动侦听响应式数据的读取,如果传入的回调函数读取了全局Map里面的响应式对象的属性,则说明该函数以来了响应式对象,把传入的回调函数赋值给currenteffect,对该函数依赖进行收集

// 当前依赖设置为全局变量,用于解决跨作用域读取的问题
let curentEffect = null

// 依赖管理对象
class Dep{
    constructor(value){
        this.effects = new Set()
        this._val = value
    }
    
    addDep(){
        if(curentEffect){
            this.effects.add(curentEffect)
        }
    }

    notify(){
        this.effects.forEach(item => item(this._val))
    }
}

let targetMap = new Map()

function handleTargetGet(target, key){
    let targetDep = targetMap.get(target)
    if(!targetDep){
        targetDep = new Dep()
        targetMap.set(target, targetDep)
    }
    targetDep.addDep(curentEffect)
    return Reflect.get(target, key)
}

function handleTargetSet(target, key, value){
    Reflect.set(target, key, value)
    const targetDep = targetMap.get(target)
    targetDep._val = value
    if(targetDep){
        targetDep.notify()
    }
}

function reactive(target) {
    return new Proxy(target, {
        get: handleTargetGet,
        set: handleTargetSet
    })
}

function effectWatch(effect) {
    curentEffect = effect
    effect()
    curentEffect = null
}

const proxyObj = reactive({})

effectWatch((value) => {
    console.log('test:', value);
    proxyObj.a
})

proxyObj.a = 1
proxyObj.a = 2

// 打印结果:
// test: undefined
// test: 1
// test: 2

2.参考资料

1.跟尤雨溪一起解读Vue3源码【中英字幕】- Vue Mastery_哔哩哔哩_bilibili

2.手写 mini-vue_哔哩哔哩_bilibili

3.gpt问答

创建虚拟dom

1.vue使用虚拟dom的原因是设计机制导致的,vue的设计机制是数据驱动视图,当数据改变的时候要自动改变视图,如果没有虚拟dom做前后对比,就不知道哪里的数据发生了改变,只能全量更新了,因此vue引入了虚拟dom和diff算法,这样就可以判断出哪里的数据发生了变动,针对变动进行真实dom的更新

function createVNode(tag, props, children) {
    return { tag, props, children }
}

虚拟dom转真实dom

  function mountElement(vnode, container) {
    const { tag, props, children } = vnode;

    // 比较重要的一部,需要把dom元素的地址保存在虚拟dom上,diff操作的时候,挂载新增、删除和修改真实dom的时候要用
    const el = (vnode.el = document.createElement(tag));

    if (props) {
      // 这里还可以判断是否要绑定属性和事件,通过正则匹配指定字符例如 :和 @ 字符
      for (let key in props) {
        const prop = props[key];
        el.setAttribute(key, prop);
      }
    }

    if (typeof children == "string") {
      el.innerText = children;
    } else {
      if (Array.isArray(children)) {
        for (let vnode of children) {
          mountElement(vnode, el);
        }
      }
    }

    container.appendChild(el);
  }

  function render(context) {
    return _h("ul", {}, [
      _h("li", { id: context?.isShow?.value }, context.user.username),
      _h("li", {}, context.user.username),
      _h("li", {}, "赵六"),
    ]);
  }

对比虚拟dom(diff)

  // 暴力解法做了简化,children长度不同,直接操作多出的部分或减少的部分,不进行精细化的对比判断
  function diff(n1, n2) {
    let el = (n2.el = n1.el);
    if (n1.tag !== n2.tag) {
      el.replaceWith(document.createElement(n2.tag));
    } else {
      const { props: oldProps } = n1;
      const { props: newProps } = n2;

      if (oldProps && newProps) {
        Object.keys(newProps).forEach((key) => {
          const newValue = newProps[key];
          const oldValue = oldProps[key];
          if (newValue !== oldValue) {
            el.setAttribute(key, newValue);
          }
        });
      }

      if (oldProps) {
        Object.keys(oldProps).forEach((key) => {
          if (!newProps[key]) {
            el.removeAttribute(key);
          }
        });
      }
    }

    const { children: oldCildren } = n1;
    const { children: newCildren } = n2;

    if (typeof newCildren == "string") {
      el.textContent = newCildren;
    } else if (Array.isArray(newCildren)) {
      if (typeof oldCildren == "string") {
        el.innerText = "";
        mountElement(n2, el);
      } else if (Array.isArray(oldCildren)) {
        const length = Math.min(oldCildren.length, newCildren.length);

        for (let index = 0; index < length; index++) {
          const oldVnode = oldCildren[index];
          const newVnode = newCildren[index];
          diff(oldVnode, newVnode);
        }

        if (newCildren.length > length) {
          for (let index = length; index < newCildren.length; index++) {
            const newVnode = newCildren[index];
            mountElement(newVnode, el);
          }
        }

        if (oldCildren.length > length) {
          for (let index = length - 1; index < newCildren.length; index++) {
            const oldVnode = oldCildren[index];
            el.removeChild(oldVnode.el);
          }
        }
      }
    }
  }

miniVue完整代码

1.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>
    <script src="./reactivity.js"></script>
    <script src="./render.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    const createComponentFn = function (el) {
      function setup() {
        const user = reactive({
          username: "张三",
        });

        const isShow = reactive({ value: true });

        setTimeout(() => {
          user.username = "李四";
        }, 3000);

        return { user, isShow };
      }

      return {
        context: setup(),
        mount(el) {
          let isMounted = false;
          let preVnode;
          watchEffect(() => {
            if (!isMounted) {
              el.innerHTML = "";
              const vnode = render(this.context);
              mountElement(vnode, el);
              preVnode = vnode;
              isMounted = true;
            } else {
              const vnode = render2(this.context);
              diff(preVnode, vnode);
              preVnode = vnode;
            }
          });
        },
      };
    };

    const el = document.getElementById("app");
    const app = createComponentFn();
    app.mount(el);
  </script>
  <style>
    #app {
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</html>

2.reactivity.js

(() => {
  // 当前依赖设置为全局变量,用于解决跨作用域读取的问题
  let curentEffect = null;

  // 依赖管理对象
  class Dep {
    constructor(value) {
      this.effects = new Set();
      this._val = value;
    }

    addDep() {
      if (curentEffect) {
        this.effects.add(curentEffect);
      }
    }

    notify() {
      this.effects.forEach((item) => item(this._val));
    }
  }

  let targetMap = new Map();

  function handleTargetGet(target, key) {
    let targetDep = targetMap.get(target);
    if (!targetDep) {
      targetDep = new Dep();
      targetMap.set(target, targetDep);
    }
    targetDep.addDep(curentEffect);
    return Reflect.get(target, key);
  }

  function handleTargetSet(target, key, value) {
    Reflect.set(target, key, value);
    const targetDep = targetMap.get(target);
    if (!targetDep) throw new Error("依赖未收集");
    targetDep._val = value;
    if (targetDep) {
      targetDep.notify();
    }
  }

  function reactive(target) {
    return new Proxy(target, {
      get: handleTargetGet,
      set: handleTargetSet,
    });
  }

  function watchEffect(effect) {
    curentEffect = effect;
    effect();
    curentEffect = null;
  }

  window.reactive = reactive;
  window.watchEffect = watchEffect;
})();

3.render.js

(() => {
  function _h(tag, props, children) {
    return {
      tag,
      props,
      children,
    };
  }

  // 暴力解法做了简化,children长度不同,直接操作多出的部分或减少的部分,不进行精细化的对比判断
  function diff(n1, n2) {
    let el = (n2.el = n1.el);
    if (n1.tag !== n2.tag) {
      el.replaceWith(document.createElement(n2.tag));
    } else {
      const { props: oldProps } = n1;
      const { props: newProps } = n2;

      if (oldProps && newProps) {
        Object.keys(newProps).forEach((key) => {
          const newValue = newProps[key];
          const oldValue = oldProps[key];
          if (newValue !== oldValue) {
            el.setAttribute(key, newValue);
          }
        });
      }

      if (oldProps) {
        Object.keys(oldProps).forEach((key) => {
          if (!newProps[key]) {
            el.removeAttribute(key);
          }
        });
      }
    }

    const { children: oldCildren } = n1;
    const { children: newCildren } = n2;

    if (typeof newCildren == "string") {
      el.textContent = newCildren;
    } else if (Array.isArray(newCildren)) {
      if (typeof oldCildren == "string") {
        el.innerText = "";
        mountElement(n2, el);
      } else if (Array.isArray(oldCildren)) {
        const length = Math.min(oldCildren.length, newCildren.length);

        for (let index = 0; index < length; index++) {
          const oldVnode = oldCildren[index];
          const newVnode = newCildren[index];
          diff(oldVnode, newVnode);
        }

        if (newCildren.length > length) {
          for (let index = length; index < newCildren.length; index++) {
            const newVnode = newCildren[index];
            mountElement(newVnode, el);
          }
        }

        if (oldCildren.length > length) {
          for (let index = length - 1; index < newCildren.length; index++) {
            const oldVnode = oldCildren[index];
            el.removeChild(oldVnode.el);
          }
        }
      }
    }
  }

  function mountElement(vnode, container) {
    const { tag, props, children } = vnode;

    const el = (vnode.el = document.createElement(tag));

    if (props) {
      // 这里还可以判断是否要绑定事件
      for (let key in props) {
        const prop = props[key];
        el.setAttribute(key, prop);
      }
    }

    if (typeof children == "string") {
      el.innerText = children;
    } else {
      if (Array.isArray(children)) {
        for (let vnode of children) {
          mountElement(vnode, el);
        }
      }
    }

    container.appendChild(el);
  }

  function render(context) {
    return _h("ul", {}, [
      _h("li", { id: context?.isShow?.value }, context.user.username),
      _h("li", {}, context.user.username),
      _h("li", {}, "赵六"),
    ]);
  }
  
 function render2(context) {
    return _h("ul", {}, [
      _h("li", { id: context?.isShow?.value }, context.user.username),
      _h("li", {}, "赵六2"),
    ]);
  }

  window.render = render;
  window.mountElement = mountElement;
  window.diff = diff;
  window.render2 = render2
})();

Vue2响应式原理

vue2和vue3响应式的差别主要是definedProperty和proxy实现属性拦截的区别,definedProperty是重写对象属性描述符的get函数和set函数,因为是直接在对象上进行的操作,因此无法监听属性的新增,而proxy用于生成一个代理目标对象的代理对象,对象进行的读取和赋值操作都会被代理对象拦截,因此可以监听属性的新增

1.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>Vue2</title>
    <script src="./reactive.js"></script>
    <script src="./vnode.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    const vm = {
      data() {
        return {
          user: {
            username: "张三",
            sex: "男",
            age: '23',
          },
          role: {
            roleId: "admin",
            roleName: "管理员",
          },
        };
      },

      mount() {
        const data = this.data();
        window.obj = data;
        reactive(data);
        let isMounted = false;
        let oldVnode = null;
        watch(() => {
          if (!isMounted) {
            const vnode = render(data);
            // 生成一份虚拟dom,用于控制台修改进行diff功能验证
            window.vnode = render(data);
            const el = document.getElementById("app");
            createElement(vnode, el);
            oldVnode = vnode;
            isMounted = true
          } else {
            diff(oldVnode, window.vnode);
            oldVnode = window.vnode;
          }
        });
      },
    };

    vm.mount();
  </script>
</html>

2.reactive.js

function reactive(data) {
  if (typeof data != "object") {
    throw new Error("请传入对象或数组");
  }
  if (!Array.isArray(data)) {
    Object.keys(data).forEach((key) => {
      let value = data[key];
      if (typeof value == "object") {
        reactive(value);
      }
      const dep = new Dep();
      Object.defineProperty(data, key, {
        get() {
          dep.add(currentSub);
          return value;
        },
        set(newVal) {
          value = newVal;
          dep.update(newVal);
        },
      });
    });
  }
}

class Dep {
  constructor() {
    this.subs = new Array();
  }

  add(fn) {
    if (typeof fn == "function") {
      this.subs.push(fn);
    }
  }

  update(value) {
    this.subs.forEach((sub) => {
      sub(value);
    });
  }
}

// 使用全局变量实现跨作用域数据传递,这样就不需要在watch函数传入data和key了
// 只要fn使用了响应式对象就可以自动把currentSub添加到对应的依赖dep中
let currentSub = null;

function watch(fn) {
  currentSub = fn;
  fn();
  currentSub = null;
}

window.reactive = reactive;
window.watch = watch;

3.vnode.js

function render(data) {
  return _h("ul", {}, [
    _h("li", {}, data.user.username),
    _h("li", {}, data.user.sex),
    _h("li", {}, data.user.age),
    _h("li", {}, data.role.roleId),
    _h("li", {}, data.role.roleName),
  ]);
}

function _h(tag, props, children) {
  return {
    tag,
    props,
    children,
  };
}

function createElement(vnode, parentEl) {
  const { tag, props, children } = vnode;

  const el = document.createElement(tag);

  vnode.el = el;

  for (let key in props) {
    el.setAttribute(key, props[key]);
  }

  if (typeof children === "string") {
    el.innerText = children;
  }

  if (Array.isArray(children)) {
    for (let vnode of children) {
      createElement(vnode, el);
    }
  }

  parentEl.appendChild(el);
}

// 对比属性
function propsDiff(oldProps, newProps, el) {
  for (let key in oldProps) {
    if (!newProps[key]) {
      el.removeAttribute(key);
    }
  }

  for (let key in newProps) {
    if (!oldProps[key]) {
      const oldVal = oldProps[key];
      const newVal = newProps[key];
      if (oldVal != newVal) {
        el.setAttribute(key, newVal);
      }
    }
  }
}

function diffChildren(oldChildren, newCildren, el) {
  if (typeof newCildren == "string") {
    el.innerText = newCildren;
    return;
  }

  if (typeof newCildren !== "string" && typeof oldChildren == "string") {
    for (let i = 0; i < newCildren.length; i++) {
      const newNode = newCildren[i];
      createElement(newNode, el);
    }
    return;
  }

  const length = Math.min(oldChildren.length, newCildren.length);
  function updateNode() {
    for (let i = 0; i < length; i++) {
      const oldNode = oldChildren[i];
      const newNode = newCildren[i];
      diff(oldNode, newNode);
    }
  }
  
  function removeNode() {
    for (let i = length; i < oldChildren.length; i++) {
      const oldNode = oldChildren[i];
      el.removeChild(oldNode.el);
    }
  }
  
  function appendNode() {
    for (let i = length; i < newCildren.length; i++) {
      const newNode = newCildren[i];
      createElement(newNode, el);
    }
  }

  if (Array.isArray(newCildren)) {
    // 不需要新增删除节点
    if (oldChildren.length == newCildren.length) {
      updateNode();
    }

    // 需要删除节点
    if (oldChildren.length > newCildren.length) {
      updateNode();
      removeNode();
    }

    // 需要新增节点
    if (newCildren.length > oldChildren.length) {
      updateNode();
      appendNode();
    }
  }
}
// 实现一个最简化的diff算法
function diff(n1, n2) {
  let el = (n2.el = n1.el);
  if (n1.tag != n2.tag) {
    const el2 = document.createElement(n2.tag);
    el.replaceWith(el2);
    el = el2;
  }

  propsDiff(n1.props, n2.props, el);

  diffChildren(n1.children, n2.children, el);
}

window.render = render;
window.createElement = createElement;
window.diff = diff;

3.Vue2实现数组的响应式处理