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) #93

Open
youngwind opened this issue Oct 4, 2016 · 0 comments
Open
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Oct 4, 2016

前言

在上一篇 #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>实例对应地发生改变。

思路

我们把上面的大目标分解成下面两个小目标。

  1. 无论是动态prop还是静态prop,都是prop,都要像处理静态prop那样,把它们解析出来,然后塞到$data当中去。这一点好办,因为我们在上一篇 vue源码学习系列之九:组件化原理探索(静态props) #92 中已经实现了静态prop的解析和渲染。
  2. 对于动态prop,需要特殊处理。要做到当父实例的数据发生改变的时候,子组件也跟着改变。

要实现第二点,又有两种思路:

  1. 当父实例的数据发生改变时,将改变传导到子组件(就像处理条件渲染 vue早期源码学习系列之七:如何实现"v-if"条件渲染 #90 那样)。子组件接收到变化信号之后,跑到父实例去拿新的数据,然后更新自己本身的数据,然后触发notify,然后更新DOM。
  2. 当父实例的数据发生改变时,父实例直接修改子组件对应的数据,然后触发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版本,实现的完整代码在这里,实现的效果如下图所示。
demo

下图说明动态prop真的被当成了指令来处理。
prop-directive

后话

在实现组件动态props的过程中,我遇到了一个隐藏得很深的问题:**在目前bathcer实现异步批处理的前提之下,如果在执行某些异步任务的过程中,产生了新的异步任务,该如何处理?**Debug了好一阵了才发现这个微博图,后来我自己想了一个办法临时处理了一下,不过还没完全想明白这样做到底好不好,所以就不在本文展开说了,之后有时间要好好想想。

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