阅读 4864

Vue源码简析(版本vue-2.4.4)

在开始之前,阅读源码你需要有扎实的基本功,还要有耐心,能把握全局,不要扣细节!

一、先看看目录

├── build --------------------------------- 构建相关的文件
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放使用Vue开发的的例子
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── package.json -------------------------- 项目依赖
├── test ---------------------------------- 包含所有测试文件
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│   ├──platforms --------------------------- 包含平台相关的代码
│   │   ├──web -----------------------------  包含了不同构建的包的入口文件
│   │   |   ├──entry-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
│   │   |   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器
│   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│   ├── core ------------------------------ 存放通用的,平台无关的代码
│   │   ├── observer ---------------------- 反应系统,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│   │   ├── instance ---------------------- 包含Vue构造函数设计相关的代码
│   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件
│   ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 包含整个代码库通用的代码复制代码

二、Vue 的构造函数是什么样的

使用 new 操作符来调用 VueVue是一个构造函数,了解了目录结构,下面来看下下入口文件

打开package.json

当我们运行npm run dev,看看干了啥,rollup也是类似webpack的打包工具,根据

TARGET=web-full-dev

去build/config.js查找

打开入口文件找到了web/entry-runtime-with-compiler.js 


依照以上查找路径,我们找到了Vue构造函数


定义了构造函数,引入依赖,调用初始化函数,最后导出Vue

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue复制代码

打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 Vue 的原型 prototype 上挂载方法或属性

1. 先进入 initMixin(Vue),在prototype上挂载

Vue.prototype._init = function (options) {} 
复制代码

2. 进入 stateMixin(Vue),在prototype上挂载 

Vue.prototype.$data 
Vue.prototype.$props 
Vue.prototype.$set = set 
Vue.prototype.$delete = del 
Vue.prototype.$watch = function(){} 
复制代码

3. 进入eventsMixin(Vue),在prototype上挂载 

Vue.prototype.$on 
Vue.prototype.$once 
Vue.prototype.$off 
Vue.prototype.$emit
复制代码

4.进入lifecycleMixin(Vue),在prototype上挂载 

Vue.prototype._update 
Vue.prototype.$forceUpdate 
Vue.prototype.$destroy  复制代码

5. 最后进入renderMixin(Vue),在prototype上挂载 

Vue.prototype.$nextTick 
Vue.prototype._render 
Vue.prototype._o = markOnce 
Vue.prototype._n = toNumber 
Vue.prototype._s = toString 
Vue.prototype._l = renderList 
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual 
Vue.prototype._i = looseIndexOf 
Vue.prototype._m = renderStatic 
Vue.prototype._f = resolveFilter 
Vue.prototype._k = checkKeyCodes 
Vue.prototype._b = bindObjectProps 
Vue.prototype._v = createTextVNode 
Vue.prototype._e = createEmptyVNode 
Vue.prototype._u = resolveScopedSlots 
Vue.prototype._g = bindObjectListeners
复制代码

根据上面的查找路径下一步到src/core/index.js

引入依赖,在Vue上挂载静态方法和属性

  1. import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    
    initGlobalAPI(Vue)
    
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    })
    
    Object.defineProperty(Vue.prototype, '$ssrContext', {
      get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
      }
    })
    
    Vue.version = '__VERSION__'
    
    export default Vue复制代码

进入initGlobalAPI(Vue),在Vue上挂载静态属性和方法

Vue.config Vue.util = util
Vue.set = set 
Vue.delete = del 
Vue.nextTick = util.nextTick 
Vue.options = { 
 components: { KeepAlive }, 
 directives: {}, 
 filters: {}, 
 _base: Vue 
} 
Vue.use 
Vue.mixin 
Vue.cid = 0 
Vue.extend 
Vue.component = function(){} 
Vue.directive = function(){} 
Vue.filter = function(){} 
复制代码

接着挂载

