第八章-挂载和更新

1、挂载子节点和元素属性

1.1挂载子节点

一个元素除了文本节点外, 还可以包含其他元素子节点, 非文本节点可以创建为数组

javascript 复制代码
const vnode = {
   type: "div",
   children: [
     {
       type: "p",
       children: "hello"
     }
   ]
}

这时候对应修改mountElement函数

javascript 复制代码
 function mountElement(vnode, container) {
    const el = createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
         patch(null, child, el)
      })
    }
    insert(el, container);
  }
  • patch的第一个参数为null, 挂载阶段, 没有旧的vnode
  • patch的第三个参数为el, 为刚刚创建的节点

1.2元素属性

javascript 复制代码
const vnode = {
   type: "div",
   props: {
     id: "foo"
   },
   children: [
     {
       type: "p",
       children: "hello"
     }
   ]
}
javascript 复制代码
function mountElement(vnode, container) {
    const el = createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
         patch(null, child, el)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        //HTML Attributes和 DOM Properties都可以设置属性
        el.setAttribute(key, vnode.props[key]) 
      }
    }
    insert(el, container);
  }

2、HTML AttributesDOM Properties

  • html attributes和dom properties的名字不总是一模一样的, 例如 class = "foo" 和 el.className

  • 不是所有的 html attributes都有之对应的dom properties, 例如 aria-*属性

  • 不是所有的 dom properties都有之对应的html attributes, 例如 el.textContent

  • html attributes的作用是设置与之对应的dom properties的初始值, 一旦值改变, 那么DOM properties始终储存着当前值, 而通过getAttribute函数得仍然是初始值

    javascript 复制代码
    <input value= "foo" />
    
    console.log(el.value)  // foo ;
    console.log(el.getAttribute("value")); // foo
    
    修改文本框的的值变成 bar
    
    console.log(el.value)  // bar ;
    console.log(el.getAttribute("value")); // foo
    // value可以通过另外一个dom properties获取默认值
    console.log(el.defaultValue) // foo
  • html attributes提供的默认值不合法, 那浏览器会使用内建的合法值作为对应的dom properties的值

    javascript 复制代码
    <input type="foo" />
    console.log(el.type) // "text"

3、正确设置元素属性

对于html文件来说, 当浏览器解析HTML代码后, 会自动分析HTML Attributes并设置合适的DOM properties,但是用户编辑vue文件中的模板不会被浏览器解析, 所以原本需要浏览器完成的工作, 现在需要框架来完成

3.1、浏览器中

css 复制代码
<button disabled> Button </button>
浏览器会对应设置 el.disabled = true

3.2、template中

css 复制代码
const button = {
  type: "button", 
  props: {
     disabled: "",  // 按钮设置了禁用
  }
}
  • 通过setAttribute设置

    arduino 复制代码
    el.setAttribute("disabled", "")  // 按钮禁用

    问题: 当设置为false的时候

    arduino 复制代码
    el.setAttribute("disabled". false); // 按钮禁用

    原因: 使用setAttribue设置的值总是会被字符串化, 上面设置等价于

    arduino 复制代码
    el.setAttribute("disabled". "false"); // 按钮禁用, 与预期相反
  • 通过DOM Properties设置

    ini 复制代码
    el.disabled = false  // 设置正常

    问题: 为设置的值为""的时候, 按钮禁用

    原因:因为el.disable为bool类型, 当设置为空字符串时, 浏览器就将值矫正为bool,即是false,所以上面代码的执行结果为

    ini 复制代码
    el.disabled = false

3.3、处理方法

注意: 存在部分属性dom properties可以读取, 但是实际应该通过setAttribute设置

ini 复制代码
<form id = "form1"></form>
<input :form="form1" />
javascript 复制代码
function  shouldSetAsProps(el, key, value) {
    if(el.tagName === "INPUT" && key === "form") return false;
    return key in el;
  }
  function mountElement(vnode, container) {
    const el = createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        let value = vnode.props[key];
        if(shouldSetAsProps(el, key, value)){
          let type = typeof el[key];
          if(type === "boolean" && value === "") {
            el[key] = true;
          } else {
            el[key] = value;
          }
        } else {
          el.setAttribute(key, value)
        }
      }
    }
    insert(el, container);
  }

3.4、抽取设置属性

