vue3源码浅析:createRenderer

3,113 阅读5分钟

vue3源码浅析:createApp

vue3源码浅析:mount

vue3源码浅析:createRenderer

createApp执行过程

createApp是Vue3的入口,执行该函数可以得到一个应用实例,createRenderer就是在这个过程中定义的,要想彻底了解createRenderer对于createApp执行过程必须先要了解其执行过程

根据源码createApp核心过程简化如下

//4.定义createAppApi
const createAppApi = (render) => {
    return function createApp() {
        const app = {
            use() {},
            mixin() {},
            component() {},
            directive() {},
            mount() {
                ...
                render(...)
                ...
            }
        };
        return app;
    }
};
//3.定义createRenderer,RendererOptions是一个对象,里面包含节点和属性的处理方法,通过重新配置RendererOptions里面的方法可以实现自定义渲染器
const createRenderer = (RendererOptions) => {
    return {
        render,
        createApp: createAppApi(render)
    }
};
//2.定义renderer
const renderer = createRenderer(RendererOptions);
//1.声明Vue和定义createApp方法
const Vue = {
    createApp(options) {
        const app = renderer.createApp(options);
        const { mount } = app;
        app.mount = (containerOrSelector) => {
            const container = document.querySelector(containerOrSelector);
            const proxy = mount(container);
            return proxy;
        }
        return app;
    }
};

createRenderer

createRenderer 源码中实际返回的是一个baseCreateRenderer函数,baseCreateRenderer返回的才是渲染器,而渲染器核心方法则是render

RendererOptions是一个对象,定义了对元素/属性的增删改查功能,其作为参数传递给createRenderer(准确的说是baseCreateRenderer),根据传递的方法不同,来满足用户对于自定义元素和属性处理方式的要求

下图中可以了解到RendererOptions和createRenderer各有哪些方法

RendererOptions

接下来我们针对rendererOptions部分方法看看vue3到底是怎么对元素和属性操作,源码位置如下:

其中nodeOps表示元素节点的操作,patchProp表示对节点属性的操作

从源码中,可以看出来处理方式都是对于DOM元素的增删改查操作

//  packages\runtime-dom\src\nodeOps.ts
const doc = (typeof document !== 'undefined' ? document : null) as Document
/**
 * @description: 
 * @param {*} child 来自createElement创建的元素
 * @param {*} parent 来自于mount传递的元素
 * @return {*}
 */
insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },
	
/**
 * @description: 创建元素
 * @param {tag} string 必须,值为遍历组件得到的所有元素的标签名
 * @return {*} DOM元素
 */   
  createElement: (tag, isSVG, is): Element =>
    isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined),

  createText: text => doc.createTextNode(text),

  createComment: text => doc.createComment(text),

  setText: (node, text) => {
    node.nodeValue = text
  },

  setElementText: (el, text) => {
    el.textContent = text
  },

  parentNode: node => node.parentNode as Element | null,

  nextSibling: node => node.nextSibling,

  querySelector: selector => doc.querySelector(selector),

  setScopeId(el, id) {
    el.setAttribute(id, '')
  },

  cloneNode(el) {
    return el.cloneNode(true)
  },

属性部分包括class/style/事件的处理

export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  switch (key) {
    // special
    case 'class':
      patchClass(el, nextValue, isSVG)
      break
    case 'style':
      patchStyle(el, prevValue, nextValue)
      break
    default:
      if (isOn(key)) {
        // ignore v-model listeners
        if (!isModelListener(key)) {
          patchEvent(el, key, prevValue, nextValue, parentComponent)
        }
      } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
        patchDOMProp(
          el,
          key,
          nextValue,
          prevChildren,
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      } else {
        // special case for <input v-model type="checkbox"> with
        // :true-value & :false-value
        // store value as dom properties since non-string values will be
        // stringified.
        if (key === 'true-value') {
          ;(el as any)._trueValue = nextValue
        } else if (key === 'false-value') {
          ;(el as any)._falseValue = nextValue
        }
        patchAttr(el, key, nextValue, isSVG)
      }
      break
  }
}

实现一个自定义createRenderer

目的:自定义一个组件,该组件功能为给定数据画出一个柱状图

template: '<bar-chart :data="dataRef" @click = "add"></bar-chart>',
setup() {
    const dataRef = ref(chartData)
    const add = () => {
        dataRef.value.push({
            title: '青铜',
            count: 200,
            color: 'brown'
        })
    }
    return {
        add,
        dataRef
    }
}

效果图

方式:通过修改createRenderer的RendererOptions利用canvas绘制柱状图,而非写一个组件文件

定义RendererOptions

从源码可知RendererOptions定义了对template得到的元素进行增删改查,同时对元素属性进行操作

