框架原理

280 阅读3分钟

VueRouter

class VueRouter {
  constructor(Vue, options) {
    this.$options = options;
    this.routeMap = {};
    this.app = new Vue({
      data: {
        current: '#/'
      }
    });

    this.init();
    this.createRouteMap(this.$options);
    this.initComponent(Vue);
  }

  // 初始化 hashchange
  init() {
    window.addEventListener('load', this.onHashChange.bind(this), false);
    window.addEventListener('hashchange', this.onHashChange.bind(this), false);
  }

  // 创建路由映射表
  createRouteMap(options) {
    options
      .routes
      .forEach(item => {
        this.routeMap[item.path] = item.component;
      });
  }

  // 注册组件
  initComponent(Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      template: '<a :href="to"><slot></slot></a>'
    });

    const _this = this;
    Vue.component('router-view', {
      render(h) {
        var component = _this.routeMap[_this.app.current];
        return h(component);
      }
    });
  }

  // 获取当前 hash 串
  getHash() {
    return window
      .location
      .hash
      .slice(1) || '/';
  }

  // 设置当前路径
  onHashChange() {
    this.app.current = this.getHash();
  }
}

vuex

export class Store {
    constructor(options = {}) {
        this.getters = {};
        this._mutations = {}; // 在私有属性前加_
        this._wrappedGetters = {};
        this._actions = {};
        this._modules = new ModuleCollection(options)
        const { dispatch, commit } = this;
        this.commit = (type) => {
            return commit.call(this, type);
        }
        this.dispatch = (type) => {
            return dispatch.call(this, type);
        }
        const state = options.state;
        const path = []; // 初始路径给根路径为空
        this._vm = new Vue({
            data: {
                state: state
            }
        });
        installModule(this, state, path, this._modules.root);

    }

    get state() {
        // return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式
        return this._vm._data.state;
    }
    commit(type) {
        this._mutations[type].forEach(handler => handler());
    }
    dispatch(type) {
        return this._actions[type][0]();
    }
}

class ModuleCollection {
    constructor(rawRootModule) {
        this.register([], rawRootModule)
    }
    register(path, rawModule) {
        const newModule = {
            _children: {},
            _rawModule: rawModule,
            state: rawModule.state
        }
        if (path.length === 0) {
            this.root = newModule;
        } else {
            const parent = path.slice(0, -1).reduce((module, key) => {
                return module._children(key);
            }, this.root);
            parent._children[path[path.length - 1]] = newModule;
        }
        if (rawModule.modules) {
            forEachValue(rawModule.modules, (rawChildModule, key) => {
                this.register(path.concat(key), rawChildModule);
            })
        }
    }
}

function installModule(store, rootState, path, module) {
    if (path.length > 0) {
        const parentState = rootState;
        const moduleName = path[path.length - 1];
        Vue.set(parentState, moduleName, module.state)
    }
    const context = {
        dispatch: store.dispatch,
        commit: store.commit,
    }
    const local = Object.defineProperties(context, {
        getters: {
            get: () => store.getters
        },
        state: {
            get: () => {
                let state = store.state;
                return path.length ? path.reduce((state, key) => state[key], state) : state
            }
        }
    })
    if (module._rawModule.actions) {
        forEachValue(module._rawModule.actions, (actionFn, actionName) => {
            registerAction(store, actionName, actionFn, local);
        });
    }
    if (module._rawModule.getters) {
        forEachValue(module._rawModule.getters, (getterFn, getterName) => {
            registerGetter(store, getterName, getterFn, local);
        });
    }
    if (module._rawModule.mutations) {
        forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
            registerMutation(store, mutationName, mutationFn, local)
        });
    }
    forEachValue(module._children, (child, key) => {
        installModule(store, rootState, path.concat(key), child)
    })

}

function registerMutation(store, mutationName, mutationFn, local) {
    const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []);
    entry.push(() => {
        mutationFn.call(store, local.state);
    });
}

function registerAction(store, actionName, actionFn, local) {
    const entry = store._actions[actionName] || (store._actions[actionName] = [])
    entry.push(() => {
        return actionFn.call(store, {
            commit: local.commit,
            state: local.state,
        })
    });
}

function registerGetter(store, getterName, getterFn, local) {
    Object.defineProperty(store.getters, getterName, {
        get: () => {
            return getterFn(
                local.state,
                local.getters,
                store.state
            )
        }
    })
}

// 将对象中的每一个值放入到传入的函数中作为参数执行
function forEachValue(obj, fn) {
    Object.keys(obj).forEach(key => fn(obj[key], key));
}

export function install(_Vue) {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate: function vuexInit() {
            const options = this.$options;
            if (options.store) {
                this.$store = options.store;
            } else if (options.parent && options.parent.$store) {
                this.$store = options.parent.$store;
            }
        }
    })
}