javascript 复制代码
function createRenderer(options) {
  const{ createElement, setElementText, insert, patchProps } = options;
  function mountElement(vnode, container) {
    const el = createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        patchProps(el, key, null, vnodeProps[key])
      }
    }
    insert(el, container);
  }
  // n1旧vnode n2新vnode, container容器
  function patch(n1, n2, container) {
    if(!n1) {
      mountElement(n2, container);
    }
  }
  function render(vnode, container) {
    if(vnode){
      // 打补丁(挂载也是一种特殊的打补丁)
      patch(container._vnode, vnode, container)
    } else {
      if(container._vnode) {
        container.innerHTML = ";"
      }
    }
    container._vnode = vnode;
  }
  return {
    render,
  }
}
const renderer = createRenderer({
  createElement(tag){
    return document.createElement(tag);
  },
  setElementText(el, text){
    console.log(text)
    el.textContent = text;
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)  // parent父节点 el需要插入的节点  anchor插入时需要插入在这个节点前面
  },
  patchProps(el,key, oldValue, newValue) {
    // 暂时放在这里
    function  shouldSetAsProps(el, key, value) {
      if(el.tagName === "INPUT" && key === "form") return false;
      return key in el;
    }
    if(shouldSetAsProps(el, key, newValue)){
      let type = typeof el[key];
      if(type === "boolean" && newValue === "") {
        el[key] = true;
      } else {
        el[key] = newValue;
      }
    } else {
      el.setAttribute(key, newValue)
    }
  }
});

const vnode = {
  type: "button",
  props: {
    disabled: ""
  },
  children:  "按钮"
}
renderer.render(vnode, document.getElementById("app"));

4、class的处理

class有三种方式绑定

javascript 复制代码
const vnode = {
  type: "p",
  props: {
    class: "foo bar"
  }
}
javascript 复制代码
const vnode = {
  type: "p",
  props: {
    class: {foo: true, bar: true}
  }
}
javascript 复制代码
const vnode = {
  type: "p",
  props: {
    class:[ "zoo", {foo: true, bar: true}]
  }
}

提供一个方法统一

javascript 复制代码
let className = [ "zoo", {foo: true, bar: true}]
function normalizeClass(className) {
  if(typeof className === "string") return className;
  if(className && Array.isArray(className)){
    let name = [];
    className.forEach(item => {
      name.push(normalizeClass(item))
    })
    return name.join(" ");
  }
  if(className && typeof className === "object") {
     let name = [];
     for(let key in className)  {
       if(className[key]) name.push(key);
     }
     return name.join(" ");
  }
  return className
}
console.log(normalizeClass(className))

按照原本代码, class in el为false, 所以最后会使用setAttribute,

但是对比 setAttribue、el.className、el.classList的性能, el.className的性能最优

更改代码

javascript 复制代码
 patchProps(el,key, oldValue, newValue) {
    // 暂时放在这里
    function  shouldSetAsProps(el, key, value) {
      if(el.tagName === "INPUT" && key === "form") return false;
      return key in el;
     }
     if(key === "class") {
       el.className = newValue || ""
     }  else if(shouldSetAsProps(el, key, newValue)){
       let type = typeof el[key];
       if(type === "boolean" && newValue === "") {
        el[key] = true;
      } else {
        el[key] = newValue;
      }
    } else {
      el.setAttribute(key, newValue)
    }
  }

5、卸载操作

之前的卸载处理

javascript 复制代码
function render(vnode, container) {
    if(vnode){
      // 打补丁(挂载也是一种特殊的打补丁)
      patch(container._vnode, vnode, container)
    } else {
      if(container._vnode) {
        container.innerHTML = ";"
      }
    }
    container._vnode = vnode;
  }

问题:

  • 没有调用生命周期函数, beforeUnmonted, Unmonted
  • 没有调用钩子函数的命令, beforeUnmonted, Unmonted
  • 没有移除绑定在DOM元素上的事件处理函数

解决:

在vnode和真实的dom元素之间建立联系

javascript 复制代码
function mountElement(vnode, container) {
    // vnode和真实的el进行绑定
    const el = vnode.el createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    insert(el, container);
  }

修改render函数

