vue源码学习(二)实例方法

vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js

vue实例方法包括以下:

  • vm.$watch
  • vm.$set
  • vm.$delete
  • vm.$on
  • vm.$once
  • vm.$off
  • vm.$emit
  • vm.$mount
  • vm.$forceUpdate
  • vm.$nextTick
  • vm.$destroy

实例方法/数据

1. vm.$watch

源码如下:

Vue.prototype.$watch = function (expOrFn, cb, options) {
    var vm = this;
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    // 创建watcher
    var watcher = new Watcher(vm, expOrFn, cb, options);
    // 选项参数中指定immediate: true 将立即以表达式的当前值触发回调
    if (options.immediate) {
        try {
            cb.call(vm, watcher.value);
        } catch (error) {
            handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
    }
    // 返回一个取消观察函数,用来停止触发回调
    return function unwatchFn() {
        watcher.teardown();
    }
};

 2. vm.$set

Vue.prototype.$set = set;
/**
 * 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
 * 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
*/

// 设置对象的属性,并在属性不存在时触发更改通知。
function set(target, key, val) {
    if (isUndef(target) || isPrimitive(target)) {
        console.warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    // 更新数组指定下标的元素,Vue.set(array, index, val), 通过splice方法实现响应式更新
    // 判断target为数组且key为合法的索引值
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 取两者最大值,并修改数组length
        target.length = Math.max(target.length, key);
        // 插入值
        target.splice(key, 1, val);
        return val
    }
    // 更新对象已有属性, Vue.set(obj, key, val),执行更新即可
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val
    }
    var ob = (target).__ob__;
    // 对象不能是 Vue 实例,或者 Vue 实例的根数据对象
    // this.$data的ob.vmCount = 1, 表示根组件, 其他子组件的vm.vmCount都是0
    if (target._isVue || (ob && ob.vmCount)) {
        console.warn(
            'Avoid adding reactive properties to a Vue instance or its root $data ' +
            'at runtime - declare it upfront in the data option.'
        );
        return val
    }
    // target不是响应式对象,新属性会被设置,但是不会做响应式处理
    if (!ob) {
        target[key] = val;
        return val
    }
    // 给对象定义新属性,通过 defineReactive 方法设置响应式,并触发依赖更新
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
}

3. vm.$delete

Vue.prototype.$delete = del;

function del(target, key) {
    // target须为对象
    if (isUndef(target) || isPrimitive(target)) {
        console.warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
    }
    // 判断target为数组且key为合法的索引值
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 通过splice删除 
        target.splice(key, 1);
        return
    }
    var ob = (target).__ob__;
    // 目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象
    if (target._isVue || (ob && ob.vmCount)) {
        console.warn(
            'Avoid deleting properties on a Vue instance or its root $data ' +
            '- just set it to null.'
        );
        return
    }
    // 如果属性不存在直接结束
    if (!hasOwn(target, key)) {
        return
    }
    // 通过delete运算符删除对象的属性
    delete target[key];
    if (!ob) {
        return
    }
    // 执行依赖通知
    ob.dep.notify();
}

实例方法/事件

与事件相关的实例方法有4个,分别是vm.$on,vm.$emit,vm.$once,vm.$off。这四个方法都是挂载在Vue的prototype属性上:

1. vm.$on

示例:

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"

实现原理:

在注册事件时将回调函数收集起来,等到事件触发时,将收集起来的回调函数依次调用即可。所以,我们就需要一个对象来存储这些回调函数。事实上,在执行new Vue()时,会执行_init一系列初始化工作,其中在initEvents方法中创建了一个_evnets属性,用来存储事件及相应的回调函数。

下面我们来看源码是如何实现该方法的:

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    // 判断是否是数组
    if(Array.isArray(event)) {
        // 遍历数组,使得数组中每一项都调用vm.$on,使回调函数可以注册到数组中指定事件名对应的事件列表中
        for (var i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn);
        }
    } else {
        // 事件列表不存在则使用空数组初始化,然后将回调函数添加到事件列表中
        (vm._events[event] || (vm._events[event] = [])).push(fn);
        // hookEvent, 提供从外部为组件实例注入声明周期方法的机会
        // 比如从组件外部为组件的 mounted 方法注入额外的逻辑, 该能力是结合 callhook 方法实现的
        if (hookRE.test(event)) {
            vm._hasHookEvent = true;
        }
    }    
    return vm;
}

2. vm.$emit

实现原理:

vm.$emit作用是触发事件,从上面我们可以知道,所有的事件监听器回调都会存储在vm._events中,所以实现思路是使用事件名从vm._events中取出对应的事件监听器回调函数列表,然后依次执行并将参数传递给监听器回调。

