Vue directive源码解析

1.directiveshow两个 Vue 自带指令通过extend方法保存在Vue.options.directives上。

var platformDirectives = {
  model: directive,
  show: show,
};

extend(Vue.options.directives, platformDirectives);

function extend(to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to;
}

2.Vue 提供了Vue.directive方法创建全局指令,添加到Vue.options.directives上。

var ASSET_TYPES = ["component", "directive", "filter"];

ASSET_TYPES.forEach(function (type) {
  Vue[type] = function (id, definition) {
    if (!definition) {
      return this.options[type + "s"][id];
    } else {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && type === "component") {
        validateComponentName(id);
      }
      if (type === "component" && isPlainObject(definition)) {
        definition.name = definition.name || id;
        definition = this.options._base.extend(definition);
      }
      if (type === "directive" && typeof definition === "function") {
        definition = { bind: definition, update: definition };
      }
      this.options[type + "s"][id] = definition;
      return definition;
    }
  };
});

3.不仅可以全局添加指令,也可以在组件选项上添加指令,仅在组件内部生效。创建组件时,会格式化组件选项上的指令,再合并全局和组件内的指令。

/**
 * 合并组件选项
 */
function mergeOptions (
  parent,
  child,
  vm
) {
    ...
    normalizeDirectives(child);// 格式化指令
    ...
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (var i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm);
      }
    }
  }

  var options = {};
  var key;
  for (key in parent) {
    mergeField(key);// 合并选项
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);// 合并选项
    }
  }
  function mergeField (key) {
    var strat = strats[key] || defaultStrat;// 获取合并策略
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options
}

4.格式化指令,保持一致的指令定义格式。如果定义指令的属性值是函数,则将赋值给bindupdate属性。

/**
 * 格式化组件指令
 */
function normalizeDirectives(options) {
  var dirs = options.directives;
  if (dirs) {
    for (var key in dirs) {
      var def$$1 = dirs[key];
      if (typeof def$$1 === "function") {
        // 值为函数类型
        dirs[key] = { bind: def$$1, update: def$$1 };
      }
    }
  }
}

5.全局指令和组件内指令是通过Object.create进行合并的,组件内指令对象的原型链指向全局的指令对象,当在组件内未找到该指令时就会去全局的指令对象内查找。

/**
 * Object.create的合并策略
 */
function mergeAssets(parentVal, childVal, vm, key) {
  var res = Object.create(parentVal || null);
  if (childVal) {
    process.env.NODE_ENV !== "production" &&
      assertObjectType(key, childVal, vm);
    return extend(res, childVal);
  } else {
    return res;
  }
}
/**
 * 设置componet,directive,filter合并策略
 */
ASSET_TYPES.forEach(function (type) {
  strats[type + "s"] = mergeAssets;
});

4.标签上的指令会被编译器解析成对象。解析结果会在组件渲染时保存在节点(VNode 实例)上。

指令解析结果:

{
  directives: [
    {
      name: "test",
      rawName: "v-test:name.fale",
      value: getName,
      expression: "getName", // 表达式
      arg: "name", // 参数
      modifiers: { fale: true }, // 修饰符
    },
  ];
}

5.节点在渲染的过程(创建 DOM 或组件,更新节点,插入到父级 DOM 元素,节点销毁)中有很多钩子被调用,用来处理节点对应标签的各种数据。 这些钩子中包括了指令的createupdatedestroy三个钩子函数,将在节点渲染的过程中被调用。

这里的钩子函数和指令定义时的钩子函数不相同。
/* 指令的钩子函数 */
var emptyNode = new VNode("", {}, []);

var directives = {
  create: updateDirectives, // 创建了DOM元素后调用
  update: updateDirectives, // 更新节点时调用
  destroy: function unbindDirectives(vnode) {
    // 销毁节点时调用
    updateDirectives(vnode, emptyNode);
  },
};

function updateDirectives(oldVnode, vnode) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode);
  }
}

6.指令的三个钩子都会调用_update函数。

function _update(oldVnode, vnode) {
  var isCreate = oldVnode === emptyNode;
  var isDestroy = vnode === emptyNode;
  var oldDirs = normalizeDirectives$1(
    oldVnode.data.directives,
    oldVnode.context
  ); // 旧标签节点上的指令数据集合(包含标签上的指令信息和指令定义)
  var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); // 新标签节点上的指令数据集合(包含标签上的指令信息和指令定义)

  var dirsWithInsert = [];
  var dirsWithPostpatch = [];

  var key, oldDir, dir;
  for (key in newDirs) {
    oldDir = oldDirs[key];
    dir = newDirs[key];
    if (!oldDir) {
      // 指令第一次绑定到元素上
      // new directive, bind
      callHook$1(dir, "bind", vnode, oldVnode); // 调用bind钩子函数
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir);
      }
    } else {
      // existing directive, update
      dir.oldValue = oldDir.value; // 更新指令的值
      dir.oldArg = oldDir.arg; // 更新指令的参数(指令参数可动态变化)
      callHook$1(dir, "update", vnode, oldVnode);
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir);
      }
    }
  }

  if (dirsWithInsert.length) {
    var callInsert = function () {
      for (var i = 0; i < dirsWithInsert.length; i++) {
        callHook$1(dirsWithInsert[i], "inserted", vnode, oldVnode);
      }
    };
    if (isCreate) {
      mergeVNodeHook(vnode, "insert", callInsert);
    } else {
      callInsert();
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, "postpatch", function () {
      for (var i = 0; i < dirsWithPostpatch.length; i++) {
        callHook$1(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode);
      }
    });
  }

  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // no longer present, unbind
        callHook$1(oldDirs[key], "unbind", oldVnode, oldVnode, isDestroy);
      }
    }
  }
}