javascript 复制代码
function render(vnode, container) {
    if(vnode){
      // 打补丁(挂载也是一种特殊的打补丁)
      patch(container._vnode, vnode, container)
    } else {
      if(container._vnode) {
        const el = container._vnode.el;  // 获取真实的Dom元素
        const parent = el.parent;        // 获取el的父元素
        if(parent) parent.removeChild(el);  // 通过父元素移除el
      }
    }
    container._vnode = vnode;
  }
  • removeChild移除的元素能够再次使用
  • removeChild的性能应该比innerHtml好, 理解上

抽取移除

javascript 复制代码
  unmount(vnode) {
    const parent = vnode.el.parent;
    if(parent) parent.removeChild(vnode.el);
  }
  • 在unmount函数, 有机会调用绑定在DOM元素上的指令钩子函数
  • 在unmount函数, 可以判断虚拟节点的类型, 组件相关的生命周期函数

6、区分vnode的类型

javascript 复制代码
function patch(n1, n2, container) {
    if(n1 && n1.type !== n2.type) {  // 如果类型不同直接重新挂载
      unmount(n1);
      n1 = null;
    }
    let { type } = n2;
    if(typeof type === "string") {  // 节点是普通标签元素
      if(!n1) {
        mountElement(n2, container);  //挂载节点
      } else {
        patchElement(n1, n2); // 更新节点
      }
    } else if(typeof  type === "object"){  // 节点是组件
      
    } else {
       // 省略了其他类型的vnode
    }
  }

7、事件的处理

7.1、描述事件

在vnode.props对象中, 凡是以on开发的属性都视为事件

7.2、把事件添加到DOM元素上

javascript 复制代码
const vnode = {
  type: "p",
  props: {
     onClick: () => {
       console.log("click");
     }
  }
}
javascript 复制代码
 patchProps(el,key, oldValue, newValue) {
    // 暂时放在这里
    function  shouldSetAsProps(el, key, value) {
      if(el.tagName === "INPUT" && key === "form") return false;
      return key in el;
    }
    if(/^on/.test(key)) {
      const name = key.slice(2).toLowerCase();
      el.addEventListener(name, newValue)
    } else if(key === "class") {
      el.className = newValue || ""
    } else if(shouldSetAsProps(el, key, newValue)){
      let type = typeof el[key];
      if(type === "boolean" && newValue === "") {
        el[key] = true;
      } else {
        el[key] = newValue;
      }
    } else {
      el.setAttribute(key, newValue)
    }
  },

当事件发生变化,

  • removeEventListener -> addEventListener (确保事件只会触发一次)
  • 把事件封装一层, 不必要进行移除, 性能更优
javascript 复制代码
if(/^on/.test(key)) {
      let invoker = el._vei;
      const name = key.slice(2).toLowerCase();
      if(newValue) {
        if(!invoker) {
          invoker = el._vei = (e) => {
             invoker.value(e);
          }
          invoker.value = newValue;
          el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
        } else {
          invoker.value  = newValue;
        }
        // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
      }
    }

问题1: 上面的代码存在事件覆盖, 需要改写

javascript 复制代码
if(/^on/.test(key)) {
      const name = key.slice(2).toLowerCase();
      let invokers = el._vei || (el._vei = {});
      let invoker = invokers[name];
      if(newValue) {
        if(!invoker) {
          invoker = el._vei = (e) => {
             invoker.value(e);
          }
          invoker.value = newValue;
          el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
        } else {
          invoker.value  = newValue;
        }
        // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
      } else if(invoker)  {
        el.removeEventListener(name, invoker)
      }
    }

问题2:可以多次调用addEventListener函数为元素绑定同一个类型的事件

javascript 复制代码
const vnode = {
  type: "p",
  props: {
    onClick: [
      () => {console.log("hello")},
      () => {console.log("world")}
    ]
  },
  children:  "按钮"
}

// 在patchProps中
 if(newValue) {
        if(!invoker) {
          invoker = el._vei = (e) => {
            if(Array.isArray(invoker.value)) {
              invoker.value.forEach(fn => fn(e))
            } else {
              invoker.value(e);
            }
          }
          invoker.value = newValue;
          el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
        } else {
          invoker.value  = newValue;
        }
        // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
      } else if(invoker)  {
        el.removeEventListener(name, invoker)
      }
    }

8、事件冒泡和更新时机问题

