◆

Vue3 源码:render 渲染与 diff 算法解析

2023-04-19 01:000
  1. 下面 createApp 最终返回 app 这个对象,其中有一个 mount 方法,mount 方法接收一个参数,这个参数 就是 index.html 中的 id 为 #app 的节点,
  2. 里面会执行一个 render 方法,接收一个虚拟节点 vnode,和 #app 的节点,最后就是要在 render 中将 vnode 虚拟节点渲染在 rootContainer 上面
  3. 其中 createApp 中的 rootComponent 就是我们写的 vue 中的组件页面,createVNode(rootComponent) 就是要讲我们写的 vue 页面转换为虚拟节点
export function createAppAPI(render) {
  return function createApp(rootComponent) {
    const app = {
      _component: rootComponent,
      mount(rootContainer) {
        console.log('基于根组件创建 vnode');
        const vnode = createVNode(rootComponent);
        console.log('调用 render,基于 vnode 进行开箱');
        render(vnode, rootContainer);
      },
    };

    return app;
  };
}

createVNode 简单来讲就是将 vue-compoentns 中一些关键的属性存储在一个 js 对象中,从而后续操作 dom 都使用这个虚拟节点,而不去操作真实 dom

export const createVNode = function (
  type: any,
  props?: any,
  children?: string | Array<any>,
) {
  const vnode = {
    type,
    props: props || {},
    children,
    component: null,
    key: props && props.key,
    el: null,
    shapeFlag: getShapeFlag(type),
  };

  // 基于 children 再次设置 shapeFlag
  if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  } else if (typeof children === 'string') {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  }
  normalizeChildren(vnode, children);

  return vnode;
};

// 用 symbol 作为唯一标识
export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');

export function createTextVNode(text: string = ' ') {
  return createVNode(Text, {}, text);
}

export function normalizeChildren(vnode, children) {
  if (typeof children === 'object') {
    // 暂时主要是为了标识出 slots_children 这个类型来
    // 暂时我们只有 element 类型和 component 类型的组件
    // 所以我们这里除了 element ,那么只要是 component 的话,那么children 肯定就是 slots 了
    if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
      // 如果是 element 类型的话,那么 children 肯定不是 slots
    } else {
      // 这里就必然是 component 了,
      vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
    }
  }
}

// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
  return typeof type === 'string'
    ? ShapeFlags.ELEMENT
    : ShapeFlags.STATEFUL_COMPONENT;
}

这里 createRenderer 是为了自定义创建、插入移除等节点方式的 api,以设配跨端情况

export function createRenderer(options) {
  const {
    createElement: hostCreateElement,
    patchProp: hostPatchProp,
    insert: hostInsert,
    remove: hostRemove,
    setElementText: hostSetElementText,
    setText: hostSetText,
    createText: hostCreateText,
  } = options;
}
  1. patch 中需要判断节点类型 Text Fragment ELEMENT STATEFUL_COMPONENT
  2. Text -> el = document.createTextNode(text) 更新 对比新旧 vnode el 通过 node.nodeValue = text;
function render(vnode, container) {
  // 首次传入的 container 就是 index.html 中定义的 root 节点
  patch(null, vnode, container, null, null);
}

// 基于 vnode 类型进行不同的处理
function patch(
  n1,
  n2,
  container = null,
  anchor = null,
  parentComponent = null,
) {
  /**
   * 原始的 App.js 是这样的,所以一开始的 vnode.type 就是下面这个对象
   * {
   *   setup() {},
   *   render() {
   *     return h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]);
   *   },
   * }
   * 所以一开始 处理的是组件类型
   * 等处理完 App.js 的内容后,紧接着就会进入到 render 函数执行
   * 也就是执行 h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)])
   * 这时候 type 就是 string
   */
  const { children, type, shapeFlag } = n2;

  switch (type) {
    case Text:
      processText(n1, n2, container);
      break;
    // 其中还有几个类型比如: static fragment comment
    case Fragment:
      processFragment(n1, n2, container);
      break;
    default:
      // if (typeof vnode.type === 'string') {
      if (shapeFlag & ShapeFlags.ELEMENT) {
        console.log('处理 element');
        // 处理 element 类型
        processElement(n1, n2, container, anchor, parentComponent);
      }
      // 虚拟节点的类型为对象时,代码节点是一个组件
      // else if (isObject(vnode.type)) {
      else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        // 处理组件
        processComponent(n1, n2, container, parentComponent);
      }
  }
}
  1. ELEMENT mountElement updateElement

