Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue源码学习系列之九:组件化原理探索(静态props) #92

Open
youngwind opened this issue Sep 29, 2016 · 0 comments
Open
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Sep 29, 2016

顾名思义

细心的读者可能已经发现,本篇的标题跟以往相比,去掉了早期两个字,这其实代表着学习方法的转换。
之前之所以要从早期源码开始看起,实在是因为面对庞大而成熟的vue源码无从下手。经过这一段时间以来的学习与探索,我已经渐渐地搞清楚了vue大部分基础功能的实现原理。当我在思考组件化原理的时候忽然发现一个问题:
我花了1个多月的时间,才前进了200多个commit,而目前vue的总commit数几近2000。如果我继续采取这种逐commit、小步伐前进的方法,那么我将花费至少1年的时间才能学习完vue的源码,这样效率实在太低。而且,我们都知道,在编写代码的过程中,为了实现同一个目标,前后我们可能重构过很多次,细究每一次的重构将会降低学习的效率。
所以此时,最佳的学习方法应该是直接跳到成熟版本的vue,直接从那里开始学习。比如,我就是从1.0.26版本开始探索组件化实现的原理。
这就是题目变更的由来。

目标

考虑以下的例子

<div id="app">
    <my-component message="hello liangshaofeng!"></my-component>
    <my-component message="hello Vue!"></my-component>
</div>
import Bue from 'Bue';

var MyComponent = Bue.extend({
    template: '<p>{{message}}</p>'
});

Bue.component('my-component', MyComponent);

const app = new Bue({
    el: '#app'
});

今天我们只考虑最简单的情况:如何将组件正确地解析,渲染,挂载到DOM当中。

思路

仔细观察上面的js代码,我们发现vue实例化一个组件可以分成三步。

  1. 使用extend定义(构造)组件MyComponent
  2. 使用component注册组件
  3. 在初始化app实例的过程中,渲染组件

我们一步步来分析。

定义组件

组件与之前说过的子实例 #90 有一个共同的地方:都应该把它当做是一个vue实例来对待。
但是,组件与子实例的不同之处在于:组件只拥有自己的数据,不能访问父实例的数据,所以对待组件又不能完全等价于子实例。
综上:自然而然我们就能想到这样一个方法:
搞一个组件构造函数VueComponent,继承于Vue,这样VueComponent就能调用到Vue的诸多方法,比如_init等等。

另一个问题,组件自己有options(构造的时候传进来的),Vue本身也有options(主要是一些directive的声明),如何处理两者的关系? → 将组件的options与Vue本身的options合并,重新覆盖组件的options,并且注入到VueComponent的自定义属性当中。
为什么要这么做?VueComponent和Vue都有自己的options,如果不合并过来的话,根据js原型链的查找方式,VueComponent的options会遮住Vue的options,导致组件没法访问到Vue的options。
(为什么组件要访问Vue的options?因为对组件DOM结构进行解析的时候也需要解析里面包含的各种指令,这需要用到Vue的options当中声明的指令)
代码如下:

/**
     * 组件构造器
     * 返回组件构造函数
     * @param extendOptions {Object} 组件参数
     * @returns {BueComponent}
     */
    Bue.extend = function (extendOptions) {
        let Super = this;
        extendOptions = extendOptions || {};
        let Sub = createClass();
        Sub.prototype = Object.create(Super.prototype);
        Sub.prototype.constructor = Sub;
        // 此处的mergeOptions就是简单的Object.assign
        Sub.options = _.mergeOptions(Super.options, extendOptions);
        return Sub;
    };

    /**
     * 构造组件构造函数本身
     * @returns {Function}
     */
    function createClass() {
        return new Function('return function BueComponent(options){ this._init(options)}')(); 
    }

这里有个值得注意的地方:
为什么需要createClass函数new Function?为什么不能直接只定义一个BueComponent构造函数,然后每次构造组件的时候都用它呢?就像只有一个Vue构造函数一样。
答案:因为我们将options放在了BueComponent的自定义属性当中,如果我们只用一个BueComponent的话,后面声明的组件的options将会覆盖前面声明组件的options。这显然不是我们想要的。