javascript 复制代码
const bol = ref(false);
effect(() => {
  const vnode = {
    type: "div",
    props: bol.value ? {
       onClick: () => {
         alert("父元素click");
       }
    } : {} ,
    children: [
      {
        type: "p",
        props: {
          onClick: () => {
            bol.value = true;
            alert("子元素click")
          }
        },
        children: "text"
      }
    ]
  }
  renderer.render(vnode, document.getElementById("app"));
})

渲染完成后, 由于bol.value为false, 不会为div元素绑定事件, 但是当鼠标点击p元素时候, click事件有p元素冒泡到div元素, 理论上div没有绑定事件, 不会触发事件的发生, 但是click元素确实发生了, 分析

  • 点击p元素, 触发click事件, bol.value 设置为true
  • bol.value为true,触发副作用函数重新执行, div元素绑定click事件
  • 事件进行冒泡, 元素div绑定的click事件

解决方法

通过事件的绑定时间和触发事件进行对比, 绑定事件小于触发事件则不执行

javascript 复制代码
  if(/^on/.test(key)) {
      const name = key.slice(2).toLowerCase();
      let invokers = el._vei || (el._vei = {});
      let invoker = invokers[name];
      if(newValue) {
        if(!invoker) {
          invoker = el._vei = function(e) {
            if(e.timeStamp < invoker.attached) return;
            if(Array.isArray(invoker.value)) {
              invoker.value.forEach(fn => fn(e))
            } else {
              invoker.value(e);
            }
          }
          invoker.value = newValue;
          invoker.attached = performance.now();
          el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
        } else {
          invoker.value  = newValue;
        }
        // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
      } else if(invoker)  {
        el.removeEventListener(name, invoker)
      }
    }

额外知识点:

  • date.now 是js的内置函数, 返回当前的时间戳,不依赖操作系统计时器, 精度较低, 受到系统时间调整或者线程调度影响
  • performance.now依赖操作系统计时器, 精度高, 在运行中受到操作系统影响较大

9、更新子节点

子节点的三种情况

less 复制代码
// 没有子节点
<div></div>
// 文本子节点
<div>Text</div>
// 多个子节点
<div>
  <p/>
  <p/>
 </div>

对应的虚拟节点

javascript 复制代码
vnode1 = {
  type: "div",
  children: null,
}
vnode2 = {
  type: "div",
  children: "Text"
}
vnode3 = {
  type: "div",
  children: [
    { type: "p", children: null},
    { type: "p", children: null},
  ]
}

所以对应更新节点的时候就会有9种情况

  • 没有 → 没有新增
  • 没有 → 文本 新增
  • 没有 → 数组 新增
  • 文本 → 没有 卸载
  • 文本 → 文本 更新
  • 文本 → 数组 更新
  • 数组 → 没有 卸载
  • 数组 → 文本 更新
  • 数组 → 数组 更新
javascript 复制代码
function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n1.props;
    for(const key in newProps) {
       if(newProps[key] !== oldProps[key]){
          patchProps(el, key, oldProps[key], newProps[key])
       }
    }
    for(const key in oldProps) {
      if(!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }
    patchChildren(n1, n2, el)
  }
javascript 复制代码
function  patchChildren(n1, n2, container) {
     if(typeof n2.children === "string"){    // 1、新节点是文本节点, 1.1、没有节点 直接设置 1.2、文本节点 直接设置  1.3、数组节点 卸载新增
       if(Array.isArray(n1.children)){
         n1.children.forEach(child => unmount(child))
       }
       setElementText(container, n2.children);
     } else if(Array.isArray(n2.children)) {   // 2、新节点是数组节点
        if(Array.isArray(n1.children)) {       // 2.1、节点是数组节点   // 这里是diff算法, 暂时全部卸载后再更新
          n1.children.forEach(child => unmount(child))
          n2.children.forEach(c => patch(null, c, container))
        } else  {                              // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
          setElementText(container, "");
          n2.children.forEach(c => patch(null, c, container))
        }
     } else {  // 3、新节点为空, 3.1、没有节点 不用处理 3.2、文本节点 设置为空, 3.3、数组节点 直接卸载
       if(Array.isArray(n1.children)) {
         n1.children.forEach(child => unmount(child))
       } else if(typeof  n1.children === "string") {
         setElementText(container, "");
       }
     }
  }
  

