Skip to content

07 支持指令 #9

@xwjie

Description

@xwjie

指令实现很复杂,今天先演示一个最简单的指令red,把元素的背景显示为红色。后续再实现表达式功能等。

属性里面支持特殊字符

首先,把使用 John ResigHTML Parser 修改一下,因为我们的指令里面有 -:@. 等特殊字符。把

var startTag = /^<([-A-Za-z0-9_]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/
attr = /([-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;

修改为

var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[-A-Za-z0-9_@.:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*
(\/?)>/
attr = /([-A-Za-z0-9_:@.]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;

修改语法树数据结构,增加指令数据结构

直接使用vue的数据结构。directives?: Array<ASTDirective>

declare type ASTElement = {
    type: 1;
    tag: string;
    //attrsList: Array<{ name: string; value: any }>;
    attrsMap: { [key: string]: any };
    parent?: ASTElement;
    children: Array<ASTNode>;
    directives?: Array<ASTDirective>;
}

declare type ASTDirective = {
  name: string;
  rawName: string;
  value: string;
  arg: ?string;
  modifiers: ?ASTModifiers;
};

declare type ASTModifiers = { [key: string]: boolean };

修改语法树生成逻辑解析指令

export function processAttrs(el, attrs) {

  let i, l, name, rawName, value, modifiers, isProp
  for (i = 0, l = attrs.length; i < l; i++) {
    name = rawName = attrs[i].name
    value = attrs[i].value


    // modifiers
    modifiers = parseModifiers(name);
    if (modifiers) {
      name = name.replace(modifierRE, '');
    }

    // parse arg
    var argMatch = name.match(argRE);
    var arg = argMatch && argMatch[1];
    if (arg) {
      name = name.slice(0, -(arg.length + 1));
    }


    // 是指令
    if (isDirective(name)) {
      addDirective(el, name, rawName, value, arg, modifiers);
    }

  }
}

function isDirective(name: String): boolean {
  return name.startsWith('x-')
}

function parseModifiers(name) {
  var match = name.match(modifierRE);
  if (match) {
    var ret = {};
    match.forEach(function (m) { ret[m.slice(1)] = true; });
    return ret
  }
}

/**
 *
 * @param {*} el
 * @param {*} name
 * @param {*} rawName
 * @param {*} value
 * @param {*} arg
 * @param {*} modifiers
 */
function addDirective(
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  modifiers: ?ASTModifiers
) {
  (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
  el.plain = false
}

解析完的树是这样子的,在原来的AST节点下面增加了 directives 节点。

{
    "type": 1,
    "tag": "div",
    "attrsMap": {
        "id": "hook-arguments-example",
        "x-demo:foo.a.b": "message"
    },
    "children": [],
    "directives": [
        {
            "name": "x-demo",
            "rawName": "x-demo:foo.a.b",
            "value": "message",
            "arg": "foo",
            "modifiers": {
                "a": true,
                "b": true
            }
        }
    ],
    "plain": false
}

修改生成渲染函数字符串

ast2render 里面,增加一个函数处理指令

/**
 * 解析指令
 * @param {*} node
 */
function getDirectiveStr(node: any) {
  let dirs = node.directives

  let str = '';

  if (dirs) {
    str += 'directives:['

    // why not use for..in, see eslint `no-restricted-syntax`
    dirs.forEach(dir => {
      str += JSON.stringify(dir) + ','
    })

    str += '],'
  }

  return str;
}

最终生成的渲染函数

增加了指令的信息,在渲染函数里面,增加了一个字段 directives

h("div",{attrs:{},
  directives:[
    {"name":"x-demo","rawName":"x-demo:foo.a.b","value":"message","arg":"foo","modifiers": {"a":true,"b":true}},
  ],
},[])

扩展snabbdom,使支持指令

我们原来使用的渲染函数是 snabbdom的h,我们测试发现,它返回的虚拟节点默认已经包含了指令信息,在data里面。所以这块我们不需要更改。

接下来我们需要扩展 snabbdom 模块,增加一个处理指令模块

"use strict";

function updateDirective(oldVnode, vnode) {
  var elm = vnode.elm, nodeDirs = vnode.data.directives;

  if (!nodeDirs)
    return;

  nodeDirs = nodeDirs || {};

  console.log('自定义指令处理:', vnode.context);
  console.log('自定义指令处理:', vnode);
  console.log('自定义指令处理:', nodeDirs);

  const vm = vnode.context
  const dirs = vm.directives

  nodeDirs.forEach(function (dir) {
    // 直接调用指令的处理函数 
    dirs[dir.name](vnode.elm, dir)
  }, vm);
}

export default {
  create: updateDirective,
  update: updateDirective
};

在 snabbdom上注册

import * as snabbdom from 'snabbdom'
import * as snabbdom_class from 'snabbdom/modules/class'
import * as snabbdom_props from 'snabbdom/modules/props'
import * as snabbdom_style from 'snabbdom/modules/style'
import * as snabbdom_directive from './directives/directive'
import * as snabbdom_eventlisteners from 'snabbdom/modules/eventlisteners'
import * as snabbdom_h from 'snabbdom/h'

const patch = snabbdom.init([ // Init patch function with chosen modules
  snabbdom_class.default, // makes it easy to toggle classes
  snabbdom_props.default, // for setting properties on DOM elements
  snabbdom_style.default, // handles styling on elements with support for animations
  snabbdom_eventlisteners.default, // attaches event listeners
  snabbdom_directive.default, // xiaowenjie add 处理指令
])

const h = snabbdom_h.default // helper function for creating vnodes

export { h, patch }

扩展框架代码

第一,把实例绑定到vnode中 context,snabdom 处理的时候再拿出来

function updateComponent(vm: Xiao) {
  let proxy = vm

  // 虚拟dom里面的创建函数
  proxy.h = h

  // 新的虚拟节点
  let vnode = vm.$render.call(proxy)

  // 把实例绑定到vnode中,处理指令需要用到
  vnode.context = vm

  // 上一次渲染的虚拟dom
  let preNode = vm.$options.oldvnode;

  log(`[lifecycle] 第${renderCount}次渲染`)

  if (preNode) {
    vnode = patch(preNode, vnode)
  }
  else {
    vnode = patch(vm.$el, vnode)
  }

  log('vnode', vnode)

  renderCount++;

  // save
  vm.$options.oldvnode = vnode;
}

然后 初始化的时候需要初始化全局指令。先使用一个把背景变成红色的指令

/**
 * 初始化全局指令
 */
function initGlobaleDedirectives() {
  Xiao.directive('red', function (el, binding) {
    el.style.backgroundColor = 'red'// binding.value
  })
}

initGlobaleDedirectives()

在 框架代码中增加实例注册方法 $directive

  static directive(name: string, cb: any) {
    globaleDedirectives[`x-${name}`] = cb
  }

  $directive(name: string, cb: any) {
    this.directives[`x-${name}`] = cb
  }

测试

测试默认的 red 指令和自定义了一个demo指令。

<body>
<h1>简单自定义指令</h1>
<div id="demo1" x-demo>使用<b>app.$directive</b>注册</div>
<div id="demo3" x-red>使用<b>默认的red指令</b></div>

<script>
	// 自定义指令测试
	var app = new Xiao();
	
	// 自定义一个把背景变成黄色的指令
	app.$directive('demo', function (el, binding) {
    	     el.style.backgroundColor = 'yellow'// binding.value
  	})
	
	app.$mount("#demo1");


	new Xiao({
		el:"#demo3"
	});
</script>

</body>

生成的vnode信息

{
    "sel": "div",
    "data": {
        "attrs": {},
        "directives": [
            {
                "name": "x-demo",
                "rawName": "x-demo",
                "value": "",
                "arg": null
            }
        ]
    },
    "children": [
        {
            "text": "使用",
            "elm": {}
        },
        {
            "sel": "b",
            "data": {
                "attrs": {}
            },
            "children": [
                {
                    "text": "app.$directive",
                    "elm": {}
                }
            ],
            "elm": {}
        },
        {
            "text": "注册",
            "elm": {}
        }
    ],
    "elm": {},
    "context": null
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions