关于运行时的包主要有两个:
packages/runtime-core:运行时的核心代码
packages/runtime-dom:运行时关于浏览器渲染的核心代码。
vue需要处理不同种宿主环境,比如浏览器端,服务端,不同宿主环境中,渲染dom方法不同,所以vue对运行时的代码做了处理,把所有浏览器dom操作放在了runtime-dom中,把整个运行时核心代码放在了runtime-core中 一下是runtime-dom里面nodeOps.ts文件内容。
const doc=document
export const nodeOps={
/**
* 插入元素到指定位置
*/
insert:(child,parent,anchor)=>{
parent.insertBefore(child,anchor||null)
},
/**
* 创建指定的element
*
*/
createElement:(tag):Element=>{
const el=doc.createElement(tag)
return el
},
/**
* 为指定的element设置textContent
*/
setElementText:(el,text)=>{
el.textContent=text
},
/**
* 删除指定元素
*/
remove: (child) => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
/**
* 创建 Text 节点
*/
createText: (text) => doc.createTextNode(text),
/**
* 设置 text
*/
setText: (node, text) => {
node.nodeValue = text
},
/**
* 创建 Comment 节点
*/
createComment: (text) => doc.createComment(text)
}
可以看出来都是使用了浏览器的api。
运行时是将vnode渲染到页面,主要包括两个环节:
1.h函数:生成vnode函数
2.render函数:渲染vnode
h函数构建
h函数是用来快速生成vnode的函数。js中没有重载的概念,根据对参数的个数以及类型判断,来决定如何调用createVNode函数,生成不同VNode(VNode的type(节点类型)有很多,比如:DOM,文本,注释,组件等等)
render函数
/**
* 渲染函数
*/
const render = (vnode, container) => {
if (vnode == null) {
//卸载
if (container._vnode) {
unmount(container._vnode)
}
} else {
//打补丁(包括挂载在和更新)
patch(container._vnode || null, vnode, container)
}
container._vnode = vnode
}
return {
render,
createApp: createAppAPI(render)
}
首先判断了需要渲染的vnode是否为null,如果是空,就将原有的vnode卸载,不为空的话执行打补丁的操作(包括挂载和更新)
const patch = (oldVNode, newVNode, container, anchor = null) => {
if (oldVNode === newVNode) {
return
}
/**
* 判断是否为相同类型节点
*/
if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
unmount(oldVNode)
oldVNode = null
}
const { type, shapeFlag } = newVNode
switch (type) {
case Text:
// Text
processText(oldVNode, newVNode, container, anchor)
break
case Comment:
processCommentNode(oldVNode, newVNode, container, anchor)
break
case Fragment:
// Fragment
processFragment(oldVNode, newVNode, container, anchor)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(oldVNode, newVNode, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件
processComponent(oldVNode, newVNode, container, anchor)
}
}
}
在patch过程中首先会判断新旧vnode是否相等,相等就直接返回,不执行任何操作,再判断新旧虚拟node是否为同一种类型,如果不是同一种类型就将原虚拟node删除,最后根据vnode上不同type类型,执行不同操作。
Element节点的挂载更新实现(Dom节点)
/**
* 处理元素 - 执行元素的挂载或更新
* @param oldVNode - 旧的虚拟节点
* @param newVNode - 新的虚拟节点
* @param container - 包含元素的容器
* @param anchor - 插入位置的锚点
*/
const processElement = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
// 如果旧虚拟节点不存在,执行挂载
mountElement(newVNode, container, anchor)
} else {
// 如果旧虚拟节点存在,执行更新
patchElement(oldVNode, newVNode)
}
}
如果旧的虚拟节点不存在,则直接挂载,如果旧的虚拟节点存在,则执行更新逻辑
挂载实现
/**
* 挂载元素 - 将虚拟节点挂载到DOM树上
* @param vnode - 虚拟节点
* @param container - 包含元素的容器
* @param anchor - 插入位置的锚点
*/
const mountElement = (vnode, container, anchor) => {
const { type, props, shapeFlag } = vnode
// 创建DOM元素
const el = (vnode.el = hostCreateElement(type))
// 如果shapeFlag表示文本子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 设置元素文本内容
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
//设置Array子节点
mountChildren(vnode.children, el, anchor)
}
// 如果存在props
if (props) {
// 遍历props,更新属性
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 插入元素到容器中
hostInsert(el, container, anchor)
}
判断子节点类型,设置文本节点或Array子节点,然后再设置props(此时旧值为null)
/**
* 挂载子节点 - 将子节点列表挂载到容器中
* @param children - 子节点列表
* @param container - 包含元素的容器
* @param anchor - 插入位置的锚点
*/
const mountChildren = (children, container, anchor) => {
// 如果children是字符串,将其转换为字符数组
if (isString(children)) {
children = children.split('')
}
// 遍历子节点列表
for (let i = 0; i < children.length; i++) {
// 规范化子节点
const child = (children[i] = normalizeVNode(children[i]))
// 执行挂载
patch(null, child, container, anchor)
}
}
mountChildren里面继续调用patch,循环挂载子元素
更新实现
/**
* 更新元素 - 更新已挂载的虚拟节点以反映最新状态
* @param oldVNode - 旧的虚拟节点
* @param newVNode - 新的虚拟节点
*/
const patchElement = (oldVNode, newVNode) => {
// 获取DOM元素引用
const el = (newVNode.el = oldVNode.el!)
// 旧节点的props
const oldProps = oldVNode.props || {}
// 新节点的props
const newProps = newVNode.props || {}
// 更新子节点
patchChildren(oldVNode, newVNode, el, null)
// 更新props
patchProps(el, oldProps, newProps, newProps)
}
先获取并设置父元素的dom,再更新子节点与props
/**
* 更新子节点 - 比较并更新虚拟节点的子节点列表
* @param oldVNode - 旧的虚拟节点
* @param newVNode - 新的虚拟节点
* @param container - 包含元素的容器
* @param anchor - 插入位置的锚点
*/
const patchChildren = (oldVNode, newVNode, container, anchor) => {
// 旧节点的子节点列表
const c1 = oldVNode && oldVNode.children
// 旧节点的shapeFlag
const prevShapeFlage = oldVNode ? oldVNode.shapeFlag : 0
// 新节点的子节点列表
const c2 = newVNode.children
// 新节点的shapeFlag
const { shapeFlag } = newVNode
// 如果新节点的shapeFlag表示文本子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果旧节点的shapeFlag表示数组子节点
if (prevShapeFlage & ShapeFlags.ARRAY_CHILDREN) {
//卸载旧的子节点
}
// 如果新旧子节点的文本内容不同
if (c1 !== c2) {
// 更新元素文本内容
hostSetElementText(container, c2 as string)
}
} else {
// 如果旧节点的shapeFlag表示数组子节点
if (prevShapeFlage & ShapeFlags.ARRAY_CHILDREN) {
// 如果新节点的shapeFlag也表示数组子节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
//进行diff比对
patchKeyedChildren(c1, c2, container, anchor)
} else {
}
} else {
// 如果旧节点的shapeFlag表示文本子节点
if (prevShapeFlage & ShapeFlags.TEXT_CHILDREN) {
// 清空旧文本
hostSetElementText(container, '')
}
// 新节点的shapeFlag表示数组子节点
}
}
}
shapeFlag表示的是子节点的类型,如果新子节点为TEXT_CHILDREN,旧子节点为ARRAY_CHILDREN,那么卸载旧子节点即可,如果旧子节点为TEXT_CHILDREN,则更新子节点文本。如果新子节点为ARRAY_CHILDREN,旧子节点也为ARRAY_CHILDREN类型,则进行diff运算,若旧子节点为TEXT_CHILDREN,则先清空旧子节点文本,然后执行挂载操作即可。
diff运算后续再整理
/**
* 为 props 打补丁
*/
const patchProps = (el: Element, vnode, oldProps, newProps) => {
//新旧props不同时才进行处理
if (oldProps !== newProps) {
//遍历新的props,一次触发hostPatchProp,赋值新属性
for (const key in newProps) {
const next = newProps[key]
const prev = oldProps[key]
if (next !== prev) {
hostPatchProp(el, key, prev, next)
}
}
//存在旧的props时
if (oldProps !== EMPTY_OBJ) {
//遍历旧的props,一次触发hostPatchProp,删除不存在于新的props中的就属性
for (const key in oldProps) {
//旧props属性不存在于新的props时,需要删掉
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null)
}
}
}
}
}
为props打补丁时先遍历新props属性名,获取对应新旧props的属性值并更新,再遍历旧props的属性名,当新props上没有该属性时,清空该属性
/**
* 为 prop 进行打补丁操作
*/
export const patchProp = (el, key, prevValue, nextValue) => {
if (key === 'class') {
patchClass(el, nextValue)
} else if (key === 'style') {
// style
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// TODO: 事件
} else if (shouldSetAsProp(el, key)) {
// 通过 DOM Properties 指定
patchDOMProp(el, key, nextValue)
} else {
// 其他属性
patchAttr(el, key, nextValue)
}
}
/**
* 通过 setAttribute 设置属性
*/
export function patchAttr(el: Element, key: string, value: any) {
if (value == null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
/**
* 为class打补丁
*/
export function patchClass(el:Element,value:string|null){
if(value==null){
el.removeAttribute('class')
}else{
el.className=value
}
}
/**
* 通过 DOM Properties 指定属性
*/
export function patchDOMProp(el: any, key: string, value: any) {
try {
el[key] = value
} catch (e: any) {}
}
element.setAttribute:设置指定元素上的某个值
dom.XX直接修改指定对象的属性
两者有个很尴尬的问题,属性名不同,所以针对不同属性,通过不同方式设置属性指定
class既可以用setAttribute设置,也可以通过className设置。只要 dom 不是 svg,则通过 className 设置 class。因为同样是10000个dom元素设置类名classname: 1.7470703125 ms,attr: 3.389892578125 ms,className性能大于attr
import { isString } from "@vue/shared"
export function patchStyle(el:Element,prev,next){
//获取style对象
const style=(el as HTMLElement).style
//判断新的样式是否为字符串
const isCssStrinbg=isString(next)
if(next&&!isCssStrinbg){
//赋值新样式
for(const key in next){
setStyle(style,key,next[key])
}
//清理就样式
if(prev&&!isString(prev)){
for (const key in prev) {
if (next[key] == null) {
setStyle(style, key, '')
}
}
}
}
}
/**
* 赋值样式
*/
function setStyle(
style:CSSStyleDeclaration,
name:string,
val:string|string[]
){
style[name]=val
}
对style属性的挂载和更新逻辑与对prrps挂载和更新逻辑一样,先遍历新style的属性,并设置值,再遍历旧style属性,取出不在新style里面的部分并设为空
/**
* 为event事件进行补丁
*/
export function patchEvent(el:Element&{_vei?:object},
rawName:string,
prevValue,
nextValue
) {
const invokers = el._vei || (el._vei = {})
//是否存在缓存事件
const existingInvoker=invokers[rawName]
console.log('rawName: ', rawName);
if(nextValue&&existingInvoker){
//存在缓存事件,更新事件
existingInvoker.value=nextValue
}else{
//获取用于addEventListener||removeEventListener的事件名
const name=parseName(rawName)
if(nextValue){
const invoker=(invokers[rawName]=createInvoker(nextValue))
el.addEventListener(name,invoker)
}else if(existingInvoker){
el.removeEventListener(name,existingInvoker)
invokers[rawName]=undefined
}
}
}
/**
* 直接返回剔除on,其余转化为小写的事件名即可
*/
function parseName(name:string){
return name.slice(2).toLowerCase()
}
/**
* 生成invoker函数
*/
function createInvoker(initialValue){
const invoker=(e:Event)=>{
invoker.value&&invoker.value()
}
invoker.value=initialValue
return invoker
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render } = Vue
const vnode = h('button', {
// 注意:不可以使用 onclick。因为 onclick 无法满足 /^on[^a-z]/ 的判断条件,这会导致 event 通过 :el[key] = value 的方式绑定(虽然这样也可以绑定 event),从而无法进入 patchEvent。在项目中,当我们通过 @click 绑定属性时,会得到 onClick 选项
onClick() {
alert('点击')
},
}, '点击')
// 挂载
render(vnode, document.querySelector('#app'))
setTimeout(() => {
const vnode2 = h('button', {
onDblclick() {
alert('双击')
},
}, '双击')
// 挂载
render(vnode2,
document.querySelector('#app'))
}, 2000);
</script>
</body>
</html>
事件的挂载主要是通过监听invoke函数实现,invoke函数内部执行invoke.value()方法,每次修改事件监听是直接更改invoke.value函数来执行。第二次绑定onDblclick时,在props挂载那里是先绑定新props,后卸载旧props(之前属性名是onClick),所以最后会卸载单击事件
Text打补丁
/**
* Text打补丁
*/
const processText = (oldVNode, newVNode, container, anchor) => {
//不存在旧的节点,则为挂载操作
if (oldVNode == null) {
//生成节点
newVNode.el = hostCreateText(newVNode.children as string)
//挂载
hostInsert(newVNode.el, container, anchor)
}
//存在旧的节点,则为更新操作
else {
const el = (newVNode.el = oldVNode.el!)
if (newVNode.children !== oldVNode.children) {
hostSetText(el, newVNode.children as string)
}
}
}
不存在旧Text节点,则为挂载操作,存在旧节点,则为更新操作
comment打补丁
/**
* 处理注释节点 - 执行注释节点的挂载或更新
* @param oldVNode - 旧的虚拟节点
* @param newVNode - 新的虚拟节点
* @param container - 包含元素的容器
* @param anchor - 插入位置的锚点
*/
const processCommentNode = (oldVNode, newVNode, container, anchor) => {
// 如果旧虚拟节点不存在,执行挂载
if (oldVNode == null) {
// 创建注释节点
newVNode.el = hostCreateComment(newVNode.children as string)
// 插入注释节点到容器中
hostInsert(newVNode.el, container, anchor)
} else {
// 如果旧虚拟节点存在,执行更新
const el = (newVNode.el = oldVNode.el!)
if (newVNode.children !== oldVNode.children) {
// 更新注释节点文本内容
hostSetCommentText(el, newVNode.children as string)
}
}
}
与Text类似
Fragement打补丁
/**
* Fragment 的打补丁操作
*/
const processFragment = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
mountChildren(newVNode.children, container, anchor)
} else {
patchChildren(oldVNode, newVNode, container, anchor)
}
}
与上文类似,核心思想都一样
组件渲染
/**
* 组件打补丁
*/
const processComponent = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
//挂载
mountComponent(newVNode, container, anchor)
}
}
当旧虚拟节点为空时直接挂载即可,因为在patch函数中会有oldVNode && !isSameVNodeType(oldVNode, newVNode)这个比较来判断是否卸载旧节点,是通过vnode.type来确定的,对于component来说此时vnode的type是component对象,所以每次重新挂载时,都自己会将旧组件卸载,所以不需要写更新操作
const mountComponent = (initialVNode, container, anchor) => {
// 创建组件实例
initialVNode.component = createComponentInstance(initialVNode)
//浅拷贝,绑定同一块内存空间
const instance = initialVNode.component
//标准化组件实例数据
setupComponent(instance)
//设置组件渲染
setupRenderEffect(instance, initialVNode, container, anchor)
}
先创建组件实例,再标准化组件实例数据,再设置组件渲染
/**
* 创建组件实例 - 根据虚拟节点创建对应的组件实例
* @param vnode - 虚拟节点,包含了组件的类型和其他信息
* @returns {object} - 返回创建的组件实例对象
*/
export function createComponentInstance(vnode) {
// 从虚拟节点中提取组件类型
const type = vnode.type
// 创建组件实例对象,并初始化其属性
const instance = {
// 唯一标识符,每次创建组件实例时递增
uid: uid++,
// 组件对应的虚拟节点
vnode,
// 组件类型
type,
// 子树,即组件内部的渲染结果
subTree: null!,
// 副作用函数,用于追踪组件的依赖和重新渲染
effect: null!,
// 更新函数,用于触发组件的更新流程
update: null!,
// 渲染函数,组件自定义的渲染逻辑
render: null!,
//生命周期相关
isMounted: false,
isUnmounted: false, //是否挂载
bc: null, // beforeCreate
c: null, // created
bm: null, // beforeMount
m: null // mounted
}
// 返回创建好的组件实例
return instance
}
创建组件实例是创建并返回组件实例inance。
/**
* 规范化组件实例数据
*/
export function setupComponent(instance) {
const setupResult = setupStatefulComponent(instance)
return setupResult
}
function setupStatefulComponent(instance) {
const Component = instance.type
const { setup } = Component
//存在setup,则直接获取setup函数的返回值即可
if (setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult)
} else {
finishComponentSetup(instance)
}
}
export function handleSetupResult(instance, setupResult) {
//存在setupResult,则把setupResult赋值给instance.render
if(isFunction(setupResult)){
instance.render = setupResult
}
finishComponentSetup(instance)
}
export function finishComponentSetup(instance) {
const Component = instance.type
if(!instance.render){
instance.render = Component.render
}
// 改变 options 中的 this 指向
applyOptions(instance)
}
function applyOptions(instance: any) {
const {
data: dataOptions,
beforeCreate,
created,
beforeMount,
mounted
} = instance.type
if (beforeCreate) {
callHook(beforeCreate, instance.data)
}
if (dataOptions) {
//触发dataOptions函数,拿到data对象
const data = dataOptions()
//如果拿到的是一个对象
if (isObject(data)) {
//则把data包装成reactive的响应性数据赋值给instance
instance.data = reactive(data)
}
}
if (created) {
callHook(created, instance.data)
}
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook?.bind(instance.data), instance)
}
//注册hooks
registerLifecycleHook(onBeforeMount, beforeMount)
registerLifecycleHook(onMounted, mounted)
}
/**
* 触发hooks
*/
function callHook(hook: Function, proxy) {
hook.bind(proxy)()
}
//组件挂载和更新的方法
再标准化组件实例数据时有setup时先拿到setup函数,没有setup时正常获取render函数。再判断是否有beforeCreate生命周期钩子需要执行,再将component中的data取出来,用reactive包裹,最后再判断是否有created生命周期钩子(执行生命周期钩子时注意要改变this指向),再注册onBeforeMount和onMounted函数
/**
* 设置组件渲染
*/
const setupRenderEffect = (instance, initialVNode, container, anchor) => {
//组件挂载和更新方法
const componentUpdateFn = () => {
//当前出于mounted之前,即执行挂载逻辑
if (!instance.isMounted) {
//获取hook
const { bm, m } = instance
//boforeMounted hook
if (bm) {
bm()
}
//从render中获取需要渲染的内容
const subTree = (instance.subTree = renderComponentRoot(instance))
//通过patch对subTree打补丁
patch(null, subTree, container)
if (m) {
m()
}
initialVNode.el = subTree.el
instance.isMounted = true
} else {
let { next, vnode } = instance
if (!next) {
next = vnode
}
//获取下一次的subTree
const nextTree = renderComponentRoot(instance)
//保存对应的subTree,一边进行更新操作
const prevTree = instance.subTree
instance.subTree = nextTree
//通过patch对subTree打补丁
patch(prevTree, nextTree, container, anchor)
//更新next
next.el = nextTree.el
}
}
/**
* 创建包含schedule的effect实例,此时依赖收集的是该处的effect
*/
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queuePreFlushCb(update)
))
//生成update函数
const update = (instance.update = () => effect.run())
//触发update函数
update()
}
设置组件渲染时主要是通过创建effect和fn函数,再直接执行fn函数,fn函数里面再通过调用render方法(注意this指向问题)创建虚拟vnode(触发依赖收集,此时收集到的是effect),再通过patch方法渲染出来(注意前后需要判断是否有onBeforeMount和onMounted函数)。 如果通过定时器修改了data中的数据,那么便触发依赖,调度器中触发fn函数,不在mounted时继续通过调用render函数触发依赖收集,生成vnode,再通过patch方法给新旧vnode打补丁。