顾名思义

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

之前之所以要从早期源码开始看起,实在是因为面对庞大而成熟的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实例化一个组件可以分成三步。

使用extend定义(构造)组件MyComponent 使用component注册组件 在初始化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)
渲染组件

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

识别组件 组件指令化 渲染、挂载组件 识别组件

在初始化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方法中完成组件的渲染与挂载。

渲染、挂载组件

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

如何处理组件的模板?也就是template参数: <p>{{message}}</p> 如何处理组件的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.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组件化了,完整的代码在这里,效果如下图所示。


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

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

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

====End====

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

分页:12
转载请注明
本文标题:vue 源码学习系列之九:组件化原理探索(静态 props)
本站链接:http://www.codesec.net/view/483131.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(36)