理解实现Vue渲染机制

215 阅读3分钟
  • index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>
</body>

</html>
<script src="./renderer.js"></script>
<script>
    // 1. 通过h函数来创建一个vnode
    const vnode = h('div', {
        class: "vnode-01",
        id:'aaa'
    }, [
        h('h2', null, "当前计数:100"),
        h('button', {
            onClick: handleClick,
        }, '+1')
    ]); // vdom

    // 2. 通过mount函数,将vnode挂载到 dev#app上
    mount(vnode, document.querySelector('#app'))

    // 3. 创建新的vnode
    setTimeout(()=>{
        let newVnode = h('div', {class: 'vnode-02', id:'aaa'}, [
        h('h2', null, "呵呵呵🍺"),
        h('button', {
            onClick: handleClick,
        }, '+9999')
    ])
        patch(vnode, newVnode)
    },5000)

    function handleClick(e){
        console.log(e,"h函数事件")
    }

</script>
  • renderer.js
 const h = (tag, props, children) => {
     // vnode -> javascript对象 -> {}
     return {
         tag,
         props,
         children
     }
 }

 const mount = (vnode, container) => {
     // vnode -> element
     // 1.创建出真实的原生,并在vnode上保留el
     const el = vnode.el = document.createElement(vnode.tag)

     // 2.处理props
     if (vnode.props) {
         for (const key in vnode.props) {
             const value = vnode.props[key]

             // 对象事件监听的判断
             if (key.startsWith("on")) {
                 // 判断是否以 on 开头,如果是事件需要把 on 截掉,同时转换成小写字母
                 // 事件监听方法里 事件名称都是小写没有 on
                 // 第一个参数是事件的类型 (如 "click" 或 "mousedown").
                 // 第二个参数是事件触发后调用的函数。
                 // 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。

                 el.addEventListener(key.slice(2).toLowerCase(), value)
             } else {
                 // 如果不是on开头就是元素的属性 
                 el.setAttribute(key, value)
             }
         }
     }

     // 3. 处理children
     if (vnode.children) {
         if (typeof vnode.children === "string") {
             // 如果是字符串 就直接插入到元素内
             el.textContent = vnode.children;
         } else {
             vnode.children.forEach(item => {
                 mount(item, el);
             })
         }
     }
     // 4. 将el挂载到container上
     container.appendChild(el)

 }


 // 节点更新 diff算法 n1旧的节点,n2新的节点
 const patch = (n1, n2) => {
     // 判断更新的节点元素类型是否一样,不一样就直接删除旧的,替换新的node节点
     if (n1.tag !== n2.tag) {
         // 先拿到旧节点的父元素
         const n1ElParent = n1.el.parentElement;
         // 通过父元素删除旧的元素
         n1ElParent.removeChild(n1.el);
         // 在旧的父元素上重新创建新的元素 
         mount(n2, n1ElParent);
     } else {
         // 1.如果新的节点和旧节点的元素一样,就统一保留旧的节点元素
         const el = n2.el = n1.el;


         // 2.处理props
         const oldProps = n1.props || {};
         const newProps = n2.props || {};
         // 2.1 获取所有的newProps添加到el里
         for (const key in newProps) {
             const oldValue = oldProps[key];
             const newValue = newProps[key];
             // 如果新的属性(或者事件方法)不等于旧的属性就把新的属性插入进去, 相同属性不用重复设置
             if (newValue !== oldValue) {
                 if (key.startsWith('on')) { // 对象监听判断
                     el.addEventListener(key.slice(2).toLowerCase(), newValue)
                 } else {
                     el.setAttribute(key, newValue)
                 }
                 // 这时候新的事件和属性都已经添加进去了
             }
         }

         // 2.2 删除旧的props
         for (const key in oldProps) {
            // 判断旧元素的属性是否在新元素的属性里存在,如果不存在就在新元素里删除旧元素的属性(或事件)
            if(!(key in newProps)){
                if(key.startsWith('on')){
                    // 在新元素里移除旧元素的事件 移除事件需要加value
                    const value = oldProps[key];
                    el.removeEventListener(key.slice(2).toLowerCase(),value);
                }else{
                    // 在新元素里移除旧元素的属性 移除属性 不需要加value
                    el.removeAttribute(key);
                }
            }

         }

        // 3. 处理children
         const oldChildren = n1.children || [];
         const newChildren = n2.children || [];

         // 情况一:newChildren本身是一个string 如果新的节点是一个字符串就直接替换旧的节点
         if(typeof newChildren === "string"){
             // 边界判断 edge case
             if(typeof oldChildren === "string"){
                if(newChildren !== oldChildren){
                    el.textContent = newChildren;
                }
             }else{
                el.innerHTML = newChildren;
             }
         }else{
             // 情况二:newChildren本身是一个数组
             if(typeof oldChildren === "string"){
                 // 如果旧的节点是一个字符串,先清空旧的字符串
                el.innerHtml = "";
                newChildren.forEach(item => {
                    mount(item, el);
                })
             }else{
                 // oldChildren: [v1, v2, v3, v8, v9]
                 // newChildren: [v1, v5, v6]
                 // 1. 前面有相同节点的元素进行patch操作
                // 对比新旧节点,拿到更短的长度
                 const commonLength = Math.min(oldChildren.length, newChildren.length)
                 for (let i = 0; i < commonLength; i++){
                     // 相等的节点会在这里处理
                     patch(oldChildren[i], newChildren[i]);
                 }

                 // 2. 如果旧的节点长做移除操作,新的节点长就做添加操作
                 // newChildren.length > oldChildren.length
                 if(newChildren.length > oldChildren.length){
                     newChildren.splice(oldChildren.length).forEach(item=>{
                         mount(item,el);
                     })
                 }

                 // 3. newChildren.length < oldChildren.length
                 if (newChildren.length < oldChildren.length){
                     oldChildren.splice(newChildren.length).forEach(item=>{
                         el.removeChild(item.el);
                     })
                 }

             }
             
         }

     }
 }