下面我们来看源码是如何实现该方法的:

// 将类数组对象转换为数组
function toArray (list, start) {
    start = start || 0;
    var i = list.length - start;
    var ret = new Array(i);
    while (i--) {
        ret[i] = list[i + start];
    }
    return ret
}

Vue.prototype.$emit = function (event) {
    var vm = this;
    {
        var lowerCaseEvent = event.toLowerCase();
        // 意思是说,HTML 属性不区分大小写,所以你不能使用 v-on 监听小驼峰形式的事件名(eventName),而应该使用连字符形式的事件名(event-name)
        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            tip(
                "Event \"" + lowerCaseEvent + "\" is emitted in component " +
                (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
                "Note that HTML attributes are case-insensitive and you cannot use " +
                "v-on to listen to camelCase events when using in-DOM templates. " +
                "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
            );
        }
    }
    // 从 vm._event 对象上拿到当前事件的回调函数数组,并一次调用数组中的回调函数,并且传递提供的参数
    var cbs = vm._events[event];
    if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        var args = toArray(arguments, 1);
        var info = "event handler for \"" + event + "\"";
        for (var i = 0, l = cbs.length; i < l; i++) {
            invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
    }
    return vm
};

3. vm.$off

Vue.prototype.$off = function (event, fn) {
    var vm = this;
    // 如果没有提供参数,则移除所有的事件监听器
    if (!arguments.length) {
        // 将vm._events重置为初始状态就等同于移除所有事件
        vm._events = Object.create(null);
        return vm
    }
    // 第一个参数为数组,则只需要将数组遍历一遍,然后数组中每一项依次调用vm.$off
    if (Array.isArray(event)) {
        for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
            vm.$off(event[i$1], fn);
        }
        return vm
    }
    var cbs = vm._events[event];
    // 判断这个事件有没有被监听
    if (!cbs) {
        // 没有就直接退出
        return vm
    }
    // 只提供了事件名,则移除该事件所有的监听器,只需要将vm._events中event对应的事件列表重置为空就行 
    if (!fn) {
        vm._events[event] = null;
        return vm
    }
    // 如果同时提供了事件与回调,则只移除这个回调的监听器
    var cb;
    var i = cbs.length;
    while (i--) {
        // 注:该遍历是从后往前循环,这样在列表中移除当前位置的监听器,不会影响列表中未遍历到
        // 的监听器位置;如果是从前往后遍历,那么当从列表中移除一个监听器时,后面的监听器会自动
        // 向前移动一个位置,会导致下一轮循环时跳过一个元素
        cb = cbs[i];
        // 从列表中找到与参数中提供的回调函数相同的函数,并将它从列表中移除
        // 检查监听器和监听器的fn属性是否与参数中提供的回调函数相同,只要有一个相同,说明需要被移除
        if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1);
            break
        }
    }
    return vm
}

4. vm.$once

Vue.prototype.$once = function (event, fn) {
    var vm = this;
    function on () {
        // vm.$off将自定义事件移除
        vm.$off(event, on);
        // 执行函数,并将参数arguments传递给函数fn
        fn.apply(vm, arguments);
    }
    // 将fn挂载在on函数的fn属性中
    on.fn = fn;
    vm.$on(event, on);
    return vm
}

实例方法/生命周期

1. vm.$mount

注:在不同的构建版本中, vm.$mount的表现都不一样

完整版vm.$mount的实现原理:

(1)基本结构:

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
    // 做些什么
    return mount.call(this, el);
}
  1. 将Vue原型上的$mount方法保存在mount中,以便后续调用;
  2. 然后Vue 原型上的$mount方法被一个新的方法覆盖了,新方法中会调用之前原始的方法,这种做法通常被称为函数劫持;
  3. 通过函数劫持,可以在原始功能上新增一些其他功能,上面代码中,vm.$mount的原始方法就是mount的核心功能,而在完整版中需要将编译功能新增到核心功能上去

(2) 获取DOM元素:

由于el参数支持元素类型或者字符串类型的选择器,所以第一步是通过el获取DOM元素:

使用query方法获取DOM元素 

function query (el) {
    if (typeof el === 'string') {
        // 如何el是字符串,则使用document.querySelector来获取DOM元素
        var selected = document.querySelector(el);
        if (!selected) {
            warn('Cannot find element: ' + el);
            // 没有获取到就创建一个空的div元素
            return document.createElement('div')
        }
        return selected
    } else {
        // 不是字符串,则认为它是元素类型,直接返回el
        return el
    }
}

 接下来就是判断Vue实例中是否存在渲染函数,只有不存在时,才会将模板编译成渲染函数:

var options = this.$options;
// 解析template/el并转换为render函数
if (!options.render) {
    var template = options.template;
    if (template) {
        // 是否是字符串
        if (typeof template === 'string') {
            // 以#开头则它将被用作选择符
            if (template.charAt(0) === '#') {
                // 获取DOM元素的innerHTML作为模板
                template = idToTemplate(template);
            }
        }
        // 判断是否是一个DOM元素
        else if (template.nodeType) {
            template = template.innerHTML;
        } else {
            // 提示用户template选项无效
            {
                warn('invalid template option:' + template, this);
            }
            return this
        }
    }
    // 用户没有设置tempalte选项, 使用getOuterHTML方法从用户提供的el选项中获取模板 
    else if (el) {
        template = getOuterHTML(el);
    }
}

// 获取DOM元素的innerHTML字符串
var idToTemplate = cached(function (id) {
    var el = query(id);
    return el && el.innerHTML
});

// 获取DOM元素的HTML字符串
function getOuterHTML(el) {
    if (el.outerHTML) {
        return el.outerHTML
    } else {
        var container = document.createElement('div');
        container.appendChild(el.cloneNode(true));
        return container.innerHTML
    }
}

获取模板之后,下一步是将模板编译成渲染函数,通过执行compileToFunctions函数可以将模板编译成渲染函数并设置到this.options上

if (template) {
    /* istanbul ignore if */
    if (config.performance && mark) {
        mark('compile');
    }

    var ref = compileToFunctions(template, {
        outputSourceRange: "development" !== 'production',
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
    }, this);
    var render = ref.render;
    var staticRenderFns = ref.staticRenderFns;
    // 当通过compileToFunctions函数得到渲染函数之后,将渲染函数设置到this.$options上
    options.render = render;
    options.staticRenderFns = staticRenderFns;

    /* istanbul ignore if */
    if (config.performance && mark) {
        mark('compile end');
        measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
    }
}

将模板编译成代码字符串并将代码字符串转换成渲染函数的过程是在compileToFunctions函数中完成的, 代码如下:

function compileToFunctions(template, options, vm) {
    options = extend({}, options);
    var warn$$1 = options.warn || warn;
    delete options.warn;

    /* istanbul ignore if */
    {
        // 检测可能的 CSP 限制
        try {
            new Function('return 1');
        } catch (e) {
            if (e.toString().match(/unsafe-eval|CSP/)) {
                // 看起来你在一个 CSP 不安全的环境中使用完整版的 Vue.js,模版编译器不能工作在这样的环境中。
                // 考虑放宽策略限制或者预编译你的 template 为 render 函数
                warn$$1(
                    'It seems you are using the standalone build of Vue.js in an ' +
                    'environment with Content Security Policy that prohibits unsafe-eval. ' +
                    'The template compiler cannot work in this environment. Consider ' +
                    'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
                    'templates into render functions.'
                );
            }
        }
    }


    var key = options.delimiters
        ? String(options.delimiters) + template
        : template;
    // 检查缓存中是否已经存在编译后的模板
    if (cache[key]) {
        return cache[key]
    }

    // 调用compile函数来编译模板,将模板编译成代码字符串并存储在compiled中的render属性中
    var compiled = compile(template, options);

    // 检查编译期间产生的 error 和 tip,分别输出到控制台
    {
        if (compiled.errors && compiled.errors.length) {
            if (options.outputSourceRange) {
                compiled.errors.forEach(function (e) {
                    warn$$1(
                        "Error compiling template:\n\n" + (e.msg) + "\n\n" +
                        generateCodeFrame(template, e.start, e.end),
                        vm
                    );
                });
            } else {
                warn$$1(
                    "Error compiling template:\n\n" + template + "\n\n" +
                    compiled.errors.map(function (e) { return ("- " + e); }).join('\n') + '\n',
                    vm
                );
            }
        }
        if (compiled.tips && compiled.tips.length) {
            if (options.outputSourceRange) {
                compiled.tips.forEach(function (e) { return tip(e.msg, vm); });
            } else {
                compiled.tips.forEach(function (msg) { return tip(msg, vm); });
            }
        }
    }

    // turn code into functions
    var res = {};
    var fnGenErrors = [];
    // 调用createFunction函数将代码字符串转换成函数。其实现原理为new Function(code)
    res.render = createFunction(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
        return createFunction(code, fnGenErrors)
    });

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    {
        if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
            warn$$1(
                "Failed to generate render function:\n\n" +
                fnGenErrors.map(function (ref) {
                    var err = ref.err;
                    var code = ref.code;

                    return ((err.toString()) + " in\n\n" + code + "\n");
                }).join('\n'),
                vm
            );
        }
    }
    // 缓存结果并返回
    return (cache[key] = res)
}

 最后通过mountComponent函数将Vue.js实例挂载到DOM元素上