type div 创建 div 节点作为 el,判断 children,数组再次执行 patch,处理 props, on 开头则为事件,使用 listen 监听,属性使用 setAttribute 设置

// h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]
function mountElement(vnode, container, anchor) {
  // 创建这个类型 type 的元素节点容器
  // vnode.el = document.createElement(vnode.type) 存储当前的 el 节点信息
  const el = (vnode.el = hostCreateElement(vnode.type));

  const { children, props, shapeFlag } = vnode;

  // h("div", { class: 'test' }, '普通字符串')
  // if (typeof children === 'string') {
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
  }
  // h("div", { class: 'test' }, ['普通字符串', h("p", {}, "主页")])
  // else if (isArray(children)) {
  else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode.children, el);
  }

  const isOn = key => /^on[A-Z]/.test(key);

  // 给 el 设置 props 中的属性
  for (let key in props) {
    const val = props[key];
    // if (isOn(key)) {
    // 	const eventName = key.slice(2).toLowerCase();
    // 	el.addEventListener(eventName, val);
    // } else {
    // 	el.setAttribute(key, val);
    // }
    hostPatchProp(el, key, null, val);
  }
  // 将生成的这个元素添加到 container 上
  // container.append(el);

  hostInsert(el, container, anchor);
}

function mountChildren(children, container) {
  children.forEach(v => patch(null, v, container));
}

props 的更新,old new 都有但是 val 变了,old 又 new 没有

function patchProp(el, key, preValue, nextValue) {
  // preValue 之前的值
  // 为了之后 update 做准备的值
  // nextValue 当前的值
  console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
  console.log(`key: ${key} 之前的值是:${preValue}`);

  if (isOn(key)) {
    // 添加事件处理函数的时候需要注意一下
    // 1. 添加的和删除的必须是一个函数,不然的话 删除不掉
    //    那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到
    // 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次
    // 存储所有的事件函数
    const invokers = el._vei || (el._vei = {});
    const existingInvoker = invokers[key];
    if (nextValue && existingInvoker) {
      // patch
      // 直接修改函数的值即可
      existingInvoker.value = nextValue;
    } else {
      const eventName = key.slice(2).toLowerCase();
      if (nextValue) {
        const invoker = (invokers[key] = nextValue);
        el.addEventListener(eventName, invoker);
      } else {
        el.removeEventListener(eventName, existingInvoker);
        invokers[key] = undefined;
      }
    }
  } else {
    if (nextValue === null || nextValue === '') {
      el.removeAttribute(key);
    } else {
      el.setAttribute(key, nextValue);
    }
  }
}

children 对比,text 节点不同直接替换即可 array

function patchChildren(n1, n2, container, anchor, parentComponent) {
  const { shapeFlag: prevShapeFlag, children: c1 } = n1;
  const { shapeFlag, children: c2 } = n2;

  // 如果 n2 的 children 是 text 类型的话
  // 就看看和之前的 n1 的 children 是不是一样的
  // 如果不一样的话直接重新设置一下 text 即可
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    if (c2 !== c1) {
      console.log('类型为 text_children, 当前需要更新');
      hostSetElementText(container, c2 as string);
    }
  } else {
    // 如果之前是 array_children
    // 现在还是 array_children 的话
    // 那么我们就需要对比两个 children 啦
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        patchKeyedChildren(c1, c2, container, anchor, parentComponent);
      }
    }
  }
}

