Skip to content

组件的挂载流程

组件需要提供一个 render 函数,渲染函数需要返回虚拟 DOM

const VueComponent = {
  data() {
    return { age: 13 };
  },
  render() {
    return h("p", [h(Text, "I'm Jiang sir"), h("span", this.age)]);
  },
};
render(h(VueComponent), document.getElementById("app"));

添加组件类型

h 方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)

export const createVNode = (type, props, children = null) => {
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT
    : 0;
  // ... 稍后可以根据类型来进行组件的挂载
};

组件的渲染

const patch = (n1, n2, container, anchor?) => {
  // 初始化和diff算法都在这里喲
  if (n1 == n2) {
    return;
  }
  if (n1 && !isSameVNodeType(n1, n2)) {
    // 有n1 是n1和n2不是同一个节点
    unmount(n1);
    n1 = null;
  }
  const { type, shapeFlag } = n2;
  switch (type) {
    // ...
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, anchor);
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container, anchor);
      }
  }
};
const mountComponent = (n2, container, anchor) => {
  const { render, data = () => ({}) } = n2.type;
  const state = reactive(data());
  const instance = {
    state, // 组件的状态
    isMounted: false, // 组件是否挂载
    subTree: null, // 子树
    update: null,
    vnode: n2,
  };
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      const subTree = render.call(state, state);
      patch(null, subTree, container, anchor);
      instance.subTree = subTree;
      instance.isMounted = true;
    } else {
      const subTree = render.call(state, state);
      patch(instance.subTree, subTree, container, anchor);
      instance.subTree = subTree;
    }
  };
  const effect = new ReactiveEffect(componentUpdateFn, () => update());
  const update = (instance.update = () => effect.run());
  update();
};
const processComponent = (n1, n2, container, anchor) => {
  if (n1 == null) {
    mountComponent(n2, container, anchor);
  } else {
    // 组件更新逻辑
  }
};

组件异步渲染

修改调度方法,将更新方法压入到队列中

const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update));
const update = (instance.update = () => effect.run());

批处理操作scheduler.ts

const queue = [];
let isFlushing = false;
const resolvedPromise = Promise.resolve();
export function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job);
  }
  if (!isFlushing) {
    isFlushing = true;
    resolvedPromise.then(() => {
      isFlushing = false;
      let copy = queue.slice(0);
      queue.length = 0; // 这里要先清空,防止在执行过程中在加入新的job
      for (let i = 0; i < copy.length; i++) {
        let job = copy[i];
        job();
      }
      copy.length = 0;
    });
  }
}

组件 Props、Attrs 实现

PropsAttrs关系是:没有定义在component.props中的属性将存储到attrs对象中

import { render, h, Text, Fragment } from "./runtime-dom.js";

const VueComponent = {
  data() {
    return { name: "jw", age: 30 };
  },
  props: {
    address: String,
  },
  render() {
    return h("p", [
      h(Text, `${this.name}今年${this.age}岁了`),
      h(Text, `${this.address}`),
      h(Text, `${this.$attrs.a}${this.$attrs.b}`),
    ]);
  },
};
render(h(VueComponent, { address: "霍营", a: 1, b: 2 }), app);

initProps

const mountComponent = (vnode, container, anchor) => {
  let { data = () => ({}), render, props: propsOptions = {} } = vnode.type; // 这个就是用户写的内容
  const state = reactive(data()); // pinia 源码就是 reactive({})  作为组件的状态
  const instance = {
    // 组件的实例
    state,
    vnode, // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
    subTree: null, // vnode组件的虚拟节点   subTree渲染的组件内容
    isMounted: false,
    update: null,
    propsOptions,
    attrs: {},
    props: {},
  };
  vnode.component = instance;
  initProps(instance, vnode.props);
};

componentProps.ts

export function initProps(instance, rawProps) {
  const props = {};
  const attrs = {};
  const options = instance.propsOptions || {}; // 获取组件用户的配置
  if (rawProps) {
    for (let key in rawProps) {
      const value = rawProps[key];
      if (key in options) {
        props[key] = value;
      } else {
        attrs[key] = value;
      }
    }
  }
  instance.props = reactive(props); // 这里应该用shallowReactive,遵循单向数据流原则
  instance.attrs = attrs;
}

属性代理

shared/index.ts

const hasOwnProperty = Object.prototype.hasOwnProperty;
export const hasOwn = (val, key) => hasOwnProperty.call(val, key);
const publicPropertiesMap = {
  $attrs: (i) => i.attrs,
};

const mountComponent = (vnode, container, anchor) => {
  // ...
  const instance = {
    // 组件的实例
    // ...
    proxy: null,
  };
  vnode.component = instance;
  initProps(instance, vnode.props);
  instance.proxy = new Proxy(instance, {
    get(target, key) {
      const { data, props } = target;
      if (data && hasOwn(data, key)) {
        return data[key];
      } else if (hasOwn(props, key)) {
        return props[key];
      }
      const publicGetter = publicPropertiesMap[key];
      if (publicGetter) {
        return publicGetter(target);
      }
    },
    set(target, key, value) {
      const { data, props } = target;
      if (data && hasOwn(data, key)) {
        data[key] = value;
        return true;
      } else if (hasOwn(props, key)) {
        console.warn(`Attempting to mutate prop "${key}". Props are readonly.`);
        return false;
      }
      return true;
    },
  });
};

组件流程整合

const mountComponent = (vnode, container, anchor) => {
  // 1) 创建实例
  const instance = (vnode.component = createComponentInstance(vnode));
  // 2) 给实例赋值
  setupComponent(instance);
  // 3) 创建渲染effect及更新
  setupRenderEffect(instance, container, anchor);
};

