2018 年首个计划是学习 vue 源码,查阅了一番资料之后,决定从第一个 commit 开始看起,这将是一场持久战!本篇介绍 directive 的简单实现,主要学习其实现的思路及代码的设计(directive 和 filter 扩展起来非常方便,符合设计模式中的开闭原则)。
- <div id="app" sd-on-click="toggle | .button">
- <p sd-text="msg | capitalize"></p>
- <p sd-class-red="error" sd-text="content"></p>
- <button class="button">Toggle class</button>
- </div>
- var app = Seed.create({
- id: 'app',
- scope: {
- msg: 'hello',
- content: 'world',
- error: true,
- toggle: function() {
- app.scope.error = !app.scope.error;
- }
- }
- });
实现功能够简单吧 -- 将 scope 中的数据绑定到 app 中。
以
为例说明:
- sd - text = "msg | capitalize"
其中 | 后面为过滤器,可以添加多个。sd-class-red 中的 red 为参数。
main.js 入口文件
- // Seed构造函数
- const Seed = function(opts) {
- };
- // 对外暴露的API
- module.exports = {
- create: function(opts) {
- return new Seed(opts);
- }
- };
directives.js
- module.exports = {
- text: function(el, value) {
- el.textContent = value || '';
- }
- };
filters.js
- module.exports = {
- capitalize: function(value) {
- value = value.toString();
- return value.charAt(0).toUpperCase() + value.slice(1);
- }
- };
就这三个文件,其中 directives 和 filters 都是配置文件,很易于扩展。
实现的大致思路如下:
属性 | 说明 | 类型 |
---|---|---|
attr | 原始属性,如 sd-text | String |
key | 对应 scope 对象中的属性名称 | String |
filters | 过滤器名称列表 | Array |
definition | 该指令的定义,如 text 对应的函数 | Function |
argument | 从 attr 中解析出来的参数(只支持一个参数) | String |
update | 更新 directive 时调用 | Function |
bind | 如果 directive 中定义了 bind 方法,则在 bindDirective 中会调用 | Function |
el | 存储当前 element 元素 | Element |
来定义 scope 中的每个属性,在其 setter 中触发指令的 update 方法。
- Object.defineProperty
- const prefix = 'sd';
- const Directives = require('./directives');
- const Filters = require('./filters');
- // 结果为[sd-text], [sd-class], [sd-on]的字符串
- const selector = Object.keys(Directives).map((name) => `[${prefix}-${name}]`).join(',');
- const Seed = function(opts) {
- const self = this,
- root = this.el = document.getElementById(opts.id),
- // 筛选出el下所能支持的directive的nodes列表
- els = this.el.querySelectorAll(selector),
- bindings = {};
- this.scope = {};
- // 解析节点
- [].forEach.call(els, processNode);
- // 解析根节点
- processNode(root);
- // 给scope赋值,触发setter方法,此时会调用与其相对应的directive的update方法
- Object.keys(bindings).forEach((key) => {
- this.scope[key] = opts.scope[key];
- });
- function processNode(el) {
- cloneAttributes(el.attributes).forEach((attr) => {
- const directive = parseDirective(attr);
- if (directive) {
- bindDirective(self, el, bindings, directive);
- }
- });
- }
- };
可以看到核心方法 processNode 主要做了两件事一个是 parseDirective,另一个是 bindDirective。
先来看看 parseDirective 方法:
- function parseDirective(attr) {
- if (attr.name.indexOf(prefix) == -1) return;
- // 解析属性名称获取directive
- const noprefix = attr.name.slice(prefix.length + 1),
- argIndex = noprefix.indexOf('-'),
- dirname = argIndex === -1 ? noprefix: noprefix.slice(0, argIndex),
- arg = argIndex === -1 ? null: noprefix.slice(argIndex + 1),
- def = Directives[dirname]
- // 解析属性值获取filters
- const exp = attr.value,
- pipeIndex = exp.indexOf('|'),
- key = (pipeIndex === -1 ? exp: exp.slice(0, pipeIndex)).trim(),
- filters = pipeIndex === -1 ? null: exp.slice(pipeIndex + 1).split('|').map((filterName) = >filterName.trim());
- return def ? {
- attr: attr,
- key: key,
- filters: filters,
- argument: arg,
- definition: Directives[dirname],
- update: typeof def === 'function' ? def: def.update
- }: null;
- }
以
为例来说明,其中 attr 对象的 name 为 sd-on-click,value 为 toggle | .button,最终解析结果为:
- sd - on - click = "toggle | .button"
- {
- "attr": attr,
- "key": "toggle",
- "filters": [".button"],
- "argument": "click",
- "definition": {"on": {}},
- "update": function(){}
- }
紧接着调用 bindDirective 方法
- /**
- * 数据绑定
- * @param {Seed} seed Seed实例对象
- * @param {Element} el 当前node节点
- * @param {Object} bindings 数据绑定存储对象
- * @param {Object} directive 指令解析结果
- */
- function bindDirective(seed, el, bindings, directive) {
- // 移除指令属性
- el.removeAttribute(directive.attr.name);
- // 数据属性
- const key = directive.key;
- let binding = bindings[key];
- if (!binding) {
- bindings[key] = binding = {
- value: undefined,
- directives: [] // 与该数据相关的指令
- };
- }
- directive.el = el;
- binding.directives.push(directive);
- if (!seed.scope.hasOwnProperty(key)) {
- bindAccessors(seed, key, binding);
- }
- }
- /**
- * 重写scope西乡属性的getter和setter
- * @param {Seed} seed Seed实例
- * @param {String} key 对象属性即opts.scope中的属性
- * @param {Object} binding 数据绑定关系对象
- */
- function bindAccessors(seed, key, binding) {
- Object.defineProperty(seed.scope, key, {
- get: function() {
- return binding.value;
- },
- set: function(value) {
- binding.value = value;
- // 触发directive
- binding.directives.forEach((directive) = >{
- // 如果有过滤器则先执行过滤器
- if (typeof value !== 'undefined' && directive.filters) {
- value = applyFilters(value, directive);
- }
- // 调用update方法
- directive.update(directive.el, value, directive.argument, directive);
- });
- }
- });
- }
- /**
- * 调用filters依次处理value
- * @param {任意类型} value 数据值
- * @param {Object} directive 解析出来的指令对象
- */
- function applyFilters(value, directive) {
- if (directive.definition.customFilter) {
- return directive.definition.customFilter(value, directive.filters);
- } else {
- directive.filters.forEach((name) = >{
- if (Filters[name]) {
- value = Filters[name](value);
- }
- });
- return value;
- }
- }
其中的 bindings 存放了数据和指令的关系,该对象中的 key 为 opts.scope 中的属性,value 为 Object,如下:
- {
- "msg": {
- "value": undefined,
- "directives": [] // 上面介绍的directive对象
- }
- }
数据与 directive 建立好关系之后,bindAccessors 中为 seed 的 scope 对象的属性重新定义了 getter 和 setter,其中 setter 会调用指令 update 方法,到此就已经完事具备了。
Seed 构造函数在实例化的最后会迭代 bindings 中的 key,然后从 opts.scope 找到对应的 value,赋值给了 scope 对象,此时 setter 中的 update 就触发执行了。
下面再看一下 sd-on 指令的定义:
- {
- on: {
- update: function(el, handler, event, directive) {
- if (!directive.handlers) {
- directive.handlers = {};
- }
- const handlers = directive.handlers;
- if (handlers[event]) {
- el.removeEventListener(event, handlers[event]);
- }
- if (handler) {
- handler = handler.bind(el);
- el.addEventListener(event, handler);
- handlers[event] = handler;
- }
- },
- customFilter: function(handler, selectors) {
- return function(e) {
- const match = selectors.every((selector) = >e.target.matches(selector));
- if (match) {
- handler.apply(this, arguments);
- }
- }
- }
- }
- }
发现它有 customFilter,其实在 applyFilters 中就是针对该指令做的一个单独的判断,其中的 selectors 就是 [".button"],最终返回一个匿名函数(事件监听函数),该匿名函数当做 value 传递给 update 方法,被其 handler 接收,update 方法处理的是事件的绑定。这里其实实现的是事件的代理功能,customFilter 中将 handler 包装一层作为事件的监听函数,同时还实现事件代理功能,设计的比较巧妙!
作者尤小右为之取名 seed 寓意深刻啊,vue 就是在这最初的代码逐步成长起来的,如今已然成为一颗参天大树了。
来源: https://juejin.im/post/5a4e60ca51882573370771e5