实现虚拟 DOM

c2c67628dcf355bee08a552a0402f60c.png

虚拟 DOM 是一种抽象,用于简化修改 UI 的行为。

// VirtualNode
//    = { tag : string
//      , properties : { property: string }
//      , children : [VirtualNode]
//      }
//    | { text : string }
//
// Diff
//    = { replace : VirtualNode }
//    | { remove : true }
//    | { create : VirtualNode }
//    | { modify : { remove :: string[], set :: { property : value }, children :: Diff[] } }
//    | { noop : true }
//
const SMVC = (function () {


  function assert(predicate, ...args) {
    if (!predicate) {
      console.error(...args);
      throw new Error("fatal");
    }
  }


  const props = new Set(["autoplay", "checked", "checked", "contentEditable", "controls",
    "default", "hidden", "loop", "selected", "spellcheck", "value", "id", "title",
    "accessKey", "dir", "dropzone", "lang", "src", "alt", "preload", "poster",
    "kind", "label", "srclang", "sandbox", "srcdoc", "type", "value", "accept",
    "placeholder", "acceptCharset", "action", "autocomplete", "enctype", "method",
    "name", "pattern", "htmlFor", "max", "min", "step", "wrap", "useMap", "shape",
    "coords", "align", "cite", "href", "target", "download", "download",
    "hreflang", "ping", "start", "headers", "scope", "span"]);


  //  DOM 对象中设置属性
  function setProperty(prop, value, el) {
    if (props.has(prop)) {
      el[prop] = value;
    } else {
      el.setAttribute(prop, value);
    }
  }


  function listener(event) {
    const el = event.currentTarget;
    const handler = el._ui.listeners[event.type];
    const enqueue = el._ui.enqueue;
    assert(typeof enqueue == "function", "Invalid enqueue");
    const msg = handler(event);
    if (msg !== undefined) {
      enqueue(msg);
    }
  }


  // 向 DOM 节点添加事件监听
  function setListener(el, event, handle) {
    assert(typeof handle == "function", "Event listener is not a function for event:", event);


    if (el._ui.listeners[event] === undefined) {
      el.addEventListener(event, listener);
    }


    el._ui.listeners[event] = handle;
  }


  function eventName(str) {
    if (str.indexOf("on") == 0) {
      return str.slice(2).toLowerCase();
    }
    return null;
  }


  // diff two virtual nodes
  function diffOne(l, r) {
    assert(r instanceof VirtualNode, "Expected an instance of VirtualNode, found", r);
    const isText = l.text !== undefined;
    if (isText) {
      return l.text !== r.text
        ? { replace: r }
        : { noop: true };
    }


    if (l.tag !== r.tag) {
      return { replace: r };
    }


    const remove = [];
    const set = {};


    for (const prop in l.properties) {
      if (r.properties[prop] === undefined) {
        remove.push(prop);
      }
    }


    for (const prop in r.properties) {
      if (r.properties[prop] !== l.properties[prop]) {
        set[prop] = r.properties[prop];
      }
    }


    const children = diffList(l.children, r.children);
    const noChildrenChange = children.every(e => e.noop);
    const noPropertyChange =
      (remove.length === 0) &&
      (Array.from(Object.keys(set)).length == 0);


    return (noChildrenChange && noPropertyChange)
      ? { noop: true }
      : { modify: { remove, set, children } };
  }


  function diffList(ls, rs) {
    assert(rs instanceof Array, "Expected an array, found", rs);
    const length = Math.max(ls.length, rs.length);
    return Array.from({ length })
      .map((_, i) =>
        (ls[i] === undefined)
          ? { create: rs[i] }
          : (rs[i] == undefined)
            ? { remove: true }
            : diffOne(ls[i], rs[i])
      );
  }


  function create(enqueue, vnode) {
    assert(vnode instanceof VirtualNode, "Expected an instance of VirtualNode, found", vnode);


    if (vnode.text !== undefined) {
      const el = document.createTextNode(vnode.text);
      return el;
    }


    const el = document.createElement(vnode.tag);
    el._ui = { listeners: {}, enqueue };


    for (const prop in vnode.properties) {
      const event = eventName(prop);
      const value = vnode.properties[prop];
      (event === null)
        ? setProperty(prop, value, el)
        : setListener(el, event, value);
    }


    for (const childVNode of vnode.children) {
      const child = create(enqueue, childVNode);
      el.appendChild(child);
    }


    return el;
  }


  function modify(el, enqueue, diff) {
    for (const prop of diff.remove) {
      const event = eventName(prop);
      if (event === null) {
        el.removeAttribute(prop);
      } else {
        el._ui.listeners[event] = undefined;
        el.removeEventListener(event, listener);
      }
    }


    for (const prop in diff.set) {
      const value = diff.set[prop];
      const event = eventName(prop);
      (event === null)
        ? setProperty(prop, value, el)
        : setListener(el, event, value);
    }


    assert(diff.children.length >= el.childNodes.length, "unmatched children lengths");
    apply(el, enqueue, diff.children);
  }


  // 将 diff 应用到真实 DOM 上
  function apply(el, enqueue, childrenDiff) {
    const children = Array.from(el.childNodes);


    childrenDiff.forEach((diff, i) => {
      const action = Object.keys(diff)[0];
      switch (action) {
        case "remove":
          children[i].remove();
          break;


        case "modify":
          modify(children[i], enqueue, diff.modify);
          break;


        case "create": {
          assert(i >= children.length, "adding to the middle of children", i, children.length);
          const child = create(enqueue, diff.create);
          el.appendChild(child);
          break;
        }


        case "replace": {
          const child = create(enqueue, diff.replace);
          children[i].replaceWith(child);
          break;
        }


        case "noop":
          break;


        default:
          throw new Error("Unexpected diff option: " + Object.keys(diff));
      }
    });
  }


  class VirtualNode {
    constructor(any) { Object.assign(this, any) }
  }


  // Create an HTML element description (a virtual node)
  function h(tag, properties, children) {
    assert(typeof tag === "string", "Invalid tag value:", tag);
    assert(typeof properties === "object", "Expected properties object. Found:", properties);
    assert(Array.isArray(children), "Expected children array. Found:", children);
    return new VirtualNode({ tag, properties, children });
  }


  // Create a text element description (a virtual text node)
  function text(content) {
    return new VirtualNode({ text: content });
  }


  // Start managing the contents of an HTML element.
  function init(root, initialState, update, view) {
    let state = initialState; // client application state
    let nodes = []; // virtual DOM nodes
    let queue = []; // msg queue


    function enqueue(msg) {
      queue.push(msg);
    }


    // draws the current state
    function draw() {
      let newNodes = view(state);
      apply(root, enqueue, diffList(nodes, newNodes));
      nodes = newNodes;
    }


    function updateState() {
      if (queue.length > 0) {
        let msgs = queue;
        queue = [];


        msgs.forEach(msg => {
          try {
            state = update(state, msg, enqueue);
          } catch (e) {
            console.error(e);
          }
        });


        draw();
      }


      window.requestAnimationFrame(updateState);
    }


    draw();
    updateState();


    return { enqueue };
  }


  return { init, h, text };
})();

下面是用此代码实现的计数器:

function view(state) {
    return [
        h("p", {}, [ text(`Counter: ${state.counter}`) ])
    ];
}


function update(state, msg) {
    return { counter : state.counter + msg }
}


const initialState = { counter: 0 };


const root = document.querySelector(".my-application");


// Start application
const { enqueue } = init(root, initialState, update, view);


// Increase the counter by one every second.
setInterval(() => enqueue(1), 1000);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值