第八节:vue3渲染原理 Runtime Core: vnode、h函数

515 阅读8分钟

runtime-core 不关心运行平台。

渲染过程是用用户传入的renderOptions 来渲染

使用方法

render(h('h1',{style: {'color': 'red'},onClick: ()=>{alert(1)}},h('span','hello lyp')),app)

源码运行流程: h方法->调用createVNode-> 生成VNode-> 调用render-> 调用createRenderer-> 传入nodeprops的处理方法:renderOptions-> 调用render-> patch比对-> 调用mountElement-> 调用renderOptions中的节点props-> 生成元素-> 调用mountChildren递归深度patch比对-> hostInsert插入容器中

创建runtime-core包

runtime-core/package.json

{
    "name": "@vue/runtime-core",
    "module": "dist/runtime-core.esm-bundler.js",
    "types": "dist/runtime-core.d.ts",
    "files": [
      "index.js",
      "dist"
    ],
    "buildOptions": {
      "name": "VueRuntimeCore",
      "formats": [
        "esm-bundler",
        "cjs"
      ]
    }
}
pnpm i

虚拟节点的实现

形状标识

利用二进制位运算|&

二进制位运算|&:适合权限的组合

let user = 增加 | 删除; if(user&增加){xxx}

export const enum ShapeFlags { // vue3提供的形状标识
    ELEMENT = 1,
    FUNCTIONAL_COMPONENT = 1 << 1,  // 2 表示2进制 向左移1位
    STATEFUL_COMPONENT = 1 << 2,    // 4 表示2进制 向左移2位
    TEXT_CHILDREN = 1 << 3, // 8
    ARRAY_CHILDREN = 1 << 4, // 16
    SLOTS_CHILDREN = 1 << 5, //32
    TELEPORT = 1 << 6, // 64
    SUSPENSE = 1 << 7, // 128
    COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, //256
    COMPONENT_KEPT_ALIVE = 1 << 9, // 512
    COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

| 或运算

二进制位运算 组合

只要有1就是1

// 16|1 = 17 ; 表示一个元素里边有一个数组类型的儿子
let r = ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN 
或运算
1:1
++:+
1000:16
===:=
1001:17

& 与运算

全是1的才是1 判断是否属于某种类型

包含的情况:&出来大于0
ShapeFlags.ELEMENT & r 
与运算
1:1
&:&
1001:17
===:=
0001:1
ShapeFlags.ARRAY_CHILDREN & r 
与运算
1000:16
&&&&:&
1001:17
===:=
1000:16
不包含情况:&出来为0
ShapeFlags.COMPONENT & r 
与运算
110:6
&&&&:&
1001:17
===:=
0000:0

createVNode实现

createVNode的写法比较死板,被h方法使用变的更灵活

export function isVNode(value: any){
    return value ? value.__v_isVNode === true : false
}

export function createVnode(type, props, children){
    // 组合方案 shapeFlag 想知道元素中包含的是多个子节点还是的单个子节点
    const shapeFlag = isString(type) ? ShapeFlags.ELEMENT:0;  // 字符串为元素1 否则文本节点0

    // 虚拟dom是一个对象,用于diff算法,可以跨平台,因为真实dom的属性比较多而且操作真实dom有可能导致页面重绘重渲染
    const vnode = {
        __v_isVNode: true,
        type,
        props,
        key: props && props['key'],
        el: null, // 虚拟节点上对应的真实节点,后续diff算法使用
        children,
        shapeFlag
    }
    if(children){  // 如果有子节点
        let type = 0;
        if(Array.isArray(children)){ // 如果是数组
            type = ShapeFlags.ARRAY_CHILDREN;
        }else{
            children = String(children);  // 防御一下 createdTextElement参数只能是字符串
            type = ShapeFlags.TEXT_CHILDREN
        }
        vnode.shapeFlag |= type  // vnode.shapeFlag = vnode.shapeFlag | type
        // 如果shapeFlag为9 说明元素中包含一个文本
        // 如果shapeFlag为17 说明元素中有多个子节点
    }
    return vnode;
}

调用结果:

const vnode = {
    __v_isVNode: true,
    type,
    props,
    key: props && props['key'],
    el: null, // 虚拟节点上对应的真实节点,后续diff算法使用
    children,
    shapeFlag
}

h实现

h 的用法

h('div')
h('div', {style:{"color": "red"}})
h('div', {style:{"color": "red"}}, 'hello')
h('div', 'hello')
h('div', null, 'hello', 'world')
h('div', null, h('span'))
h('div', null, [h('span')])

实现

// 其余的 大于三个之外的 肯定是children
export function h(type, propsOrChildren?, children?){ 
    const l = arguments.length;
    if (l === 2) { // 只有属性,或者只有一个元素儿子的时候
        if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
            if (isVNode(propsOrChildren)) { // h('div',h('span'))
                // 虚拟节点就包装成数组
                // 因为元素可以循环创建  文本的话就不用包装为数组了
                return createVNode(type, null, [propsOrChildren])
            }
            // h('div',{style:{color:'red'}});
            return createVNode(type, propsOrChildren);  
        } else { // 传递儿子列表的情况 ,文本的话就不用包装为数组了
            // h('div',[h('span'),h('span')]) 或者 h('div', 'hello')
            return createVNode(type, null, propsOrChildren); 
        }
    }else{
        if(l > 3){ // 超过3个除了前两个都是儿子
            children = Array.prototype.slice.call(arguments,2);
        } else if( l === 3 && isVNode(children)){
            // 儿子是元素将其包装成数组 h('div',null,[h('span')])
            children = [children]; 
        }
        // h('div',null,'jw')  children有两种情况 文本、数组
        return createVNode(type,propsOrChildren,children) 
    }
}

