2024.08 vue render函数的实现

417 阅读3分钟

render函数原理,h将通过配置项生成vnoderender函数将vnode转换成真实的dom并且挂载在指定容器上面。其中将虚拟dom转换成真实dom的过程也是patch算法的核心 主要用户比较新旧两个节点找出差异,然后找到差异并将其应用到真实的dom上。

去哪拉vue源码和调试方法参考下方链接,不做过多的描述

调试方法:参考

源码执行流程http://127.0.0.1:5500/packages/runtime-core/src/renderer.ts主要逻辑代码在这个文件中。流程如下:

image-20240702213047097.png index.html使用type=module可以使用esmodule的语法来引入和导出函数以及变量,index.js暴露出去两个函数,我们最终就是要实现这两个函数

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="demo"></div>
  </body>
  <script type="module">
    import Vue from '../src/index.js'
    const { h, render } = Vue
    console.log(h, render)
  </script>
</html>

index.js

// 将vnode转成真实dom函数
function render(vnode, container) {
  // 判断vnode为空,则直接返回
  if (vnode == null) return
}
​
// 创建vnode节点函数
function h(tagName, props, children) {
  return {}
}
​
​
// 暴露render函数和h函数
export default { render, h }
​

调试源码,这样一段代码使用h函数可以转换为一个对象,我们实现的时候只实现其中的关键属性(type,props),其他属性不进行实现,最终实现效果可以达到将传入的{class:'red',attrs:{msg:'msg'},onClick:()=>{console.log('hello')}}对应转换为div节点的属性以及事件

const vnode = h('div',{class:'red',attrs:{msg:'msg'},onClick:()=>{console.log('hello')}},[h('p', {}, 'hello world')])
  {
    "__v_isVNode": true,
    "__v_skip": true,
    "type": "div",
    "props": {
        "class": "red",
        "attrs": {
            "msg": "msg"
        }
    },
    "key": null,
    "ref": null,
    "scopeId": null,
    "slotScopeIds": null,
    "children": "hello",
    "component": null,
    "suspense": null,
    "ssContent": null,
    "ssFallback": null,
    "dirs": null,
    "transition": null,
    "el": null,
    "anchor": null,
    "target": null,
    "targetAnchor": null,
    "staticCount": 0,
    "shapeFlag": 9,
    "patchFlag": 0,
    "dynamicProps": null,
    "dynamicChildren": null,
    "appContext": null,
    "ctx": null
}

完整代码如下

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="demo"></div>
  </body>
  <script type="module">
    import Vue from '../src/index.js'
    const { h, render } = Vue
    const vnode = h(
      'div',
      {
        class: 'red',
        onClick: () => {
          console.log('hello')
        }
      },
      'hello'
    )
    render(vnode, document.querySelector('#demo'))
    console.log(vnode,document.querySelector('#demo'))
  </script>
</html>

index.js文件中用到的方法名和源码中一致方便阅读

import { isOn } from './utils.js'
// 将vnode转成真实dom函数
function render(vnode, container) {
  // 判断vnode为空,说明是个空节点,直接卸载
  if (vnode == null) return
  // 将vnode转换成真实dom
  processElement(vnode, container)
}

// 创建vnode节点函数
function h(tagName, props, children) {
  return {
    type: tagName.toLowerCase(),
    props: props,
    children: children
  }
}

// 处理元素
function processElement(vnode, container) {
  mountElement(vnode, container)
}

// 挂在元素
function mountElement(vnode, container) {
  let el
  const { props } = vnode
  // 设置元素
  el = vnode.el = createElement(vnode.type)
  // 判断vnode.children是否是数组,是数组创建子元素,不是创建text元素
  if (Array.isArray(vnode.children)) {
      // 创建子元素
    vnode.children.forEach(node => {
      render(node, el)
    });
  } else {
    setElementText(el, vnode.children)
  }
  // 设置props
  if (props) {
    for (const key in props) {
      patchProp(el, key, null, props[key])
    }
  }
  container.appendChild(el)
}

// 创建元素
function createElement(tag) {
  const el = document.createElement(tag)
  return el
}

// 设置元素文本
function setElementText(el, text) {
  el.textContent = text
}

// 设置属性
function patchProp(el, key, prevValue, nextValue) {
  if (key === 'class') {
    // 设置class属性
    patchClass(el, nextValue)
  } else if (isOn(key)) {
    // 判断事件
    patchEvent(el, key, prevValue, nextValue)
  }
}

// 设置class属性
function patchClass(el, value) {
  if (!value) {
    el.removeAttribute('class')
  } else {
    el.className = value
  }
}

// 给元素添加事件
function patchEvent(el,key,prevValue,nextValue){
  el.addEventListener(key.slice(2).toLowerCase(),nextValue)
}

// 暴露render函数和h函数
export default { render, h }

util.js

// 判断是否是事件
export const isOn = key =>
  key.charCodeAt(0) === 111 /* o */ &&
  key.charCodeAt(1) === 110 /* n */ &&
  // uppercase letter
  (key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97)

页面可以渲染出来内容并且点击文案可以打印出来结果说明基础的例子已经实现

image.png

源码地址