Open youngwind opened 7 years ago
在上一篇 #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>实例对应地发生改变。
<my-component>
我们把上面的大目标分解成下面两个小目标。
要实现第二点,又有两种思路:
显然,第二种方式更为简洁,父子实例之间只需要进行一次通信。 但是第二种方式有一个关键点还没想通:程序如何知道,当父实例的哪个数据发生改变时,要修改子组件对应的数据呢?也就是说,如何将父实例的数据与子组件的动态prop一一映射起来? 这个问题似曾相识,因为我们曾经解决过:
只更新数据变动相关的DOM,必须有个这样的对象,将DOM节点和对应的数据一一映射起来,这里引入Directive(指令)的概念
没错,我们采取的的思路跟如何实现动态数据绑定#87 一模一样,所以可以直接复用Directive、Watcher这一套东西。
如何实现动态数据绑定
ok,思路理清之后,开始敲代码。先从解析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"
/** * 获取动态数据绑定属性值, * 比如 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; };
上面我们已经成功将所有的prop(包括静态prop和动态prop)都从<my-component b-bind:name="user.name" message="hello"></my-component>上面解析出来了,解析的结果是一个props数组。接下来我们来看看如何应用这个props数组。 再次明确一下思路,无论是静态还是动态prop,都需要直接将属性塞到组件的$data当中去。如果是动态属性,还需要走Directive、Watcher那一套。
<my-component b-bind:name="user.name" message="hello"></my-component>
/** * 应用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指令的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版本,实现的完整代码在这里,实现的效果如下图所示。
下图说明动态prop真的被当成了指令来处理。
在实现组件动态props的过程中,我遇到了一个隐藏得很深的问题:在目前bathcer实现异步批处理的前提之下,如果在执行某些异步任务的过程中,产生了新的异步任务,该如何处理?Debug了好一阵了才发现这个微博图,后来我自己想了一个办法临时处理了一下,不过还没完全想明白这样做到底好不好,所以就不在本文展开说了,之后有时间要好好想想。
前言
在上一篇 #92 中,我们已经实现了通过静态props传递数据,今天我们来看看,如何实现动态props传递数据。
问题具象
考虑下面的情况
注意:组件
<my-component>
有两个prop,其中name是动态prop,message是静态prop。 我们的目标是:在正确渲染组件的前提下,当#app的user.name1或者user.name2发生改变的时候,<my-component>
实例对应地发生改变。思路
我们把上面的大目标分解成下面两个小目标。
要实现第二点,又有两种思路:
显然,第二种方式更为简洁,父子实例之间只需要进行一次通信。 但是第二种方式有一个关键点还没想通:程序如何知道,当父实例的哪个数据发生改变时,要修改子组件对应的数据呢?也就是说,如何将父实例的数据与子组件的动态prop一一映射起来? 这个问题似曾相识,因为我们曾经解决过:
没错,我们采取的的思路跟
如何实现动态数据绑定
#87 一模一样,所以可以直接复用Directive、Watcher这一套东西。ok,思路理清之后,开始敲代码。先从解析props(包括动态和静态的)开始。
解析props
我们从改造之前写好的_initProps方法入手。
其中的getBindAttr函数是为了获取动态prop的值,无论是
b-bind:name="user.name"
还是:name="user.name"
都会被当做动态prop,这跟vue的缩写处理是一样的。应用props
上面我们已经成功将所有的prop(包括静态prop和动态prop)都从
<my-component b-bind:name="user.name" message="hello"></my-component>
上面解析出来了,解析的结果是一个props数组。接下来我们来看看如何应用这个props数组。 再次明确一下思路,无论是静态还是动态prop,都需要直接将属性塞到组件的$data当中去。如果是动态属性,还需要走Directive、Watcher那一套。请注意,这里的compileGetter是之前已经实现的,目的是根据给出的path路径,从数据对象中解析出对应的数据。在实现计算属性的文章有提到过。 #89
prop指令
既然prop要走指令那一套,那么就得实现prop指令的bind和update方法。
实现效果
至此,我们已经基本实现了组件动态props传递数据,参考的依然是vue的1.0.26版本,实现的完整代码在这里,实现的效果如下图所示。![demo](https://raw.githubusercontent.com/youngwind/blog/master/image/93/demo.gif)
下图说明动态prop真的被当成了指令来处理。![prop-directive](https://raw.githubusercontent.com/youngwind/blog/master/image/93/prop-directive.png)
后话
在实现组件动态props的过程中,我遇到了一个隐藏得很深的问题:在目前bathcer实现异步批处理的前提之下,如果在执行某些异步任务的过程中,产生了新的异步任务,该如何处理?Debug了好一阵了才发现这个微博图,后来我自己想了一个办法临时处理了一下,不过还没完全想明白这样做到底好不好,所以就不在本文展开说了,之后有时间要好好想想。