7.在_update函数中,先调用normalizeDirective$1函数,根据指令名称在组件$options.directives查找到指令的定义,并和对应的标签指令数据保存在一起。

如果在组件 $options.directives上未找到指令的定义,就会从它的原型链上查找,也就从全局指令上查找。
var emptyModifiers = Object.create(null);

function normalizeDirectives$1(dirs, vm) {
  var res = Object.create(null);
  if (!dirs) {
    // $flow-disable-line
    return res;
  }
  var i, dir;
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i];
    if (!dir.modifiers) {
      // 没有修饰符
      // $flow-disable-line
      dir.modifiers = emptyModifiers;
    }
    res[getRawDirName(dir)] = dir; // 可能存在同名指令、但参数和修饰符不同
    dir.def = resolveAsset(vm.$options, "directives", dir.name, true); // 根据指令名称获取指令定义,并保存在标签指令数据上
  }
  // $flow-disable-line
  return res;
}
//获取原始名称(包含前缀、参数、修饰符)
function getRawDirName(dir) {
  return (
    dir.rawName || dir.name + "." + Object.keys(dir.modifiers || {}).join(".")
  );
}

8.当没有旧标签指令时(指令第一次绑定到元素上),调用指令定义的bind钩子函数,如果指令定义中还有insert钩子函数,就将指令数据添加到 insert 队列中暂存。

/*
 * 调用指令钩子函数(格式化钩子函数参数)
 */
function callHook$1(dir, hook, vnode, oldVnode, isDestroy) {
  var fn = dir.def && dir.def[hook];
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
    } catch (e) {
      handleError(
        e,
        vnode.context,
        "directive " + dir.name + " " + hook + " hook"
      );
    }
  }
}

9.否则,即新旧标签指令都存在,就调用指令定义的update钩子函数,如果指令定义中还有componentUpdated钩子函数,就将指令数据添加到componentUpdated队列中暂存。

10.暂存在insert队列中的指令,会在节点生成的 DOM 元素插入到父级 DOM 元素后调用它定义的 insert 钩子函数。而暂存在componentUpdated队列中的指令,会在节点以及子节点更新完成后调用它定义的componentUpdated钩子函数。

mergeVNodeHook 封装了节点默认的 insertcomponentUpdated 钩子函数。默认钩子函数和指令定义的钩子函数依次添加到数组中,保存在封装函数的 fns 属性上,调用封装钩子函数时,将会依次调用 fns 数组上的钩子函数。
function mergeVNodeHook(def, hookKey, hook) {
  if (def instanceof VNode) {
    def = def.data.hook || (def.data.hook = {}); // 节点钩子函数集合
  }
  var invoker;
  var oldHook = def[hookKey]; // 特定阶段的节点钩子函数

  function wrappedHook() {
    hook.apply(this, arguments);
    // important: remove merged hook to ensure it's called only once
    // and prevent memory leak
    remove(invoker.fns, wrappedHook); // 调用完指令定义的钩子函数后移除
  }

  if (isUndef(oldHook)) {
    // no existing hook
    invoker = createFnInvoker([wrappedHook]);
  } else {
    /* istanbul ignore if */
    if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
      // 已经封装过的,周期函数已经发生合并
      // already a merged invoker
      invoker = oldHook;
      invoker.fns.push(wrappedHook); // 添加新的周期函数
    } else {
      // existing plain hook
      invoker = createFnInvoker([oldHook, wrappedHook]); // 封装周期函数,在原始钩子函数执行之后,执行新增的钩子函数
    }
  }

  invoker.merged = true; // 标识周期函数发生合并
  def[hookKey] = invoker; // 更新周期函数
}
/*
 *封装函数,将原始函数保存在封装函数的fns属性上。当原始函数发生变更时,只修改fns属性就可以
 */
function createFnInvoker(fns, vm) {
  function invoker() {
    var arguments$1 = arguments;

    var fns = invoker.fns;
    if (Array.isArray(fns)) {
      var cloned = fns.slice();
      for (var i = 0; i < cloned.length; i++) {
        invokeWithErrorHandling(
          cloned[i],
          null,
          arguments$1,
          vm,
          "v-on handler"
        );
      }
    } else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler");
    }
  }
  invoker.fns = fns;
  return invoker;
}

你可能感兴趣的:(javascript,vue.js,前端)