Vue.js實現原理-實例方法和全局API

前端閱讀室
完整代碼 ( github.com/mfaying/sim…)

事件相關的實例方法

在eventsMixin中掛載到Vue構造函數的prototype中前端

vm.$on

將回調fn註冊到事件列表中便可,_events在實例初始化時建立。vue

Vue.prototype.$on = function(event, fn) {
  const vm = this;
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$on(event[i], fn);
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn);
  }
  return vm;
};
複製代碼

vm.$off

支持offoff('eventName')off('eventName', fn)off(['eventName1', 'eventName2'])off(['eventName1', 'eventName2'], fn)多種狀況node

Vue.prototype.$off = function(event, fn) {
  const vm = this;
  if (!arguments.length) {
    vm._events = Object.create(null);
    return vm;
  }

  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$off(event[i], fn);
    }
    return vm;
  }

  const cbs = vm._events[event];
  if (!cbs) {
    return vm;
  }
  if (!fn) {
    vm._events[event] = null;
    return vm;
  }

  if (fn) {
    const cbs = vm._events[event];
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
  }

  return vm;
};
複製代碼

vm.$once

先移除事件監聽,再執行函數。git

Vue.prototype.$once = function(event, fn) {
  const vm = this;
  function on() {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn;
  vm.$on(event, on);
  return vm;
};
複製代碼

vm.$emit

取出對應event回調函數列表,再遍歷執行github

Vue.prototype.$emit = function(event) {
  const vm = this;
  let cbs = vm._events[event];
  if (cbs) {
    const args = Array.from(arguments).slice(1)
    for (let i = 0, l = cbs.length; i < l; i ++) {
      try {
        cbs[i].apply(vm, args)
      } catch (e) {
        console.error(e, vm, `event handler for "${event}"`)
      }
    }
  }
  return vm;
};
複製代碼

生命週期相關的實例方法

vm.$forceUpdate

執行_watcher.update(前面介紹過原理),手動通知實例從新渲染緩存

Vue.prototype.$forceUpdate = function() {
  const vm = this;
  if (vm._watcher) {
    vm._watcher.update();
  }
};
複製代碼

vm.$destroy

vm.$destroy能夠銷燬一個實例app

  1. 先觸發beforeDestroy生命週期
  2. 刪除當前組件與父組件之間的鏈接
  3. 從狀態的依賴列表中將watcher移除
  4. 銷燬用戶使用vm.$watch所建立的watcher實例
  5. 將模板中全部指令解綁vm.__patch__(vm._vnode, null)
  6. 觸發destroyed生命週期
  7. 移除全部事件監聽器
Vue.prototype.$destroy = function() {
  const vm = this;
  if (vm._isBeingDestroyed) {
    return;
  }
  // callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true;
  const parent = vm.$parent;
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm);
  }
  if (vm._watcher) {
    vm._watcher.teardown();
  }
  let i = vm._watchers.length;
  while (i--) {
    vm._watchers[i].teardown();
  }
  vm._isDestroyed = true;
  // vm.__patch__(vm._vnode, null);
  // callHook(vm, 'destroyed')
  vm.$off();
};
複製代碼

vm.$nextTick

nextTick接收一個回調函數做爲參數,它的做用是將回調延遲到下次DOM更新週期以後執行。若是沒有提供回調且支持Promise的環境中,則返回一個Promise。異步

使用示例:函數

new Vue({
  // ...
  methods: {
    example: function() {
      this.msg = 1;
      this.$nextTick(function () {
        // DOM如今更新了
      })
    }
  }
})
複製代碼

異步更新隊列

在同一輪事件循環中即便有數據發生了兩次相同的變化,也不會渲染兩次。由於Vue.js會將受到通知的watcher實例添加到隊列中緩存起來,添加到隊列以前會檢查是否已經存在相同的watcher,只有不存在,纔會將watcher實例添加到隊列中。下次事件循環會讓隊列裏的watcher觸發渲染流程並清空隊列。oop

什麼是事件循環

JavaScript是單線程的腳本語言,任什麼時候候都只有一個主線程來處理任務。當處理異步任務時,主線程會掛起這任務,當任務處理完畢,JavaScript會將這個事件加入一個隊列,咱們叫事件隊列,被放入事件隊列中的事件不會當即執行其回調,而是等待當前執行棧中的全部任務執行完畢後,主線程會去查找事件隊列中是否有任務。