10、文本节点和注释节点

  • 文本节点

    因为文本节点不具有标签名称, 需要人为创造唯一的标识

    javascript 复制代码
    // 创建唯一的标识
    const  Text = Symbol()
    const newVnode = {
     type: "Text",
     children: "文本内容"
    }
    
    const Comment = Symbol()
    const newVnode = {
     type: Comment,
     children "注释内容"
    }
    javascript 复制代码
     if(type === Text){
          // 前面有判断,能进入到这个判断分支的, 要不节点为空, 要不同为Text节点
          if(!n1) {
            const el = n2.el = createText(n2.children);
            insert(el, container, null);
          } else {
            const el = n2.el = n1.el;
            if(n1.children !== n2.children) {
              setText(el, n2.children)
            }
          }
          
      }
    javascript 复制代码
    const renderer = createRenderer({
      ....
      createText(text) {
        return document.createTextNode(text)
      },
      setText(el, text) {
        el.nodeValue = text;
      }
    });
  • 注释节点: 处理方式跟文本节点类似, 不同的是需要使用document.createComment函数创建注释节点

11、Fragment

以下代码在vue2中实现不了, 但是vue3中可以

html 复制代码
<List>
  <Item/>
 </List>
html 复制代码
<!-- list.vue -->
<template>
 <ul>
   <slot/>
  </ul>
 </template>
html 复制代码
<!-- item.vue -->
<template>
  <li>1</li>
  <li>1</li>
</template>
javascript 复制代码
 const vnode = {
    type: Fragment,
    children: [
      {type: "li", children: "1"},
      {type: "li", children: "2"},
      {type: "li", children: "3"},
    ]
 }
javascript 复制代码
 if(type === Fragment) {
      if(!n1) {
        n2.children.forEach(c => patch(null, c, container))
      } else {
        patchChildren(n1, n2, container)
      }
    }
javascript 复制代码
function unmount(vnode) {
  if(vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
  }
  const parent = vnode.el.parent;
  if(parent) parent.removeChild(vnode.el);
}

重点, 其实就相当于隔了一层挂载, 隔了一层生成, 这一层是没有没有任何内容, 没有属性, 没有方法, 所以不需要处理属性方法更新,也不需要卸载元素

全部代码

javascript 复制代码
const Text = Symbol();
const Comment = Symbol();
const Fragment = Symbol();
function createRenderer(options) {
  const{ createElement, setElementText, insert, patchProps, unmount, setText, createText } = options;
  function mountElement(vnode, container) {
    const el = vnode.el =  createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    insert(el, container);
  }
  
  function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n1.props;
    for(const key in newProps) {
       if(newProps[key] !== oldProps[key]){
          patchProps(el, key, oldProps[key], newProps[key])
       }
    }
    for(const key in oldProps) {
      if(!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }
    patchChildren(n1, n2, el)
  }
  function  patchChildren(n1, n2, container) {
     if(typeof n2.children === "string"){    // 1、新节点是文本节点, 1.1、没有节点 直接设置 1.2、文本节点 直接设置  1.3、数组节点 卸载新增
       if(Array.isArray(n1.children)){
         n1.children.forEach(child => unmount(child))
       }
       setElementText(container, n2.children);
     } else if(Array.isArray(n2.children)) {   // 2、新节点是数组节点
        if(Array.isArray(n1.children)) {       // 2.1、节点是数组节点   // 这里是diff算法, 暂时全部卸载后再更新
          n1.children.forEach(child => unmount(child))
          n2.children.forEach(c => patch(null, c, container))
        } else  {                              // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
          setElementText(container, "");
          n2.children.forEach(c => patch(null, c, container))
        }
     } else {  // 3、新节点为空, 3.1、没有节点 不用处理 3.2、文本节点 设置为空, 3.3、数组节点 直接卸载
       if(Array.isArray(n1.children)) {
         n1.children.forEach(child => unmount(child))
       } else if(typeof  n1.children === "string") {
         setElementText(container, "");
       }
     }
  }
  
  // n1旧vnode n2新vnode, container容器
  function patch(n1, n2, container) {
    if(n1 && n1.type !== n2.type) {
      unmount(n1);
      n1 = null;
    }
    let { type } = n2;
    if(typeof type === "string") {  // 节点是普通标签元素
      if(!n1) {
        mountElement(n2, container);  //挂载节点
      } else {
        patchElement(n1, n2); // 更新节点
      }
    } else if(typeof  type === "object"){  // 节点是组件
    
    } else if(type === Text){
      // 前面有判断,能进入到这个判断分支的, 要不节点为空, 要不同为Text节点
      if(!n1) {
        const el = n2.el = createText(n2.children);
        insert(el, container, null);
      } else {
        const el = n2.el = n1.el;
        if(n1.children !== n2.children) {
          setText(el, n2.children)
        }
      }
    } else if(type === Fragment) {
      if(!n1) {
        n2.children.forEach(c => patch(null, c, container))
      } else {
        patchChildren(n1, n2, container)
      }
    } else {
      // 省略了其他类型的vnode
    }
  }
  function render(vnode, container) {
    if(vnode){
      // 打补丁(挂载也是一种特殊的打补丁)
      patch(container._vnode, vnode, container)
    } else {
      if(container._vnode) {
        unmount(container._vnode);
      }
    }
    container._vnode = vnode;
  }
  return {
    render,
  }
}

