vue2.5.0源码学习

Posted on By yuanfang

vue2.5.0学习预备知识

注意attention: 转载学习来源–Vue.js 技术揭秘

MVVM之监测一个普通对象的变化

注意attention: 转载学习来源-JavaScript实现MVVM之我就是想监测一个普通对象的变化

Object.defineProperty {#define_property}

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者

    var o = {}; // 创建一个新对象

    // 在对象中添加一个属性与数据描述符的示例
    Object.defineProperty(o, "a", {
        value : 37, // 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
        writable : true, // 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
        enumerable : true, //当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
        configurable : true //当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
    });
    // 对象o拥有了属性a,值为37

    // 在对象中添加一个属性与存取描述符的示例
    var bValue;
    Object.defineProperty(o, "b", {
        get : function(){ // 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)
            return bValue;
        },
        set : function(newValue){ // 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
            bValue = newValue;
        },
        enumerable : true,
        configurable : true
    });

    o.b = 38; // 对象o拥有了属性b,值为38

    // o.b的值现在总是与bValue相同,除非重新定义o.b

    // 数据描述符和存取描述符不能混合使用
    Object.defineProperty(o, "conflict", {
        value: 0x9f91102, 
        get: function() { 
            return 0xdeadbeef; 
        } 
    });
    // throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
// 对于对象的监控
var oldVal = obj[key];
Object.defineProperty(obj, key, {
    get: function(){
        return oldVal;
    },
    set: (function(newVal){
        if(oldVal !== newVal){
            if(OP.toString.call(newVal) === '[object Object]'){
                this.observe(newVal);
            }
            this.$callback(newVal, oldVal);
            oldVal = newVal;
        }

    }).bind(this)
});

// 对于数组的监控
overrideArrayProto(array){
    // 保存原始 Array 原型
    var originalProto = Array.prototype,
        // 通过 Object.create 方法创建一个对象,该对象的原型就是Array.prototype
        overrideProto = Object.create(Array.prototype),
        self = this,
        result;
    // 遍历要重写的数组方法
    Object.keys(OAM).forEach(function(key, index, array){
        var method = OAM[index],
            oldArray = [];
        // 使用 Object.defineProperty 给 overrideProto 添加属性,属性的名称是对应的数组函数名,值是函数
        Object.defineProperty(overrideProto, method, {
            value: function(){
                oldArray = this.slice(0);

                var arg = [].slice.apply(arguments);
                // 调用原始 原型 的数组方法
                result = originalProto[method].apply(this, arg);
                // 对新的数组进行监测
                self.observe(this);
                // 执行回调
                self.$callback(this, oldArray);

                return result;
            },
            writable: true,
            enumerable: false,
            configurable: true
        });
    }, this);

    // 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto
    array.__proto__ = overrideProto;

}

rollup

// src/main.js
import foo from './foo.js';
export default function () {
  console.log(foo);
}

// src/foo.js
export default 'hello world!';

// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
};

// 命令行 rollup main.js -f cjs -o cjs_bundle.js

nextTick事件机制

  1. 转载学习文档– JavaScript 运行机制详解:再谈Event Loop–作者:阮一峰

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在”任务队列“的尾部添加一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到执行。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick 和 setImmediate。它们可以帮助我们加深对”任务队列”的理解。 process.nextTick 方法可以在当前”执行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像.

  1. MessageChannel– MDN MessageChannel
//在以下代码块中,您可以看到使用MessageChannel构造函数实例化了一个channel对象。当iframe加载完毕,我们使用MessagePort.postMessage方法把一条消息和MessageChannel.port2传递给iframe。handleMessage处理程序将会从iframe中(使用MessagePort.onmessage监听事件)接收到信息,将数据其放入innerHTML中。

var channel = new MessageChannel();
var para = document.querySelector('p');
    
var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;

ifr.addEventListener("load", iframeLoaded, false);
    
function iframeLoaded() {
  // otherWindow其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
  // transfer 可选 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
  otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}