createRenderer实现

渲染过程是用你传入的renderOptions 来渲染

render方法就是采用runtime-dom中提供的方法将虚拟节点转化成对应平台的真实节点渲染到指定容器中。

export function createRenderer(renderOptions){
    // 结构出 node和props的渲染方法
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        setText: hostSetText,
        setElementText: hostSetElementText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,
    } = renderOptions
    // 老节点 新节点 容器
    const patch = (n1,n2,container) => {
        // 初始化和diff算法都在这里

        // 没有更新
        if(n1 === n2) return
        if(n1 === null){
            // 初始化渲染
            // 后续还有组件的初次渲染,目前只写元素的初始化渲染
        }else{
            // 更新流程
        }
    }
    const render = (vnode, container) => {
        console.log(vnode, container)
        
        if(vnode === null){
            // 如果当前vnode是空的话  说明想把container清空 卸载逻辑

        }else{
            //既有初始化逻辑 又有更新逻辑 第一次没有_vnode属性 就是null
            patch(container._vnode || null,vnode,container)
        }
        // 保存vnode
        container._vnode = vnode;
    }
    return {
        render
    }
}

创建真实DOM

// 递归深度处理元素的子节点
const mountChildren = (children,container) =>{
    for(let i = 0; i < children.length;i++){
        patch(null,children[i],container);
    }
}

//挂载元素
const mountElement = (vnode,container) =>{
    const {type,props,shapeFlag} = vnode
    // 创建真实元素,挂载到虚拟节点上  后续用于复用节点和更新
    let el = vnode.el = hostCreateElement(type); 
    if(props){ // 处理属性
        for(const key in props){ // 更新元素属性
            hostPatchProp(el,key,null,props[key]); 
        }
    }
    if(shapeFlag & ShapeFlags.TEXT_CHILDREN){ // 文本
        hostSetElementText(el, vnode.children);
    }else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN){ // 多个儿子
        mountChildren(vnode.children,el);
    }
    hostInsert(el,container); // 插入到容器中
}