为了更好地理解组件的构造结果,可以看下图。
组件构造
注:代码经过babel处理,所以看起来有点凌乱。

注册组件

上面讲完了构造组件,现在我们来看看注册组件。
注册组件其实就是声明组件与自定义标签的对应关系,比如声明MyComponent组件对应于标签,这样程序解析到才知道:“哦,原来它就是MyComponent组件。”
为什么要有注册组件这一步呢?
如果之前一直用React的人应该跟我有同样的疑问。因为在React中构造完组件之后,就可以直接在jsx中使用了,并没有注册这一个步骤。如下所示。

var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});
// 你看,React不需要将HellMessage注册成<hello-message>
ReactDOM.render(<HelloMessage name="John" />, mountNode);

个人热为可能是基于以下的考虑:
与React相比,Vue的侵入性要小得多。Vue需要直接应用在普通的DOM结构上,然而,在这些普通的DOM结构当中,可能之前就已经存在自定义标签了,Vue提供的注册功能正好可以解决这个命名冲突的问题。
也就是说,假如没有注册功能,直接把组件MyComponent对应成标签,要是万一之前的DOM结构里面已经有这样一个自定义的标签,也叫mycomponent,这不就懵逼了吗?

所以,注册功能只需要完成组件与标签名的映射就可以了。相关的代码如下所示:

/**
     * 注册组件
     * vue的组件使用方式与React不同。React构建出来的组件名可以直接在jsx中使用
     * 但是vue不是。vue的组件在构建之后还需要注册与之相对应的DOM标签
     * @param id {String}, 比如 'my-component'
     * @param definition {BueComponent} 比如 MyComponent
     * @returns {*}
     */
    Bue.component = function (id, definition) {
        this.options.components[id] = definition;
        return definition;
    };

注册结果如下图所示。
组件注册

渲染组件

这一步比较复杂,让我们将它细分为以下三个步骤。

  1. 识别组件
  2. 组件指令化
  3. 渲染、挂载组件

识别组件

在初始化app这个Vue实例的过程中,当DOM遍历解析到的时候,由于我们在上面已经进行了组件注册,所以我们知道那是一个组件,需要特殊处理。
相关代码如下:

/**
 * 渲染节点
 * @param node {Element}
 * @private
 */
exports._compileElement = function (node) {
    // 判断节点是否是组件
    // 这个函数具体做什么,下面会讲到
    if (this._checkComponentDirs(node)) {
        return;
    }
    // ....
};

组件指令化

在我们识别出标签是一个组件之后,该如何对待这个组件呢?
文章开头就讲到过,组件与子实例是类似的,我们当初处理“v-if”条件渲染的时候,就是检查到“v-if”是一个特殊的指令,然后就将“v-if”里面的DOM结构当成Vue实例来处理。
这里,我们可以采用类似的方法,引入组件指令的概念,把当做一个组件指令。
相关代码如下。

/**
 * 判断节点是否是组件指令,如 <my-component></my-component>
 * 如果是,则构建组件指令
 * @param node {Element}
 * @returns {boolean}
 * @private
 */
exports._checkComponentDirs = function (node) {
    let tagName = node.tagName.toLowerCase();
    if (this.$options.components[tagName]) {
        let dirs = this._directives;
        dirs.push(
            new Directive('component', node, this, {
                expression: tagName
            })
        );
        return true;
    }
};

下面上图证明组件真的被当成了指令来处理。
component-directive

既然把组件当成是一个组件指令,那么,剩下的就是如何编写指令的bind方法了。我们将在bind方法中完成组件的渲染与挂载。

渲染、挂载组件

要想渲染组件,有两个关键点。

  1. 如何处理组件的模板?也就是template参数:<p>{{message}}</p>
  2. 如何处理组件的props?也就是message="hello, liangshaofeng!"message="hello, Vue!"

模板处理

