Vue源码学习(一):基本流程

在看Vue的源码之前,先来了解一个概念 :虚拟节点。

前端发展很多年,直到出现了虚拟DOM,才可以从操作DOM解脱出来。

JQuery的出现,简化了操作DOM的过程,但是还是摆脱不了操作DOM。

而虚拟DOM的目的是,使用虚拟节点代替真实节点,所有操作都发生在虚拟节点,然后通过diff算法对比新旧两棵虚拟DOM,计算出更新真实DOM的最少操作,由框架代替用户执行这些操作,所以用户可以把大量的精力放在业务逻辑上

vue中的VNode

VueJS的虚拟DOM是基于开源Snabbdom的。

Snabbdom

基本使用:

var snabbdom = require('snabbdom');
var patch = snabbdom.init([
    require('snabbdom/modules/class').default,
    require('snabbdom/modules/props').default,
    require('snabbdom/modules/style').default,
    require('snabbdom/modules/eventlisteners').default
]);

var h = require('snabbdom/h').default;

var container = document.getElementById('container');

var vnode = h('div#container.two.classes', { on: { click: onClick } }, [
    h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
    ' and this is just normal text',
    h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
]);

patch(container, vnode);

function onClick() {
    console.log('点击');
}

更新节点:

var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
  h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

patch比较新旧的虚拟节点,直接修改dom树。

主要就是调用h函数创建虚拟节点vnode,创建好后调用patch函数生成真实的dom节点。

Vue初始化流程

Vue最基本的使用:




    



    

{{message}}

Vue源码学习(一):基本流程_第1张图片

刷新页面,来到Vue的初始化方法。往下走,进入_init()方法。如上图所示这个方法是在在initMix函数中定义的,继续往下。

Vue源码学习(一):基本流程_第2张图片

来到了_init()方法,开始处理options。options是我们创建Vue的时候传进来的:

Vue源码学习(一):基本流程_第3张图片

继续看这段代码:

      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // 取出构造函数中的Options 也就是Vue.options
        options || {},
        vm
      )

resolveConstructorOptions函数取出Vue构造函数的options,其实就是Vue.options,再和传进来的参数进行合并。而Vue.options在哪定义的呢?详细可以看这里:人人都能懂的Vue源码系列—03—resolveConstructorOptions函数-上,是在src/platforms/web/runtime/index.js中定义的。

Vue.options的默认值:


Vue源码学习(一):基本流程_第4张图片

来到mergeOptions方法:

Vue源码学习(一):基本流程_第5张图片

关键函数:

    function mergeField (key) {
      var strat = strats[key] || defaultStrat;
      options[key] = strat(parent[key], child[key], vm, key);
    }

strats定义了一系列方法来处理这些参数,我们先看看strats.data:

Vue源码学习(一):基本流程_第6张图片
Vue源码学习(一):基本流程_第7张图片

最后是返回了一个函数,这个函数在什么时候被调用呢?

Vue源码学习(一):基本流程_第8张图片

可以在Call Stack里面看到是在初始化data的时候调用的。这个后面再详细看。

处理完options,开始初始化Proxy:initProxy。Proxy是啥?ES6中的Proxy,可以把某个对象包起来,得到一个代理对象,调用这个代理对象的值会间接调用原始对象的值,因此可以在调用的过程中做一些处理,比如当没有这个值的时候返回一个默认值等。详细可看理解Javascript的Proxy。Proxy的基本使用:

let data = {}
let dataProxy = new Proxy(data, {
    get(obj, prop) {
        if (prop === 'name') {
            if (!obj.name) {
                return 'Anonymous'
            }
        }
        return obj[prop]
    }
})
console.log(dataProxy.name)

那Vue这里的initProxy要做什么呢?

Vue源码学习(一):基本流程_第9张图片

initProxy里面会定义一个属性:_renderProxy。如果当前环境不支持Proxy,那么_renderProxy直接就是vm本身。前面也提到,Proxy就是让直接调用变成间接调用,然后在中间做些额外处理。

hasProxy源码:

    var hasProxy =
      typeof Proxy !== 'undefined' && isNative(Proxy);
  
  function isNative (Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
  }

确保Proxy是原生代码,没有被修改。

hasHandler的源码:

    var hasHandler = {
      has: function has (target, key) {
        var has = key in target;
        var isAllowed = allowedGlobals(key) ||
          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
        if (!has && !isAllowed) {
          if (key in target.$data) { warnReservedPrefix(target, key); }
          else { warnNonPresent(target, key); }
        }
        return has || !isAllowed
      }
    };

has方法隐藏某些属性,不被in运算符发现。当查看vm是否存在某个属性时会来到这个方法。这里主要的作用就是发一些警告。