channel.port1.onmessage = handleMessage;
function handleMessage(e) {
  para.innerHTML = e.data;
}
// 跨域读写
// https://a.com
var iframe = document.createElement('iframe');
iframe.style.css = 'position:absolute;width:1px;height:1px;left:-9999px;';
document.body.appendChild(iframe);
iframe.src = 'https://b.com';
iframe.addEventListener('load',function(){
    // otherWindow其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
    // 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
    iframe.contentWindow.postMessage('aaa', 'https://b.com');
}, false);

window.addEventListener('message', function(event) {
    // 处理接收回来的事件
    // event.origin调用 postMessage  时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。
    console.log("收到" + event.origin + "消息:" + event.data);
})

// https://b.com
window.addEventListener("message", function(event) {
    // 设置或者读取cookie啥的
    // event.source对发送消息的窗口对象的引用; 可以使用此来在具有不同origin的两个窗口之间建立双向通信。
    event.source.postMessage('我来自b.com', 'https://a.com');
}, false);

  1. 转载学习文档理解 JavaScript 中的 macrotask 和 microtask

Macrotask 和 microtask 都是属于上述的异步任务中的一种,我们先看一下他们分别是哪些 API :
1)macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
2)microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

  1. vue2 源码实现
// /src/core/util/env.js
// 闭包实现 返回函数queueNextTick,callbacks对象保存需要回调的,timerFunc设置异步执行方式
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

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

  // An defer asynchronousring mechanism.
  // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
  // but microtasks actually has too high a priority and fires in between
  // supposedly sequential events (e.g. #4521, #6690) or even between
  // bubbling of the same event (#6566). Technically setImmediate should be
  // the ideal choice, but it's not available everywhere; and the only polyfill
  // that consistently queues the callback after all DOM events triggered in the
  // same loop is by using MessageChannel.
  /* istanbul ignore if */
  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
      setImmediate(nextTickHandler)
    }
  } else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel() // 使用MessageChannel
    const port = channel.port2
    channel.port1.onmessage = nextTickHandler
    timerFunc = () => {
      port.postMessage(1)
    }
  } else
  /* istanbul ignore next */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    // use microtask in non-DOM environments, e.g. Weex
    const p = Promise.resolve()
    timerFunc = () => {
      p.then(nextTickHandler)
    }
  } else {
    // fallback to setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

performance

// Start with one mark.
performance.mark("mySetTimeout-start");

// Wait some time.
setTimeout(function() {
  // Mark the end of the time.
  performance.mark("mySetTimeout-end");

  // Measure between the two different markers.
  performance.measure(
    "mySetTimeout",
    "mySetTimeout-start",
    "mySetTimeout-end"
  );

  // Get all of the measures out.
  // In this case there is only one.
  var measures = performance.getEntriesByName("mySetTimeout");
  var measure = measures[0];
  console.log("setTimeout milliseconds:", measure.duration)

  // Clean up the stored markers.
  performance.clearMarks();
  performance.clearMeasures();
}, 1000);
// vue2源码 /src/core/util/perf.js
if (process.env.NODE_ENV !== 'production') {
  const perf = inBrowser && window.performance
  /* istanbul ignore if */
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag)
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag)
      perf.clearMarks(startTag)
      perf.clearMarks(endTag)
      perf.clearMeasures(name)
    }
  }
}

cache缓存

/**
 * Create a cached version of a pure function.
 */
export function cached(fn){
  const cache = Object.create(null)
  return (function cachedFn (str) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  })
}

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

let aaDom = idToTemplate('aa')
// 比如节流函数
function throttle(fn, wait) {
    let last = 0;
    return (function(){
        let now = new Date();
        if(now - last < wait) {
            return;
        }
        last = now;
        return fn.apply(this, arguments);
    });
}

this.orderBtnEveThro = throttle(this.orderBtnEve, 1000);

passive

由于浏览器无法预先知道一个事件处理函数中会不会调用 preventDefault(),它需要等到事件处理函数执行完后,才能去执行默认行为,然而事件处理函数执行是要耗时的,这样一来就会导致页面卡顿;
我们可以通过传递 passive 为 true 来明确告诉浏览器,事件处理程序不会调用 preventDefault 来阻止默认滑动行为。

