侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue3源码学习10-runtime-dom实现

蜗牛
2022-06-26 / 0 评论 / 0 点赞 / 6 阅读 / 9760 字 / 正在检测是否收录...

摘要

前面主要是vue的源码仿写,最主要的是vue的响应式,以及依赖收集。是一个简易版本,和官方源码还是有很多细微差别的,例如数组代理之后改变数组长度,会触发更新之类的。数组还会被收集长度这种依赖关系,以及数组的一些splice,push,shift,unshift,pop这些方法重写,来完成修复一些数组在vue依赖更新中的BUG。

Vue 中为了解耦,将逻辑分成 2 个模块

  • 运行时 核心(runtime)(不依赖平台的 browsweer test 小程序 app canvas....) 靠的是虚拟 DOM
  • 针对不同平台运行时,vue 是针对浏览器平台的
  • 渲染器

构建自己的runtime-dom

这个功能主要是为了提供一个操作dom的方法,新建一个rumtime-dom的文件夹在packages中。然后cd 到该目录下运行pnpn init,生成的package.json,修改成如下 。

{
  "name": "@vue/runtime-dom",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
    "buildOptions":{
    "name":"VueRuntimeDOM",
    "formats":[
      "global",
      "cjs",
      "esm-budler"
    ]
  }
}

修改项目的package.json中dev的参数。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "node scripts/dev.js runtime-dom -f global"
  },

然后参考着官方的文件。创建如下文件

9ec2af9a91890e8c26b66.png

nodeOps.ts中编写需要的dom操作方法。

export const nodeOps = {
  // 增 删 改 查
  insert(child, parent, anchor = null) {
    parent.insertBefore(child, anchor)
  },
  remove(child) {
    const parentNode = child.parentNode
    if (parentNode) {
      parentNode.removeChild(child)
    }
  },
  setElementText(el, text) {
    el.textContent = text
  },
  setText(node, text) {
    node.nodeValue = text
  },
  querySelector(selector) {
    return document.querySelector(selector)
  },
  parentNode(node) {
    return node.parentNode
  },
  nextSibling(node) {
    return node.nextSibling
  },
  createElement(tagName) {
    return document.createElement(tagName)
  },
  createText(text) {
    return document.createTextNode(text)
  },
}

patchProp.ts主要是操作样式的方法

export function patchProp(el, key, prevValue, nextValue) {
  // 类名 el.className
  //样式 el.style
  // events
  // 普通属性
}

先打个小样,后期慢慢填充。
而主要文件index.ts中就是将这些合并起来

import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const renerOptions = Object.assign(nodeOps, { patchProp })

编写runtime-dom内容

runtime-dom主要是提供一个虚拟dom的操作方法。前端在代码编写的过程中,要设置类名,style样式,绑定事件,还有设置普通属性。还有node自身的属性操作。例如将节点增加到指定位置,删除节点等等,这些是dom原生就有的功能,可以进一步封装使用。这里学习一下他的核心思想。
所以runtime-dom的核心就是提供渲染器需要的options。实际上runtime-dom并未做什么事情。
所以patchProp.ts的代码编写就是这样的。

// dom属性的操作api

import { patchAttr } from './modules/attr'
import { patchClass } from './modules/class'
import { patchEvent } from './modules/event'
import { patchStyle } from './modules/style'

// null 值
// 值 值
// 值 null
export function patchProp(el, key, prevValue, nextValue) {
  // 类名 el.className
  if (key === 'class') {
    patchClass(el, nextValue)
  } else if (key === 'style') {
    //样式 el.style
    patchStyle(el, prevValue, nextValue)
  } else if (/^on[^a-z]/.test(key)) {
    // events addEventListener
    patchEvent(el, key, nextValue)
  } else {
    // 普通属性 el.setAttribute(key, prevValue)
    patchAttr(el, key, nextValue)
  }

  //样式 el.style
  // events addEventListener
  // 普通属性 el.setAttribute(key, prevValue)
}

传入class的时候

<div class="a"></div> ==> <div class="b"></div>
这个时候是需要被操纵的元素dom,还有最新传入的class值。这里简单的来看是不需要旧的class值的,直接覆盖新值就可以了。
所以modules/class.ts的文件就是抛出一个pathcClass函数,这个函数接受了(el,nextValue)

