Skip to content

Mini-vue

html
<div id="app"></div>
<script>
//vdom部分
function h(tag, props, children) {
  return {
    tag,
    props,
    children,
  };
}
function mount(vnode, container) {
  const { tag, props, children } = vnode;
  const el = (vnode.el = document.createElement(tag));
  if (props) {
    for (let key in props) {
      const value = props[key];
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        el.setAttribute(key, value);
      }
    }
  }
  if (children) {
    if (Array.isArray(children)) {
      children.forEach((child) => {
        if (typeof child === "string") {
          el.append(child);
        } else if (typeof child === "object") {
          mount(child, el);
        }
      });
    } else {
      el.append(children);
    }
  }
  container.append(el);
}

function patch(n1, n2) {
  if (n1.tag === n2.tag) {
    const el = (n2.el = n1.el);
    //diff props

    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    //添加新的属性或更改原来已有但变化了的属性
    for (let key in newProps) {
      const oldValue = oldProps[key];
      const newValue = newProps[key];
      if (newValue !== oldValue) {
        el.setAttribute(key, newValue);
      }
    }

    //移除新属性中没有的属性
    for (let key in oldProps) {
      if (!(key in newProps)) {
        el.removeAttribute(key);
      }
    }

    //diff children
    const oldChildren = n1.children;
    const newChildren = n2.children;

    if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (oldChildren !== newChildren) {
          el.innerHTML = newChildren;
        }
      }
    } else if (typeof oldChildren === "string" && Array.isArray(newChildren)) {
      el.innerHTML = "";
      newChildren.forEach((child) => mount(child, el));
    } else if (Array.isArray(oldChildren) && Array.isArray(newChildren)) {
      const minLength = Math.min(oldChildren.length, newChildren.length);
      for (let i = 0; i < minLength; i++) {
        patch(oldChildren[i], newChildren[i]);
      }
      if (oldChildren.length === minLength) {
        for (let i = minLength; i < newChildren.length; i++) {
          mount(newChildren[i], el);
        }
      } else {
        for (let i = minLength; i < oldChildren.length; i++) {
          el.removeChild(oldChildren[i].el);
        }
      }
    }
  } else {
    //replace
  }
}

//reactivity部分
let activeEffect = null;
class Dep {
  subs = new Set();
  depend() {
    if (activeEffect) {
      this.subs.add(activeEffect);
    }
  }
  notify() {
    this.subs.forEach((sub) => sub());
  }
}

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

const targetMap = new WeakMap();

function getDep(target, key) {
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map());
  }
  const depMap = targetMap.get(target);
  if (!depMap.has(key)) {
    depMap.set(key, new Dep());
  }
  return depMap.get(key);
}

const reactiveHandlers = {
  get(target, key, receiver) {
    // dep
    const dep = getDep(target, key);
    dep.depend();
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const dep = getDep(target, key);
    const ret = Reflect.set(target, key, value, receiver);
    dep.notify();
    return ret;
  },
};

function reactive(raw) {
  return new Proxy(raw, reactiveHandlers);
}


//component组件实例
//container要挂载的dom元素
function mountApp(component, container) {
  let isMounted = false;
  let oldVdom;
  watchEffect(() => {
    if (!isMounted) {
      //第一次挂载
      oldVdom = component.render();
      mount(oldVdom, container);
      isMounted = true;
    } else {
      //数据变化,要进行更新
      const newVdom = component.render();
      patch(oldVdom, newVdom);
      oldVdom = newVdom;
    }
  });
}

const App = {
  data: reactive({
    count: 0,
  }),
  render() {
    return h("div", null, [
      h(
        "div",
        {
          onClick: () => App.data.count++,
        },
        String(this.data.count)
      ),
    ]);
  },
};
//一个点击自增的计数器
mountApp(App, document.getElementById("app"));

</script>