Vue 源码解析(实例化前) - 初始化全局API(三)

栏目: JavaScript · 发布时间: 5年前

内容简介:这两天的把 1w 多行的 vue 过了一遍,看了一下把剩下在实例化 vue 构造函数前做的所有事情,做了一个总结,写下了这个最终章,文中有哪些问题,还希望大家可以指出。通过在初始化的时候,给

这两天的把 1w 多行的 vue 过了一遍,看了一下把剩下在实例化 vue 构造函数前做的所有事情,做了一个总结,写下了这个最终章,文中有哪些问题,还希望大家可以指出。

正文

为VNode原型添加 child 属性监听

var VNode = function VNode(
  tag,
  data,
  children,
  text,
  elm,
  context,
  componentOptions,
  asyncFactory
) {
  this.tag = tag;
  this.data = data;
  this.children = children;
  this.text = text;
  this.elm = elm;
  this.ns = undefined;
  this.context = context;
  this.fnContext = undefined;
  this.fnOptions = undefined;
  this.fnScopeId = undefined;
  this.key = data && data.key;
  this.componentOptions = componentOptions;
  this.componentInstance = undefined;
  this.parent = undefined;
  this.raw = false;
  this.isStatic = false;
  this.isRootInsert = true;
  this.isComment = false;
  this.isCloned = false;
  this.isOnce = false;
  this.asyncFactory = asyncFactory;
  this.asyncMeta = undefined;
  this.isAsyncPlaceholder = false;
};

var prototypeAccessors = { child: { configurable: true } };

prototypeAccessors.child.get = function () {
  return this.componentInstance
};

Object.defineProperties(VNode.prototype, prototypeAccessors);
复制代码

通过 Object.definePropertiesVNode 的原型绑定了对象 prototypeAccessorsprototypeAccessors 设置 child 是可修改的状态。

初始化Mixin

initMixin(Vue);
复制代码

在初始化的时候,给 initMixin 传入了 Vue 构造函数:

function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    var vm = this;
    
    vm._uid = uid$3++;

    var startTag, endTag;
    
    if (config.performance && mark) {
      startTag = "vue-perf-start:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    vm._isVue = true;
    // 合并选项
    if (options && options._isComponent) {
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    
    {
      initProxy(vm);
    }
    
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // 在data/props之前解决注入
    initState(vm);
    initProvide(vm); // 解决后提供的data/props
    callHook(vm, 'created');

    if (config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}
复制代码
Vue.prototype._init = function (options) {}
复制代码

在初始化的时候,为 Vue 在 原型上,添加了个 _init 方法,这个方法在实例化 vue 构造函数的时候会被调用:

function Vue(options) {
    if (!(this instanceof Vue)
    {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}
复制代码

这里对 _init 不做太多的解释,大家看一下 _init 都做了什么,等到接下来讲解实例化后 vue 的时候,会对这里做详细的解释,包括生命周期的实现。

state 的 mixin

stateMixin(Vue);
复制代码

在处理完 initMixin 后,接着对 state 做了 mixin 的处理,给 stateMixin 传入了 Vue 构造函数。

function stateMixin(Vue) {
  var dataDef = {};
  dataDef.get = function () { return this._data };
  var propsDef = {};
  propsDef.get = function () { return this._props };
  {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      );
    };
    propsDef.set = function () {
      warn("$props is readonly.", this);
    };
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef);
  Object.defineProperty(Vue.prototype, '$props', propsDef);

  Vue.prototype.$set = set;
  Vue.prototype.$delete = del;

  Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
      }
    }
    return function unwatchFn() {
      watcher.teardown();
    }
  };
}
复制代码

官方给该方法的解释是:

在使用object.defineproperty时,流在某种程度上存在直接声明的定义对象的问题,因此我们必须在此处按程序构建该对象。
复制代码
var dataDef = {};
dataDef.get = function () { return this._data };
var propsDef = {};
propsDef.get = function () { return this._props };
{
dataDef.set = function () {
  warn(
    'Avoid replacing instance root $data. ' +
    'Use nested data properties instead.',
    this
  );
};
propsDef.set = function () {
  warn("$props is readonly.", this);
};
}
Object.defineProperty(Vue.prototype, '$data', dataDef);
Object.defineProperty(Vue.prototype, '$props', propsDef);
复制代码