Vue.prototype.$isServer 
Vue.version = '__VERSION__'
复制代码



根据上面的查找路径下一步到runtime/index.js,安装平台特有的工具

Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安装平台特定的 指令 和 组件
Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount复制代码

根据上面的查找路径最后一步到web/entry-runtime-with-compiler.js

  1. 缓存来自 web-runtime.js 文件的 $mount 函数,const mount = Vue.prototype.$mount
  2. 在 Vue 上挂载 compile然后覆盖覆盖了 Vue.prototype.$mount
  3. Vue.compile = compileToFunctions 就是将模板 template 编译为render函数。

到这整个Vue构造函数就还原了


三、通过一个例子说明整个流程

index.html

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js grid component example</title>
    </head>
  <body>
    <div id="app">
        <ol>
          <li v-for="todo in todos">
            {{ todo.text }}
          </li>
        </ol>
        <Child></Child>
    </div>
  </body>
</html>
复制代码

grid.js

let Child = {
	data: function() {
	  return {child: '你好哈'}
	},
	template: '<div>{{child}}</div>'
}
new Vue({
	el: '#app',
	data: {
		todos: [
                    {text: '学习 JavaScript'}, 
                    {text: '学习 Vue'}, 
                    {text: '整个牛项目'}]
	},
	components: {'Child': Child}
})复制代码

想要知道Vue都干了什么,首先查看构造函数function Vue (options) { this._init(options) }

  • new Vue({//传入上面的内容 }) ,首先进入Vue.prototype._init,构造函数第一个挂载的方法

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
复制代码

_init() 方法在一开始的时候,在 this 对象上定义了两个属性:_uid_isVue,然后判断有没有定义 options._isComponent这里会走 else 分支,也就是这段代码:

vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )mergeOptions使用策略模式合并传入的options和Vue.options复制代码

mergeOptions使用策略模式合并传入的options和Vue.options合并后的代码结构, 可以看到通过合并策略components,directives,filters继承了全局的, 这就是为什么全局注册的可以在任何地方使用,因为每个实例都继承了全局的, 所以都能找到


      接着调用了initLifecycleinitEventsinitRender、initState,且在 initState 前后分别回调了生命周期钩子 beforeCreatecreated,看到这里,也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。created 仅仅代表数据状态的初始化完成。重点看下initState()

initState (vm) {
  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)
  }
}复制代码

通过initData看Vue的数据响应系统,由于只是传了data,所以执行initData(vm)

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  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]
      proxy(vm, `_data`, key)
  }
  observe(data, true /* asRootData */)
}
复制代码

proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}复制代码

取出data中的key,进行循环,通过proxy代理,可以直接通过 this.属性 访问data中的值,在实例对象上对数据进行代理,这样我们就能通过this.todos 来访问 data.todos 了 接下啦,正式的数据响应系统observe(data, true /* asRootData */),将数据通过Object.defineProperty进行get,set处理,使得数据响应

class Observer {
  constructor(value) {
    this.value = value 
    this.dep = new Dep() 
    this.vmCount = 0 
    def(value, '__ob__', this) 
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) 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], obj[keys[i]])
    }
  }
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
} 
复制代码

Observer 类中,我们使用 walk 方法对数据data的属性循环调用 defineReactive 方法,defineReactive 方法很简单,仅仅是将数据data的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据data的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取data属性值的时候,通过 getset 即能获取到通知。

