阅读vue源码(2)

481 阅读3分钟

new Vue

创建一个Vue实例只执行了这一个方法,但是这里面有一大堆方法...

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    vm._self = vm
    // 初始化实例的各种数据
    initLifecycle(vm)
    // 添加各种事件
    initEvents(vm)
    // 在实例上添加createElement
    initRender(vm)
    callHook(vm, 'beforeCreate')
    // 获取inject,并对其observe
    initInjections(vm) // resolve injections before data/props
    // 这里处理的数据就多了,data、methods都有
    initState(vm)
    // 将project挂载到实例的_provided上
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    ...
    // 这个el就是options里的那个宿主元素
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

这里可以稍总结下:beforeCreate之前都是在初始化数据之外的各种方法,beforeCreate和created之间都在响应化数据

initLifecycle

export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent

// 有parent的就不是root节点,在parent的children中push一个当前实例
if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}

// 定义根节点
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

// 以下就是清空各种数据
vm.$children = []
vm.$refs = {}
vm._watcher = null
...
}

initRender

export function initRender (vm: Component) {
  ...
  // 主要用在render函数的 with(this){} 里边的_c就是这里,与用户无关
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // render(h){},里的h就是这里,就是经常提到的渲染函数
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 
  // 使用科里化定义上边两个方法,目的就是保留当前的实例,以便在with(this)中使用
  ...
}

initInjections

这里的执行顺是在initData之前的,源码中也有说明

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    // 用defineReactive方法对inject每个属性进行数据拦截
    Object.keys(result).forEach(key => {
      ...
    })
    toggleObserving(true)
  }
}

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    ...
    for (let i = 0; i < keys.length; i++) {
      ...
      // 这里是一个寻找inject属性的迭代,直到在parent中找到为止
      ...
    }
    return result
  }
}

initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // props的响应化处理类似initData,就不做赘述了
  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)
  }
}

initMethods

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
      ...
     // 这里做了是否和props重名的校验,不能以_、$开头,必须是fn
      ...
  }
  // 这里就是为什么可以直接this.fn,做了bind并绑定了作用域
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  ...
  // 不能和methods、props重名的校验
  ...
  // 这里做了proxy的转发,_data是在instance/index的initMixin作了拦截处理的,这就是为啥data中的数据可以直接访问
  proxy(vm, `_data`, key)
  observe(data, true /* asRootData */)
}

数据拦截和依赖收集

vue的响应式原理就是就是对数据的进行拦截,只有经过数据拦截过的属性才能响应式

// observe(data),首先判断data有没有一个ob,没有就添加
export function observe (value: any, asRootData: ?boolean): 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
}

Observer

创建Observer实例有两个作用:

  • 在对象上添加ob属性,每个ob属性都会挂上一个dep的实例,dep的作用在于收集与当前属性相关的Watcher实例
  • 对val值进行数据拦截,dep的收集过程就在这里,对象和数组的处理不同
export class Observer {
  ...
  constructor (value: any) {
    this.value = value
    // dep存在于每个被劫持的对象,收集跟这个属性有关的watcher
    this.dep = new Dep()
    this.vmCount = 0
    // 在value上挂载_ob_指向这个ob实例
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      ...
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 对象的数据劫持
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 数组的数据劫持
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

dep做的事情就比较简单了,存、删watcher,逐个派发watcher的update

export default class Dep {
  ...
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target

这个target不是挂在每个dep上,而是挂在Dep构造函数上的,这个指向是在哪里改变的呢?

export default class Watcher {
  ...
  constructor (...) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    }
    return value
  }
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () {
    if (this.active) {
      const value = this.get()
      ...
    }
  }
  ...
}

在new Watcher()时,最后会执行get方法 => pushTarget(this)

defineReactive

数据拦截和依赖收集