一开始,声明了两个对象 dataDefpropsDef ,并分别添加了 setget , 在我们操作 vm.$data 的时候,返回的就是 this._data$props 也是如此;

Vue.prototype.$set = set;
Vue.prototype.$delete = del;
复制代码

这里就是给 vue 实例绑定了 setdel 方法,这两个方法在之前的章节将结果,链接在 Vue 源码解析(实例化前) - 初始化全局API(二)

Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
      }
    }
    return function unwatchFn() {
      watcher.teardown();
    }
};
复制代码

这里,就是我们在做 vue 项目当中,经常使用的 api 之一,这里就是它的具体实现。

具体的用法,和参数的意思,可以看官方的 api 文档,那里对参数的解释和用法写的非常清楚,我这里就讲一下怎么实现的,具体意思,大家看 api 文档就好。

官方api文档路径: vm.$watch( expOrFn, callback, [options] )

在一开始,把当前的 this 指针存储在一个 vm 当变量当中;

检测如果当前的 cb 是对象的话,返回一个 createWatcher 方法:

function createWatcher(
  vm,
  expOrFn,
  handler,
  options
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}
复制代码

这是 createWatcher 的实现。

其实就是对在调用 vm.$watch 时接受到参数,包括当前的 vue 实例,然后还是第一步要检查 handler 是不是对象,这里的 handler 就是之前的 cb

如果是对象的话,就用 handler 覆盖 optionshandler.handler 去当作当前的 handler 去使用;

如果 handler 是字符串的话,就去把当前 vue 实例的该属性,当作 handler 去使用;

最后,返回一个新的 vm.$watch

options = options || {};
options.user = true;
复制代码

检查接收的参数 options 是否存在,不存在就设置一个空对象;

设置的 options.usertrue

var watcher = new Watcher(vm, expOrFn, cb, options);
复制代码

在设置完 options.user 后,就实例化了 Watcher 这个构造函数,这里是非常核心的一块内容,希望大家可以仔细看看,这一块当作一个大分类来讲,先把下面的两行给讲了:

if (options.immediate) {
  try {
    cb.call(vm, watcher.value);
  } catch (error) {
    handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
  }
}
return function unwatchFn() {
  watcher.teardown();
}
复制代码

如果设置了 options.immediatetrue ,那么把当前 cbthis 指向 vue 的实例化构造函数,并把 watcher.value 传给 cb

最后返回一个函数,调用 watcher.teardown

Watcher 实现

Watcher 构造函数

var Watcher = function Watcher(
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1;
  this.active = true;
  this.dirty = this.lazy;
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = expOrFn.toString();
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get();
};

Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
   
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

Watcher.prototype.addDep = function addDep(dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

Watcher.prototype.cleanupDeps = function cleanupDeps() {
  var i = this.deps.length;
  while (i--) {
    var dep = this.deps[i];
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this);
    }
  }
  var tmp = this.depIds;
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  this.newDepIds.clear();
  tmp = this.deps;
  this.deps = this.newDeps;
  this.newDeps = tmp;
  this.newDeps.length = 0;
};

Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

Watcher.prototype.run = function run() {
  if (this.active) {
    var value = this.get();
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};

Watcher.prototype.evaluate = function evaluate() {
  this.value = this.get();
  this.dirty = false;
};

Watcher.prototype.depend = function depend() {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
};

Watcher.prototype.teardown = function teardown() {
  if (this.active) {
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this);
    }
    var i = this.deps.length;
    while (i--) {
      this.deps[i].removeSub(this);
    }
    this.active = false;
  }
};
复制代码

这是有关 Watcher 构造函数所有实现的代码。

Watcher初始化

this.vm = vm;
if (isRenderWatcher) {
    vm._watcher = this;
}
复制代码

当前的 watcher 对象的 vm 属性指向的是 vue 实例化对象;

如果 isRenderWatchertrue 时, vue_watcher 指向当前 this

vm._watchers.push(this);
复制代码