function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  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) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

  1. 其中 let childOb = !shallow && observe(val),进行递归调用,将所有data数据包含子集进行get set化,进行响应
  2. 其中在Observe类中,如果属性是数组,会进行改造

    if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          this.walk(value)
        }export const arrayMethods = Object.create(arrayProto)
    ;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    .forEach(function (method) {
      const original = arrayProto[method]
      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
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    复制代码

重写数组中的上述方法,当在对数组进行这些操作时,ob.dep.notify(),通知相应的改变


initData(vm)执行完成,相应系统就完成了,这时候执行callHook(vm, 'created') 触发created,继续回到_init(),执行到 vm.$mount(vm.$options.el)

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }复制代码

进入$mount会先获取挂载el节点,然后先判断有没有传入render方法,没有在去找有没有传入template,

本例都没有,就会去取
getOuterHTML(el)作为当期模板

Vue.prototype.$mount = function (
  el,
  hydrating
){
  el = el && query(el)
  const options = this.$options
  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, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
复制代码

有了模板经过compileToFunctions,将模板编译为ast语法树,经过静态优化, 最后处理成render函数,本例中的render函数,使用了with(this), 将this作用域提前, {{ todo.text }} 所以我们可以直接在模板中使用属性,不加this! 当然加上this也是可以的 解除了我的很多迷惑,为什么模板中可以不加this(react中的render是要使用this的), 其中的v-for转换成_l,根据之前的Vue.prototype._l = renderList

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}复制代码

  • 生成了render函数后接着进入mountComponent,
  • 首先调用了
    beforeMount函数,
  • 接着执行vm._watcher = new Watcher(vm, updateComponent, noop)
  • 最后callHook(vm, 'mounted'),执行mounted,所以在mounted执行之前就已经挂载到dom上

所以重点 vm._watcher = new Watcher(vm, updateComponent, noop)

function mountComponent (
  vm,
  el,
  hydrating
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

查看Watcher代码

class Watcher {
  constructor (
    vm,
    expOrFn,
    cb,
    options
  ) {
    this.vm = vm
    vm._watchers.push(this)

    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // 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 = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    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)
      }
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
复制代码

执行构造函数由于 this.lazy=false; this.value = this.lazy ? undefined : this.get();

执行get方法 其中pushTarget(this),给Dep.target添加静态属性this(当前new Watcher()实例 )

 function pushTarget (_target) { 

 if (Dep.target) targetStack.push(Dep.target) 

 Dep.target = _target

 }

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }
复制代码

接着执行 this.getter.call(vm, vm)复制代码
this.getter就是
updateComponent = () => {
vm._update(vm._render(), hydrating)
}      复制代码
  1. 开始先调用vm._render()

  Vue.prototype._render = function (){
    const vm = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options

    let vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
  }
复制代码

开始执行之前编译好的render函数了,在执行render函数时,通过获取todos属性等,触发相应

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}复制代码

的get方法,这个时候Dep.target已经存在静态属性,Watcher实例了

所以相应的dep实例就会收集对应的Watcher实例了

复制代码

执行完之后返回vnode,



updateComponent = () => {
vm._update(vm._render(), hydrating)
} 
其中vm._render()执行render函数返回vnode作为参数
接下来执行vm._update
这是首次渲染,所以执行
vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )复制代码

Vue.prototype._update = function (vnode, hydrating) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
复制代码

vm.__patch__( vm.$el, vnode, hydrating, false, vm.$options._parentElm, vm.$options._refElm ) 根据vnode中树,创建对应元素,插入到父节点中,通过对vnode递归循环创建所有子节点 插入到父节点中 其中如果遇到子元素是组件,例如本例中Child,会创建对应VueComponent的实例,执行 和new Vue()一样的流程

如果还没有 prevVnode 说明是首次渲染,直接创建真实DOM。如果已经有了 prevVnode 说明不是首次渲染,那么就采用 patch 算法进行必要的DOM操作。这就是Vue更新DOM的逻辑。只不过我们没有将 virtual DOM 内部的实现。

当改变属性值时,会触发对应的属性的set方法,由于之前执行render的时候触发了get,收集了对应的Watcher,所以改变值时触发set,通知之前收集的Watcher实例执行,重新计算render方法进行patch操作

最后盗取一张图:



写了半天,实在写不下去了,以后有好的语言,再来整理吧!



关注下面的标签,发现更多相似文章
评论

查看更多 >