Vue源码学习-运行时

102 阅读12分钟

关于运行时的包主要有两个:

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打补丁。