最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 跟我一起读源码丨Vue源码之依赖收集

    正文概述 掘金(前端精)   2020-12-15   362

    tip:Vue版本:v2.6.12,浏览器:谷歌,阅读方式:在静态html 引用 Vue 包 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> 进行断点阅读

    文章篇幅有点长,泡杯咖啡,慢慢看 ~

    我从「依赖收集」中学习到了什么?

    1. 观察者模式

    观察者模式的基本概念:

    下面这段代码是 Vue 源码中经过运算的结果,可以让小伙伴们的脑袋瓜先有个简单的结构:

    // 基础数据
    data: {
      a: 1, // 关联 dep:id=0 的对象,a如果发生变化,this.a=3,调用 notify,
      b: 2, // 关联 dep:id=1 的对象...
      // ...
    }
    
    dep = {
      id: 0,
      // 通知观察者们
      notify() {
        this.subs.forEach(item => {
          item.update();
        });
      },
      // 观察者们
      subs: [
        {
          id: 1,
          update() {
            // 被目标者通知,做点什么事
          }
        },
        {
          id: 2,
          update() {
            // 被目标者通知,做点什么事
          }
        }
      ]
    
    };
    
    dep = {
      id: 1,
      //...
    

    2. defineProperty 对一级/多级对象进行拦截

    对于一级对象的拦截相信小伙伴们都会啦。
    这里阐述一下对于多级对象设置拦截器的封装,看下这段代码:

    const obj = { message: { str1: 'hello1', str2: 'hello2' } };
    function observer(obj) {
      if (!(obj !== null && typeof obj === 'object')) {
        return;
      }
      walk(obj);
    }
    function walk(obj) {
      let keys = Object.keys(obj);
      for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i]);
      }
    }
    function defineReactive(obj, key) {
      let val = obj[key];
      observer(val);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
          console.log('get :>> ', key, val);
          return val;
        },
        set(newVal) {
          console.log('set :>> ', key, newVal);
          observer(newVal);
          val = newVal;
        }
      });
    }
    observer(obj);
    

    解释:observer 这个方法表示如果当前是一个对象,就会继续被遍历封装拦截。

    我们对 obj 进行操作,看控制台的输出:

    obj.message
    // get :>>  message { str1: "hello1", str2: "hello2"}
    
    /* 这个例子说明了:不管是在 get/set str1,都会先触发 message 的 get*/
    obj.message.str1
    // get :>>  message { str1: "hello1", str2: "hello2" }
    // get :>>  str1 hello1
    obj.message.str1="123"
    // get :>>  message { str1: "123", str2: "hello2" }
    // set :>>  str1 123
    
    // 重点:
    obj.message={test: "test"}
    // set :>>  message { test: "test" }
    obj.message.test='test2'
    // get :>>  message { test: "test2" }
    // set :>>  test test2
    /* 
    有些小伙伴可能会有疑惑,这里进行 obj.message={test: "test"} 赋值一个新对象的话,
    不就无法检测到属性的变化,为什么执行 obj.message.test='test2' 还会触发到 set 呢?
    返回到上面,在 defineReactive 方法拦截器 set 中,我们做了这样一件事:
    set(newVal) {
      // 这里调用 observer 方法重新遍历,如果当前是一个对象,就会继续被遍历封装拦截
      observer(newVal)
      // ...
    }
    */
    

    延伸到实际业务场景:「获取用户信息然后进行展示」。我在 data 设置了一个 userInfo: {},ajax 获取到结果进行赋值 this.userInfo = { id: 1, name: 'refined' },就可以显示到模板 {{ userInfo.name }},之后再进行 this.userInfo.name = "xxx",也会进行响应式渲染了。

    3. defineProperty 对数组的拦截丨Object.create 原型式继承丨原型链丨AOP

    我们都知道 defineProperty 只能拦截对象,对于数组的拦截 Vue 有巧妙的扩展:

    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    var methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ];
    methodsToPatch.forEach(function (method) {
      var original = arrayProto[method];
      Object.defineProperty(arrayMethods, method, {
        enumerable: true,
        configurable: true,
        value: function mutator(...args) {
          console.log('set and do something...');
          var result = original.apply(this, args);
          return result;
        }
      });
    });
    function protoAugment(target, src) {
      target.__proto__ = src;
    }
    var arr = [1, 2, 3];
    protoAugment(arr, arrayMethods);
    
    arr.push(4)
    // set and do something...
    

    解释:Object.create(arrayProto); 为原型式继承,即 arrayMethods.__proto__ === Array.prototype === true ,所以现在的 arrayMethods 就可以用数组的所有方法。

    代码中的 target.__proto__ = src,即 arr.__proto__ = arrayMethods,我们已经对 arrayMethods 自己定义了几个方法了,如 push。

    现在我们进行 arr.push,就可以调用到 arrayMethods 自定义的 push 了,内部还是有调用了 Array.prototype.push 原生方法。这样我们就完成了一个拦截,就可以检测到数组内容的修改。

    原型链机制:Array.prototype 本身是有 push 方法的,但原型链的机制就是,arr 通过 __proto__ 找到了 arrayMethods.push,已经找到了,就不会往下进行找了。

    可以注意到,封装的这几个方法 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse',都是涉及到数组内容会被改变的,那如果我要调用 arr.map 方法呢?还是刚刚讲的 原型链 机制,arrayMethods 没有 map 方法,就继续顺着 __proto__ 往下找,然后找到 Array.prototype.map

    不得不说,这个数组的扩展封装,可以学习到很多,赞赞赞 ~

    上面讲的例子都是对一个数组内容的改变。细节的小伙伴会发现,如果我对整个数组进行赋值呢,如:arr = [4,5,6],拦截不到吧,是的。其实我只是把这个例子和上面第二点的例子拆分出来了。我们只需要对上面 observer 方法,进行这样一个判断,即

    function observer(value) {
      if (!(value !== null && typeof value === 'object')) {
        return;
      }
      if (Array.isArray(value)) {
        protoAugment(value, arrayMethods);
      } else {
        walk(value);
      }
    }
    

    多级对象和数组的拦截概念其实很像,只是对象只需要逐级遍历封装拦截器,而数组需要用AOP的思想来封装。

    4. 微任务(microtask)的妙用丨event loop

    直接来一手例子:

    var waiting = false;
    function queue(val) {
      console.log(val);
      nextTick();
    }
    function nextTick() {
      if (!waiting) {
        waiting = true;
        Promise.resolve().then(() => {
          console.log('The queue is over, do something...');
        });
      }
    }
    
    queue(1);
    queue(2);
    queue(3);
    
    // 1
    // 2
    // 3
    // The queue is over, do something...
    

    解释:主程序方法执行完毕之后,才会执行 promise 微任务。这也可以解释,为什么 Vue 更新动作是异步的【即:我们没办法立即操作 dom 】,因为这样做可以提高渲染性能,后面会具体讲这块。

    5. 闭包的妙用

    这里也直接来一手例子,个人认为这个闭包用法是成就了依赖收集的关键 ~

    var id = 0;
    var Dep = function () {
      this.id = id++;
    };
    Dep.prototype.notify = function notify() {
      console.log('id :>> ', this.id, ',通知依赖我的观察者们');
    };
    function defineReactive(obj, key) {
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {},
        set() {
          dep.notify();
        }
      });
    }
    var obj = { str1: 'hello1', str2: 'hello2' };
    defineReactive(obj, 'str1');
    defineReactive(obj, 'str2');
    obj.str1 = 'hello1-change';
    obj.str2 = 'hello2-change';
    
    // id :>>  0 ,通知依赖我的观察者们
    // id :>>  1 ,通知依赖我的观察者们
    

    这也是第一点讲到的关联 dep 对象,现在每个属性都可以访问到词法作用域的属于自己的 dep 对象,这就是闭包。

    6. with 改变作用域

    这里只是模拟一下 Vue 的渲染函数

    function render() {
      with (this) {
        return `<div>${message}</div>`;
      }
    }
    var data = { message: 'hello~' };
    render.call(data);
    // <div>hello~</div>
    

    这就是我们平时在 <template> 中不用写 {{ this.message }} 的原因,而是如:

    <template>
      <div> {{ message }} </<div>
    </template>
    

    上面这 6 点是个人觉得有学习到东西的地方,当然要深入理解依赖收集,我们需要走一遍流程。如果你当前在电脑前,我会告诉你需要打第几行的断点,让我们一起读源码吧,go go go ~

    深入源码

    tip:为了阅读质量,我会把一些相对与流程无关的代码省略掉,代码中类似「✅ :123」,表示需要打的断点,谷歌浏览器上开启调试 ctrl + o ,输入 :123 即可跳转至 123 行。

    本地新建 index.html,引入 Vue 包,打开浏览器浏览

    <body>
      <div id="app">
        <div>{{message }}</div>
        <button @click="handleClick">change</button>
      </div>
      <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
      <script>
        var app = new Vue({
          el: '#app',
          data: {
            message: 'hello world'
          },
          methods: {
            handleClick() {
              this.message = 'hello world 2';
            }
          }
        });
      </script>
    </body>
    

    断点 ✅ :4700 initData方法

    顾名思义,初始化我们写的 data 数据并做一些操作,在这个方法里有两个方法值得我们关注,proxy(vm, "_data", key);observe(data, true);

    function initData (vm) {
      var data = vm.$options.data;
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {};
      var keys = Object.keys(data);
      var i = keys.length;
      while (i--) {
        var key = keys[i];
    ✅ :4734  proxy(vm, "_data", key);
      }
    ✅ :4738  observe(data, true);
    }
    

    tip:在遇到方法的时候,我们用步入的方式可以快速定位到方法,如图:

    跟我一起读源码丨Vue源码之依赖收集

    步入到 proxy 方法

    function proxy (target, sourceKey, key) { 
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      };
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val;
      };
      Object.defineProperty(target, key, sharedPropertyDefinition);
    ✅ :4633  }
    

    分析:这个方法是在 while 里的,这里循环遍历了我们写在 data 上的对象。当前 target = vm ,key = message,走到这个 4633 断点,控制台打印 target,如图: 跟我一起读源码丨Vue源码之依赖收集

    上面我们提到了一个 with 的例子:Vue会进行 render.call(vm)。这样子我们就会触发到 message 的 get 方法,这是一个入口,后续会做一系列的操作。

    步入到 observe 方法

    function observe (value, asRootData) {
      if (!isObject(value)) {
        return
      }
      var ob;
    ✅ :4633  ob = new Observer(value);
    }
    

    分析:可以理解成这个方法开始正在对 data 上的 可观察数据 进行观察的一些提前准备,如:往属性上附加 get/set 拦截器,然后分别在 get/set 里做点什么...

    步入到 new Observer [可观测类]

    这是第一个核心类,接下来我们还会分别讲到其他的两个类,每个类都是核心

    var Observer = function Observer (value) {
      this.dep = new Dep();
      if (Array.isArray(value)) {
        protoAugment(value, arrayMethods);
      } else {
    ✅ :935  this.walk(value);
      }
    };
    

    分析:这里有个数组 or 对象的判断,对于数组拦截我们在上面已经有讲过了,我们现在关注 walk 方法。

    步入到 walk 方法

    Observer.prototype.walk = function walk (obj) {
      var keys = Object.keys(obj);
      for (var i = 0; i < keys.length; i++) {
    ✅ :947  defineReactive$$1(obj, keys[i]);
      }
    };  
    

    继续步入到 defineReactive$$1 方法

    function defineReactive$$1 (
      obj, // obj -> data
      key, // key -> 'message'
      val
    ) {
    ✅ :1021  var dep = new Dep();
     
      val = obj[key];
     
      var childOb = observe(val);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          var value = val;
    ✅ :1041  if (Dep.target) {
            dep.depend();
            if (childOb) {
              childOb.dep.depend();
              if (Array.isArray(value)) {
                dependArray(value);
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          var value = val;
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          val = newVal;
          childOb = observe(newVal);
    ✅ :1070 dep.notify();
        }
      });
    }
    

    分析:这个方法可以说是依赖收集中的核心,通过 get 方法添加依赖,通过 set 方法通知观察者。我们上面讲到的 proxy 方法,可以把它当做第一层拦截器,当我们触发一级拦截器之后,就会到二级拦截器 defineReactive$$1 里定义的 get/set 方法。

    new Dep() [观察目标类] 这个是第二个核心类。

    还记得我们在上面说过这个方法是一个 “闭包” 吗?是的,在当前方法内部 Object.defineProperty(obj, key, { 以上的所有变量/方法,是各个属性各自独立拥有的。

    至此,我们对于 data 上属性的 get/set 封装 就讲完了 。

    如何对数据进行依赖收集?

    断点 ✅ :4074

    updateComponent = function () {
    ✅ :4067  vm._update(vm._render(), hydrating);
    };
    
    ✅ :4074 new Watcher(vm, updateComponent);
    

    分析:Watcher类,这个是第三个核心类,观察者类。和上面说的 Observer[可观擦类]、Dep[观察目标类],总共三个。这个代码片段是在 mounted 钩子之前调用的,也就是我们之前对 data 数据先进行了 get/set 封装之后,就要开始进行 render 了,在 render 之前,需要创建 render 观察者,为了方便我们这里叫它 renderWatcher。除了 renderWatcher,我们还有 computedWatcherwatchWatcher,这两个分别是 计算属性侦听器 观察者,在 Vue 中主要是这三个类型的观察者。

    步入到 new Watcher [观察者类]

    var Watcher = function Watcher (
      vm,
      expOrFn
    ) {
      this.getter = expOrFn;
      this.deps = [];
      this.newDeps = [];
      this.depIds = new Set();
      this.newDepIds = new Set();
    ✅ :4467  this.get();
    };
    

    分析:

    • deps:缓存每次执行观察者函数时所用到的dep所有实例。
    • depIds:缓存每次执行观察者函数时所用到的dep所有实例 id,用于判断。
    • newDeps:存储本次执行观察者函数时所用到的dep所有实例。
    • newDepIds:存储本次执行观察者函数时所用到的dep所有实例 id,用于判断。

    步入到 get 方法

    Watcher.prototype.get = function get () {
    ✅ :4474  pushTarget(this);
      var vm = this.vm;
    ✅ :4478  this.getter.call(vm, vm);
    ✅ :4491  popTarget();
    ✅ :4492  this.cleanupDeps();
    };
    

    分析【这段分析比较详细】:pushTarget 和 popTarget 是一对方法,分别用来记录当前的观察者,和剔除当前观察者

    Dep.target = null;
    var targetStack = [];
    function pushTarget (target) {
      targetStack.push(target);
      Dep.target = target;
    }
    function popTarget () {
      targetStack.pop();
      Dep.target = targetStack[targetStack.length - 1];
    }
    

    Dep.target 为全局唯一的,因为在一个时刻内,就只会有一个观察者函数在执行,把当前的 观察者实例 赋值给 Dep.target, 后续只要访问 Dep.target 就能知道当前的观察者是谁了。

    我们继续步入 this.getter.call(vm, vm),【以下这几个步入我们就简单过一下】

    updateComponent = function () {
    ✅ :4067  vm._update(vm._render(), hydrating);
    };
    

    步入 vm._update(vm._render(), hydrating)

    Vue.prototype._render = function () {
    ✅ :3551  vnode = render.call(vm._renderProxy, vm.$createElement);
    };
    

    步入 render.call(vm._renderProxy, vm.$createElement),在谷歌会新打开一个 tab 用来执行下面这个函数

    (function anonymous(
    ) {
    with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(message))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("change")])])}
    })
    

    关键部分来了,这个是 Vue 的渲染函数。我们现在只要关注,它这里是会读取到 this.message 的,所以会触发 message 的 get 方法,也就是说当前观察者 renderWatcher 依赖了 message ,所以就会开始对它进行 “收集”。

    谷歌浏览器器,直接点击下一步「 ||> 」,

    我们就可以看到光标跳到了 defineReactive$$1 方法内部我们的 get 方法,开始进行“依赖收集” 了

        get: function reactiveGetter () {
          var value = val;
    ✅ :1041  if (Dep.target) {
            dep.depend();
            if (childOb) {
              childOb.dep.depend();
              if (Array.isArray(value)) {
                dependArray(value);
              }
            }
          }
          return value
        },
    

    当前的 Dep.target 是有值的,所以执行 dep.depend 开始进行依赖, 步入 dep.depend

    Dep.protJavaScriptotype.depend = function depend () {
      if (Dep.target) {
    ✅ :731  Dep.target.addDep(this);
      }  
    };
    

    步入 Dep.target.addDep(this)

    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);
        }
      }
    ✅ :4059  };
    

    dep.addSub(this) 把当前的 watcher 实例 push 到 subs 数组,并且判断如果当前 观察者 被 观察目标 添加到 subs 数组里,就不会继续添加,过滤重复数据。 走到这个 4059 断点,控制台打印 dep,如:

    dep = {
      id:3,
      subs:[
        renderWatcher 实例
      ]
    }
    

    跳出继续往下走会调用 4491 popTarget() ,剔除当前 观察者。

    接着步入 this.cleanupDeps()

    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;
    };
    

    这里把 this.deps = this.newDeps,缓存到 deps 里,然后清空newDeps,来做下一次的收集。

    至此,我们就完成了一个 依赖收集 ~

    更新依赖数据如何 notify 观察者做出 update ?

    当用户点击change按钮

    this.message = 'hello world 2';
    

    光标自动跳转至 message 对应的 set 方法,执行 dep.notify() 进行通知观察者进行 update 动作

    Dep.prototype.notify = function notify () {
      for (var i = 0, l = subs.length; i < l; i++) {
    ✅ :745  subs[i].update();
      }
    };
    

    步入 subs[i].update()

    Watcher.prototype.update = function update () {
    ✅ :4543  queueWatcher(this);
    };
    

    步入 queueWatcher()

    function queueWatcher (watcher) {
      var id = watcher.id;
      if (has[id] == null) {
        has[id] = true;
        if (!flushing) {
          queue.push(watcher);
        } else {
          var i = queue.length - 1;
          while (i > index && queue[i].id > watcher.id) {
            i--;
          }
          queue.splice(i + 1, 0, watcher);
        }
        if (!waiting) {
          waiting = true;
    ✅ :4403  nextTick(flushSchedulerQueue);
        }
      }
    }
    

    flushSchedulerQueue 方法

    function flushSchedulerQueue () {
      flushing = true;
      var watcher, id;
     
      queue.sort(function (a, b) { return a.id - b.id; });
    
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        has[id] = null;
    ✅ :4311  watcher.run();
      }
    }
    

    分析[结合上面 queueWatcherflushSchedulerQueue 两个方法]:

    flushSchedulerQueue 方法:

    queue.sort 需要排序是原因:

    确保 watcher 的更新顺序与它们被创建的顺序一致。

    1. 对于父子组件来说,组件的创建顺序是父组件先被创建,然后子组件再被创建,所以父组件的renderWatcher的id是小于子组件的。
    2. 对于用户自定义watcher【watchWatcher】和 renderWatcher,用户自定义watcher是先于组件的renderWatcher被创建的。
    3. 如果子组件在父组件的监视程序运行期间被销毁,则会跳过子组件的watcher。

    queueWatcher 方法:

    1. 这里进行了 watcher id 的重复判断,因为在一个 renderWatch 中可能会依赖多个观察目标,当我们同时改变多个依赖的值 ,经过判断 watcher.id 一样就不用把两次更新 push 到 队列,避免渲染性能消耗,如:
    this.message1 = 'hello world 1';
    this.message2 = 'hello world 2';
    // 更多...      
    

    或 循环改变同一个依赖

    for (let i = 0; i < 10; i++) {
      this.message++;
    }
    
    1. flushing 表示 queue 队列的更新状态,flushing=true 代表队列正在更新中。

    这里的 else 分支,主要是判断一种边界情况, i--,从后往前遍历,其实目的是看刚进入的这个 watcher 在不在当前更新队列中。注意这里的 index 是来自 flushSchedulerQueue 方法内部定义的,是全局的。 我们可以看到跳出 while 的条件为:

    • queue[i].id === watcher.id

    我们可以这样理解,当前在更新一个 id 为 3 的 watcher,然后又进来了一个 watcher,id 也为3。相当于需要重新更新一次 id 为 3 的 watcher,这样才能获取到最新值保证视图渲染正确。用代码解释如:

    // ...
    <div>{{ message }}</div>
    // ...
    
    new Vue({
      el: '#app',
      data: {
        message: 'hello world'
      },
      watch: {
        message() {
          this.message = 'hello world 3';
        }
      },
      methods: {
        handleClick() {
          this.message = 'hello world 2';
        }
      }
    });
    

    点击按钮更新 message 之后,又用 watch 监听其变化,然后在内部再对 message 进行更新,我们试着读一下这段代码的更新流程。首先,用户自定义watcher【watchWatcher】是先于 renderWatcher 被创建的,所以我们在更新 message 的时候,会先执行 watch ,触发到内部方法又更新了一次 message,为了保证视图渲染正确,我们需要在执行一次这个 watcher 的 update。

    • queue[i].id < watcher.id

    分析:更新队列中有 id 为1,2,5 三个 watcher,当前正在更新id为 2 的watcher,当 queueWatcher 被调用并传进来一个 id 为 3 的watcher,于是就将这个 watcher 放到 2 的后面,确保 watcher 的更新顺序与它们被创建的顺序一致。

    我们都知道,flushSchedulerQueue 方法是一个微任务。在对queue操作之后,主程序方法执行完毕之后,开始执行微任务,进行 queue 的调度更新,watcher.run()

    至此,我们就完成了当观察目标改变时通知观察者更新的动作。

    总结

    以上举的例子是一个简单 renderWatcher 的一个流程闭环,依赖收集通知更新。Vue 有renderWatcher【视图观察者】,computedWatcher【计算属性观察者】 和 watchWatcher【侦听器观察者】,主要这三个类型的观察者。

    主要的三个类 Dep【观察目标类】,Observe【可观测类】,Watcher【观察者类】。

    我们可以理解,在依赖被改变的时候通知观察者的一过程,一切都是为了视图渲染,在这过程中会进行一些性能优化 / 处理一些边界情况,最终保证视图渲染的完整性。

    个人觉得源码有点晦涩难懂,但还是得自己多过几遍才能熟悉。这边还是建议亲自阅读几遍源码,看一些他人的总结还是会有点模糊,所以本篇文章提供了 断点 参考。帮助小伙伴快速定位源码比较精髓的位置。

    了解源码的运作,也可以让我们更加的知道,我们需要怎么去调用框架提供的 api 会更加优化。

    后话

    Vue3 已经出来了,我们看完 Vue2 就可以对比看看 Vue3 的更强大之处了, 这边就不再举例 computedWatcherwatchWatcher 了,小伙伴们可以动手 debug 看看 ~ 。

    可以从页面的 initState 方法作为入口:

    function initState (vm) {
      vm._watchers = [];
      var opts = vm.$options;
      if (opts.props) { initProps(vm, opts.props); }
      if (opts.methods) { initMethods(vm, opts.methods); }
      if (opts.data) {
        initData(vm);
      } else {
        observe(vm._data = {}, true /* asRootData */);
      }
    ✅ :4645  if (opts.computed) { initComputed(vm, opts.computed); }
      if (opts.watch && opts.watch !== nativeWatch) {
    ✅ :4647  initWatch(vm, opts.watch);
      }
    }
    

    感兴趣的小伙伴也可以 debug 看 computed 这种场景

    computed: {
      c1() {
        return this.c2 + 'xxx';
      },
      c2() {
        return this.message + 'xxx';
      }
    }
    

    computed 是 “lazy” 的,它不参与 queue 的更新,而是如果在模板上有用到 computed 属性,才会去进行获取计算后的值。


    起源地下载网 » 跟我一起读源码丨Vue源码之依赖收集

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元