Vue 3 内部原理讲解,深刻理解 Vue 3,建立你本身的 Vue 3。
Deep Dive into Vue 3. Build Your Own Vue 3 From Scratch.html
本文完整内容可见,从零开始建立你本身的Vue 3vue
你能学到什么node
Vue 3有三个核心模块,分别是:react
reactivity
)模块compiler
)模块renderer
)模块reactivity
模块用来建立响应式对象,咱们能够监听这些对象的修改,当执使用了这些响应式对象的代码执行时,他们就会被跟踪,当响应式对象的值发生了变化时,这些被追踪的代码会从新执行。git
compiler
模块是用来处理模板的,它把模板编译成 render
函数,它能够发生在浏览器的运行阶段,但更多的是在Vue项目构建时进行编译。github
renderer
模块处理 VNode
,把组件渲染到 web 页上。它包含三个阶段:web
Render
阶段,调用 render
函数并返回一个 VNode
;Mount
阶段,render
接收 VNode
,而后进行 JavaScript DOM 操做来建立 web 页;Patch
阶段,render
接收新旧两个 VNode
,比较两者的不一样,而后进行页面的局部更新。compiler
把 HTML 编译成 render
函数;reactivity
模块初始化 reactive
对象;renderer
模块的 Render
阶段,调用引用了 reactive
对象的 render
函数,这样就监听了响应式对象,render
函数返回 VNode
;renderer
模块的 Monut
阶段,用 VNode
生成真实的 DOM 并渲染到页面上;reactive
对象发生了变化,将再次调用 render
函数建立新的 VNode
,这时将进入 renderer
模块的 Patch
阶段,更新页面的变化。你能学到什么算法
render
函数存在的必要性tempalte
和 render
的使用场景Virtual DOM 就是用 JavaScript 对象来描述真实的 DOM 节点。typescript
例如,有这样一段 HTML:编程
<div id="div"> <button @click="click">click</button> </div>
用 Virtual DOM 来表示能够是这样的(为何说能够是这样,由于这彻底取决于你的设计):
const vDom = { tag: 'div', id: 'div', children: [{ tag: 'button', onClick: this.click }] }
render
函数,把 Virtual DOM 渲染成本身想要的东西,而不只仅是 DOM。首先咱们知道,当你在编写 Vue 组件或页面时,通常会提供一个 template
选项来写 HTML 内容。根据Vue 3 总览这一部份内容的介绍,Vue 会先走编译阶段,把 template
编译成 render
函数,因此说最终的 DOM 必定是从 render
函数输出的。所以 render
函数能够用来代替 template
,它返回的内容就是 VNode
。直接使用 render
反而能够省去 complier
过程。
Vue 中提供的 render
函数是很是有用的,由于有些状况用 template
来表达业务逻辑会必定程度受到限制,这种状况你须要一种比较灵活的编程方式来表达底层的逻辑。
例如,当你有一个需求是大量的文本输入框,这中需求你要写的标签并很少,可是却揉了大量的交互逻辑,你须要在模板上添加大量的逻辑代码(好比控制关联标签的显示),然而,你的 JavaScript 代码中也有大量的逻辑代码。
render
函数的存在可让你在一个地方写业务逻辑,这时你就不用太多的去考虑标签的问题了。
有一段 template
以下:
template: '<div id="foo" @click="onClick">hello</div>'
Vue 3 中用 render
函数实现以下:
import { h } from 'vue' render() { return h('div', { id: 'foo', onClick: this.onClick }, 'hello') }
因为这是纯粹的 JavaScript,因此若是你须要实现 template
中相似 v-if
、v-for
这样的功能,直接经过三元表达式作到。
import { h } from 'vue' render() { let nodeToReturn // v-if="ok" if(this.ok) { nodeToReturn = h('div', { id: 'foo', onClick: this.onClick }, 'ok') } else { // v-for="item in list" const children = this.list.map(item => { return h('p', { key: item.id }, item.text) }) nodeToReturn = h('div', {}, children) } return nodeToReturn }
这就是 render
基本使用用法,就是 JavaScript 代码而已。
通常来讲咱们用 tempate
能够知足大多数场景来,可是你必定了解过 slot
这个东西,若是只使用 tempate
你将没法操做 slot
中的内容,若是你须要程序式地修改传进来的 slot
内容,你就必须用到 render
函数了(这也是大多数使用 render
函数的场景)。
下面咱们用一个例子来讲明。
好比咱们要实现这样一个组件:实现层级缩进效果,即相似 HTML 中嵌套的 UL 标签,看起来就像这样:
level 1 level 1-1 level 1-2 level 1-2-1 level 1-2-2
咱们的模板是这样写的,实际上 Stack
组件就是给每个 slot
都增长一个左边距:
<Stack size="10"> <div>level 1</div> <Stack size="10"> <div>level 1-1</div> <div>level 1-2</div> <Stack size="10"> <div>level 1-2-1</div> <div>level 1-2-2</div> </Stack> </Stack> </Stack>
如今咱们只用 template
是没法实现这种效果的,众所周知,template
只能把默认的 slot
渲染出来,它不能程序式处理 slot
的值。
咱们先用 template
来实现这个组件,stack.html:
const Stack = { props: { size: [String, Number] }, template: ` <div class="stack"> <slot></slot> </div> ` }
这样因为不能处理 slot
内容,那么它的表现效果以下,并无层级缩进:
level 1 level 1-1 level 1-2 level 1-2-1 level 1-2-2
咱们如今尝试用 render
函数实现 Stack
组件:
const { h } = Vue const Stack = { props: { size: [String, Number] }, render() { const slot = this.$slots.default ? this.$slots.default() : [] return h('div', { class: 'stack' }, // 这里给每一项 slot 增长一个缩进 class slot.map(child => { return h('div', { class: `ml${this.$props.size}` }, [ child ]) })) } }
render
函数中咱们能够经过 this.$slots
拿到插槽内容,经过 JavaScript 把它处理成任何咱们想要的东西,这里咱们给每一项 slot
添加了一个 margin-left: 10px
缩进,看下效果:
level 1 level 1-1 level 1-2 level 1-2-1 level 1-2-2
完美,咱们实现了一个用 template
几乎实现不了的功能。
原则:
render
render
函数template
,这样更高效,且 template
更容易被 complier
优化你可能会想,为何不直接编译成 VNode
,而要在中间加一层 render
呢?
这是由于 VNode
自己包含的信息比较多,手写太麻烦,也许你写着写着不自觉就封装成了一个 helper
函数,h
函数就是这样的,它把公用、灵活、复杂的逻辑封装成函数,并交给运行时,使用这样的函数将大大下降你的编写成本。
知道了为何要有 render
后,才须要去设计实现它,其实主要是实现 h
函数。
你能学到什么
VNode
render
的具体渲染原理diff
算法的做用在Vue 3总览章节中,咱们已经初步认识了编译器(complier
)和渲染器( renderer
)的做用。
render
函数VNode
,把组件渲染到 web 页上咱们有这样一段 HTML:
<div id="div"> <button @click="click">click</button> </div>
编译器会先把它处理成 render
函数,相似下面的代码:
import { h } from 'vue' render() { return h('div', { id: 'div', }, [ h('button', { onClick: this.click }, 'click') ]) }
渲染器经过 render
函数获取对应的 VNode
,相似这样:
const vDom = { tag: 'div', id: 'div', children: [{ tag: 'button', onClick: this.click, text: 'click' }] }
上面是一个很简单的例子,实际上,Vue 3中的编译器作了不少的优化工做,好比判断你的节点是静态的仍是动态的、缓存事件的绑定等等。因此若是你的组件用 template
实现的话,反而会被 Vue 优化。
咱们经过 Vue 3在线模板编译系统 生成一段真实代码:
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createBlock("div", { id: "div" }, [ _createVNode("button", { onClick: _ctx.click }, "click", 8 /* PROPS */, ["onClick"]) ])) } // Check the console for the AST
能够看到和咱们手写的 render
函数仍是有比较大的差别。
render
函数返回结果就是 h
函数执行的结果,所以 h
函数的输出为 VNode
。
因此须要先设计一下咱们的 VNode
。
一个 html
标签有它的标签名、属性、事件、样式、子节点等诸多信息,这些内容都须要在 VNode
中体现。
<div id="div"> div text <p>p text</p> </div>
const elementVNode = { tag: 'div', props: { id: 'div' }, text: 'div text', children: [{ tag: 'p', props: null, text: 'p text' }] }
上面的代码显示了 DOM 变成 VNode
的表现形式,VNode
各属性解释:
tag
:表示 DOM 元素的标签名,如 div
、span
等props
:表示 DOM 元素上的属性,如id
、class
等children
:表示 DOM 元素的子节点text
:表示 DOM 元素的文本节点这样设计 VNode
彻底没有问题(实际上 Vue 2 就是这样设计的),可是 Vue 3 设计的 VNode
并不包含 text
属性,而是直接用 children
代替,由于 text
本质也是 DOM 的子节点。
在保证语义讲得通的状况下尽量复用属性,可使 VNode
对象更加轻量。
基于此咱们把刚才的 VNode
修改为以下形式:
const elementVNode = { tag: 'div', props: { id: 'div' }, children: [{ tag: null, props: null, children: 'div text' }, { tag: 'p', props: null, children: 'p text' }] }
什么是抽象内容呢?组件就属于抽象内容,好比下面这一段模板内容:
<div> <MyComponent></MyComponent> </div>
MyComponent
是一个组件,咱们预期渲染出 MyComponent
组件全部的内容,而不是一个 MyComponent
标签,这用 VNode
如何表示呢?
上一段内容咱们其实已经经过 tag
是否为 null
来区分元素节点和文本节点了,那这里咱们能够经过 tag
是不是字符串判断是标签仍是组件呢?
const elementVNode = { tag: 'div', props: null, children: [{ tag: MyComponent, props: null }] }
理论上是能够的,Vue 2 中就是经过 tag
来判断的,具体过程以下,能够在这里看源码:
VNode.tag
若是不是字符串,则建立组件类型的 VNode
VNode.tag
是字符串
html
或 svg
标签,则建立正常的 VNode
VNode
VNode
以上这些判断都是在挂载(或 patch
)阶段进行的,换句话说,一个 VNode
表示的内容须要在代码运行阶段才知道。这就带来了两个难题:没法从 AOT
的层面优化、开发者没法手动优化。
若是能够提早知道 VNode
类型,那么就能够对其进行优化,因此这里咱们能够定义好一套用来判断 VNode
类型的规则,随即是用 FLAG = 1
这样的数字表示仍是其它方法。
这里咱们给 VNode
增长一个字段 shapeFlag
(这是为了和 Vue 3 保持一致),它是一个枚举类型变量,具体以下:
export const enum ShapeFlags { // html 或 svg 标签 ELEMENT = 1, // 函数式组件 FUNCTIONAL_COMPONENT = 1 << 1, // 普通有状态组件 STATEFUL_COMPONENT = 1 << 2, // 子节点是纯文本 TEXT_CHILDREN = 1 << 3, // 子节点是数组 ARRAY_CHILDREN = 1 << 4, // 子节点是 slots SLOTS_CHILDREN = 1 << 5, // Portal PORTAL = 1 << 6, // Suspense SUSPENSE = 1 << 7, // 须要被keepAlive的有状态组件 COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 已经被keepAlive的有状态组件 COMPONENT_KEPT_ALIVE = 1 << 9, // 有状态组件和函数式组件都是“组件”,用 COMPONENT 表示 COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT }
如今咱们能够修改咱们的 VNode
以下:
const elementVNode = { shapeFlag: ShapeFlags.ELEMENT, tag: 'div', props: null, children: [{ shapeFlag: ShapeFlags.COMPONENT, tag: MyComponent, props: null }] }
shapeFlag
如何用来判断 VNode
类型呢?按位运算便可。
const isComponent = vnode.shapeFlag & ShapeFlags.COMPONENT
熟悉一下按位运算。
a & b
:对于每个比特位,只有两个操做数相应的比特位都是1时,结果才为1,不然为0。a | b
:对于每个比特位,当两个操做数相应的比特位至少有一个1时,结果为1,不然为0。咱们把 ShapeFlags
对应的值列出来,以下:
ShapeFlags | 操做 | bitmap |
---|---|---|
ELEMENT | 0000000001 |
|
FUNCTIONAL_COMPONENT | 1 << 1 | 000000001 0 |
STATEFUL_COMPONENT | 1 << 2 | 00000001 00 |
TEXT_CHILDREN | 1 << 3 | 0000001 000 |
ARRAY_CHILDREN | 1 << 4 | 000001 0000 |
SLOTS_CHILDREN | 1 << 5 | 00001 00000 |
PORTAL | 1 << 6 | 0001 000000 |
SUSPENSE | 1 << 7 | 001 0000000 |
COMPONENT_SHOULD_KEEP_ALIVE | 1 << 8 | 01 00000000 |
COMPONENT_KEPT_ALIVE | 1 << 9 | 1 000000000 |
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
根据上表展现的基本 flags
值能够很容易地得出下表:
ShapeFlags | bitmap |
---|---|
COMPONENT | 00000001 1 0 |
上面咱们已经看到了 children
能够是数组或纯文本,但真实场景多是:
null
这里咱们能够增长一个 ChildrenShapeFlags
的变量表示 children
的类型,可是基于以前的设计原则,咱们彻底能够用 ShapeFlags
来表示,那么同一个 ShapeFlags
如何既用来表示 VNode
的类型,又用来表示其 children
的类型呢?
仍然是按位运算,咱们经过 JavaScript 代码判断 children
类型,而后和当前 VNode
进行按位或运算便可。
咱们增长以下函数用来专门处理子节点类型,这和 Vue 3 中的处理一致:
function normalizeChildren(vnode, children) { let type = 0 if (children == null) { children = null } else if (Array.isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'string') { children = String(children) type = ShapeFlags.TEXT_CHILDREN } vnode.shapeFlag |= type }
这样咱们就能够直接经过 shapeFlag
同时判断 VNode
及其 children
类型了。
为何children
也须要标识呢?缘由只有一个: 为了patch
过程的优化。
至此,咱们能够定义 VNode
结构以下:
export interface VNodeProps { [key: string]: any } export interface VNode { // _isVNode 是 VNode 对象 _isVNode: true // el VNode 对应的真实 DOM el: Element | null shapeFlag: ShapeFlags.ELEMENT, tag: | string | Component | null, props: VNodeProps | null, children: string | Array<VNode> }
实际上,Vue 3 中对 VNode
的定义要复杂的多,这里就不去细看了。
首先咱们实现一个最简单的 h
函数,能够是这样的,接收三个参数:
tag
标签名props
DOM 上的属性children
子节点咱们新建一个文件 h.ts
,内容以下:
function h(tag, props, children){ return { tag, props, children } }
咱们用以下的 VNode
来表示 <div class="red"><span>hello</span></div>
:
import { h } from './h' const vdom = h('div', { class: 'red' }, [ h('span', null, 'hello') ])
看一下实际输出内容:
const vdom = { "tag": "div", "props": { "class": "red" }, "children": [ { "tag": "span", "props": null, "children": "hello" } ] }
基本符合预期,可是这里有同窗可能又要问了:“这个 vdom
和写的 h
函数没什么不一样,为何不直接写 VNode
?”
这是由于咱们如今的 h
函数所作的仅仅就是返回传入的参数,实际上根据咱们对 VNode
的定义,还缺乏一些字段,不过你也能够直接写 VNode
,但这样会增长大量的额外工做。
如今咱们补全 h
函数,添加 _isVNode
、el
和 shapeFlag
字段。
function h(tag, props = null, children = null) { return { _isVNode: true, el: null, shapeFlag: null, tag, props, children } }
这里的 _isVNode
永远为 true
,el
不是在建立 VNode
的时候赋值,因此不用处理,咱们主要处理 shapeFlag
,实际上 shapeFlag
有 10 种类型,咱们这里只实现一个最简单的判断:
function h(tag, props = null, children = null) { let shapeFlag = null // 这里为了简化,直接这样判断 if (typeof tag === 'string') { shapeFlag = ShapeFlags.ELEMENT } else if(typeof tag === 'object'){ shapeFlag = ShapeFlags.STATEFUL_COMPONENT } else if(typeof tag === 'function'){ shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT } return { _isVNode: true, el: null, shapeFlag, tag, props, children } }
如今咱们须要处理一下 children
的类型了,VNode 章节中咱们讲过其判断逻辑,那么 h
函数如今完整逻辑以下:
function h(tag, props = null, children = null) { let shapeFlag = null // 这里为了简化,直接这样判断 if (typeof tag === 'string') { shapeFlag = ShapeFlags.ELEMENT } else if(typeof tag === 'object'){ shapeFlag = ShapeFlags.STATEFUL_COMPONENT } else if(typeof tag === 'function'){ shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT } const vnode = { _isVNode: true, el: null, shapeFlag, tag, props, children } normalizeChildren(vnode, vnode.children) return vnode } function normalizeChildren(vnode, children) { let type = 0 if (children == null) { children = null } else if (Array.isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'object') { type = ShapeFlags.SLOTS_CHILDREN } else if (typeof children === 'string') { children = String(children) type = ShapeFlags.TEXT_CHILDREN } vnode.shapeFlag |= type }
如今咱们从新写一个测试代码看一下 h
函数输入结果:
import { h } from './h' const MyComponent = { render() {} } const vdom = h('div', { class: 'red' }, [ h('p', null, 'hello'), h('p', null, null), h(MyComponent) ]) console.log(vdom); // vdom: // { // _isVNode: true, // el: null, // shapeFlag: 17, // tag: 'div', // props: { class: 'red' }, // children: [ // { // _isVNode: true, // el: null, // shapeFlag: 9, // tag: 'p', // props: null, // children: 'hello' // }, // { // _isVNode: true, // el: null, // shapeFlag: 1, // tag: 'p', // props: null, // children: null // }, // { // _isVNode: true, // el: null, // shapeFlag: 4, // tag: [Object], // props: null, // children: null // } // ] // }
至此已经完成了 h
函数的基本设计,能够获得想要的 VNode
了,下一步就是把 VNode
渲染到页面上。
获得 VNode
以后,咱们须要把它渲染到页面上,这就是渲染器的 Mount
阶段。
首先,新建一个 render.ts
文件,用来处理挂载相关代码。
mount
函数应该是这样,接收一个 VNode
做为参数,并把生成的 DOM 放进指定的容器 container
中,实现以下:
function mount(vnode, container) { const el = document.createElement(vnode.tag) contianer.appendChild(el); }
这就是挂载所作的核心事情,不过这里咱们还缺乏具体要实现的内容:
shapeFlag
生成不一样 DOMvnode.el
VNode
的类型问题这里咱们须要先了解一下普通有状态组件和函数式组件分别是什么,如下仅作理解用。
const MyComponent = { render() { return h('div', null, 'stateful component') } }
function MyFunctionalComponent() { return h('div', null, 'function component') }
咱们根据 vnode.shapeFlag
的值来对各类类型 VNode
进行渲染操做。
function mount(vnode, container) { if (vnode.tag === null) { mountTextElement(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.ELEMENT) { mountElement(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { mountStatefulComponent(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) { mountFunctionalComponent(vnode, container) } } function mountTextElement(vnode, container) { ... } function mountElement(vnode, container) { ... } function mountStatefulComponent(vnode, container) { ... } function mountFunctionalComponent(vnode, container) { ... }
function mountTextElement(vnode, container) { const el = document.createTextNode(vnode.children) container.appendChild(el) }
function mountElement(vnode, container) { const el = document.createElement(vnode.tag) container.appendChild(el) }
普通有状态组件就是一个对象,经过 render
返回其 VNode
, 所以其渲染方法以下:
function mountStatefulComponent(vnode, container) { const instance = vnode.tag instance.$vnode = instance.render() mount(instance.$vnode, container) instance.$el = vnode.el = instance.$vnode.el }
函数式组件的 tag
为一个函数,返回值为 VNode
,所以其渲染方法以下:
function mountFunctionalComponent(vnode, container){ const $vnode = vnode.tag() mount($vnode, container) vnode.el = $vnode.el }
这里为了简化,这里咱们假设 props
的每一项都是 DOM 的 attribute
,因此咱们能够这样作:
function mountElement(vnode, container) { const el = document.createElement(vnode.tag) if(vnode.props){ for(const key in vnode.props){ const value = vnode.props[key] el.setAttribute(key, value) } } container.appendChild(el); }
实际上,Vue 3 中 props
是一个扁平化的结构,它同时包含了 property
、attribute
、event listener
等,每一项都须要单独处理,以下:
props: { id: 'div', class: 'red', key: 'key1', onClick: this.onClick }
简单解释 property
、attribute
的区别就是:attribute
是 DOM 自带的属性,如:id
、class
;property
是自定义的属性名,如:key
、data-xxx
。
咱们知道 children
能够是字符串或数组,所以实现方法以下:
function mountElement(vnode, container) { const el = document.createElement(vnode.tag) // props if(vnode.props){ for(const key in vnode.props){ const value = vnode.props[key] el.setAttribute(key, value) } } // children if(vnode.children){ if(typeof vnode.children === 'string'){ el.textContent = vnode.children }else{ vnode.children.forEach(child => { mount(child, el) }) } } container.appendChild(el); }
VNode
及其 DOM这个只须要增长一行代码便可,其它函数相似:
function mountTextElement(vnode, container) { const el = document.createTextNode(vnode.children) vnode.el = el // (*) container.appendChild(el) }
如今咱们实现了渲染器 mount
全部的功能,完整代码以下:
function mount(vnode, container) { if (vnode.tag === null) { mountTextElement(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.ELEMENT) { mountElement(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { mountStatefulComponent(vnode, container) } else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) { mountFunctionalComponent(vnode, container) } } function mountTextElement(vnode, container) { const el = document.createTextNode(vnode.children) vnode.el = el container.appendChild(el) } function mountElement(vnode, container) { const el = document.createElement(vnode.tag) // props if(vnode.props){ for(const key in vnode.props){ const value = vnode.props[key] el.setAttribute(key, value) } } // children if(vnode.children){ if(typeof vnode.children === 'string'){ el.textContent = vnode.children }else{ vnode.children.forEach(child => { mount(child, el) }) } } vnode.el = el container.appendChild(el) } function mountStatefulComponent(vnode, container) { const instance = vnode.tag instance.$vnode = instance.render() mount(instance.$vnode, container) instance.$el = vnode.el = instance.$vnode.el } function mountFunctionalComponent(vnode, container){ const $vnode = vnode.tag() mount($vnode, container) vnode.el = $vnode.el }
如今咱们能够检验一下写的是否正确,新建 vdom.html,添加以下代码,并在浏览器中打开:
import { h } from './h' import { mount } from './render' const MyComponent = { render() { return h('div', null, 'stateful component') } } function MyFunctionalComponent() { return h('div', null, 'function component') } const vdom = h('div', { class: 'red' }, [ h('p', null, 'text children'), h('p', null, null), h(MyComponent), h(MyFunctionalComponent) ]) console.log(vdom); mount(vdom, document.querySelector("#app"))
浏览器渲染结果,全部内容均正常显示。
至此,咱们已经了解了 Vue 3 的基本渲染原理,并实现了一个简易版本的渲染器。
未完待续~(因为内容太多,后续将不在本文继续增长)
你能学到什么
reactive
设计理念本文完整内容可见,build-your-own-vue-next