组件options当中的template是一个字符串,代表着一个DOM结构。如何将这个字符串“

{{message}}

”转化成对应的DOM结构呢?在不考虑兼容性的情况下,我们直接采用DOMParser,代码如下:

// compiler/transclude.js
/**
 * 将template模板转化成DOM结构,
 * 举例: '<p>{{user.name}}</p>'  -> 对应的DOM结构
 * @param el {Element} 原有的DOM结构
 * @param options {Object}
 * @returns {DOM}
 */
module.exports = function (el, options) {
    let tpl = options.template;
    if (tpl) {
        var parser = new DOMParser();
        var doc = parser.parseFromString(tpl, 'text/html');
        // 此处生成的doc是一个包含html和body标签的HTMLDocument
        // 想要的DOM结构被包在body标签里面
        // 所以需要进去body标签找出来
        return doc.querySelector('body').firstChild;
    } else {
        return el;
    }
};

props处理

组件是有自己的数据属性的,这跟子实例不同。子实例不仅能访问自己的数据,还能访问父实例的数据。但是组件只能访问自己的数据,不能访问父实例/父组件的数据,组件想要访问的数据必须显式地通过props传递给它,像这样:<my-component message="hello liangshaofeng!"></my-component>。这是实现组件化的通用手法,React也是如此。
所以,我们需要把message解析出来,并且将message存储到组件实例的$data当中,这样组件里面的{{message}}才能解析成"hello liangshaofeng!"。
这一部分的代码如下所示:

/**
 * 初始化组件的props,将props解析并且填充到$data中去
 * @private
 */
exports._initProps = function () {
    let isComponent = this.$options.isComponent;
    if (!isComponent) return;
    let el = this.$options.el;
    let attrs = Array.from(el.attributes);
    attrs.forEach((attr) => {
        let attrName = attr.name;
        let attrValue = attr.value;
        this.$data[attrName] = attrValue;
    });
};

bind方法

在处理完模板解析和props解析之后,我们终于来到了最后一步,编写组件指令的bind方法,真正地初始化组件实例。代码如下。

// component.js
module.exports = {
    bind: function () {
        // 判断该组件是否已经被挂载
        if (!this.el.__vue__) {
            // 这里的anchor作为锚点,是之前常用的方法了
            this.anchor = document.createComment(`${config.prefix}component`);
            _.replace(this.el, this.anchor);
            this.setComponent(this.expression);
        }
    },

    update: function () {
        // update方法暂时不做任何事情
    },

    /**
     * @param value {String} 组件标签名, 如 "my-component"
     */
    setComponent: function (value) {
        if (value) {
            // 这里的Component就是那个带有options自定义属性的BueComponent构造函数啊!
            this.Component = this.vm.$options.components[value];
            this.ComponentName = value;
            this.mountComponent();
        }
    },

    /**
     * 构建、挂载组件实例
     */
    mountComponent: function () {
        let newComponent = this.build();
        // 就是在这里将组件生成的DOM结构插入到真实DOM中
        newComponent.$before(this.anchor);
    },

    /**
     * 构建组件实例
     * @returns {BueComponent}
     */
    build: function () {
        if (this.Component) {
            let options = {
                name: this.ComponentName,   // "my-component"
                el: this.el.cloneNode(), 
                parent: this.vm,
                isComponent: true
            };
            // 实例化组件
            let child = new this.Component(options);
            return child;
        }
    }
};

实现效果

至此,我们已经实现了最简单的vue组件化了,完整的代码在这里,效果如下图所示。
demo

遗留问题

本篇所实现的只是最为简单的组件化。还有许多问题没有考虑到,比如:

  1. 局部注册与全局注册的区别。
  2. 动态props的传递
  3. 父子组件之间的嵌套与通信
  4. ......

====End====

@youngwind youngwind added the Vue label Sep 29, 2016
@youngwind youngwind changed the title vue源码学习系列之九:组件化原理探索 vue源码学习系列之九:组件化原理探索(静态props) Oct 4, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant