likes
comments
collection
share

Vue3的createApp和h函数实现

作者站长头像
站长
· 阅读数 16

前言

大家好,上一篇学习记录Vue3的reactive、computed等api的简单实现 ,今天继续来完成从组件到vnode再渲染成dom元素的完整过程,也就是createApp和h函数的简单实现。

createApp和h函数的实现

基本使用

const { reactive, createApp, h } = VueRuntimeDom;

// App组件
const App = {
    setup(props) {
        const state = reactive({ name: '李四' });
        return {
          state
        }
    },
    render(proxy) {
        return h('div', proxy.state.name);
    }
}

// 组件挂载在div下
createApp(App, { name: '张三', age: 18 }).mount('#app');

实现

createApp方法

  • createApp函数会调用createRenderer,返回一个包含createApp方法的对象
  • 再去执行对象的createApp方法,返回一个app实例
  • 实例上有mount方法,覆盖该方法,先执行清空容器,再执行原先的mount方法

这样的好处是创建app实例和挂载到页面分开执行

// dom操作和dom属性的更新的方法
const rendererOptions = {
    createElemet,
    //...
}
function createApp(rootComponent, rootProps) {
  let app = createRenderer(rendererOptions).createApp(rootComponent, rootProps)
  let { mount } = app;
  
  // 执行mount之前将容器清空,再传入
  app.mount = function (container) {
    container = document.querySelector(container);
    container.innerHTML = ''
    mount(container)
  }
  return app
}

createRenderer方法

  • createRenderer方法返回一个对象,包含createApp方法
  • createApp方法返回app实例,包含一些属性和mount方法
  • mount方法和vue2差不多,先创建vnode,再根据vnode将其渲染成真实dom元素 下面再去看createVnode和render函数的实现
function createRenderer(renderOptiondom) {
    const render = (vnode, container) => {
      // ...
    }
    return {
        createApp(rootComponent, rootProps) {
            const app = {
            _component: rootComponent,
            _props: rootProps,
            _container: null,
            mount(container) {
              // 
              const vnode = createVnode(rootComponent, rootProps);
              render(vnode, container);
              app._container = container;
            }
          }
          return app
        }
    }
}

createVnode方法

  • 首先vnode是通过ShapeFlags的位运算来区分vnode的类型,减少判断
  • vnode实际就是一个对象,对dom属性进行描述和vue2一样
  • normalizeChildren方法,标记子元素
const ShapeFlags = {
  ELEMENT: 1, // 普通div标签
  FUNCTION_COMPONENT: 1 << 1, // 函数式组件
  STATEFUL_COMPONENT: 1 << 2, // 普通组件
  TEXT_CHILDREN: 1 << 3, // 子元素是文本
  ARRAY_CHIDLREN: 1 << 4, // 子元素是数组
  SLOTS_CHILDREN: 1 << 5, // 子元素是插槽
  TELEPORT: 1 << 6,
  SUSPENS: 1 << 7,
  COMPOENNT_SHOULD_KEEP_ALIVE: 1 << 8,
  COMPONENT_KEPT_ALIVE: 1 << 9,
  COMPONENT: 1 << 1 | 1 << 2 // 组件
}

function createVnode(type, props, children = null) {
    const shapeFlag = typeof type === 'string' ? ShapeFlags.ELEMENT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : 0;
    const vnode = {
        _v_isVnode: true,
        type,
        props,
        children,
        key: props && props.key,
        el: null,
        shapeFlag,
        component: {}
     }
     // 标记子元素
     normalizeChildren(vnode, children);
     return vnode;
}

function normalizeChildren(vnode, children) {
    let type = 0;
    if(Array.isArray(children)) {
        type = ShapeFlags.ARRAY_CHIDLREN
    } else if(children != null) {
        type = ShapeFlags.TEXT_CHILDREN
    }
    vnode.shapeFlag |= type;
}

h函数的实现

  • h函数和createVnode方法类似,主要在render函数返回时一个vnode
function h(type, propsOrChildren, children) {
  const i = arguments.length;
  if (i === 2) {
    
    if (isObject(propsOrChildren)) {
      // h('div', h('span', 'hello'))
      if (propsOrChildren._v_isVnode) {
        return createVnode(type, null, [propsOrChildren])
      }
      // h(App, {name: '123'})
      return createVnode(type, propsOrChildren)
    } else {
      // h('div', 'hello')
      return createVnode(type, null, propsOrChildren)
    }
  } else {
    if (i > 3) {
      // h('div', {}, '1', '2', '3')
      children = Array.prototype.slice.call(arguments, 2);
    } else if (i === 3 && children._v_isVnode) {
      // h('div', {name: '123'}, h('span', 'hello'));
      children = [children]
    }
    return createVnode(type, propsOrChildren, children)
  }
}

render方法

  • render方法实际执行的patch方法
  • patch方法,传入新旧vnode,再传入容器,新建是传入的老vnode是null
  • patch传入的vnode分三种情况: 组件、元素、文本
  • 文本:文本将会被直接加入到父容器
  • 元素:dom元素会被直接创建,加入到父容器,子元素数组的话,会递归将子元素patch
  • 组件:创建组件实例,组件执行setup函数,然后在effect中执行render函数,让effect函数和render关联,实现数据更新时再次执行render

