【闲谈Vue1.0】- 2.instance - Vue实例(1)

文件结构

|- api  ---------------------- 对外暴露的API
|    data.js  ---------------- 数据相关方法
|    dom.js  ----------------- DOM相关方法
|    events.js  -------------- 事件相关方法
|    lifecycle.js  ----------- 生命周期
|- internal  ----------------- 内部使用的方法
|    events.js  -------------- 事件
|    init.js  ---------------- 初始化
|    lifecycle.js  ----------- 生命周期
|    state.js  --------------- 状态
|    misc.js  ---------------- 未知
|- vue.js  ------------------- 入口

instance中分为两部分,对外暴露接口的api和基于mixin提供内部方法的internal。由于这个内容比较多,就分成两章来说明。我们就先来看api。


数据操作API - data.js

$get( ) , $set( ) - 设置,获取属性值

Vue.prototype.$get = function (exp, asStatement) {
  // @parsers
  // 转换属性表达式
  // 返回一个这样的对象
  // {
  //   传进来的表达式
  //   exp: 'a.b.c',
  //   读取这个属性的值的方法
  //   get: function() ...,
  //   设置这个属性的值的方法
  //   set: function() ...
  // }
  var res = parseExpression(exp)
  if (res) {
    if (asStatement) {
      var self = this
      return function statementHandler () {
        ...
        var result = res.get.call(self, self)
        ...
        return result
      }
    } else {
      ...
      return res.get.call(this, this)
      ...
    }
  }
}

Vue.prototype.$set = function (exp, val) {
  var res = parseExpression(exp, true)
  if (res && res.set) {
    res.set.call(this, this, val)
  }
}

$delete( ) - 删除属性值

Vue.prototype.$delete = function (key) {
  // @util/lang
  // 用于删除vm上的属性。
  del(this._data, key)
}

$watch( ) - 监控属性值

Vue.prototype.$watch = function (expOrFn, cb, options) {
  var vm = this
  var parsed
  // 解析表达式
  if (typeof expOrFn === 'string') {
    // @parsers/directive
    // 比如解析返回以下格式
    // {
    //   // 表达式
    //   expression: 'a + 1',
    //   // 以及跟表达式的过滤器
    //   filters: [
    //     { name: 'uppercase', args: null }
    //   ]
    // }
    parsed = parseDirective(expOrFn)
    expOrFn = parsed.expression
  }
  // 解析完成后,给这个表达式开启一个观察者Watcher。
  var watcher = new Watcher(..., expOrFn, ...)
  ...
  return function unwatchFn () {
    watcher.teardown()
  }
}

$eval( ) - 解析表达式指令获取指

Vue.prototype.$eval = function (text, asStatement) {
  // 这里的话,个人觉得没有必要对filterRE的判断。
  // 因为如果filter没有的话,dir.filters是为空的。
  // 统一逻辑能让代码结构更加清晰。
  // 可能是性能考虑才在这里加上这个预判断的。
  if (filterRE.test(text)) {
    var dir = parseDirective(text)
    var val = this.$get(dir.expression, asStatement)
    return dir.filters
      ? this._applyFilters(val, null, dir.filters)
      : val
  } else {
    return this.$get(text, asStatement)
  }
}

$interpolate( ) - 解析模板

Vue.prototype.$interpolate = function (text) {
  // @parsers/text
  // 负责解析模板,比如会将'{{msg}} world'解析成下面这个数组
  // [{
  //   // 内容是否是html
  //   html: false,
  //   // 开头是否为 * ,表示只绑定一次
  //   oneTime: false,
  //   // 是否是标签
  //   tag: true,
  //   // 标签里的变量值
  //   value: "msg"
  // },
  // {
  //   value: "world"
  // }]
  var tokens = parseText(text)
  var vm = this
  if (tokens) {
    if (tokens.length === 1) {
      return vm.$eval(tokens[0].value) + ''
    } else {
      return tokens.map(function (token) {
        // 这里直接调的$eval
        // 所以支持了嵌套属性
        return token.tag
          ? vm.$eval(token.value)
          : token.value
      }).join('')
    }
  } else {
    return text
  }
}

$log( ) - 打印实例属性信息

Vue.prototype.$log = function (path) {
  var data = path
    ? getPath(this._data, path)
    : this._data
  if (data) {
    // clean是本模块内的,内置方法
    // 功能是清除data对象上所有的getter/setter
    // 实现的比较巧妙,序列化后反序列化
    data = clean(data)
  }
  // 如果属性为空,则还将computed和props打印出来
  if (!path) {
    ...
  }
  console.log(data)
}

DOM操作API - dom.js

$nextTick( ) - DOM更新后的回调

Vue.prototype.$nextTick = function (fn) {
  // @util
  // dom更新后,将调用fn
  nextTick(fn, this)
}

$appendTo( ) - 将元素插到另一个元素内部结尾处

Vue.prototype.$appendTo = function (target, cb, withTransition) {
  // insert是本模块内部抽象出的一个核心方法
  // 抽象程度比较高
  // 后续的其他方法也依赖于它,这里着重说明下,每个参数的意思
  // vm:Vue实例
  // target:目标组件
  // cb:回调
  // withTransition:是否有过渡动画
  // append:
  //         withTransition为true时的操作
  //         append的实现较简单(本模块内部实现),不赘述
  // appendWithTransition:
  //         withTransition为false的操作
  //         带过渡的dom操作的方法的实现基本都依赖于transition模块
  return insert(
    this, target, cb, withTransition,
    append, appendWithTransition
  )
}