vue 实例化对象的 _watchers 数组添加一个数组项,就是当前的 watcher 实例化对象;

if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
} else {
    this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$1;
this.active = true;
this.dirty = this.lazy;
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
复制代码

这里就是 watcher 初始化的一些属性值;

var bailRE = /[^\w.$]/;
function parsePath(path) {
    if (bailRE.test(path)) {
      return
    }
    var segments = path.split('.');
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]];
      }
      return obj
    }
}
if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
} else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
}
复制代码

如果接收到的 expOrFn 是个函数的话,当前 thisgetter 就指向它;

否则,通过 parsePath 去格式化当前的路径;

如果 expOrFn 并不是已单词字符结尾的,就直接返回,设置一个空的 noop 函数给当前实例的 getter 属性;

expOrFn 进行切割,遍历切割后的 expOrFn ,并把切割后的每个数组项当作要返回的函数的接收到的 obj 的属性。

this.value = this.lazy ? undefined : this.get();
复制代码

如果 lazytrue 的话, this.value 的就是 undefinend ,否则就是调用 this.get 方法

Watcher 获取值

function pushTarget(target) {
    targetStack.push(target);
    Dep.target = target;
}

function popTarget() {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
}
Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
   
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
复制代码

Vue 源码解析(实例化前) - 响应式数据的实现原理 讲解 Dep 构造函数的时候,涉及到过,这里开始仔细讲解,具体调用的地方,可以去之前的章节查看。

get 在一开始的时候,讲调用了 pushTarget 方法,并把当前 watcher 实例化对象传过去;

pushTarget 方法,就是给 targetStack 数组添加一个数组项,就是当前的 watcher 实例化对象;

把当前的 watcher 实例化对象 指向 Dep.target

this.getterthis 指向 vue 的实例化对象,并调用它,把当前的值去做获取返回到 value

如果设置了 this.deeptrue ,就代表用户想要发现对象内部值的变化,这个时候调用 traverse 函数,目的是递归遍历一个对象以唤起所有转换的getter,以便将对象内的每个嵌套属性收集为“深度”依赖项,把最后的结果更新到 value

popTarget 把在 targetStack 数组中的最后一个删除,并把 Dep.target 指向删除后的数组的最后一个数组项。

在这里,其实就是给在获取当前数据时的 watcher 在一开始做了存储 ( targetStack ),在所有值的展示和处理做完以后,在清空了存储 ( targetStack

this.cleanupDeps();
复制代码

清除依赖项集合,接下来讲。

return value
复制代码

最后返回 value

Watcher 添加队列

Watcher.prototype.addDep = function addDep(dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};
复制代码

检查队列是否存在当前的 id ,该 id 其实就是 Dep 的实例化对象的 id ,把它添加到对应的队列里面去;

这里其实比较简单明了,就不做太复杂的解释了,大家看一眼就明白了。

Watcher 清空队列

Watcher.prototype.cleanupDeps = function cleanupDeps() {
  var i = this.deps.length;
  while (i--) {
    var dep = this.deps[i];
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this);
    }
  }
  var tmp = this.depIds;
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  this.newDepIds.clear();
  tmp = this.deps;
  this.deps = this.newDeps;
  this.newDeps = tmp;
  this.newDeps.length = 0;
};
复制代码

把当前 Watcher 监听队列里的 Watcher 对象从后往前清空,在把一些属性初始化。

Watcher 更新

Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

复制代码

如果是懒更新的话,设置 dirtytrue

如果是同步更新的话,直接调用 run 方法;

否则,调用 queueWatcher 方法。

// 将观察者推入观察者队列。
// 具有重复ID的工作将被跳过,除非在刷新队列时将其推送。
function queueWatcher(watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      // 如果已经刷新,则根据其ID拼接观察程序
      // 如果已经超过了它的ID,它将立即运行。
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // 刷新队列
    if (!waiting) {
      waiting = true;

      if (!config.async) {
        flushSchedulerQueue();
        return
      }
      nextTick(flushSchedulerQueue);
    }
  }
}
复制代码

如果设置的 config.async 是同步的,那么就刷新两个队列并运行 Watcher 结束当前方法;