// 老节点 新节点 容器
const patch = (n1,n2,container) => {
    // 初始化和diff算法都在这里

    // 没有更新
    if(n1 === n2) return
    if(n1 === null){
        // 初始化渲染
        // 后续还有组件的初次渲染,目前只写元素的初始化渲染
        mountElement(n2,container)
    }else{
        // 更新流程
    }
}

文本的处理

文本的处理 需要自己增加类型。因为不能通过document.createElement('text')创建节点

使用

// 用法1
render(h(Text,'hello'),app)  
// 用法2
render(h('h1',{style: {'color': 'red'},onClick: ()=>{alert(1)}},h('span','lyp'),'hello'),app)

处理实现

  • 1、根节点如果是文本直接插入
  • 2、处理子集的时候也要考虑文本节点
// vnode.ts 创建Text类型
export const Text = Symbol('Text')

// 如果子节点是 字符串 创建Text类型的vnode
const normalize = (child, i) =>{
    if(isString(child)){
        // 处理后要进行替换 否则children中放的依旧是字符串
        let vnode = createVNode(Text, null, child)  
        child[i] = vnode
    }
    return child
}
    
// 递归处理子节点
const mountChildren = (children,container) =>{
    for(let i = 0; i < children.length;i++){
        // 2、处理子集的时候也要考虑文本节点
        let child = normalize(children, i)
        patch(null,child,container);
    }
}

// 处理文本
const processText= (n1,n2,container) => {
    if(n1 === null){
        n2.el = hostCreateText(n2.children)
        hostInsert(n2.el, container)
    }
}

// 老节点 新节点 容器
const patch = (n1,n2,container) => {
    //....

    const {type, shapeFlag} = n2
    if(n1 === null){
        // 初始化渲染
        // 后续还有组件的初次渲染,目前只写元素的初始化渲染
        switch(type){ // 1、根节点如果是文本直接插入
            case Text:
                processText(n1,n2,container);
                break;
            default:
                // 是元素的话 渲染元素 
                if(shapeFlag & ShapeFlags.ELEMENT){
                    mountElement(n2,container)
                }
        }
    }else{
        // 更新流程
    }
}

Fragment的实现

为了让Vue3支持多根节点模板,Vue.js 提供Fragment来实现,核心就是一个无意义的标签包裹多个节点

使用

render(h(Fragment,[h(Text,'hello'),h(Text,'lyp')]),app)

实现

// vnode.ts 创建Fragment类型
export const Fragment = Symbol('Fragment')

// 处理文本Fragment
const processFragment = (n1, n2, container, anchor) => {
    if(n1 == null){ 
        mountChildren(n2.children,container);
    }else{
        patchChildren(n1,n2,container);
    }
}

// 老节点 新节点 容器
const patch = (n1,n2,container) => {
    //....

    const {type, shapeFlag} = n2
    if(n1 === null){
        switch(type){
            case Text:
                processText(n1,n2,container);
                break;
            // 处理Fragment入口
            case Fragment:
                processFragment(n1,n2,container, anchor);
                break;
            default:
                // 是元素的话 渲染元素 
                if(shapeFlag & ShapeFlags.ELEMENT){
                    mountElement(n2,container)
                }
        }
    }else{
        // 更新流程
    }
}

卸载DOM

用法