$before( ) - 将元素插到另一个元素的前面

Vue.prototype.$before = function (target, cb, withTransition) {
  // 基本同上
  // beforeWithCb:
  //         除了特别简单的dom操作在本模块内部直接实现
  //         比如上面的append。
  //         其他真正对于dom操作的方法都是依赖于util/dom模块
  //         比如before,remove等
  return insert(
    ..., beforeWithCb, beforeWithTransition
  )
}

$after( ) - 将元素插到另一个元素的后面

Vue.prototype.$after = function (target, cb, withTransition) {
  target = query(target)
  if (target.nextSibling) {
    // 目标不是最后一个Node的case
    // 则插入到下一个Node的前面
    this.$before(target.nextSibling,...)
  } else {
    // 目标是最后一个Node的case
    // 则通过父节点插入
    this.$appendTo(target.parentNode,...)
  }
  return this
}

$remove( ) - 移除元素

Vue.prototype.$remove = function (cb, withTransition) {
  ...
  if (this._isFragment) {
    // @util/dom
    // 为fragment时,则删除系列节点
    removeNodeRange(...)
  } else {
    // 为dom时,直接删除节点
    var op = withTransition === false
      ? removeWithCb
      : removeWithTransition
    op(...)
  }
  return this
}

事件操作API - events.js

$on( ) - 监听事件

Vue.prototype.$on = function (event, fn) {
  // 将注册的事件回调推入回调队列
  (this._events[event] || (this._events[event] = []))
    .push(fn)
  // 修改注册事件的回调数量
  // 不仅自身的回调数量+1
  // 递归其祖先vm,其回调数量都+1
  // 这个数量的同步,在后续的broadcast中价值就体现出来了
  modifyListenerCount(this, event, 1)
  return this
}

$on( ) - 监听事件,仅一次

Vue.prototype.$once = function (event, fn) {
  var self = this
  function on () {
    // 删除回调
    self.$off(event, on)
    // 执行方法
    fn.apply(this, arguments)
  }
  // 在$off中,有一条策略是根据cb的fn属性匹配,来删除回调的
  on.fn = fn
  this.$on(event, on)
  return this
}

$off( ) - 删除事件回调

Vue.prototype.$off = function (event, fn) {
  // 删除事件回调有几种策略
  var cbs
  // case 1:参数为空时,删除所有事件及其回调
  if (!arguments.length) {
    ...
    return this
  }
  // case 2:事件压根都没有注册过,直接返回
  cbs = this._events[event]
  if (!cbs) {
    return this
  }
  // case 3:参数只传了事件名,则删除该事件的所有回调
  if (arguments.length === 1) {
    ...
    return this
  }
  // case 4:删除指定事件的某个回调
  var cb
  var i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      ...
      break
    }
  }
  return this
}

$emit( ) - 触发事件

Vue.prototype.$emit = function (event) {
  var isSource = typeof event === 'string'
  // ~_~ 对象形式的传值,没有暴露给外面,内部使用。
  event = isSource
    ? event
    : event.name
  var cbs = this._events[event]
  ...
  if (cbs) {
    ...
    var args = toArray(arguments, 1)
    // 将回调队列里的方法依次执行
    for (var i = 0, l = cbs.length; i < l; i++) {
      var cb = cbs[i]
      var res = cb.apply(this, args)
      ...
    }
  }
  return shouldPropagate
}

$broadcast( ) - 广播事件

Vue.prototype.$broadcast = function (event) {
  var isSource = typeof event === 'string'
  event = isSource
    ? event
    : event.name
  // 性能优化点,_eventsCount价值体现出来了
  // 组件以及其子孙组件,都没有注册事件
  // 则直接略过
  if (!this._eventsCount[event]) return
  var children = this.$children
  var args = toArray(arguments)
  if (isSource) {
    // 这里对参数进行包装的目的应该是
    // 区分信号来源,如果是对象形式的,都是用于内部传递的
    args[0] = { name: event, source: this }
  }
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i]
    var shouldPropagate = child.$emit.apply(child, args)
    // 判断是否允许传播
    if (shouldPropagate) {
      // 向下递归广播
      child.$broadcast.apply(child, args)
    }
  }
  return this
}

$dispatch( ) - 向上冒泡事件

Vue.prototype.$dispatch = function (event) {
  ...
  var parent = this.$parent
  var args = toArray(arguments)
  args[0] = { name: event, source: this }
  // 向上递归触发
  while (parent) {
    // 判断是否允许传播
    shouldPropagate = parent.$emit.apply(parent, args)
    parent = shouldPropagate
      ? parent.$parent
      : null
  }
  return this
}

生命周期API - lifecycle.js

$mount( ) - 安装组件

Vue.prototype.$mount = function (el) {
  ...
  el = query(el)
  if (!el) {
    el = document.createElement('div')
  }
  // @internal/lifecycle
  // 用来编译节点
  // 这里面比较复杂,到后面在详细说明。
  this._compile(el)
  // 省略安装hooks
  ...
  return this
}

$destroy( ) - 销毁组件

Vue.prototype.$destroy = function (remove, deferCleanup) {
  // @internal/lifecycle
  // 用来删除节点
  this._destroy(remove, deferCleanup)
}

$compile( ) - 编译片段

Vue.prototype.$compile = function (el, host, scope, frag) {
  // @internal/lifecycle
  // 用来编译片段
  return compile(el, this.$options, true)(
    this, el, host, scope, frag
  )
}
*****
Written by Zheng Yu on 30 November 2016