export function patchClass(el, nextValue) {
  if (nextValue == null) {
    el.removeAttribute('class') // 如果不需要class了直接移除
  } else {
    el.className = nextValue
  }
}

传入style值的时候

<div style="color:red;font-size:14px;"></div> ==> <div style="color:yellow"></div>
这样的操作,好像直接可以旧值覆盖新值,不用做比较。如果你是一个vue开发的话,就知道这样一种写法<div :style="{color:'red',fontSize:'14px'}"></div>,style可以动态的改变,作为一个对象。这样的话,如果直接覆盖,是不会识别font-size的。所以需要做一个新旧值的对比。

export function patchStyle(el, prevValue, nextValue) {
  // 样式需要比较差异
  for (let key in nextValue) {
    // 用新的直接覆盖
    el.style[key] = nextValue[key]
  }

  if (prevValue) {
    for (let key in prevValue) {
      if (nextValue[key] == null) {
        el.style[key] = null
      }
    }
  }
}

传入绑定事件

通常原生的JS在一个dom元素上绑定一个事件,然后换绑定另一个事件。要经历一个绑定->解绑 ->再绑定新的事件。这样的操作十分耗费性能。而如果我们绑定一个自定义的事件,然后在里面绑定要绑定的方法,这样当要绑定的方法更换的时候,不需要重新解绑再绑定,而只需要更新要绑定的方法就行。
所以event.ts

function createInvoker(callback) {
  const invoker = (e) => invoker.value()
  invoker.value = callback
  return invoker
}

export function patchEvent(el, eventName: string, nextValue) {
  // 可以先移除时间,再重新绑定事件
  // remove => add event
  // 这样操作每次都要卸载再安装
  // 可以绑定一个自定义事件,然后里面调用绑定的方法
  let invokers = el._vei || (el._vei = {})

  let exits = invokers[eventName] // 先看有没有缓存过

  //如果绑定的是一个空
  if (exits && nextValue) {
    // 已经绑定过事件了
    exits.value = nextValue
  } else {
    // onClic=> click
    let event = eventName.slice(2).toLowerCase()
    if (nextValue) {
      const invoker = (invokers[eventName] = createInvoker(nextValue))
      el.addEventListener(event, invoker)
    } else if (exits) {
      // 如果有老值,需要将老的绑定事件移除
      el.removeEventListener(event, exits)
      invokers[eventName] = undefined
    }
  }
}

这样第一次进入的时候是没有值的,所以el.vei是一个空对象,并且invokers也没有值,那么就不存在缓存了方法名。当进入到下一步的时候要判断传入的时候是空,这样就可以解绑对应的方法。当有值的时候,就进入到了上面说的,绑定一个自定义事件。这样el.vei中就有了一个{onClick:(e)=>invoker.value()}。如果你这时候绑定的是一个a方法那么就会是这样{onClick:(e)=>a()}。这样当你要绑定成b方法的时候就变成了{onClick:(e)=>b()}
这里并未细致考虑绑定多方法的问题。vue3是通过数组存储来完成。

传入自定义属性

简单点就是有这个自定义属性就添加,没有值就移除它。

export function patchAttr(el, key, nextValue) {
  if (nextValue) {
    el.setAttribute(key, nextValue)
  } else {
    el.removeAttribute(key)
  }
}

结尾

平常在编写的时候用的都是renderh这样的函数,来渲染虚拟dom,而不是像文章开头一样,编写很多的api。那么为了 这样的操作,vue3d都是交由runtime-core来操作。
也就是说runtime-dom的index.ts改成

import { createRenderer } from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const renerOptions = Object.assign(nodeOps, { patchProp }) // domApi 属性api

export function render(vnode, container) {
  // 渲染器的创建的时候传入options
  createRenderer(renerOptions).render(vnode, container)
}
export * from '@vue/runtime-core'

新建的runtime-core就有h.ts和renderer.ts的函数。
index.ts

export { createRenderer } from './renderer'
export { h } from './h'

h.ts

export function h() {}

renderer.ts

export function createRenderer(renerOptions) {
  const render = (vnode, container) => {}
  return {
    render,
  }
}
git:[@github/MicroMatrixOrg/vue3-plan/tree/runtime-dom)]
0

评论区