render(h('h1',{style: {'color': 'red'},onClick: ()=>{alert(1)}},h('span','lyp'),'hello'),app

setTimeout(()=>{
    render(null,app)
},1000)

实现

// 卸载的实现
const unmount = (vnode) =>{hostRemove(vnode.el)}

const render = (vnode, container) => {
    console.log(vnode, container)

    if(vnode === null){
        // 如果当前vnode是空的话  说明想把container清空 卸载逻辑
        if(container._vnode){// 之前渲染过就卸载
            unmount(container._vnode); // 找到对应的真实节点将其卸载
        }
    }else{
        //既有初始化逻辑 又有更新逻辑 第一次没有_vnode属性 就是null
        patch(container._vnode || null,vnode,container)
    }
    // 保存vnode 表示渲染过
    container._vnode = vnode;
}

完整版

步骤

  • 1、如果当前vnode是空的话 说明想把container清空 卸载逻辑
  • 2、否则 既有初始化逻辑 又有更新逻辑 第一次没有_vnode属性 就是nullpatch(container._vnode || null,vnode,container)
  • 3、老节点和新节点 相同时 无更新
  • 4、老节点为null 是 初始化流程 否则为更新流程
  • 5、判断是 文本类型还是元素类型
  • 6、文本类型创建文本节点并插入
  • 7、元素类型创建元素
  • 8、添加配置的属性
  • 9、处理子节点,也要考虑是元素还是文本
  • 10、插入container

代码实现

import { isString } from '@vue/shared'
import { createVNode } from './vnode'
import {ShapeFlags, Text} from './vnode'


export function createRenderer(renderOptions){
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        setText: hostSetText,
        setElementText: hostSetElementText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,
    } = renderOptions

    // 如果子节点是 字符串 创建Text类型的vnode
    const normalize = (child) =>{
        if(isString(child)){
            return createVNode(Text, null, child)
        }
        return child
    }
    // 递归处理子节点
    const mountChildren = (children,container) =>{
        for(let i = 0; i < children.length;i++){
            // 处理自己的时候也要考虑文本节点
            let child = normalize(children[i])
            patch(null,child,container);
        }
    }
    // 挂载根元素
    const mountElement = (vnode,container) =>{
        const {type,props,shapeFlag} = vnode
        let el = vnode.el = hostCreateElement(type); // 8、创建真实元素,挂载到虚拟节点上  后续用于复用节点和更新
        if(props){ // 9、处理属性
            for(const key in props){ // 更新元素属性
                hostPatchProp(el,key,null,props[key]); 
            }
        }
        if(shapeFlag & ShapeFlags.TEXT_CHILDREN){ // 文本
            hostSetElementText(el, vnode.children);
        }else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN){ // 10、多个儿子
            mountChildren(vnode.children,el);
        }
        hostInsert(el,container); // 插入到容器中
    }

    // 7、处理文本
    const processText= (n1,n2,container) => {
        if(n1 === null){
            n2.el = hostCreateText(n2.children)
            hostInsert(n2.el, container)
        }
    }
    // 处理元素
    const processElement = (n1, n2, container) => {
        if (n1 == null) {
            // 初始化流程
            mountElement(n2, container)
        } else {
            // 更新流程
            // patchElement(n1, n2); // 比较两个元素
        }
    }
    // 参数:老节点 新节点 容器
    const patch = (n1,n2,container) => {
        // 初始化和diff算法都在这里

        // 3、没有更新
        if(n1 === n2) return

        const {type, shapeFlag} = n2
        console.log(type)
        if(n1 === null){
            // 4、初始化渲染
            // 后续还有组件的初次渲染,目前只写元素的初始化渲染
            switch(type){ // 5、如果是文本直接插入
                case Text:
                    processText(n1,n2,container);
                    break;
                default:
                    // 6、是元素的话 渲染元素 
                    if(shapeFlag & ShapeFlags.ELEMENT){
                        processElement(n2,container)
                    }
            }
        }else{
            // 更新流程
        }
    }

    // 卸载的实现
    const unmount = (vnode) =>{hostRemove(vnode.el)}

    const render = (vnode, container) => {
        console.log(vnode, container)
        
        if(vnode === null){
            // 1、如果当前vnode是空的话  说明想把container清空 卸载逻辑
            if(container._vnode){// 之前渲染过就卸载
                unmount(container._vnode); // 找到对应的真实节点将其卸载
            }
        }else{
            //2、既有初始化逻辑 又有更新逻辑 第一次没有_vnode属性 就是null
            patch(container._vnode || null,vnode,container)
        }
        // 保存vnode 表示渲染过
        container._vnode = vnode;
    }
    return {
        render
    }
}