function patchKeyedChildren(
  c1: any[],
  c2: any[],
  container,
  parentAnchor,
  parentComponent,
) {
  let i = 0;
  const l2 = c2.length;
  let e1 = c1.length - 1;
  let e2 = l2 - 1;

  const isSameVNodeType = (n1, n2) => {
    return n1.type === n2.type && n1.key === n2.key;
  };

  while (i <= e1 && i <= e2) {
    const prevChild = c1[i];
    const nextChild = c2[i];

    if (!isSameVNodeType(prevChild, nextChild)) {
      console.log('两个 child 不相等(从左往右比对)');
      console.log(`prevChild:${prevChild}`);
      console.log(`nextChild:${nextChild}`);
      break;
    }

    console.log('两个 child 相等,接下来对比这两个 child 节点(从左往右比对)');
    patch(prevChild, nextChild, container, parentAnchor, parentComponent);
    i++;
  }

  while (i <= e1 && i <= e2) {
    // 从右向左取值
    const prevChild = c1[e1];
    const nextChild = c2[e2];

    if (!isSameVNodeType(prevChild, nextChild)) {
      console.log('两个 child 不相等(从右往左比对)');
      console.log(`prevChild:${prevChild}`);
      console.log(`nextChild:${nextChild}`);
      break;
    }
    console.log('两个 child 相等,接下来对比这两个 child 节点(从右往左比对)');
    patch(prevChild, nextChild, container, parentAnchor, parentComponent);
    e1--;
    e2--;
  }

  if (i > e1 && i <= e2) {
    // 如果是这种情况的话就说明 e2 也就是新节点的数量大于旧节点的数量
    // 也就是说新增了 vnode
    // 应该循环 c2
    // 锚点的计算:新的节点有可能需要添加到尾部,也可能添加到头部,所以需要指定添加的问题
    // 要添加的位置是当前的位置(e2 开始)+1
    // 因为对于往左侧添加的话,应该获取到 c2 的第一个元素
    // 所以我们需要从 e2 + 1 取到锚点的位置
    const nextPos = e2 + 1;
    const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
    while (i <= e2) {
      console.log(`需要新创建一个 vnode: ${c2[i].key}`);
      patch(null, c2[i], container, anchor, parentComponent);
      i++;
    }
  } else if (i > e2 && i <= e1) {
    // 这种情况的话说明新节点的数量是小于旧节点的数量的
    // 那么我们就需要把多余的
    while (i <= e1) {
      console.log(`需要删除当前的 vnode: ${c1[i].key}`);
      hostRemove(c1[i].el);
      i++;
    }
  } else {
    // 左右两边都比对完了,然后剩下的就是中间部位顺序变动的
    // 例如下面的情况
    // a,b,[c,d,e],f,g
    // a,b,[e,c,d],f,g

    let s1 = i;
    let s2 = i;
    const keyToNewIndexMap = new Map();
    let moved = false;
    let maxNewIndexSoFar = 0;
    // 先把 key 和 newIndex 绑定好,方便后续基于 key 找到 newIndex
    // 时间复杂度是 O(1)
    for (let i = s2; i <= e2; i++) {
      const nextChild = c2[i];
      keyToNewIndexMap.set(nextChild.key, i);
    }

    // 需要处理新节点的数量
    const toBePatched = e2 - s2 + 1;
    let patched = 0;
    // 初始化 从新的index映射为老的index
    // 创建数组的时候给定数组的长度,这个是性能最快的写法
    const newIndexToOldIndexMap = new Array(toBePatched);
    // 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在
    for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;

    // 遍历老节点
    // 1. 需要找出老节点有,而新节点没有的 -> 需要把这个节点删除掉
    // 2. 新老节点都有的,—> 需要 patch
    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i];

      // 优化点
      // 如果老的节点大于新节点的数量的话,那么这里在处理老节点的时候就直接删除即可
      if (patched >= toBePatched) {
        hostRemove(prevChild.el);
        continue;
      }

      let newIndex;
      if (prevChild.key != null) {
        // 这里就可以通过key快速的查找了, 看看在新的里面这个节点存在不存在
        // 时间复杂度O(1)
        newIndex = keyToNewIndexMap.get(prevChild.key);
      } else {
        // 如果没key 的话,那么只能是遍历所有的新节点来确定当前节点存在不存在了
        // 时间复杂度O(n)
        for (let j = s2; j <= e2; j++) {
          if (isSameVNodeType(prevChild, c2[j])) {
            newIndex = j;
            break;
          }
        }
      }

      // 因为有可能 nextIndex 的值为0(0也是正常值)
      // 所以需要通过值是不是 undefined 或者 null 来判断
      if (newIndex === undefined) {
        // 当前节点的key 不存在于 newChildren 中,需要把当前节点给删除掉
        hostRemove(prevChild.el);
      } else {
        // 新老节点都存在
        console.log('新老节点都存在');
        // 把新节点的索引和老的节点的索引建立映射关系
        // i + 1 是因为 i 有可能是0 (0 的话会被认为新节点在老的节点中不存在)
        newIndexToOldIndexMap[newIndex - s2] = i + 1;
        // 来确定中间的节点是不是需要移动
        // 新的 newIndex 如果一直是升序的话,那么就说明没有移动
        // 所以我们可以记录最后一个节点在新的里面的索引,然后看看是不是升序
        // 不是升序的话,我们就可以确定节点移动过了
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        } else {
          moved = true;
        }

        patch(prevChild, c2[newIndex], container, null, parentComponent);
        patched++;
      }
    }

    // 利用最长递增子序列来优化移动逻辑
    // 因为元素是升序的话,那么这些元素就是不需要移动的
    // 而我们就可以通过最长递增子序列来获取到升序的列表
    // 在移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动
    // 通过 moved 来进行优化,如果没有移动过的话 那么就不需要执行算法
    // getSequence 返回的是 newIndexToOldIndexMap 的索引值
    // 所以后面我们可以直接遍历索引值来处理,也就是直接使用 toBePatched 即可
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : [];
    let j = increasingNewIndexSequence.length - 1;

    // 遍历新节点
    // 1. 需要找出老节点没有,而新节点有的 -> 需要把这个节点创建
    // 2. 最后需要移动一下位置,比如 [c,d,e] -> [e,c,d]

    // 这里倒循环是因为在 insert 的时候,需要保证锚点是处理完的节点(也就是已经确定位置了)
    // 因为 insert 逻辑是使用的 insertBefore()
    for (let i = toBePatched - 1; i >= 0; i--) {
      // 确定当前要处理的节点索引
      const nextIndex = s2 + i;
      const nextChild = c2[nextIndex];
      // 锚点等于当前节点索引+1
      // 也就是当前节点的后面一个节点(又因为是倒遍历,所以锚点是位置确定的节点)
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;

      if (newIndexToOldIndexMap[i] === 0) {
        // 说明新节点在老的里面不存在
        // 需要创建
        patch(null, nextChild, container, anchor, parentComponent);
      } else if (moved) {
        // 需要移动
        // 1. j 已经没有了 说明剩下的都需要移动了
        // 2. 最长子序列里面的值和当前的值匹配不上, 说明当前元素需要移动
        if (j < 0 || increasingNewIndexSequence[j] !== i) {
          // 移动的话使用 insert 即可
          hostInsert(nextChild.el, container, anchor);
        } else {
          // 这里就是命中了  index 和 最长递增子序列的值
          // 所以可以移动指针了
          j--;
        }
      }
    }
  }
}

组件挂载

// 组件初始化
function mountComponent(vnode, container, parentComponent) {
  // 先创建一个 component instance
  const instance = (vnode.component = createComponentInstance(
    vnode,
    parentComponent,
  ));

  console.log('当前组件实例:', instance);
  // 这里会将 instance 中注入 setup 的结果 setupState 以及 App.js 的 render 函数
  setupComponent(instance);

  setupRenderEffect(instance, vnode, container, parentComponent);
}
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue3 源码:render 渲染与 diff 算法解析加载中