render函数被执行完之后,元素即被挂载到了指定容器

const TEXT = Symbol('text');
function createRenderer() {
    const processComponent = () => {}
    const processElement = () => {}
    const processText = () => {}
    const patch = (n1, n2, contanier) => {
        const { shapeFlag, type } = n2;
        // 处理文本节点
        if (type === TEXT) {
          processText(n1, n2, contanier)
        } else if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理普通标签
          processElement(n1, n2, contanier)
        } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          // 处理组件
          processComponent(n1, n2, contanier)
        }
      }
    
    const render = (vnode, container) => {
      patch(null, vnode, container)
    }
    return {
        createApp(rootComponent, rootProps) {
            const app = {
            // ...
            mount(container) {
              // ....
              render(vnode, container);
            }
          }
          return app
        }
    }
}

processComponent/processElement/processText方法

  • 处理文本,使用ocument.createTextNode,再传入到容器
  • 处理普通标签,直接创建,children数组需要递归处理
const TEXT = Symbol('text');
// 普通标签
 const processElement = (n1, n2, contanier) => {
   mountElement(n2, contanier)
 }
  
  const mountElement = (vnode, contanier) => {
    const { props = {}, shapeFlag, type, children } = vnode;
    // 创建标签
    const el = document.createElement(type);
    
    // ...设置标签属性省略
    
    // 处理子元素
    if (children) {
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        el.textContent = children;
      } else if (shapeFlag & ShapeFlags.ARRAY_CHIDLREN) {
        // children数组,递归处理
        for (const child of children) {
          let n2 = child;
          // h('div', {}, ['1', '2']) 针对文本,先生成文本类型vnode
          if (!n2._v_isVnode) {
            n2 = createVnode(TEXT, null, child)
          }
          // 递归挂载
          patch(null, n2, el);
        }
      }
    }
    // 元素插入容器
    contanier.insertBefore(el, null)
  }

// 处理文本
function processText(n1, n2, contanier) {
    contanier.insertBefore(document.createTextNode(n2.children), null)
}



processComponent方法

  • createComponentInstance:根据vnode创建组件实例
  • steupComponent:执行vnode传入的setup,对setup返回的对象或者函数挂载在instance上
  • setupRenderEffect:在effect函数中执行render函数,让render函数和effect函数产生关联,方便数据更新时再次触发
const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key);
// 处理组件
const processComponent = (n1, n2, contanier) => {
  mountComponent(n2, contanier)
}
  
const mountComponent = (initialVnode, contanier) => {
const instance = initialVnode.component = createComponentInstance(initialVnode)
steupComponent(instance);
setupRenderEffect(instance, contanier);
}
  
// 创建组件实例
const createComponentInstance = (vnode) => {
  const instance = {
    vnode,
    type: vnode.type,
    props: {},
    attrs: {},
    setupState: {},
    ctx: {}, // 代理数据
    proxy: {},
    render: null,
    isMounted: false
  }
  instance.ctx = { _: instance }
  return instance;
}

// 处理组件中setup数据
const steupComponent = (instance) => {
  // 代理,快速访问到响应式数据 proxy.props.name ===> proxy.name
  // setupState是setup函数执行返回的对象
  instance.proxy = new Proxy(instance.ctx, {
    get({ _: instance }, key) {
      const { props, setupState } = instance;
      if (hasOwn(props, key)) {
        return props[key]
      } else if (hasOwn(setupState, key)) {
        return setupState[key]
      }
    },
    set(target, key, value) {
      const { props, setupState } = target._;
      if (hasOwn(props, key)) {
        props[key] = value;
      } else if (hasOwn(setupState, key)) {
        setupState[key] = value;
      }
    }
  })
  
  
  const { props, children, shapeFlag, type, attrs } = instance.vnode;
  instance.props = props;
  instance.children = children;
  // 组件
  if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    const { setup, render } = type; // 组件定义的render函数
    // setup存在将会被执行
    if (setup) {
      const res = setup(props, { attrs });
      // ... 省略template模板解析生成的render函数
      // res对象赋值给setupState,函数就赋值给render
      instance[isObject(res) ? 'setupState' : 'render'] = setupRes;
    }
    // setup未返回函数,取组件的render函数
    instance.render = instance.render || render;
  }
  
  const setupRenderEffect = (instance, contanier) => {
    effect(() => {
      if (!instance.isMounted) {
        const proxy = instance.proxy;
        // render执行,取到一个vnode
        const subTree = instance.render.call(proxy, proxy);
        // 继续挂载vnode
        patch(null, subTree, contanier)
        instance.isMounted = true
      }
    })
  }
}

最后

这里主要就是对于Vue3从createApp函数,将App组件挂载到页面的简单实现过程,这里只是对创建dom到页面,对于更新的diff场景未涉及,将在下一遍中更新。对Vue3响应式感兴趣的朋友看上一篇文章。