npm-link VUEX watch 怎么不生效


🐱 背景

前端项目 package.json 相当于后端 maven 项目 pom.xml 文件管理项目组件依赖。需要走 npm install --save-dev xxxx 引入方式。
对于项目中存在多项目共用的前端组件开发,不希望每次修改以发布版本再 npm install 下载包调试。
可以选用 npm-link 方式 将前端组件 link 到场景 UI 中完成开发/联调/bug 修改工作。
最近前端同学发现,npm-link 方式引入的前端组件中引入 VUEX,且对 store 属性 watch 事件是不会生效。
个人觉得不应该,npm-link 就简单的将前端组件 link 到 UI,可以说是原封不动,包括 node_modules(最后发现也坏在此处)。
搜索 google 和百度都没有有效的帖子。

🐯 问题跟进

1⃣ 搞懂 VUEX store 的 watch 原理

store watch 的初始化

vue 初始化时,会调用 initState 其中,会针对本 vue 的 watch 完成 initWatch 初始化。其中初始化过程中会调用 Vue.prototype.$watch (注意,此处初始化用到的还是 vue 原型方法 $watch) 其中会触发一次 watch handler 方法。

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) {
      var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
      pushTarget();
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
      popTarget();
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };

new Watcher 对象第一次获取 watcher.value 时,触发 watcher 对象的 Dep 依赖。

var Watcher = function Watcher (vm,  expOrFn,  cb,  options,  isRenderWatcher) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // 隐藏不需要关注的代码
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
    // 隐藏不需要关注的代码
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get();
};
Watcher.prototype.get = function get () {
  // 指定 Dep.target 为 watcher
  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);
    }
    // 退出 Dep.target 的指向  
    popTarget();
    this.cleanupDeps();
  }
  return value
};
// 触发真实 get 时,完成了 watcher 的 Dep 依赖
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 忽略 set
    get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
            dep.depend();
            if (childOb) {
                childOb.dep.depend();
                if (Array.isArray(value)) {
                    dependArray(value);
                }
            }
        }
        return value
	}
}
// 完成 Dep.target 添加依赖,此时的 Dep.target 是 watther 本身。而 this 为 store 的 dep 对象。
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
// watcher 完成 addDep时,除了给自身 depId和 deps 加上 store dep对象,同样把自身watcher作为 store dep 的子关联
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);
    }
  }
};

Dep.prototype.depend 完成调用时,store watcher 已经完成与 store 的 dep 对象的绑定过程。以上 store watch init 的链路如下,

depend (vue.common.dev.js:726)
reactiveGetter (vue.common.dev.js:1038)
prototypeAccessors$1.state.get (vuex.esm.js:438)
(匿名) (vue.common.dev.js:514)
get (vue.common.dev.js:4490)
Watcher (vue.common.dev.js:4479)
Vue.$watch (vue.common.dev.js:4953)
createWatcher (vue.common.dev.js:4913)
initWatch (vue.common.dev.js:4895)
initState (vue.common.dev.js:4656)
Vue._init (vue.common.dev.js:5010)
VueComponent (vue.common.dev.js:5157)
createComponentInstanceForVnode (vue.common.dev.js:3307)
init (vue.common.dev.js:3136)

store watch 的触发

this.$store.commit('xxx', xxxx) 触发时,在改值的同事会触发本 store Dep 的 notify (通知)。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 忽略 get
    set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
            return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
            customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
            setter.call(obj, newVal);
        } else {
            val = newVal;
        }
        childOb = !shallow && observe(newVal);
        // 触发通知
        dep.notify();
    }
});
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
 // 忽略不重要代码
  for (var i = 0, l = subs.length; i < l; i++) {
    // watcher update
    subs[i].update();
  }
};

其中 notify 的中遍历 Dep 的 subs 并更新,此处回想 watcher 初始化时 watcher#addDep 可见 subs 是包含 watcher 的,所以 store 属性变化也就能通知到 watcher 了。store watch notify 的链路如下,

handler (list_left.vue:411)
invokeWithErrorHandling (vue.common.dev.js:1868)
run (vue.common.dev.js:4579)
flushSchedulerQueue (vue.common.dev.js:4323)
(匿名) (vue.common.dev.js:1994)
flushCallbacks (vue.common.dev.js:1920)
Promise.then(异步)
timerFunc (vue.common.dev.js:1947)
nextTick (vue.common.dev.js:2004)
queueWatcher (vue.common.dev.js:4415)
update (vue.common.dev.js:4555)
notify (vue.common.dev.js:741)
reactiveSetter (vue.common.dev.js:1066)
proxySetter (vue.common.dev.js:4639)
changeAjlb (nav.vue:431)

以上,watcher initwatcher 触发 总结来说就是这个图。

搞懂了,store watcher 这摊子事情,排查就相对简单了。**抓住 1 个位置即可,在 initWatcher 的时候是否完成了 Watcher#addDep
** 结果发现,在 Watcher.prototype.get方法中 pushTarget(this) Dep 指向 webpack://${web_app}/./node_nodules/vue/dist/vue,common.dev.js
而在 Object.defineProperty#get 方法中 Dep.target 代码 Dep 指向
webpack://${web_app}/${web_component}/node_modules/vue/dist/vue.common.dev.js
摆明 Dep 已经不是原来的 Dep 了,导致 store Dep 与 watcher 没加成,导致 store watcher 不被触发。

此时,我回想幸好是个女生,不然我就去楼下抽根烟了。这个后端 jar 包冲突可太像了。

3⃣ 如何解决问题

在场景层将 vue 定义成 window 全局对象。在组件内使用 window.Vue 装载 vuex。自此,问题终结。修改方式:

// 在前端应用入口文件中
import vue from 'vue';
import vuex from 'vuex';
window.Vue = vue;
vue.use(vuex);
// 在被引用的组件入口文件中
import vuex from 'vuex';
if (window.Vue) {
    window.Vue.use(vuex);
} else {
    window.use(vuex);
}

🐰 总结

  1. npm-link 固然解决了不用老改组件版本号调试的问题,但因为 npm-link 的组件会使用自身的 node_modules 导致,部分原本期望与场景 ui 共享的对象可能不共享。
  2. 前端代码排查确是不如后端代码排查方便,啥 console.warn 都么得耗时长。

🐑 附件


文章作者: beluga
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 beluga !
  目录