1)创建组件实例

component.ts

export function createComponentInstance(vnode) {
  const instance = {
    // 组件的实例
    data: null,
    vnode, // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
    subTree: null, // vnode组件的虚拟节点   subTree渲染的组件内容
    isMounted: false,
    update: null,
    attrs: {},
    props: {},
    proxy: null,
    propsOptions: vnode.type.props,
  };
  return instance;
}

2)设置组件属性

const publicPropertiesMap = {
  $attrs: (i) => i.attrs,
};
const PublicInstanceProxyHandlers = {
  get(target, key) {
    const { data, props } = target;
    if (data && hasOwn(data, key)) {
      return data[key];
    } else if (hasOwn(props, key)) {
      return props[key];
    }
    const publicGetter = publicPropertiesMap[key];
    if (publicGetter) {
      return publicGetter(target);
    }
  },
  set(target, key, value) {
    const { data, props } = target;
    if (data && hasOwn(data, key)) {
      data[key] = value;
      return true;
    } else if (hasOwn(props, key)) {
      console.warn(`Attempting to mutate prop "${key}". Props are readonly.`);
      return false;
    }
    return true;
  },
};
export function setupComponent(instance) {
  const { props, type } = instance.vnode;
  initProps(instance, props);
  instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers);
  const data = type.data;
  if (data) {
    if (!isFunction(data))
      return console.warn("The data option must be a function.");
    instance.data = reactive(data.call(instance.proxy));
  }
  instance.render = type.render;
}

3)渲染 effect

const setupRenderEffect = (instance, container, anchor) => {
  const { render } = instance;
  const componentUpdateFn = () => {
    // 区分是初始化 还是要更新
    if (!instance.isMounted) {
      // 初始化
      const subTree = render.call(instance.proxy, instance.proxy); // 作为this,后续this会改
      patch(null, subTree, container, anchor); // 创造了subTree的真实节点并且插入了
      instance.subTree = subTree;
      instance.isMounted = true;
    } else {
      // 组件内部更新
      const subTree = render.call(instance.proxy, instance.proxy);
      patch(instance.subTree, subTree, container, anchor);
      instance.subTree = subTree;
    }
  };
  // 组件的异步更新
  const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update));
  // 我们将组件强制更新的逻辑保存到了组件的实例上,后续可以使用
  const update = (instance.update = () => effect.run());
  update();
};

属性更新

const My = {
  props: { address: String },
  render() {
    return h("div", this.address);
  },
};
const VueComponent = {
  data() {
    return { name: "jw", age: 30, flag: false };
  },
  render() {
    return h(Fragment, [
      h("button", { onClick: () => (this.flag = !this.flag) }, "切换渲染"),
      h(My, { address: this.flag ? "霍营" : "回龙观" }),
    ]);
  },
};
render(h(VueComponent), app);
const updateComponent = (n1, n2) => {
  const instance = (n2.component = n1.component);
  const { props: prevProps } = n1;
  const { props: nextProps } = n2;
  updateProps(instance, prevProps, nextProps);
};
const processComponent = (n1, n2, container, anchor) => {
  if (n1 == null) {
    mountComponent(n2, container, anchor);
  } else {
    // 组件更新逻辑
    updateComponent(n1, n2);
  }
};

props.ts

export const hasPropsChanged = (prevProps = {}, nextProps = {}) => {
  const nextKeys = Object.keys(nextProps);
  if (nextKeys.length !== Object.keys(prevProps).length) {
    return true;
  }
  for (let i = 0; i < nextKeys.length; i++) {
    const key = nextKeys[i];
    if (nextProps[key] !== prevProps[key]) {
      return true;
    }
  }
  return false;
};
export function updateProps(instance, prevProps, nextProps) {
  if (hasPropsChanged(prevProps, nextProps)) {
    // 比较前后属性是否一致
    for (const key in nextProps) {
      // 循环props
      instance.props[key] = nextProps[key]; // 响应式属性更新后会重新渲染
    }
    for (const key in instance.props) {
      // 循环props
      if (!(key in nextProps)) {
        delete instance.props[key];
      }
    }
  }
}

这里我们将更新逻辑放到componentFn中,因为除了属性更新之外,插槽也会导致页面更新

const shouldUpdateComponent = (n1, n2) => {
  const { props: prevProps, children: prevChildren } = n1;
  const { props: nextProps, children: nextChildren } = n2;

  if (prevChildren || nextChildren) return true;

  if (prevProps === nextProps) return false;
  return hasPropsChanged(prevProps, nextProps);
};
const updateComponent = (n1, n2) => {
  const instance = (n2.component = n1.component);
  if (shouldUpdateComponent(n1, n2)) {
    instance.next = n2; // 将新的虚拟节点放到next属性上
    instance.update(); // 属性变化手动调用更新方法
  }
};
export function updateProps(prevProps, nextProps) {
  for (const key in nextProps) {
    // 循环props
    prevProps[key] = nextProps[key]; // 响应式属性更新后会重新渲染
  }
  for (const key in prevProps) {
    // 循环props
    if (!(key in nextProps)) {
      delete prevProps[key];
    }
  }
}
function updateComponentPreRender(instance, next) {
  instance.next = null;
  instance.vnode = next;
  updateProps(instance, instance.props, next.props);
}
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    // ...
  } else {
    let { next } = instance;
    if (next) {
      updateComponentPreRender(instance, next);
    }
    const subTree = render.call(instance.proxy, instance.proxy);
    patch(instance.subTree, subTree, container, anchor);
    instance.subTree = subTree;
  }
};

Released under the MIT License.