function createElement(tag){
  return document.createElement(tag);
}
function setElementText(el, text){
  el.textContent = text;
}
function insert(el, parent, anchor = null) {
  parent.insertBefore(el, anchor)  // parent父节点 el需要插入的节点  anchor插入时需要插入在这个节点前面
}
function patchProps(el,key, oldValue, newValue) {
  // 暂时放在这里
  function  shouldSetAsProps(el, key, value) {
    if(el.tagName === "INPUT" && key === "form") return false;
    return key in el;
  }
  if(/^on/.test(key)) {
    const name = key.slice(2).toLowerCase();
    let invokers = el._vei || (el._vei = {});
    let invoker = invokers[name];
    if(newValue) {
      if(!invoker) {
        invoker = el._vei = function(e) {
          if(e.timeStamp < invoker.attached) return;
          if(Array.isArray(invoker.value)) {
            invoker.value.forEach(fn => fn(e))
          } else {
            invoker.value(e);
          }
        }
        invoker.value = newValue;
        invoker.attached = performance.now();
        el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
      } else {
        invoker.value  = newValue;
      }
      // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
    } else if(invoker)  {
      el.removeEventListener(name, invoker)
    }
  } else if(key === "class") {
    el.className = newValue || ""
  } else if(shouldSetAsProps(el, key, newValue)){
    let type = typeof el[key];
    if(type === "boolean" && newValue === "") {
      el[key] = true;
    } else {
      el[key] = newValue;
    }
  } else {
    el.setAttribute(key, newValue)
  }
},
function unmount(vnode) {
  if(vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
  }
  const parent = vnode.el.parent;
  if(parent) parent.removeChild(vnode.el);
}
function createText(text) {
  return document.createTextNode(text)
}
function setText(el, text) {
  el.nodeValue = text;
}

const renderer = createRenderer({
  createElement,
  setElementText,
  insert,
  patchProps,
  createText,
  setText
});
相关推荐
酷爱码21 分钟前
Linux实现临时RAM登录的方法汇总
linux·前端·javascript
LuckyLay24 分钟前
Vue百日学习计划Day16-18天详细计划-Gemini版
前端·vue.js·学习
想要飞翔的pig40 分钟前
uniapp+vue3页面滚动加载数据
前端·vue.js·uni-app
HarryHY41 分钟前
git提交库常用词
前端
SoraLuna41 分钟前
「Mac畅玩AIGC与多模态41」开发篇36 - 用 ArkTS 构建聚合搜索前端页面
前端·macos·aigc
霸王蟹1 小时前
React Fiber 架构深度解析:时间切片与性能优化的核心引擎
前端·笔记·react.js·性能优化·架构·前端框架
benben0441 小时前
Unity3D仿星露谷物语开发44之收集农作物
前端·游戏·unity·游戏引擎
会功夫的李白1 小时前
uniapp自动构建pages.json的vite插件
前端·uni-app·vite
一口一个橘子1 小时前
[ctfshow web入门] web77
前端·web安全·网络安全
yyywoaini~1 小时前
wordcount程序
前端·javascript·ajax