異步任務有兩種類型:微任務和宏任務。當執行棧中的全部任務都執行完畢後,會去檢查微任務隊列中是否有事件存在,若是有則一次執行微任務隊列中事件對應的回調,直到爲空。而後去宏任務隊列中取出一個事件,把對應的回調加入當前執行棧,當執行棧中的全部任務都執行完畢後,檢查微任務隊列,如此往復,這個循環就是事件循環

屬於微任務的事件有:

  1. Promise.then
  2. MutationObserver
  3. Object.observe
  4. process.nextTick
  5. ...

屬於宏任務的事件有

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. MessageChannel
  5. requestAnimationFrame
  6. I/O
  7. UI交互事件
  8. ...

下次DOM更新週期實際上是下次微任務執行時更新DOM。vm.$nextTick實際上是將回調添加到微任務中。只有特殊狀況下才會降級成宏任務。

nextTick的實現

nextTick通常狀況會使用Promise.then將flushCallbacks添加到微任務隊列中 withMacroTask包裹的函數所使用的nextTick方法會將回調添加到宏任務中。

const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

let microTimerFunc;
let macroTimerFunc;
function isNative() {
  // 實現忽略
  return true;
}
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else if (
  typeof MessageChannel !== "undefined" &&
  (isNative(MessageChannel) ||
    MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = () => {
    port.postMessage(1);
  };
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
let useMacroTask = false;

if (typeof Promise !== "undefined" && isNative(Promise)) {
  const p = Promise.resolve();
  microTimerFunc = () => {
    p.then(flushCallbacks);
  };
} else {
  microTimerFunc = macroTimerFunc;
}

export function withMacroTask(fn) {
  return (
    fn._withTask ||
    (fn._withTask = function() {
      useMacroTask = true;
      const res = fn.apply(null, arguments);
      useMacroTask = false;
      return res;
    })
  );
}

export function nextTick(cb, ctx) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx);
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    if (useMacroTask) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  if (!cb && typeof Promise !== "undefined") {
    return new Promise(resolve => {
      _resolve = resolve;
    });
  }
}
複製代碼

vm.$mount

想讓Vue.js實例具備關聯的DOM元素,只有使用vm.$mount方法這一種途經。

Vue.prototype.$mount = function(el) {
    el = el && query(el);

    const options = this.$options;
    if (!options.render) {
      let template = options.template;
      if (template) {
        if (typeof template === "string") {
          if (template.charAt(0) === "#") {
            template = idToTemplate(template);
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          if (process.env.NODE_ENV !== "production") {
            console.warn("invalid template option:" + template, this);
          }
          return this;
        }
      } else if (el) {
        template = getOuterHTML(el);
      }

      if (template) {
        const { render } = compileToFunctions(template, options, this);
        options.render = render;
      }
    }

    return mountComponent(this, el);

    // return mount.call(this, el);
  };
}

function mountComponent(vm, el) {
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== "production") {
      // 在開發環境發出警告
    }
    callHook(vm, "beforeMount");

    // 掛載
    // _update 調用patch方法執行節點的比對和渲染操做
    // _render 執行渲染函數,獲得一份最新的VNode節點樹
    // vm._watcher = new Watcher(
    // vm,
    // () => {
    // vm._update(vm._render());
    // },
    // noop
    // );

    callHook(vm, "mounted");
    return vm;
  }
}

function createEmptyVNode() {}
function callHook() {}

function idToTemplate(id) {
  const el = query(id);
  return el && el.innerHTML;
}

function query(el) {
  if (typeof el === "string") {
    const selected = document.querySelector(el);
    if (!selected) {
      return document.createElement("div");
    }
    return selected;
  } else {
    return el;
  }
}

function getOuterHTML(el) {
  if (el.outerHTML) {
    return el.outerHTML;
  } else {
    const container = document.createElement("div");
    container.appendChild(el.cloneNode(true));
    return container.innerHTML;
  }
}

const cache = {};

function compile() {
  // 03章節介紹過的生成代碼字符串
  return {
    render: ""
  };
}

function compileToFunctions(template, options, vm) {
  // options = extend({}, options);

  // 檢查緩存
  const key = options.delimiters
    ? String(options.delimiters) + template
    : template;
  if (cache[key]) {
    return cache[key];
  }

  const compiled = compile(template, options);

  const res = {};
  res.render = createFunction(compiled.render);

  return (cache[key] = res);
}