否则的话,执行 nextTick 后执行 flushSchedulerQueue

var MAX_UPDATE_COUNT = 100;

var queue = [];
var activatedChildren = [];
var has = {};
var circular = {};
var waiting = false;
var flushing = false;
var index = 0;

// 重置计划程序的状态
function resetSchedulerState() {
  index = queue.length = activatedChildren.length = 0;
  has = {};
  {
    circular = {};
  }
  waiting = flushing = false;
}

function flushSchedulerQueue() {
  flushing = true;
  var watcher, id;
  //在刷新之前排队队列。
  //这可以确保:
  // 1.组件从父级更新为子级。 (因为父母总是在孩子面前创建)
  // 2.组件的用户观察者在其渲染观察者之前运行(因为在渲染观察者之前创建用户观察者)
  // 3.如果在父组件的观察程序运行期间销毁了组件,可以跳过其观察者。
  queue.sort(function (a, b) { return a.id - b.id; });

  //不要缓存长度,因为可能会推送更多的观察程序
  //当我们运行现有的观察程序时
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    //在开发构建中,检查并停止循环更新。
    if (has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? ("in watcher with expression \"" + (watcher.expression) + "\"")
              : "in a component render function."
          ),
          watcher.vm
        );
        break
      }
    }
  }

  // 重置状态前保留发布队列的副本
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  resetSchedulerState();

  // 调用组件更新和激活的钩子
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}
function callUpdatedHooks(queue) {
    var i = queue.length;
    while (i--) {
      var watcher = queue[i];
      var vm = watcher.vm;
      if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'updated');
      }
    }
}
function callActivatedHooks(queue) {
  for (var i = 0; i < queue.length; i++) {
    queue[i]._inactive = true;
    activateChildComponent(queue[i], true /* true */);
  }
}
复制代码

Watcher 运行

Watcher.prototype.run = function run() {
  if (this.active) {
    var value = this.get();
    if (value !== this.value || isObject(value) || this.deep) {
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};
复制代码

只有 activetrue 的时候,执行 run 才有效果,因为 active 在调用 teardown 方法的时候会变成 false

检查新值和旧值是否相同,如果不相同的话,把新值和旧值传递给 cb 回调,并把 this 指向 vue 实例。

Vue 源码解析(实例化前) - 初始化全局API(三)

这样可能大家理解的会更容易点。

Watcher 评估

Watcher.prototype.evaluate = function evaluate() {
  this.value = this.get();
  this.dirty = false;
};
复制代码

评估观察者的价值,这只适用于懒惰的观察者。

Watcher 依赖

Watcher.prototype.depend = function depend() {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
};
复制代码

这里就是真正的实现通知依赖的部分。

Watcher 卸载

Watcher.prototype.teardown = function teardown() {
  if (this.active) {
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this);
    }
    var i = this.deps.length;
    while (i--) {
      this.deps[i].removeSub(this);
    }
    this.active = false;
  }
};
复制代码

清空所有依赖,从后往前。

结束语

本来是准备这一章把 Vue 构造函数实例化前要做的所有事情都写完,发现在 statemixin 时候,涉及到了 watcher ,但是发现了,就先讲解了,之后还是有同样量的内容,所以还是准备单拿出来一讲,篇幅太长了对大家学习和吸收并不友好。

接下来的一章,会讲到:

eventsminxin $on $once $off $emit

lifecycleminxin updated $forceUpdate $destroy

renderminxin $nextTick render

下一章,就是 vue 源码解析(实例化前) - 初始化全局 API(最终章)了,文中有写的不对的,还希望大家可以积极指出。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

任性

任性

电子工业出版社 / 2015-10-1 / 49.00

《任性:互联网语言表达的调性和技巧》是一本深度介绍互联网调性的书,也是从社会化媒体运作的角度较为系统地讲解互联网语言表达的书,它以独特的视角,从技术、需求和表现形式三种驱动力展开,从理论、策略、方法、技巧、实践等角度详细解析了互联网表达的变化和社会媒体的运营。《任性:互联网语言表达的调性和技巧》适合互联网从业人员阅读。一起来看看 《任性》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具