vue源码学习系列之十:组件化原理探索(动态props)
youngwind opened this issue · 0 comments
前言
在上一篇 #92 中,我们已经实现了通过静态props传递数据,今天我们来看看,如何实现动态props传递数据。
问题具象
考虑下面的情况
<div id="app">
<my-component :name="user.name1" message="近况如何?"></my-component>
<my-component :name="user.name2" message="How are you?"></my-component>
</div>
import Bue from 'Bue'
var MyComponent = Bue.extend({
template: '<div>' +
'<p>Hello,{{name}}</p>' +
'<p>{{message}}</p>' +
'</div>',
props: {
// 对props的声明,本来应该写一些prop验证之类的,不过尚未实现这个功能
name: {},
message: {}
}
});
Bue.component('my-component', MyComponent);
const app = new Bue({
el: '#app',
data: {
user: {
name1: '梁少峰',
name2: 'youngwind'
}
}
});
注意:组件<my-component>
有两个prop,其中name是动态prop,message是静态prop。
我们的目标是:在正确渲染组件的前提下,当#app的user.name1或者user.name2发生改变的时候,<my-component>
实例对应地发生改变。
思路
我们把上面的大目标分解成下面两个小目标。
- 无论是动态prop还是静态prop,都是prop,都要像处理静态prop那样,把它们解析出来,然后塞到$data当中去。这一点好办,因为我们在上一篇 #92 中已经实现了静态prop的解析和渲染。
- 对于动态prop,需要特殊处理。要做到当父实例的数据发生改变的时候,子组件也跟着改变。
要实现第二点,又有两种思路:
- 当父实例的数据发生改变时,将改变传导到子组件(就像处理条件渲染 #90 那样)。子组件接收到变化信号之后,跑到父实例去拿新的数据,然后更新自己本身的数据,然后触发notify,然后更新DOM。
- 当父实例的数据发生改变时,父实例直接修改子组件对应的数据,然后触发notify,然后更新DOM。
显然,第二种方式更为简洁,父子实例之间只需要进行一次通信。
但是第二种方式有一个关键点还没想通:程序如何知道,当父实例的哪个数据发生改变时,要修改子组件对应的数据呢?也就是说,如何将父实例的数据与子组件的动态prop一一映射起来?
这个问题似曾相识,因为我们曾经解决过:
只更新数据变动相关的DOM,必须有个这样的对象,将DOM节点和对应的数据一一映射起来,这里引入Directive(指令)的概念
没错,我们采取的的思路跟如何实现动态数据绑定
#87 一模一样,所以可以直接复用Directive、Watcher这一套东西。
ok,思路理清之后,开始敲代码。先从解析props(包括动态和静态的)开始。
解析props
我们从改造之前写好的_initProps方法入手。
/**
* 初始化组件的props,将props解析并且填充到$data中去
* 在这个过程中,如果是动态属性, 那么会在父实例生成对应的directive和watcher,用于prop的动态更新
* @private
*/
exports._initProps = function () {
let {el, props, isComponent} = this.$options;
if (!isComponent || !props) return;
let compiledProps = this.compileProps(el, props); // 解析props
this.applyProps(compiledProps); // 应用props
};
/**
* 解析props参数, 包括动态属性和静态属性
* @param el {Element} 组件节点,比如: <my-component b-bind:name="user.name" message="hello"></my-component>
* @param propOptions {Object} Vue.extend的时候传进来的prop对象参数, 形如 {name:{}, message:{}}
* @returns {Array} 解析之后的props数组,
* 形如: [
* {
* "name":"name", // 动态prop
* "options":{}, // 原先Vue.extend传过来的属性对应的参数, 暂时未空, 之后会放一些参数校验之类的
* "raw":"user.name", // 属性对应的值
* "dynamic":true, // true代表是动态属性,也就是从父实例/组件那里获取值
* "parentPath":"user.name" // 属性值在父实例/组件中的路径
* },
* {
* "name":"message", // 静态prop
* "options":{},
* "raw":"hello"
* }
* ]
*/
exports.compileProps = function (el, propOptions) {
let names = Object.keys(propOptions);
let props = [];
names.forEach((name) => {
let options = propOptions[name] || {};
let prop = {
name,
options,
raw: null
};
let value;
if ((value = _.getBindAttr(el, name))) {
// 动态props
prop.raw = value;
prop.dynamic = true;
prop.parentPath = value;
} else if ((value = _.getAttr(el, name))) {
// 静态props
prop.raw = value;
}
props.push(prop);
});
return props;
};
其中的getBindAttr函数是为了获取动态prop的值,无论是b-bind:name="user.name"
还是:name="user.name"
都会被当做动态prop,这跟vue的缩写处理是一样的。
/**
* 获取动态数据绑定属性值,
* 比如 b-bind:name="user.name" 和 :name="user.name"
* @param node {Element}
* @param name {String} 属性名称 比如"name"
* @returns {string} 属性值
*/
exports.getBindAttr = function (node, name) {
return exports.getAttr(node, `:${name}`) || exports.getAttr(node, `${config.prefix}bind:${name}`);
};
/**
* 获取节点属性值,并且移除该属性
* @param node {Element}
* @param attr {String}
* @returns {string}
*/
exports.getAttr = function (node, attr) {
let val = node.getAttribute(attr);
if (val) {
node.removeAttribute(attr);
}
return val;
};
应用props
上面我们已经成功将所有的prop(包括静态prop和动态prop)都从<my-component b-bind:name="user.name" message="hello"></my-component>
上面解析出来了,解析的结果是一个props数组。接下来我们来看看如何应用这个props数组。
再次明确一下思路,无论是静态还是动态prop,都需要直接将属性塞到组件的$data当中去。如果是动态属性,还需要走Directive、Watcher那一套。
/**
* 应用props
* 如果是动态属性, 需要额外走Directive、Watcher那一套流程
* 因为只有这样,当父实例/组件的属性发生变化时,才能将变化传导到子组件
* @param props {Array} 解析之后的props数组
*/
exports.applyProps = function (props) {
props.forEach((prop) => {
if (prop.dynamic) {
// 动态prop
let dirs = this.$parent._directives;
dirs.push(
new Directive('prop', null, this, {
expression: prop.raw, // prop对应的父实例/组件的哪个数据, 如:user.name
arg: prop.name // prop在当前组件中的属性键值, 如:name
})
);
} else {
// 静态prop
this.initProp(prop.name, prop.raw, prop.dynamic);
}
});
};
/**
* 将prop设置到当前组件实例的$data中去, 这样一会儿initData的时候才能监听到这些数据
* 如果是动态属性, 还需要跑到父实例/组件那里去取值
* @param path {String} 组件prop键值,如"name"
* @param val {String} 组件prop值,如果是静态prop,那么直接是"How are you"这种。
如果是动态prop,那么是"user.name"这种,需要从父实例那里去获取实际值
* @param dynamic {Boolean} true代表是动态prop, false代表是静态prop
*/
exports.initProp = function (path, val, dynamic) {
if (!dynamic) {
// 静态prop
this.$data[path] = val;
} else {
// 动态prop
this.$data[path] = compileGetter(val)(this.$parent.$data);
}
};
请注意,这里的compileGetter是之前已经实现的,目的是**根据给出的path路径,从数据对象中解析出对应的数据。**在实现计算属性的文章有提到过。 #89
prop指令
既然prop要走指令那一套,那么就得实现prop指令的bind和update方法。
// directives/prop.js
module.exports = {
bind: function () {
// this.arg == "name"; this.expression == "user.name", true代表是动态prop
// 对于动态prop,在bind方法中完成**把prop塞到$data中的任务**
this.vm.initProp(this.arg, this.expression, true);
},
update: function (value) {
// 当父实例对应的数据放生改变时,就会执行这里的方法
// 将新的数据设置到组件的$data中, 从而会引发组件数据的更新
this.vm.$set(this.arg, value);
}
};
实现效果
至此,我们已经基本实现了组件动态props传递数据,参考的依然是vue的1.0.26版本,实现的完整代码在这里,实现的效果如下图所示。
后话
在实现组件动态props的过程中,我遇到了一个隐藏得很深的问题:**在目前bathcer实现异步批处理的前提之下,如果在执行某些异步任务的过程中,产生了新的异步任务,该如何处理?**Debug了好一阵了才发现这个微博图,后来我自己想了一个办法临时处理了一下,不过还没完全想明白这样做到底好不好,所以就不在本文展开说了,之后有时间要好好想想。