export let supportsPassive = false
if (inBrowser) {
  try {
    const opts = {}
    // passive: Boolean,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
    Object.defineProperty(opts, 'passive', ({
      get () {
        /* istanbul ignore next */
        supportsPassive = true
      }
    }: Object)) // https://github.com/facebook/flow/issues/285
    window.addEventListener('test-passive', null, opts)
  } catch (e) {}
}

compositionstart/compositionend

注意attention: input 的 compositionstart 和 compositionend 事件(禁止非直接输入)

在 web 开发中,我们通常需要对输入的内容进行校验。这段代码虽然执行起来没有什么问题,但是会产生非直接输入,比方说我们输入“树莓派”,中间过程会输入拼音,每次输入字母都会触发input事件,然而当中文输入完成之前,都属于非直接输入。

  • compositionstart 是指中文输入法开始输入触发,每次输入开始仅执行一次,执行的开始是 end 事件结束了才会触发;
  • compositonupdate 是指中文输入法在输入时触发,也就是可能得到 shu’mei 这种内容,这里返回的内容是实时的,仅在 start 事件触发后触发,输入时实时触发;
  • compositionend 是指中文输入法输入完成时触发,这是得到的结果就是最终输入完成的结果,此事件仅执行一次。 需要特别注意的是:该事件触发顺序在 input 事件之后,故而需要在此事件的处理逻辑里调用一次 input 里边的业务逻辑;
// 添加标记位 lock ,当用户未输入完时,lock 为 true
var lock = false;
var inputEle = document.getElementById('inputEle');
// input 事件中的处理逻辑, 这里仅仅打印文本
var todo = function (text) {
    console.log(text)
};
inputEle.addEventListener('compositionstart', function () {
    lock = true;
});
inputEle.addEventListener('compositionend', function (event) {
    lock = false;
    // compositionend 事件发生在 input 之后,故此需要此处调用 input 中逻辑
    todo(event.target.value);
});
inputEle.addEventListener('input', function (event) {
    // 忽略一切非直接输入,不做逻辑处理
    if (!lock) todo(event.target.value);
});

Snabbdom

参考 snabbdom 章节

FLIP

注意attention: 学习参考文章-使用 FLIP 来提高 Web 动画的性能

LIP 是将一些开销高昂的动画,如针对 width,height,left 或 top 的动画,映射为 transform 动画。通过记录元素的两个快照,一个是元素的初始位置(First – F),另一个是元素的最终位置(Last – L),然后对元素使用一个 transform 变换来反转(Invert – I),让元素看起来还在初始位置,最后移除元素上的 transform 使元素由初始位置运动(Play – P)到最终位置;

// Get the first position.
var first = el.getBoundingClientRect();

// Now set the element to the last position.
el.classList.add('totes-at-the-end');

// Read again. This forces a sync
// layout, so be careful.
var last = el.getBoundingClientRect();

// You can do this for other computed
// styles as well, if needed. Just be
// sure to stick to compositor-only
// props like transform and opacity
// where possible.
var invert = first.top - last.top;

// Invert.
el.style.transform =
    `translateY(${invert}px)`;

// Wait for the next frame so we
// know all the style changes have
// taken hold.
requestAnimationFrame(function() {

  // Switch on animations.
  el.classList.add('animate-on-transforms');

  // GO GO GOOOOOO!
  el.style.transform = '';
});

// Capture the end with transitionend
el.addEventListener('transitionend',
    tidyUpAnimations);
// 或者利用 Web Animations API https://github.com/web-animations/web-animations-js
// Get the first position.
var first = el.getBoundingClientRect();

// Move it to the end.
el.classList.add('totes-at-the-end');

// Get the last position.
var last = el.getBoundingClientRect();

// Invert.
var invert = first.top - last.top;

// Go from the inverted position to last.
var player = el.animate([
  { transform: `translateY(${invert}px)` },
  { transform: 'translateY(0)' }
], {
  duration: 300,
  easing: 'cubic-bezier(0,0,0.32,1)',
});

// Do any tidy up at the end
// of the animation.
player.addEventListener('finish',
    tidyUpAnimations);
// requestAnimationFrame
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);