Vue原理:初次渲染patch实现

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

初次渲染时通过调用_render函数创建虚拟DOM返回给_update函数,在_update函数内部主要是通过patch函数进行虚拟DOM至真实DOM的创建并替换$el渲染至浏览器中

初次渲染时调用patch函数,会将当前的真实DOM和通过_render函数生成的虚拟DOM作为参数传入

patch(vm.$el, vnode);
复制代码

初始化patch函数

export function patch(oldVnode, vnode) {
}
复制代码

oldVnode:旧的虚拟节点,可能是真实DOM,也可能是上次渲染使用的虚拟DOM,若是真实DOM,则存在nodeType属性,否则无

vnode:新的虚拟DOM,需要渲染至浏览器展示的最新数据

patch函数中,可以根据oldVnode.nodeType来判断是初次渲染操作还是数据变换导致更新操作,接下来的重点便是实现初次渲染流程

export function patch(oldVnode, vnode) {
  /**
   * 判断是更新还是初次渲染
   */
  const isRealElement = oldVnode.nodeType;
  
  // 是真实节点
  if (isRealElement) { 
    // 初次渲染操作
  }else {
    // 更新操作
  }
}
复制代码

现在有如下HTML

<div id="app">
  <p>{{name}}</p>
</div>
复制代码

实例化Vue

const vm = new Vue({
  el: '#app',
  data() {
    return {
      name: 'nordon',
    }
  },
})
复制代码

在初次渲染时oldVnode是真实DOM节点#app,那么我们是可以获取到当前元素以及其父节点document.body

const oldEle = oldVnode;
const parentEle = oldEle.parentNode;
复制代码

通过虚拟DOM创建的新的真实DOM如下(具体创建细节下文实现)

<div id="app">
  <p>nordon</p>
</div>
复制代码

将新的真实DOM替换掉老的真实DOM,这个过程通过父节点进行操作,将新的真实DOM插入到老的真实DOM后面,然后再将老的真实DOM删除掉,此时页面上便是需要展示的内容

parentEle.insertBefore(el, oldEle.nextSibling);
parentEle.removeChild(oldEle);
复制代码

Tips:在真实的DOM操作中,是没有办法直接将自己删除掉的,都是利用父节点进行删除

整个渲染过程核心点便是如何通过递归创建真实DOM节点,函数如下


/**
 * 根据虚拟节点 创建真实DOM
 */
function createEle(vnode) {
  const { tag, children, key, data, text } = vnode;

  // 是标签
  if (typeof tag === "string") {
    vnode.el = document.createElement(tag)

    updateProperties(vnode) // 增加属性

    children.forEach(child => {
      // 递归创建儿子节点, 将儿子节点放到父节点中
      return vnode.el.appendChild(createEle(child))
    })
  } else {
    // 是文本
    // 虚拟dom上映射着真实dom, 方便后续更新操作
    vnode.el = document.createTextNode(text)
  }

  return vnode.el
}
复制代码

需要注意在创建真实DOM过程中,需要对属性进行处理

/** 
 * 属性处理
*/
function updateProperties(vnode) {
  let newProps = vnode.data;
  let el = vnode.el
	
  // 遍历属性
  for (const key in newProps) {
    if(key === 'style') { /// 样式进行处理
      for (const styleName in newProps.style) {
        el.style[styleName] = newProps.style[styleName]
      }
    }else if(key === 'class'){ // 样式进行处理
      el.className = newProps[key]
    } else { // 普通属性处理
      el.setAttribute(key, newProps[key])
    }
  }
}
复制代码

至此整个patch流程如下

export function patch(oldVnode, vnode) {
  /**
   * 判断是更新还是初次渲染
   */
  const isRealElement = oldVnode.nodeType;
  
  // 是真实节点
  if (isRealElement) { 
    // 初次渲染操作
    const oldEle = oldVnode; // div#app
    const parentEle = oldEle.parentNode; // document.body
    
    let el = createEle(vnode);
    parentEle.insertBefore(el, oldEle.nextSibling);
    parentEle.removeChild(oldEle);

    // 需要将渲染好的结果 返回
    return el
  }else {
    // 更新操作
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/7034325564731326501