新人看Vue2源码(二)

285 阅读5分钟

前言

疯狂搬砖了几个星期,每次坐到电脑前都觉得好累,精力被消耗,热情被消磨...这时候才逐渐意识到了已经做了社畜几个月了,不是当年那个充满精力,在图书馆学啥就学啥的热血青年。

src/core/instance/lifecycle.js

还记得src/platforms/web/runtime/index.js中有这么一段代码吗

//  实现一个$mount方法,调用mountComponent
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

其中的 mountComponent 具体做了什么在上一篇中我并没有分析它,在本篇中会展开说

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    // 如果没有render就赋值为一个创建空vdom的函数
    vm.$options.render = createEmptyVNode;
  }
  
  //  调用beforeMount钩子
  callHook(vm, "beforeMount");

  //  组件更新函数声明
  let updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && config.performance && mark) {
    ...
    };
  } else {
    updateComponent = () => {
      //  首先执行render -> vdom
      //  然后_update将vdom转为dom
      vm._update(vm._render(), hydrating);
    };
  }

  // 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
  
  //  新建一个watcher,立即调用一次updateComponent
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          //  调用beforeUpdate
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, "mounted");
  }
  return vm;
}

总结:

  • 调用 beforeMount
  • 声明组件更新函数 updateComponent
  • 新建一个watcher,并立即调用一次 updateComponent
  • 如果 $vnode 为null,说明已经将虚拟dom转换为真实dom,此时调用 mounted 函数

我们再来看一下vm._update的函数干了什么

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this;
    const prevEl = vm.$el;
    const prevVnode = vm._vnode;
    const restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.

    //  初始化是没有preVnode
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // updates
      // diff
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

总结:

  • 判断当前处理的元素是否有 _vnode 的选项
    • 没有的话,通过 _patch 初始化渲染,将处理后的真实dom挂载到$el选项中
    • 有的话,通过 _patch 将之前的旧vnode和当前的新vnode进行diff操作,最后挂载到$el

src/core/vdom/patch.js

接下来就是看这 __patch__ 函数做了什么

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    //  一开始初始化的时候没有vnode,进去的是真实元素,走的第一个if
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }
        
        
        // oldElm指的是宿主元素
        // replacing existing element
        const oldElm = oldVnode.elm
        
        // parentElm指的是宿主元素的父元素
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // 在宿主元素的兄弟节点创建一个解析好的真实dom元素
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        // 删除掉旧的元素
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

这个过程可以debugger的时候看到

<body>
    <div id="app">
      <h2>初始化</h2>
      <div>
        <p>counter --- {{counter}}</p>
      </div>
    </div>
  </body>
  <script src="../../dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app",
      data: {
        counter: 1,
      },
    });

    window.app = app;
  </script>

image.png

总结:

  • patch 函数更新的方式是通过 vnode 在宿主元素的兄弟节点插入一个解析好的真实dom,然后把旧的dom移除

吐槽

关于 new Vue() 发生了什么大体上已经知道了流程,但是更具体的做了什么,这点还需要时间去研究,毕竟不知道开发团队当时的设计和思路啥的,盲人摸象式的阅读还是挺难受的。。希望有经验的大佬务必能给点建议

initState

    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, "beforeCreate");
    initInjections(vm); // resolve injections before data/props
    
    //  data/props/methods/computed/watch
    initState(vm);
    
    initProvide(vm);

还记得上节 initMixin 中看到的几个初始化函数吗,我们最常用的组件状态都是在 initState 中,而且可以推测到这个函数与响应式有关,所以我们就了解这个函数吧。

export function initState (vm: Component) {
  vm._watchers = []
  const 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)
  }
}

小结:

  • 初始化状态的顺序 props > methods > data > computed > watch
  • 小提示:不知道大家有没有试过 props 里和 data 里的属性重名,结果搞出 bug 的情况,这时候就可以大概推测到他们之间有个顺序关系,这个名字被占了就不会被后面的覆盖

再来看initData

function initData (vm: Component) {
  let data = vm.$options.data
  
  // 判断是否是函数,函数就执行获取里面的数
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    
  // 判断是否是纯对象,警告
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    
    // 下面两个if是判断是否有和props和method中重名的属性,警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 响应式
  observe(data, true /* asRootData */)
}

小结:

  • 判断是否是 data 选项是否是函数,函数就执行获取真实的对象
  • 检测是否是纯对象,非纯对象进行警告
  • 遍历属性,如果有propsmethods中重名的属性,进行一个警告
  • 响应式处理对象

响应式处理

响应式处理嘛,大家都知道vue2用的 Object.defineProperty,自己写也能写出来,但是我还没见过源码是怎么处理的,多了哪些细节,现在就来看一下吧。