function createFunction(code) {
  return new Function(code);
}
複製代碼

Vue.extend

使用基礎Vue構造器建立一個"子類"

let cid = 1;
const ASSET_TYPES = ["component", "directive", "filter"];

exports.extend = function(Vue) {
  Vue.extend = function(extendOptions) {
    extendOptions = extendOptions || {};
    const Super = this;
    const SuperId = Super.cid;
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId];
    }

    // const name = extendOptions.name || Super.options.name;
    const name = extendOptions.name;
    if (process.env.NODE_ENV !== "production") {
      if (!/^[a-zA-Z][\w-]*$/.test(name)) {
        console.warn("");
      }
    }
    const Sub = function VueComponent(options) {
      this._init(options);
    };

    Sub.prototype = Object.create(Super.prototype);
    Super.prototype.constructor = Sub;
    Sub.cid = cid++;

    Sub.options = { ...Super.options, ...extendOptions };

    Sub["super"] = Super;

    if (Sub.options.props) {
      initProps(Sub);
    }

    if (Sub.options.computed) {
      initComputed(Sub);
    }

    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;

    ASSET_TYPES.forEach(type => {
      Sub[type] = Super[type];
    });

    if (name) {
      Sub.options.components[name] = Sub;
    }

    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = Object.assign({}, Sub.options);

    cachedCtors[SuperId] = Sub;
    return Sub;
  };
};

function initProps(Comp) {
  const props = Comp.options.props;
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key);
  }
}

function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperties(target, key, sharedPropertyDefinition);
}

function initComputed(Comp) {
  const computed = Comp.options.computed;
  for (const key in computed) {
    definedComputed(Comp.prototype, key, computed[key]);
  }
}
複製代碼

Vue.nextTick

和前面介紹過的原理同樣

Vue.set

和前面介紹過的原理同樣

Vue.delete

和前面介紹過的原理同樣

Vue.directive、Vue.filter、Vue.component

// Vue.filter、Vue.component、Vue.directive原理
const ASSET_TYPES = ["component", "directive", "filter"];

function isPlainObject() {}

exports.filterAndOther = function(Vue) {
  Vue.options = Object.create(null);
  ASSET_TYPES.forEach(type => {
    Vue.options[type + "s"] = Object.create(null);
  });

  ASSET_TYPES.forEach(type => {
    Vue.directive = function(id, definition) {
      ASSET_TYPES.forEach(type => {
        Vue.options[type + "s"] = Object.create(null);
      });
      ASSET_TYPES.forEach(type => {
        Vue[type] = function(id, definition) {
          if (!definition) {
            return this.options[type + "s"][id];
          } else {
            if (type === "component" && isPlainObject(definition)) {
              definition.name = definition.name || id;
              definition = Vue.extend(definition);
            }
            if (type === "directive" && typeof definition === "function") {
              definition = { bind: definition, update: definition };
            }
            this.options[type + "s"][id] = definition;
            return definition;
          }
        };
      });
    };
  });
};
複製代碼

Vue.use

會調用install方法,將Vue做爲參數傳入,install方法會被同一個插件屢次調用,插件只會安裝一次。

exports.use = function(Vue) {
  Vue.use = function(plugin) {
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = []);
    if (installedPlugins.indexOf(plugin) > -1) {
      return this;
    }

    const args = Array.from(arguments).slice(1);
    args.unshift(this);
    if (typeof plugin.install === "function") {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === "function") {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this;
  };
};
複製代碼

Vue.mixin

全局註冊一個混入(mixin),影響註冊以後建立的每一個Vue.js實例。插件做者可使用混入向組件注入自定義行爲(例如:監聽生命週期鉤子)。不推薦在應用代碼中使用

function mergeOptions() {}
exports.mixin = function(Vue) {
  Vue.mixin = function(mixin) {
    this.options = mergeOptions(this.options, mixin);
    return this;
  };
};
複製代碼

Vue.compile

前面介紹過的將模板編譯成渲染函數的原理

Vue.version

返回Vue.js安裝版本號,從Vue構建文件配置中取 完整代碼 (github.com/mfaying/sim…)

參考

《深刻淺出Vue.js》

前端閱讀室
相關文章
相關標籤/搜索