更好的浏览体验请参看博客原文
话不多说,先来一张vue3创立利用实例到挂载整颗dom树的流程图。
本文指标
简略实现 vue3 中的 createApp
和 mount
两个 API。尽管只有两个 API,然而这两个 API 实现了 vue3 根实例的创立、组件的解析、vnode 数的构建...
通过这两个 API 的实现,就可能理解到整个 vue3 我的项目的整体挂载流程,能够说是 vue3 的外围 API。
本文代码 GitHub 仓库地址
// 定义根组件import { h } from 'vue' // 文中只有 h 办法应用 vue 官网提供import { reactive } from './vic/reactive' // 本人实现的 reactiveimport { createApp } from './vic' // 本人实现的 createAppconst App = { components: { SubCom }, setup() { const count = reactive({ value: 0 }) const inc = () => { count.value++ } return () => h('div', [h('h1', count.value), h('button', { onClick: inc }, '++'), h(SubCom)]) }}// 定义子组件const SubCom = { setup() { return () => h('div', [h('h2', '我是子组件')]) }}// 挂载createApp(App).mount(document.getElementById('app'))
代码实现(联合流程图看代码正文)
import { effect } from './reactive'let uid = 0/** * 1. 创立 context,用来承载一些全局的配置、全局注册的指令、组件... * 2. 创立 app 实例(vue 我的项目惟一的实例,目前次要关注它提供的 mount 办法) */export const createApp = (rootComponent, rootProps = null) => { const context = { config: { devtools: true, performance: false, globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined, warnHandler: undefined }, mixins: [], components: {}, directives: {}, provides: Object.create(null) } const app = { /** * 1. 创立根组件的 vnode * 2. 调用 render 办法,将 vnode 渲染到实在 dom 上 */ mount: (rootContainer) => { const vnode = createVNode(rootComponent, rootProps) vnode.appContext = context render(vnode, rootContainer) }, components: {} } return app}/** * 极简版的创立 vnode * @param type 当 type 为对象时,示意这是个组件,当 type 为字符串时,示意这是个原生 dom 标签 */function createVNode(type, props) { const vnode = { __v_isVNode: true, type, props, key: props, scopeId: 1, children: null, component: null, el: null, shapeFlag: 4, // 示意组件,因为这里只在创立根组件的 vnode 的时候用到,所以这里先写死 patchFlag: 0, appContext: null } return vnode}// render 什么也没做,光是调用了 patch 办法// patch 的第一个参数传入 null 示意这是首次渲染,没有上一次的 vnode 进行 difffunction render(vnode, container) { patch(null, vnode, container)}// 整个 vue 利用递归挂载的终点// 依据 shapeFlag 的不同,抉择挂载组件还是挂载 dom 元素function patch(n1, n2, container,) { const { shapeFlag } = n2 if (typeof n2.type === "symbol") { processText(n1, n2, container) return } if (shapeFlag === 17 || shapeFlag === 9) { processElement( n1, n2, container ) } else if (shapeFlag === 4) { // 首次 mount 走的就是这个分支 processComponent( n1, n2, container ) }}// 啥也没干,调了 mountComponent 办法function processComponent(n1, n2, container) { mountComponent(n2, container)}/** * 1. 依据 vnode 创立了它的实例 * 2. 对实例进行各种解决,包裹 props、data、调用组件的 setup 办法、生成组件的 render 办法 * 3. 调用实例的 render 办法,失去组件下的第一额实在 dom 的 vnode,递归调用 patch(此时 patch 传入的就是实在 dom 的 vnode 了) */function mountComponent(initialVNode, container) { const instance = (initialVNode.component = createComponentInstance( initialVNode, null, null )) setupComponent(instance) setupRenderEffect(instance, initialVNode, container)}// 依据组件 vnode 创立相应的实例// 这里须要关注两个属性:appContext、components,这两个属性都继承了根实例的相应属性// 这样做有利于在每个组件中拿到我的项目共享的一些属性和办法,在任何组件都可能拿到全局注册过的组件function createComponentInstance(vnode, parent, suspense) { // inherit parent app context - or - if root, adopt from root vnode const appContext = (parent ? parent.appContext : vnode.appContext) || {} const instance = { uid: uid++, vnode, parent, appContext, type: vnode.type, root: null, // to be immediately set next: null, subTree: null, // will be set synchronously right after creation update: null, // will be set synchronously right after creation render: null, // state ctx: {}, data: {}, props: {}, attrs: {}, slots: {}, refs: {}, setupState: {}, setupContext: null, // per-instance asset storage (mutable during options resolution) components: Object.create(appContext.components || []) } instance.ctx = { _: instance } instance.root = parent ? parent.root : instance return instance}// 这里次要是调用组件的 setup 办法失去了实例的 render 办法(其实还有更多工作,如解决 data、props...此处暂且不管)function setupComponent(instance) { const Component = instance.type const { setup } = Component const setupResult = setup(instance.props, {}) instance.render = setupResult}/** * 1. 应用 effect 包裹响应式操作 * 2. 响应式操作外面次要做两件事 * 2.1 调用实例的 render 办法失去 subTree * 2.2 递归调用 patch(subTree, container) */function setupRenderEffect( instance, initialVNode, container) { // create reactive effect for rendering // Vic 创立更新函数 instance.update = effect(function componentEffect() { const { el } = initialVNode // Vic 在这里创立组件根 dom 的 vNode 树 (这棵树外面的 children 也创立出 vnode) const subTree = (instance.subTree = renderComponentRoot(instance)) el && container.removeChild(el) // Vic 这个办法外面真正开始构建内存中的 dom 树 patch( null, subTree, container ) initialVNode.el = subTree.el })}// 调用实例的 render 办法,失去该组件实例下根 dom 节点的 vnode (由 h 函数来生成)function renderComponentRoot(instance) { const { props, slots, attrs, emit, render } = instance let result // 函数式组件 result = render(props, { attrs, slots, emit }) return result}// 没啥说的,调用 mountElement 办法function processElement(n1, n2, container) { if (n1 == null) { mountElement(n2, container) }}/** * 前置: 能走到这里来就意味着 vnode 是一个 dom 的 vnode (这里的 type 肯定是一个字符串) * 1. 依据 type 创立 dom 节点 * 2. 将创立进去的 dom 节点增加到 container 外面 * 3. 解决 props (属性和事件) * 4. mountChildren (遍历 children,递归调用 patch) */function mountElement(vnode, container) { const { type, props } = vnode let el = vnode.el = document.createElement(type) container.appendChild(el) Object.entries(props || {}).forEach(([key, val]) => { if (key.startsWith('on')) { el.addEventListener(key.substr(2).toLocaleLowerCase(), val) } else { el[key] = val } }) if (!Array.isArray(vnode.children)) { // 递归的终止点 el.appendChild(document.createTextNode(vnode.children)) } else { mountChildren(vnode.children, el, null) }}// 对每一个 child 进行 patchfunction mountChildren(children, container) { for (let i = 0; i < children.length; i++) { const child = children[i] patch( null, child, container ) }}// 乏善可陈(递归的终止点)function processText(n1, n2, container) { n2.el = document.createTextNode(n2.children) container.appendChild(n2.el)}
最初
本文旨在厘清 mount 递归挂载 vue3 利用的整个过程。函数名称与 vue-next 源码保持一致,然而暗藏了大量的细节以及与 mount 无关的分支。