本例中RendererOptions需要定义的方法有以下三个:

  1. 元素创建,通过定义createElement,返回的DOM元素作为后续操作的el

    本例子因为是要通过canvas绘制柱状图,el将作为一个对象,通过判断el内tag的类型来创建canvas

  2. 元素插入,通过定义insert,将组件转变成DOM元素插入到div#app内

  3. 元素属性操作,将属性加入到el上

因为只要求实现元素的增和查的作用,和对元素属性的操作

const {createRenderer} = Vue
const renderer = createRenderer({
    /**
     * @description: 创建DOM元素
     * @param {*} tag 来自template模板内所有元素的标签名
     * @return {*}
     */
    createElement(tag) {
        //tag的值为'bar-chart',直接作为对象返回,后续会作为一个el元素操作
        return { tag }
    },
    /**
     * @description: 元素属性操作
     * @param {*} el 来自createElement创建的元素
     * @param {*} key 创建元素的所有属性名
     * @param {*} prevValue 首次执行为空
     * @param {*} nextValue 创建元素的当前的属性名
     * @return {*}
     */
    patchProp(el, key, prevValue, nextValue) {
        //将元素属性加入到el中
        el[key] = nextValue;
    },
    /**
     * @description: 插入元素
     * @param {*} child 来自createElement返回的元素,本例中为el对象
     * @param {*} parent 来自于mount传递的元素
     * @return {*}
     */
    insert(child, parent) {
        //draw完成canvas的绘制并返回canvas元素
        const childNode = draw(child);
        //将canvas元素插入到div#app中
        parent.appendChild(childNode);
        //如果组件有绑定事件,则执行
        if (child.onClick) {
            childNode.addEventListener('click', () => {
                child.onClick();
            })
        }
    },
})

重写createApp方法

首先要知道为什么要重写createApp方法,通过改变RendererOptions我们已经改变了renderer中的render方法,希望程序按照我们的方法执行,那就需要将renderer方法进行替换,而renderer的替换需要动到createApp方法,所以需要重写createApp方法

那么要怎么重写呢,要点如下

  1. 替换renderer
  2. 保留其他的功能,重点有以下两个
    1. 得到app并返回实现链式调用
    2. 重写mount方法将container元素传给mount
function createCanvasApp(...args) {
    const app = renderer.createApp(...args);
    const {
        mount
    } = app;
    app.config.isCustomElement = tag => tag === 'bar-chart'
    app.mount = (containerOrSelector) => {
        const container = document.querySelector(containerOrSelector);
        container.innerHTML = ''
        const proxy = mount(container)
        return proxy
    }
    return app
}

canvas绘制柱状图

canvas绘制分两步:

  1. canvas元素创建
  2. 通过传递的data创建柱状图
/**
 * @description: 柱状图绘制
 * @param {*} canvas canvas元素
 * @param {*} props 即前文的child对象,表示tag标签名和数据或者事件
 * @return {*}
 */
const drawBarChart = (canvas, props) => {
    const width = canvas.width = window.innerWidth;
    const height = canvas.height = window.innerHeight;
    const OriginPos = {
        x: 50,
        y: 100,
    };
    const step = 20;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, width, height);
    const {
        data
    } = props;
    data.forEach((chartDatas, index) => {
        ctx.fillStyle = chartDatas.color;
        ctx.fillRect(OriginPos.x + step * index, OriginPos.y, 10, chartDatas.count);
    });
}
/**
 * @description: 通过传递过来的创建canvas需要的tag标签名和数据或者事件
 * @param {*} child 一个对象,表示tag标签名和数据或者事件
 * @return {*} canvas元素
 */
const draw = (child) => {
    let canvas;
    if (child.tag == 'bar-chart') {
        canvas = document.createElement('canvas');
    }
    drawBarChart(canvas, child);
    return canvas;
}

通过自定义渲染器绘制柱状图

const chartData = [{
        title: '青铜',
        count: 200,
        color: 'brown'
    },
    {
        title: '砖石',
        count: 300,
        color: 'skyblue'
    },
    {
        title: '王者',
        count: 50,
        color: 'gold'
    },
]
createCanvasApp({
    template: '<bar-chart :data="dataRef" @click = "add"></bar-chart>',
    setup() {
        const dataRef = ref(chartData)
        const add = () => {
            dataRef.value.push({
                title: '青铜',
                count: 200,
                color: 'brown'
            })
        }
        return {
            add,
            dataRef
        }
    }
})
.mount("#app")

未完成功能:事件处理更新组件

此部分会触发parentNodenextSibling,需要对RendererOptions的两个函数parentNodenextSibling重定义,这部分需要了解下diff过程有机会补充上