乐趣区

关于javascript:Vue源码学习初始化更新流程分析

本篇文章通过一个栗子🌰来聊聊 Vue 初始化和更新数据的大抵流程:

<div id="demo">
    <child :list="list"></child>
    <button @click="handleAdd">add</button>
</div>
<script>
    Vue.component('child', {
        props: {
            list: {
                type: Array,
                default: () => []
            }
        },
        template: '<p>{{list}}</p>'
    })

    new Vue({
        el: "#demo",
        data() {
          return {list: [1,2]
          }
        },
        methods: {handleAdd() {this.list.push(Math.random())
            }
        }
    })
</script>

很简略的例子,一个父组件一个子组件,子组件承受一个 list,父组件有个按钮,能够往 list 里 push 数据扭转 list。

初始化流程:

  1. 首先从 new Vue({el: "#app"}) 开始,会执行 _init 办法。

    function Vue (options) {
       // 省略...
       this._init(options)
    }
  2. _init 办法的最初执行了 vm.$mount 挂载实例。

    Vue.prototype._init = function (options) {
       var vm = this;
       // 省略...
       if (vm.$options.el) {vm.$mount(vm.$options.el);
       }
    }
  3. 如果此时运行的版本是 runtime with compiler 版本,这个版本的 $mount 会被进行重写,减少了把 template 模板转成 render 渲染函数的操作,但最终都会走到 mountComponent 办法。

    Vue.prototype.$mount = function (el, hydrating) {el = el && inBrowser ? query(el) : undefined;
         return mountComponent(this,el,hydrating);
    };
    
    var mount = Vue.prototype.$mount; // 缓存上一次的 Vue.prototype.$mount
    
    Vue.prototype.$mount = function (el, hydrating) { // 重写 Vue.prototype.$mount
         // 省略,将 template 转化为 render 渲染函数
         return mount.call(
           this,
           el,
           hydrating
         )
    };
  4. mountComponent 里触发了 beforeMountmounted 生命周期,更重要的是创立了 Watcher,传入的 updateComponent 就是 Watcher 的 getter

    function mountComponent(vm, el, hydrating) {
         // 执行生命周期函数 beforeMount
         callHook(vm, 'beforeMount');
    
         var updateComponent;
         // 如果开发环境
         if ("development" !== 'production' && config.performance && mark) {// 省略...} else {updateComponent = function () {
                 vm._update(vm._render(), // 先执行_render, 返回 vnode
                     hydrating
                 );
             };
         }
    
         new Watcher(
             vm,
             updateComponent,
             noop,
             null,
             true // 是否渲染过得观察者
         );
        
         if (vm.$vnode == null) {
             vm._isMounted = true;
             // 执行生命周期函数 mounted
             callHook(vm, 'mounted');
         }
         return vm
     }
  5. 在创立 Watcher 时会触发 get() 办法,pushTarget(this)Dep.target 设置为以后 Watcher 实例。

    function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {if (typeof expOrFn === 'function') {this.getter = expOrFn;}
       this.value = this.lazy ?  // 这个有是组件才为真
           undefined :
           this.get(); // 计算 getter,并从新收集依赖项。获取值};
    
     Watcher.prototype.get = function get() {pushTarget(this);
         var value;
         var vm = this.vm;
         try {value = this.getter.call(vm, vm);
         } catch (e) { } finally {popTarget();
         }
         return value
     };
  6. Watcherget() 里会去读取数据,触发 initData 时应用 Object.defineProperty 为数据设置的 get,在这里进行依赖收集。咱们晓得 Vue 中每个响应式属性都有一个 __ob__ 属性,寄存的是一个 Observe 实例,这里的 childOb 就是这个 __ob__,通过 childOb.dep.depend() 往这个属性的 __ob__ 中的 dep 里收集依赖,如下图。

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: Function
    ) {
      /* 在闭包中定义一个 dep 对象 */
      const dep = new Dep()
    
      let childOb = observe(val)
      Object.defineProperty(obj, key, {
       enumerable: true,
       configurable: true,
       get: function reactiveGetter () {
         /* 如果本来对象领有 getter 办法则执行 */
         const value = getter ? getter.call(obj) : val
         if (Dep.target) {
           /* 进行依赖收集 */
           dep.depend()
           if (childOb) {childOb.dep.depend()
           }
           if (Array.isArray(value)) {dependArray(value)
           }
         }
         return value
       },
       set: function reactiveSetter (newVal) {}})
    }
  7. 在咱们的例子中,这个 list 会收集两次依赖,所以它 __ob__ 的 subs 里会有 两个 Watcher,第一次是在父组件 data 中的 list,第二次是在创立组件时调用 createComponent,而后又会走到 _init => initState => initProps,在 initProps 内对 props 传入的属性进行依赖收集。有两个 Watcher 就阐明 list 扭转时要告诉两个中央,这很好了解。
    .
  8. 最初,触发 getter,下面说过 getter 就是 updateComponent,外面执行 _update 更新视图。

上面来说说更新的流程:

  1. 点击按钮往数组中增加一个数字,在 Vue 中,为了监听数组变动,对数组的罕用办法做了重写,所以先会走到 ob.dep.notify() 这里,ob 就是 list 的 __ob__ 属性,下面保留着 Observe 实例,外面的 dep 中有两个 Watcher,调用 notify 去告诉所有 Watcher 对象更新视图。

    [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    .forEach(function (method) {const original = arrayProto[method]
     def(arrayMethods, method, function mutator () {
       let i = arguments.length
       const args = new Array(i)
       while (i--) {args[i] = arguments[i]
       }
       /* 调用原生的数组办法 */
       const result = original.apply(this, args)
    
       const ob = this.__ob__
       let inserted
       switch (method) {
         case 'push':
           inserted = args
           break
         case 'unshift':
           inserted = args
           break
         case 'splice':
           inserted = args.slice(2)
           break
       }
       if (inserted) ob.observeArray(inserted)
    
       /*dep 告诉所有注册的观察者进行响应式解决 */
       ob.dep.notify()
       return result
     })
    })
  2. notify 办法里去告诉所有 Watcher 更新,执行 Watcherupdate 办法,update 里的 queueWatcher 过滤了一些反复的 Watcher, 但最终会走到 Watcherrun() 办法。

    Dep.prototype.notify = function notify() {var subs = this.subs.slice();
       for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}
    };
    
    Watcher.prototype.update = function update() {if (this.lazy) {this.dirty = true;} else if (this.sync) {this.run();
     } else {queueWatcher(this);
     }
    };
  3. run 办法里会调用 get(), get 办法里回去触发 Watcher 的 getter,下面说过,getter 就是 updateComponent

    Watcher.prototype.run = function run() {if (this.active) {
     /* get 操作在获取 value 自身也会执行 getter 从而调用 update 更新视图 */
     const value = this.get()}
    }
    
    updateComponent = function () {
      vm._update(vm._render(),
          hydrating
      );
     };
  4. 最初在 _update 办法中,进行 patch 操作,patch 里的具体逻辑就不在这里说了,有趣味的小伙伴能够去看看我的另一篇文章《Vue 源码学习 - 虚构 DOM+Diff 算法》。

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~

退出移动版