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

顾名思义 细心的读者可能已经发现,本篇的标题跟以往相比,去掉了 早期 两个字,这其实代表着学习方法的转换。 之前之所以要从

顾名思义

细心的读者可能已经发现,本篇的标题跟以往相比,去掉了 早期 两个字,这其实代表着学习方法的转换。

之前之所以要从早期源码开始看起,实在是因为面对庞大而成熟的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'});

今天我们只考虑最简单的情况: 如何将<my-component>组件正确地解析,渲染,挂载到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。这显然不是我们想要的。

为了更好地理解组件的构造结果,可以看下图。

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

注:代码经过babel处理,所以看起来有点凌乱。

注册组件

上面讲完了构造组件,现在我们来看看注册组件。

注册组件其实就是声明组件与自定义标签的对应关系,比如声明MyComponent组件对应于<my-component>标签,这样程序解析到<my-component>才知道:“哦,原来它就是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; };

注册结果如下图所示。

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

渲染组件

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

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

识别组件

在初始化app这个Vue实例的过程中,当DOM遍历解析到<my-component message="hello liangshaofeng">的时候,由于我们在上面已经进行了组件注册,所以我们知道那是一个组件,需要特殊处理。

相关代码如下:

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

组件指令化

在我们识别出<my-component>标签是一个组件之后,该如何对待这个组件呢?

文章开头就讲到过,组件与子实例是类似的,我们当初处理“v-if”条件渲染的时候,就是检查到“v-if”是一个特殊的指令,然后就将“v-if”里面的DOM结构当成Vue实例来处理。

这里,我们可以采用类似的方法, 引入 组件指令 的概念,把<my-component>当做一个组件指令。

相关代码如下。

/** * 判断节点是否是组件指令,如 <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; }};

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

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

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

渲染、挂载组件

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

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

模板处理

组件options当中的template是一个字符串,代表着一个DOM结构。如何将这个字符串“<p>{{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.jsmodule.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组件化了,完整的代码在这里,效果如下图所示。

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

遗留问题

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

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

====End====

未登录用户
全部评论0
到底啦