开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
我们在前面已经大致的了解了 vue2 和 vue3 的一些特性, 这里我们针对 vue3 的模板渲染原理 ,进行一下深度的剖析 我们先通过一个导图,来大致了解以下 vue3 的渲染器
下面是一个 demo 大家可以配合注释 食用
// 模板渲染原理
/**
* 用来判定 此 props 是否需要使用属性的方式添加(<input form='form1'> 这种 form 的属性是只读的,其在 Properties 存在,但是不能使用属性设置,需要使用 Atrribute 设置)
* @param {*} el dom元素
* @param {*} key 需要给 dom 元素设置的属性
* @param {*} value 需要给 dom 元素设置的属性值
* @returns
*/
function shouldSetAsProps(el,key,value){
// 特殊处理
if(key === 'from' && el.tagName === 'INPUT') return false
// 兜底
return key in el
}
/**
* 卸载操作 根据 vnode 获取要卸载的真实 DOM 元素,获取 el 的父元素, 调用 removeChild 移除元素
* @param {*} vnode 虚拟节点
*/
function unmount(vnode){
const parent = vnode.el.parentNode;
if(parent) parent.removeChild(vnode.el)
}
// 全局变量 ,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance){
currentInstance = instance
}
// 自定义渲染器 为实现在多端完成功能实现, 需要接收一个配置项,可以使得渲染器不仅能够完成渲染到浏览器的工作。
/**
*
* @param {*} options
* @returns
*/
function createRenderer(options){
const { createElement, insert, setElementText, patchProps } = options
/**
* patch 函数的实现 首次渲染时,container._vnode 是不存在的 即 undefined,意味着 n1 是 undefined,则 patch 函数* 会忽略 n1,直接将 n2 渲染到容器中。
* @param {*} n1 旧 vnode
* @param {*} n2 新 vnode
* @param {*} container 挂载容器
*/
function patch(n1,n2,container){
// 挂载方法 vnode type 类型如果是 object 则代表是组件 需要调用 mountComponent 方法
function mountElement(vnode,container){
// 创建 DOM 元素,让 vnode.el 引用真实 DOM 元素, 让 vnode 与真实 DOM 之间建立联系,方便卸载操作的完善执行
const el = vnode.el = document.createElement(vnode.type)
// 处理子节点,如果子节点是字符串,代表元素具有文本节点
if(typeof vnode.children === 'string'){
// 因此只需要设置元素的 textContent 属性即可
setElementText(el,vnode.children)
// 如果子节点是数组的话, 循环遍历递归调用
} else if(Array.isArray(vnode.children)){
// patch 的第一个参数是 null 因为是挂载阶段,没有旧的 vnode
vnode.children.forEach(child=>patch(null,child,el))
}
// 处理 props
if(vnode.props){
for(const key in vnode.props){
// 调用 patchProps 即可, 第三个参数因为这里是挂载,没有新旧比对, 所以传 null
patchProps(el,key,null,vnode.props[key])
}
}
// 将元素添加到容器中
container.appendChild(el)
}
if(n1 && n1.type !== n2.type){
// 如果新旧 vnode 类型不同,则直接卸载旧 vnode
unmount(n1)
n1 = null
}
// 代码运行到这里 证明 n1 和 n2 类型相同, 需要进一步判断是否是组件
const { type } = n2 // 如果类型是 string 则证明是标签元素, 如果是 object 则表示是组件
if(typeof type === 'string'){
// 如果 n1 不存在,则表示挂载,调用 mountElement 函数完成挂载
if(!n1){
mountElement(n2,container)
} else {
// n1 存在,意味着打补丁
patchElement(n1,n2)
}
}else if(typeof type === 'object'){
// 处理组件类型
mountComponent(n2,container)
}else if(typeof type === 'xxx'){
// 处理其他类型
}
}
// 完成组件渲染任务
function mountComponent(vnode,container,anchor){
// 通过 vnode 获取组件的选项对象, 即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render , 组件自身状态 data, 组件的生命周期函数
const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdated, updated, setup } = componentOptions
function resolveProps(options,propsData){
const props = {}
const attrs = {}
for(const key in propsData){
// 以字符串 on 开头的 props 无论是否显示的声明, 都将其添加到 props 数据中, 而不是 attrs
if(key in options || key.startsWith('on')){
props[key] = propsData[key]
}else{
attrs[key] = propsData[key]
}
}
return [props,attrs]
}
beforeCreate && beforeCreate()
// 调用 data 函数得到原始数据,并调用 reactive 函数 将其包装为响应式对象
const state = data ? reactive(data()) : null
// 拿到 props 和 attrs 通过 reslovePrps 方法
const [props,attrs] = resolveProps(propsOption,vnode.props)
// 定义组件实例, 一个组件实例本质上就是一个对象,包含组件有关的状态信息
const instance = {
state, // 组件自身状态
isMounted:false, // 组件挂载状态
subTree:null, // 组件所需要渲染的内容
props: shallowReactive(props), // props 是一个 浅响应对象
mounted:[], // 在组件实例中添加 mounted 数组, 用来存储通过 onMounted 函数注册的生命周期钩子函数
}
// 定义 emit 函数, 接收两个参数 event 事件名, payload, 传递给事件处理函数的参数
function emit(event,...payload){
// 根据约定对事件名称进行处理, 例如 change => onChange
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
// 根据事件处理后的事件名称去 props 中寻找对应的事件处理函数
const handler = instance.props[eventName]
if(handler){
// 调用事件处理函数并传递参数
handler(...payload)
}else{
console.error('事件不存在')
}
}
// setupContext
const setupContext = { attrs,emit }
// 调用setup函数之前, 设置当前组件实例
setCurrentInstance(instance)
// 调用 setup 函数, 将只读版本的 props 作为第一个参数传递,避免用户修改 props, 将 setupContext 作为第二个参数传递
const setupResult = setup(shallowReadonly(instance.props,setupContext))
// setup 函数执行完毕, 重置当前组件实例
setCurrentInstance(null)
// setupState 用来存储由 setup 返回的数据
let setupState = null
// 如果 setup 函数返回的是函数, 则将其作为渲染函数
if(typeof setupResult === 'function'){
// 报告冲突
if(render) console.error('setup函数返回渲染函数,render 选项将被忽略')
// 将 setupResult 作为渲染函数
render = setupResult
}else{
// 如果 setup 返回值不是函数, 则作为数据状态 赋值给 setupState
setupState = setupResult
}
vnode.component = instance
// 上下文对象, 劫持对 组件选项中 各个 API 的访问
const renderContext = new Proxy(instance,{
get(t,k,r){
const { state, props } = t
if( state && k in state){
return state[k]
}else if(k in props){
return props[k]
}else if(setupState && k in setupState){
// 渲染上下文需要增加对 setupState 的支持
return setupState[k]
}else{
console.error('不存在');
}
},
set(t,k,v,r){
const { state, props } = t
if(state && k in state){
state[k] = v
}else if(k in props){
props[k] = v
}else if(setupState && k in setupState){
setupState[k] = v
}else{
console.error('不存在')
}
}
})
// 将组件实例设置到 vonde 上, 用于后续更新
vonde.component = instance
created && created.call(renderContext)
// 将组件的 render 函数调用 包装到 effect 内, 使其有能力触发组件的自更新
effect(()=>{
// 执行渲染函数,获取组件要渲染的内容, 即 render 函数返回的 虚拟 DOM, 将其 this 设置为 state, 这就是 vue 中可以通过 this 访问 data 中的状态的原理
const subTree = render.call(renderContext,renderContext)
if(!instance.isMounted){
beforeMount && beforeMount.call(renderContext)
// 调用 patch 函数来挂载组件所描述的内容 即 subTree
patch(null,subTree,container,anchor)
instance.isMounted = true
mounted && mounted.call(renderContext)
} else {
beforeUpdated && beforeUpdated.call(renderContext)
patch(instance.subTree,subTree,container,anchor)
updated && updated.call(renderContext)
}
instance.subTree = subTree
},{
// 指定该副作用函数的调度器 为 queueJob
scheduler: queueJob
})
}
function render(vnode,container){
if(vnode){
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
patch(container._vnode,vnode,container)
}else{
if(container._vnode){
// 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
unmount(container._vnode)
}
}
// 把 vnode 存储到 container._vnode 下, 即后续渲染中的旧 vnode
container._vnode = vnode
}
return {
render
}
}
// 使用 在创建 renderer 时传入配置项 这个配置项主要用于指定是在 DOM 还是在其他端渲染, createRenderer方法不考虑在什么端使用
const renderer = createRenderer({
// 用于创建元素
createElement(tag){
return document.createElement(tag)
},
// 用于设置元素的文本节点
setElementText(el,text){
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el,parent,anchor = null){
parent.insertBefore(el,anchor)
},
/**
* 属性设置的相关操作封装到 patchProps 中,作为渲染器的选项传递
* @param {*} el 容器元素
* @param {*} key 需要设置给容器的属性
* @param {*} prevValue 上一个属性
* @param {*} nextValue 将要设置的属性
*/
patchProps(el,key,prevValue,nextValue){
console.log('patchProps',el,'--',key,'--',prevValue,'--',nextValue);
// 对 class 进行处理
if(key === 'class'){
el.className = changeClass(nextValue) || ''
}else if(shouldSetAsProps(el,key,nextValue)){
// 获取该 DOM Properties 的类型
const type = typeof el[key];
// 如果 type 是布尔值类型, value 是空字符串,则将值矫正为 true(这种情况会发生在比如属性是 disable的时候)
if(type === 'boolean' && nextValue === ''){
el[key] = true
}else{
el[key] = nextValue
}
}else{
// 如果要设置的属性没有对应的 DOM Properties ,则直接使用 setAttribute 设置属性
el.setAttribute(key,nextValue)
}
}
})
// 对 class 进行序列化处理
function changeClass(value){
let res = ''
function classNameChange(value){
if(typeof value === 'string'){
res += value + ' '
}else if(Object.prototype.toString.call(value) === '[object Object]'){
Object.keys(value).forEach(item=>{
console.log('item',item);
value[item] ? (res += item + ' ') : ''
})
}else if(Array.isArray(value)){
value.forEach(item=>classNameChange(item))
}
return res
}
return classNameChange(value).trim()
}
// demo
const vondes = {
type:'p',
props:{
class:[
'black',
{
'reds':true,
'blues':false
},
'sdfas'
],
id:'100'
},
children:'text'
}
renderer.render(vondes,document.querySelector('#app'))
console.log(changeClass(vondes.props.class));
// 调度器, 组件自身的响应式数据发生变化是, 因为 effect 的执行是同步的, 所以需要将 相关联的副作用进行缓存, 可以对 任务进行去重, 且避免多次执行副作用函数带来的开销
// 缓存任务队列, 使用 set 数据结构
const queue = new Set()
// 一个标志, 代表是否正在刷新任务队列
let isFlushing = false
// 创建一个立即 resolve 的 Promise
const p = Promise.resolve()
// 调度器的主要函数, 用来将一个任务 添加到缓存队列中, 并且开始刷新队列
function queueJob(job){
// 将 job 添加到任务队列 queue中
queue.add(job)
// 如果还没有开始刷新队列,则刷之
if(!isFlushing){
// 将该标志设置为 true , 以避免重复刷新
isFlushing = true
// 在微任务中 刷新缓冲队列
p.then(()=>{
try{
// 执行任务队列中的任务
queue.forEach(job=>job())
} finally {
// 重置状态
isFlushing = false
queue.clear = 0
}
})
}
}