虚拟dom

  • 用JS对象模拟DOM(虚拟DOM)
  • 把此虚拟DOM转成真实DOM并插入页面中(render)
  • 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  • 把差异对象应用到真正的DOM树上(patch)
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
  // 因为 vnode 和 oldVnode 是相同的 vnode,所以我们可以复用 oldVnode.elm。
  const elm = vnode.elm = oldVnode.elm
  let oldCh = oldVnode.children
  let ch = vnode.children

  // 如果 oldVnode 和 vnode 是完全相同,说明无需更新,直接返回。
  if (oldVnode === vnode) return

  // 调用 update hook
  if (vnode.data) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  }

  // 如果 vnode.text 是 undefined
  if (vnode.text === undefined) {
    // 比较 old children 和 new children,并更新
    if (oldCh && ch) {
      if (oldCh !== ch) {
        // 核心逻辑(最复杂的地方):怎么比较新旧 children 并更新,对应上面
        // 的数组比较
        updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      }
    }
    // 添加新 children
    else if (ch) {
      // 首先删除原来的 text
      if (oldVnode.text) api.setTextContent(elm, '')
      // 然后添加新 dom(对 ch 中每个 vnode 递归创建 dom 并插入到 elm)
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    }
    // 相反地,如果原来有 children 而现在没有,那么我们要删除 children。
    else if (oldCh) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    // 最后,如果 oldVnode 有 text,删除。
    else if (oldVnode.text) {
      api.setTextContent(elm, '');
    }
  }
  // 否则 (vnode 有 text),只要 text 不等,更新 dom 的 text。
  else if (oldVnode.text !== vnode.text) {
    api.setTextContent(elm, vnode.text)
  }
}
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
  let oldStartIdx = 0, newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx
  let idxInOld
  let elmToMove
  let before

  // 遍历 oldCh 和 newCh 来比较和更新
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 1⃣️ 首先检查 4 种情况,保证 oldStart/oldEnd/newStart/newEnd
    // 这 4 个 vnode 非空,左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标。
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    }
    /**
     * 2⃣️ 然后 oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
     * 对有相同 vnode 的 4 种情况执行对应的 patch 逻辑。
     * - 如果同 start 或同 end 的两个 vnode 是相同的(情况 1 和 2),
     *   说明不用移动实际 dom,直接更新 dom 属性/children 即可;
     * - 如果 start 和 end 两个 vnode 相同(情况 3 和 4),
     *   那说明发生了 vnode 的移动,同理我们也要移动 dom。
     */
    // 1. 如果 oldStartVnode 和 newStartVnode 相同(key相同),执行 patch
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 不需要移动 dom
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 2. 如果 oldEndVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 不需要移动 dom
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 3. 如果 oldStartVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      // 把获得更新后的 (oldStartVnode/newEndVnode) 的 dom 右移,移动到
      // oldEndVnode 对应的 dom 的右边。为什么这么右移?
      // (1)oldStartVnode 和 newEndVnode 相同,显然是 vnode 右移了。
      // (2)若 while 循环刚开始,那移到 oldEndVnode.elm 右边就是最右边,是合理的;
      // (3)若循环不是刚开始,因为比较过程是两头向中间,那么两头的 dom 的位置已经是
      //     合理的了,移动到 oldEndVnode.elm 右边是正确的位置;
      // (4)记住,oldVnode 和 vnode 是相同的才 patch,且 oldVnode 自己对应的 dom
      //     总是已经存在的,vnode 的 dom 是不存在的,直接复用 oldVnode 对应的 dom。
      api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      // 这里是左移更新后的 dom,原因参考上面的右移。
      api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }

    // 3⃣️ 最后一种情况:4 个 vnode 都不相同,那么我们就要
    // 1. 从 oldCh 数组建立 key --> index 的 map。
    // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
    //    以它的 key 从上面的 map 里拿到 index;
    // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
    // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
    //    创建对应的 dom 并插入。
    else {
      // 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index 的
      // 映射,方便我们之后通过 key 去拿下标。
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // 尝试通过 newStartVnode 的 key 去拿下标
      idxInOld = oldKeyToIdx[newStartVnode.key]
      // 下标不存在,说明 newStartVnode 是全新的 vnode。
      if (idxInOld == null) {
        // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        newStartVnode = newCh[++newStartIdx]
      }
      // 下标存在,说明 old children 中有相同 key 的 vnode,
      else {
        elmToMove = oldCh[idxInOld]
        // 如果 type 不同,没办法,只能创建新 dom;
        if (elmToMove.type !== newStartVnode.type) {
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        }
        // type 相同(且key相同),那么说明是相同的 vnode,执行 patch。
        else {
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
  }

  // 上面的循环结束后(循环条件有两个),处理可能的未处理到的 vnode。
  // 如果是 new vnodes 里有未处理的(oldStartIdx > oldEndIdx
  // 说明 old vnodes 先处理完毕)
  if (oldStartIdx > oldEndIdx) {
    before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  }
  // 相反,如果 old vnodes 有未处理的,删除 (为处理 vnodes 对应的) 多余的 dom。
  else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

模板

// <%% %%> 用于转义
// <% 脚本
// <%= 输出脚本值
// <%- 输出脚本值,unescape
// <%# 注释
enum State {
  EVAL,
  ESCAPED,
  RAW,
  COMMENT,
  LITERAL
}

// 定义一个正则表达式来匹配分隔符
const REGEXP = /(<%%|%%>|<%=|<%-|<%#|<%|%>)/;

const start = "<%";
const end = "%>";
const escpStart = "<%%";
const escpEnd = "%%>";
const escpout = "=";
const unescpout = "-";
const comt = "#";

export default class Template {
  public template: string;
  private tokens: string[] = [];
  private source: string = "";
  private state?: State;
  private fn?: Function;

  public constructor(template: string) {
    this.template = template;
  }

  public compile() {
    this.parseTemplateText();
    this.transformTokens();
    this.wrapit();
  }

  public render(local: object) {
    return this.fn.call(null, local, escape);
  }

  private wrapit() {
    this.source = `\
        const __out = [];
        const __append = __out.push.bind(__out);
        with(local||{}) {
        ${this.source}
        }
        return __out.join('');\
        `;
    // eslint-disable-next-line
    this.fn = new Function("local", "escapeFn", this.source);
  }

  /**
   * token解析
   * 将<% if (codintion) { %>
   * 解析为token数组,例如['<%', ' if (condition) { ', '%>']
   */
  private parseTemplateText() {
    let str = this.template;
    const arr = this.tokens;
    let res = REGEXP.exec(str);
    let index;

    while (res) {
      index = res.index;
      // 前置字符串
      if (index !== 0) {
        arr.push(str.substring(0, index));
        str = str.slice(index);
      }

      arr.push(res[0]);
      str = str.slice(res[0].length);
      res = REGEXP.exec(str);
    }

    if (str) {
      arr.push(str);
    }
  }

  private transformTokens() {
    if (!this.tokens.length) {
      return;
    }

    this.tokens.forEach((tok, idx) => {
      let closing: string;
      // 基本的语法检查
      // 检查标签是否关闭
      if (tok.includes(start) && !tok.includes(escpStart)) {
        closing = this.tokens[idx + 2];
        if (closing == null || !closing.includes(end)) {
          throw new Error(`${tok} 未找到对应的标签`);
        }
      }

      // 开始扫描token
      switch (tok) {
        case start:
          this.state = State.EVAL;
          break;
        case start + escpout:
          this.state = State.ESCAPED;
          break;
        case start + unescpout:
          this.state = State.RAW;
          break;
        case start + comt:
          this.state = State.COMMENT;
          break;
        case escpStart:
          this.state = State.LITERAL;
          this.source += `;__append('<%');\n`;
          break;
        case escpEnd:
          this.state = State.LITERAL;
          this.source += `;__append('%>');\n`;
          break;
        case end:
          this.state = undefined;
          break;
        default:
          // 非分隔符
          if (this.state != null) {
            switch (this.state) {
              case State.EVAL:
                // 代码
                this.source += `;${tok}\n`;
                break;
              case State.ESCAPED:
                this.source += `;__append(escapeFn(${stripSemi(tok)}));\n`;
                break;
              case State.RAW:
                this.source += `;__append(${stripSemi(tok)});\n`;
                break;
              case State.LITERAL:
                this.source += `;__append('${transformString(tok)}');\n`;
                break;
              case State.COMMENT:
                // do nothing
                break;
            }
          } else {
            this.source += `;__append('${transformString(tok)}');\n`;
          }
      }
    });
  }
}

function transformString(str: string) {
  // 转义\\
  str = str.replace(/\\/g, "\\\\");
  // 转换换行符
  str = str.replace(/\n/g, "\\n");
  str = str.replace(/\r/g, "\\r");

  // 转换单引号
  str = str.replace(/'/g, "\\'");
  return str;
}

function stripSemi(str) {
  return str.replace(/;(\s*$)/, "$1");
}

const _MATCH_HTML = /[&<>'"]/g;
const _ENCODE_HTML_RULES = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&#34;",
  "'": "&#39;"
};
function encode_char(c) {
  return _ENCODE_HTML_RULES[c] || c;
}
function escape(markup: string) {
  return markup == null ? "" : String(markup).replace(_MATCH_HTML, encode_char);
}
;__append('\n<html>\n  <head>');
;__append(escapeFn( title ));
;__append('</head>\n  <body>\n    ');
;__append('<%');
;__append(' 转义 ');
;__append('%>');
;__append('\n    ');
;__append('\n    ');
;__append( before );
;__append('\n    ');
; if (show) {
;__append('\n      <div>root</div>\n    ');
; }
;__append('\n  </body>\n</html>\n');

模板2

var TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g
    var reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g
    var code = 'var r=[];\n', cursor = 0
    var match;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}