export function observe(value: any, asRootData: ?boolean): Observer | void {
  //  属性是非对象或者是vnode的直接跳过
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  
  // 如果经过响应式处理过了,直接取__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;
}

小结:

  • observe 对非对象和vnode 不生效
  • 已经经过响应式处理的对象会有 __ob__ 标识,并直接返回 __ob__
  • 数组,纯对象会进行响应式的处理
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  //  此处dep的目的:
  //  如果使用Vue.set或者delete添加或删除属性,负责通知更新
  //  举个例子,你直接删除某个属性,defineProperty无法拦截到
  constructor(value: any) {
    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);
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  // 针对普通对象,遍历每个key进行defineReactive
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  /**
   * Observe a list of Array items.
   */
  // 针对数组
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

总结:

  • 对于需要响应式处理的对象,会生成一个 dep 与其对应。这个 dep 在对象添加或删除属性的时候负责通知。
  • 在对象中添加了__ob__属性,值为Observer实例
  • 根据对象是普通对象还是数组进行分开的处理

普通对象的响应式处理

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //  创建key和dep一一对应的关系
  const dep = new Dep();

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

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

  //  对于嵌套对象进行递归的响应式拦截
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;

      //  如果存在,说明此处调用触发者是一个Watcher实例
      //  dep 与 watcher 是 n对n 的关系
      if (Dep.target) {
        //  建立dep 和 Dep.target之间的依赖关系
        dep.depend();
        if (childOb) {
          //  建立ob内部dep和Dep.target之间依赖关系
          childOb.dep.depend();
          //  如果是数组,数组内部所有项目都要做相同处理
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const 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 (process.env.NODE_ENV !== "production" && 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();
    },
  });
}

针对ObserverDep我们来看个例子,理清楚调用链是怎样的

const app = new Vue({
  el: "#app",
  data: {
    obj: {
      a: 'asd'
    }
  },
});

首先

initData() -> observe({obj}) -> ob = new Observer({obj}) -> Observer中this.dep = new Dep() 这时创建了一个ob, 一个dep

然后

walk({obj}) -> keys = ["obj"]; 对keys中每个key进行defineReactive -> defineReactive(obj,key)中 dep = new Dep() 这时又创建了一个dep

之后

var childOb = !shallow && observe(val); 其中val = {a: "asd"} 再重复执行observe

最后

我们可以知道这个例子中有 2个ob,4个dep

数组如何做的响应式

我们都知道,Vue2 中响应式拦截用的是 Object.defineProperty,这个方法拦截不到 push/pop/splice/... 这些方法,所以 Vue2 中是在原来这些方法的基础上做了些处理。接下来我们一起来看下他们是怎么做的。


// can we use __proto__?
export const hasProto = '__proto__' in {}

//  获取原型
const arrayProto = Array.prototype

//  复制一份
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  
  //  重写原来的方法,用一个mutator函数替代
  def(arrayMethods, method, function mutator (...args) {
    
    //  先执行原本的方法
    const result = original.apply(this, args)
    
    //  获取响应式对象
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    
    // 通过push/unshift/splice添加的元素也需要进行响应式的处理
    if (inserted) ob.observeArray(inserted)
    // notify change
    //  通知更新
    ob.dep.notify()
    return result
  })
})

class Observer {
    if (Array.isArray(value)) {
      //  判断浏览器是否有__proto__,有的话可以重写原型
      //  没有的话。。我也不知道哪些浏览器会没有这玩意,总之会直接把那几个方法定义到数组中去
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
          copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
}
    


/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
 
 //  替换原型
function protoAugment(target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src;
  /* eslint-enable no-proto */
}

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}

总结

本来还想写一下WatcherObserverDep 的,但是再写下去感觉太长了,而且不好整理...就把它移到下一篇中说吧。


问: 组件执行 $mount 的时候做了哪些事情

答:首先执行beforeMount,然后声明组件更新函数 updateComponent,然后新建一个watcher并调用这个updateComponent将解析好的真实dom挂载到宿主元素上,最后执行mounted


问:patch函数做完diff后,是怎么处理旧的dom

答:patch 函数更新的方式是通过 vnode 在宿主元素的兄弟节点插入一个解析好的真实dom,然后把旧的dom移除


问:如果propsdatamethods中有同名的属性foo,那么访问 this.foo其实是访问这三个选项中的哪个

答:因为在 initState 中执行顺序是 props > methods > data,所以访问的是this.foo = this.props.foo


问:Vue2 中数组是如何做的响应式处理

答:先判断对象是否有__proto__属性,有的话先通过Object.create将其复制下来,然后针对七个改变数组的方法重写,分别是push/pop/unshift/shift/splice/sort/reserve。复写的方式是先调用数组本身对应的方法,然后对push/unshift/splice这三个添加的元素进行响应式处理,最后通知更新。如果没有__proto__的话,就直接往数组实例中塞入这七个改写后的方法。