  • 创建一个dep实例:
    • 由于get、set对dep有引用,所有该实例会一直存到内存中,数据太多就会产生大量的dep
    • 这个dep和observe的dep作用是一样的,只不过现在的dep对应对象的每个属性,observe的dep对应包含属性的那个对象
  • get在拦截数据时并不会触发:
    • getter指向哪个fn,取决于Dep.target的指向,Dep.target是组件那个watcher时,getter对应new Watcher()时定义的updateComponent,指向用户定义的watcher时指的就是那个watcher的回调
    • dep.depend就是在收集依赖,先在Dep.target指向的那个watcher中添加这个dep,再在dep中添加这个watcher,就是说dep中有watcher,watcher中有dep
  • set做的两件事就比较简单了
    • 首先如果新值是对象,那还要再重新进项数据拦截
    • 派发dep的notify,就是循环调用dep收集的watcher的update方法,这里有一个queueWatcher,这是个存放watcher的队列,并不是改一下数据就执行一次,它会根据watcher的id限制watcher的入栈,id相同的watcher只会进去第一个,但是取的值就是最后一个watcher的
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 这里定义的dep不会挂载到对象本身,因为get和set中会用到,所以会保留到内存中
  const dep = new Dep()
  ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      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) {
      ...
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

initComputed

后续更新

initWatch

后续更新

以上操作,数据的各种处理逻辑基本上就完事儿了,开始vm.$mount

$mount 生成render函数

mount的执行顺序就是

  • 先找到render
  • 没找到render,就先去找template,可以在这里看到render、template等的优先级
    • render优先级是最高的,有定义render就直接执行mount
    • template
      • #开头的,用idToTemplate解析
      • 也可以是元素节点,就取innerHTML
    • el代表宿主元素,例如#app
    • 找到template后,传入compileToFunctions,获取render函数
  • 执行初始化时mount变量存储的那个mountComponent方法
// entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  ...
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          ...
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      ...
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      ...
    }
  }
  return mount.call(this, el, hydrating)
}

createCompilerCreator

这个方法做了三件事:

  • parse:获取ast抽象语法树,parse(template)
  • optimize:给静态节点打上标记
  • generate:生成render函数
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

先写个测试文件

<body>
<div id="app">
    <h1>
      <span>sss</span>
    </h1>
    <p>{{obj.cur}}</p>
    <button @click="handleClick">编辑</button>
</div>

<script src="../dist/vue.js"></script>

<script>
  var vm = new Vue({
    el: '#app',
    data: {
      obj: {
        cur: 1
      }
    },
    methods: {
      handleClick() {
        this.obj.cur ++
      }
    }
  })

</script>
</body>

ast就是类似这样的

看下optimize具体是做了什么优化,以上述h1标签为例

<h1>
  <span>sss</span>
</h1>

这样的标签被认为是可优化的,会在h1节点上添加两个属性

  • staticRoot:true,标识这是一个静态的根节点
  • static: true,标识这是个静态节点

span标签只会被标记一个static:true 假如标签是这样

<h1>sss</h1>

就不会被标记,可能觉得这个优化点太小了 最终的由generate生成的code中的render是这样的:

"with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(obj.foo))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("编辑")])])}"

挂载render函数

  • 调用createFunction,new Function(render),创建了一个return执行render函数的函数
  • 在实例的$options上挂载了render和staticRenderFns

mountComponent 开始挂载

接下来就是触发callHook(vm,'beforeMount')了,这里可以稍作总结下:所有的数据都响应式,render函数也挂载到了组件实例上,最具代表性的vnode还没有出现,下一步肯定就是要生成vnode了

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')
  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
  // 这里就是之前依赖收集提到的组件级别的update方法
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 组件级别的watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

watcher里保留着后续更新的方法:

  • 组件实例vm,某个dep变化了,就可以知道要更新哪个组件
  • deps保留着该watcher相关联的dep,属性变化,执行get,就是updateComponent,与依赖收集那里就接上了
  • updateComponent,这个分两步
    • 执行回调vm._render(),的到vnode,挂载到实例上
    • 调用_update里的的__patch__

new Watcher()后,会立即调用watcher的get方法,就是上边的updateComponent

执行render函数,得到vnode

render,就是上边那个with开头的闭包函数,得到vnode,这里又涉及到依赖收集:

  • render中对变量的访问,会触发数据拦截的get方法,接着往watcher添加dep,dep添加watcher,在此刻看来dep和watcher是一对一的关系,但是一旦用户添加多个watcher,甚至组件的内部组件又引用到parent的属性,那就变成多对多了...
  • 属性中又有子属性,又在watcher中添加这个子的dep,子的dep也添加了这个watcher,如果是数组,又去递归添加子的子的dep...

个人认为执行render生成vnode,是一个由内而外的操作,比如:

_c('p',[_v(_s(obj.foo))])
  • 先是访问obj.foo,就是触发以上的get操作,取到了值,收集了依赖,update会使用到
  • 又对val做了_s转换即toString
  • 执行更外层的_v,就是createTextVNode,这个详细可以看render-helpers/index.js,这里只是最里层的text节点,到这已经生成了一个tag是undefined的text类型的vnode
  • 最后执行_c,就是initRender在实例上挂载的包着createElement的科里化函数,生成Vnode,new Vnode()过程会将子的vnode挂载到父的vnode的children上
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(obj.foo))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("编辑")])])}
})

以这段render语句为例,整个过程就是一层层地执行上边的步骤,最终生成一个类似这样的只有一个根的vnode:

接下来就是执行实例的__patch__了,由于__patch__太过于复杂,后面单独再写一篇...

纯手打,有错误还望指出,一起进步 源码调试地址