Vue 源码学习 (二)

92 阅读2分钟

new Vue 发生了什么

  • 从上一篇文章可知,vue其实就是一个function实现的类. 在new实例化时它调用了一个方法
function Vue (options) {
  this._init(options)
}
  • this._init() 方法在 src/core/instance/init.js 中定义
    Vue.prototype._init = function(options?: Object) {
        const vm: Component = this;
        // a uid
        vm._uid = uid++;

        let startTag, endTag;

        // a flag to avoid this being observed
        vm._isVue = true;
        // merge options
        if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options);
        } else {
            vm.$options = mergeOptions(
                resolveConstructorOptions(vm.constructor),
                options || {},
                vm
            );
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== "production") {
            initProxy(vm);
        } else {
            vm._renderProxy = vm;
        }
        // expose real self
        vm._self = vm;
        initLifecycle(vm); // 初始化$parent, $root, $children, $refs
        initEvents(vm); // 处理父组件传递的监听器
        initRender(vm); // $slots, $scopedSlots,_c(), $createElement()
        callHook(vm, "beforeCreate");
        initInjections(vm); // 获取注入数据
        initState(vm); // 初始化组件中props、methods、data、computed、watch
        initProvide(vm); // 提供数据
        callHook(vm, "created");
        if (vm.$options.el) {
            vm.$mount(vm.$options.el);
        }
    };
}
  • 上面代码把不同的功能逻辑拆成一些单独的函数执行。让人看起来一目了然
  • 并且在初始化的最后,判断是否有传入el。如果有,则调用$mount进行挂载。

Vue 实例挂载的实现

Vue中我们是通过$mount进行挂载的。$mount在多个文件都有定义。以下是以带编译版本的$mount的分析。

  • src/platform/web/runtime/index.js
// public mount method
Vue.prototype.$mount = function(
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating);
};

这里是 $mount 方法的定义。传入2个参数,第一个是 el ,表示挂载的元素,可以是字符串和 DOM 元素。如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。
之所以这么设计,是为了可以复用。因为它是可以被 runtime only 版本的 Vue 直接使用的。

这里实际调用的是 mountComponent 方法

  • 方法定义在 src/core/instance/lifecycle.js
export function mountComponent(
    vm: Component,
    el: ?Element,
    hydrating?: boolean
): Component {
    vm.$el = el;
    if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode;
    }
    callHook(vm, "beforeMount");

    let updateComponent;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
        updateComponent = () => {
        ...省略
    } else {
        // 定义组件更新函数
        // _render()执行可以获得虚拟dom,VNode
        // _update()将虚拟dom转换为真实dom
        updateComponent = () => {
            vm._update(vm._render(), hydrating);
        };
    }

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(
        vm,
        updateComponent,
        noop,
        {
            before() {
                if (vm._isMounted && !vm._isDestroyed) {
                    callHook(vm, "beforeUpdate");
                }
            }
        },
        true /* isRenderWatcher */
    );
    hydrating = false;

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, "mounted");
    }
    return vm;
}

这里现时定义了 updateComponent 方法。其中有两个关键参数
_render: 执行可以获得虚拟 VNode;
_update 将虚拟 dom 转换为真实 dom
最后 new 了一个 Watcher, 并在其回调中传入 updateComponent 方法。 Watcher 类在这里起到两个作用, 一是在初始化时执行回调函数, 二是监听 vm 实例中数据发生变化时执行回调函数

  • src/platform/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount;
// 主要是扩展了$mount方法
Vue.prototype.$mount = function(
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && query(el);
    // 处理el和template
    const options = this.$options;
    // resolve template/el and convert to render function
    // render不存在时才考虑el和template
    if (!options.render) {
        let template = options.template;
        if (template) {
            if (typeof template === "string") {
                // template是选择器
                if (template.charAt(0) === "#") {
                    template = idToTemplate(template);
                }
            } else if (template.nodeType) {
                // template是dom元素
                // template: document.querySelector('#app'),
                template = template.innerHTML;
            } else {
                return this;
            }
        } else if (el) {
            // el作为template
            template = getOuterHTML(el);
        }
        // 编译过程
        if (template) {
            // 将template字符串转换为render函数
            const { render, staticRenderFns } = compileToFunctions(
                template,
                {
                    outputSourceRange: process.env.NODE_ENV !== "production",
                    shouldDecodeNewlines,
                    shouldDecodeNewlinesForHref,
                    delimiters: options.delimiters,
                    comments: options.comments
                },
                this
            );
            options.render = render;
            options.staticRenderFns = staticRenderFns;
        }
    }
    return mount.call(this, el, hydrating);
};

由上可得知,上面代码主要是先缓存$mount, 然后重新定义(拓展) $mount 方法,也就是最初我们说所的可复用。
首先,只有当 render 方法不存在时, 先是判断 $option.template. 假如是选择器, 则获取其 innerHTML, 如果是 dom 元素也是获取其 innerHTML。 否则返回它本身(本身就是字符串)。假如是 el。 获取的则是它outerHTML。最终。不管是 el 还是 template 字符串,都会转换成render方法。这是 Vue 的在线编译过程。它是调用 compileToFunctions 方法实现的。
最后调用最初缓存的 mount 进行挂载