vue3源码 - 渲染render(一)

292 阅读4分钟

前言

vue3的渲染原理将围绕 runtime-domruntime-core去实现, runtime-dom注重对dom的操作, runtime-core提供核心的方法,用来处理渲染, 渲染篇共分为4篇去讲,当前为第一篇: (介绍runtime-dom中属性操作和节点操作篇)

节点操作

下面代码讲述的主要是对应dom的增删改查等等进行一系列的封装

// 节点相关操作
// runtime-dom/src/nodeOps.ts
const doc = (typeof document !== 'undefined' ? document : null)
export const nodeOps = {
  // 创建元素
  createElement: (tag) => doc.createElement(tag),
  // 删除元素
  remove: child => {
    const parent = child.parentNode
    if(parent) {
      parent.removeChild(child)
    }
  },
  // 插入元素
  insert: (child, parent, anchor = null) => {
    parent.insertBefore(child, anchor)
  },
  // 创建文本
  createText: text => doc.createTextNode(text),
  // 创建注释
  createComment: text => doc.createComment(text),
  // 设置文本
  setText: (node, text) => {
    node.nodeValue = text
  },
  // 设置元素文本
  setElementText: (el, text) => {
    el.textContent = text
  },
  // 父节点
  parentNode: node => node.parentNode,
  // 兄弟节点
  nextSibling: node => node.nextSibling,
  // 获取dom元素
  querySelector: selector => doc.querySelector(selector),
  // 设置id属性
  setScopeId(el, id) {
    el.setAttribute(id, '')
  },
  // 克隆节点
  cloneNode(el) {
    const cloned = el.cloneNode(true)
    // if (`_value` in el) {
    //   ;(cloned as any)._value = (el as any)._value
    // }
    return cloned
  },
}

属性操作

我们将所有关于对dom属性操作的api放入到patchProp.ts这个文件中

// runtime-dom/src/patchProp.ts
import { isOn } from "@vue/shared"
import { patchAttr } from "./modules/attrs"
import { patchClass } from "./modules/class"
import { patchEvent } from "./modules/events"
import { patchStyle } from "./modules/style"

/**
 * 属性相关
 * @param el 元素
 * @param key 键
 * @param prevValue 旧值 
 * @param nextValue 新值
 */
export const patchProp = (el, key, prevValue, nextValue) => {
  // class操作
  if(key === 'class') {
    patchClass(el, nextValue)  
  }else if(key === 'style') { // style操作
    patchStyle(el, prevValue, nextValue)
  }else if(isOn(key)) {  // 判断事件
    patchEvent(el, key, nextValue)
  }else { // 其他属性操作
    patchAttr(el, key, nextValue)
  }
  
}

在上面这段代码中, 我们判断了传过来的key的什么类型的, 然后调用对应的处理函数

class操作

// modules/class.ts
export function patchClass(el, value) {
  if(value == null) {
    value = ''
  }
  el.className = value
}

在浏览器中, 为一个元素设置calss的三种方式, 即使用setAttribute、el.className或者el.classList 从vue设计与实现中可以知道使用el.className的性能最优,因此我们这里跟着vue3使用el.calssName俩添加类名

上面的代码通过传过来的类名value, 然后为对应dom(el)设置类名

style操作

// modules/style.ts
export function patchStyle(el, prevValue, nextValue) {
  const style = el.style // 获取样式
  if(nextValue == null) {
    el.removeAttribute('style') // 删除所有样式
  }else {
    // 1.老的里新的有没有
    if(prevValue) {
      for(let key in prevValue) {
        if(nextValue[key] == null) {
          style[key] = ''
        }
      } 
    }
    // 2.新的里面需要复制到style里
    for(let key in nextValue) {
      style[key] = nextValue[key]
    }

  }
}

可以看到设置styleclass并不一样, 因为一个dom可以设置多个class, 而style只能设置一个

上面的代码先是获取到了对应传过来的style样式, 然后去对比之前的style, 如果出现重复,就用最新的去代替旧的, 如果没有出现过, 就直接设置到style里面

event操作

// modules/event.ts
// 举例
// h('div', {onClick:function() {console.log('on')}})
export function patchEvent(el, key, nextValue) {
  // 对函数的缓存 => _vei存储列表
  const invokers = el._vei ?? (el._vei = {})

  const exists = invokers[key] // 判断缓存中是否有这个事件相对应的函数

  if (nextValue && exists) { // 绑定事件
    // 事件存在, 更新invoker.value的值即可
    invoker.value = nextValue
  } else {
    const eventName = key.slice(2).toLowerCase() // 举例中click
    if (nextValue) {
      let invoker =  invokers[eventName] = createInvoker(nextValue) // {click: fn} 一一对应
      el.addEventListener(eventName, invoker) 
    } else {
      el.removeEventListener(eventName, exists)
      invokers[eventName] = undefined // 移除了之后
    }
  }
}
function createInvoker(value) {
  const invoker = (e) => {
    invoker.value(e)
  }
  invoker.value = value
  return invoker
}

这里, 我们采用了一种性能更优的方式来完成事件的处理,在绑定事件时,我们可以绑定一个伪造的时间处理函数invoker, 然后将真正的事件处理函数设置为invoker.value, 这样子,当事件更新的时候, 我们将不再需要调用removeEventListener函数来移除上一次绑定的事件, 只需要更新invoker.value的值即可

看一下上面代码, 我们先从el._vei中读取对应的invoker, 如果invoker不存在,则将伪造的的invoker作为时间处理函数, 并将它缓存到el._vei当中;然后将真正的事件处理函数赋给invoker.value属性,然后把伪造的invoker函数作为时间处理函数绑定到元素上, 可以看到, 当事件触发时,执行的实际是伪造的事件函数, 在其内部间接的执行了真正的事件处理函数invoker.value(e)

attrs操作

// modules/attrs.ts
export function patchAttr(el, key, value) {
  if(value == null) {
    el.removeAttribute(key)
  }else {
    el.setAttribute(key, value)
  }
}

上面的代码先是判断了传过来的值即(value),如果是个空, 则删除对应的属性, 如果不为空, 则新增或者更新当前key的值