人人都能懂的Vue源码系列—07—initProxy

继续往下,来到initState()方法


Vue源码学习(一):基本流程_第10张图片
  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 */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

先看initData()方法


Vue源码学习(一):基本流程_第11张图片
Vue源码学习(一):基本流程_第12张图片

这里就会调用前面提到的mergedInstanceDataFn()方法取出data。接着判断data、props、methods里面有没有重名的。最后就是重头戏,监听对象:

observe(data, true /* asRootData */);

...

  /**
   * Attempt to create an observer instance for a value,
   * returns the new observer if successfully observed,
   * or the existing observer if the value already has one.
   */
  function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

其他的先忽略,直接到创建Observer这里:

  /**
   * Observer class that is attached to each observed
   * object. Once attached, the observer converts the target
   * object's property keys into getter/setters that
   * collect dependencies and dispatch updates.
   */
  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };

这里首先把被观察的对象保存在Observer中,然后创建一个Dep,这个很重要,后面详细说。接着给对象定义一个变量__ob__保存Observer,所以引用关系变成了:
Observer -> value -> Object -> __ob__ -> Observer

注意这个Object还保存在vm的_data中。

总之经过这一层vm的data多了一个属性ob,它就是Observer。

接下来看看this.walk(value):

Vue源码学习(一):基本流程_第13张图片

遍历obj的key然后调用defineReactive$$1方法。

  /**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      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
      },
      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();
      }
    });
  }

这段代码还挺长的,但主要工作就是给obj定义key属性,并重写它的get和set方法。开头还定义了一个Dep对象,它主要用来做依赖收集以及发通知的。当get方法被调用的时候,如果Dep.target不为空,就会调用dep.depend()收集起来,这里的Dep.target通常就是Watcher。当组件第一次渲染的时候,会把Dep.target设置为被渲染组件的Watcher,在渲染的过程中调用到get方法时,Watcher就会被收集到该变量的dep中。当setter方法被调用时再通知给dep下面的所有watcher,watcher就会调用update方法。

到这里data初始化结束。

接下来调用$mount方法。

Vue源码学习(一):基本流程_第14张图片
  var mount = Vue.prototype.$mount;
  Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
    el = el && query(el);

    /* istanbul ignore if */
    if (el === document.body || el === document.documentElement) {
      warn(
        "Do not mount Vue to  or  - mount to normal elements instead."
      );
      return this
    }

    var options = this.$options;
    // resolve template/el and convert to render function
    if (!options.render) {
      var template = options.template;
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
            /* istanbul ignore if */
            if (!template) {
              warn(
                ("Template element not found or is empty: " + (options.template)),
                this
              );
            }
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          {
            warn('invalid template option:' + template, this);
          }
          return this
        }
      } else if (el) {
        template = getOuterHTML(el);
      }
      if (template) {
        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile');
        }

        var ref = compileToFunctions(template, {
          outputSourceRange: "development" !== 'production',
          shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;

        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile end');
          measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
        }
      }
    }
    return mount.call(this, el, hydrating)
  };

这里先取出被挂载到的元素。往下走,options.render和options.template都为空,所以通过getOuterHTML(el)取出Html字符串,它会通过compileToFunctions被转换成render方法:

Vue源码学习(一):基本流程_第15张图片

        (function anonymous(
        ) {
            with (this) { return _c('div', { attrs: { "id": "app" } }, [_c('p', [_v(_s(message))])]) }
        })

是不是很像Snabbdom创建虚拟节点的方法。其实render方法就是创建虚拟节点的。我们写vue文件的template其实也会被转成render函数,不过是在本地编译的时候就转好了。

接下来调用mount函数。


Vue源码学习(一):基本流程_第16张图片
Vue源码学习(一):基本流程_第17张图片

接下来是关键的代码。updateComponent调用vm的_render。接下来创建Watcher。

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
...
  /**
   * A watcher parses an expression, collects dependencies,
   * and fires callback when the expression value changes.
   * This is used for both the $watch() api and directives.
   */
  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    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$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    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();
  };

expOrFn即updateComponent,this.getter = expOrFn = updateComponent。 在Watcher的构造函数最后调用了get方法。get方法的定义:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

pushTarget(this)把Dep.target设置为当前Watcher。接着调用this.getter,也就是updateComponent,也就是vm._update。

vm._update(vm._render(), hydrating);

_render函数会创建虚拟节点。


Vue源码学习(一):基本流程_第18张图片

这里的render就是前面把Html编译成的函数。拿到虚拟节点后传给_update。


__patch__会比对前后虚拟节点的最小差异创建新的dom节点并进行替换。

彻底理解Vue中的Watcher、Observer、Dep

你可能感兴趣的:(Vue源码学习(一):基本流程)