Vue.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

对比下图更加清晰: 

2. vm.$forceUpdate

 如何实现的? 

只需要执行watcher的update方法,就可以让实例重新渲染。

Vue.js的每一个实例都有一个watcher。当状态发生改变时,会通知到组件级别,然后组件内部使用虚拟DOM进行更详细的重新渲染操作。事实上,组件就是Vue.js实例,所以组件级别的watcher和Vue.js 实例上的watcher说的是同一个watcher.

实现代码如下:

Vue.prototype.$forceUpdate = function () {    
    var vm = this;
    if (vm._watcher) {
        vm._watcher.update();
    }
};

vm._watcher就是Vue.js实例的watcher,每当组件依赖的数据发送变化时,都会自动触发Vue,js实例中_watcher的update方法。

重新渲染的实现原理不难,Vue.js得自动渲染是通过变换侦测来侦测数据,即当数据发生变化时,Vue,js实例重新渲染。而vm.$$forceUpdate是手动通知Vue.js实例重新渲染。

3. vm.$nextTick

 异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

 源码如下:

Vue.nextTick = nextTick;
Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

var isUsingMicroTask = false;
 
// 回调列表
var callbacks = [];
// 标记是否已经向队列中添加一个任务
var pending = false;
  
// 执行所有回调并清空列表
function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}
 
var timerFunc;

 // 判断浏览器是否支持Promise, 首选 Promise.resolve().then()
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
        // 在微任务队列中放入flushCallbacks函数
        p.then(flushCallbacks);
        // 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入一种奇怪的状态,即回调被推到微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理计时器。所以我们可以通过添加空计时器“强制”刷新微任务队列
        if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
    // 在Promise不可用下选择MutationObserver
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function () {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
    isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // 再就是 setImmediate,它其实已经是一个宏任务了,但仍然比 setTimeout 要好
    timerFunc = function () {
        setImmediate(flushCallbacks);
    }
} else {
    // 最后没办法,则使用 setTimeout
    timerFunc = function () {
        setTimeout(flushCallbacks, 0);
    }
}
 
function nextTick (cb, ctx) {
    var _resolve;
    // 用 callbacks 数组存储经过包装的 cb 函数
    callbacks.push(function () {
        if (cb) {
            // 用 try catch 包装回调函数,便于错误捕获
            try {
                cb.call(ctx);
            } catch (e) {
                handleError(e, ctx, 'nextTick');
            }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });
    if (!pending) {
        pending = true;
        // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
        timerFunc();
    }
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function (resolve) {
            _resolve = resolve;
        })
    }
}

 4. vm.$destroy

实现原理:

第一部分:防止多次执行

Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
        return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
}

解释:

1. 为了防止vm.$destroy被反复执行,先对属性_isBeingDestroyed进行判断,为true则说明Vue.js实例正在被销毁,直接使用return 语句 退出函数执行逻辑;

2. 属性_isBeingDestroyed为false,则调用callhook函数触发beforeDestroy钩子函数;

第二部分:清理当前组件与父组件之间的连接

需要将当前组件实例从父组件实例的$children属性中删除即可。

说明: Vue实例的$children属性存储了所有子组件

var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm);
}

解释:

1. 判断当前实例有无父级,同时父级没有被销毁且不是抽象组件,那么将自己从父级的子列表中删除,也就是将自己的实例从父级的$children属性中删除;

2. 事实上,子组件在不同父组件中是不同的Vue实例,所以一个子组件实例的父级只有一个,销毁操作也只需要从父级的子组件列表中销毁当前这个Vue实例

完整代码:

Vue.prototype.$destroy = function () {
  var vm = this;
  // 标识是否在销毁
  if (vm._isBeingDestroyed) {
    return
  }
  // 触发钩子函数beforeDestroy
  callHook(vm, 'beforeDestroy');
  vm._isBeingDestroyed = true;
  // 删除自己与父级之间的连接
  var parent = vm.$parent;
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm);
  }
  // 从watcher监听的所有状态的依赖列表中移除watcher
  if (vm._watcher) {
    vm._watcher.teardown();
  }
  var i = vm._watchers.length;
  // 将从vm.$watcher创建的watcher实例从它所监听的状态的依赖列表中移除
  while (i--) {
    vm._watchers[i].teardown();
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--;
  }
  // 表示实例已经被销毁
  vm._isDestroyed = true;
  // 将模板中的所有指令解绑
  vm.__patch__(vm._vnode, null);
  // 触发destroyed钩子函数
  callHook(vm, 'destroyed');
  // 移除实例上的所有事件监听器
  vm.$off();
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null;
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null;
  }
};

参考资料:

https://cn.vuejs.org/v2/api/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值