vue2.6 从入口分析源码的理解

137 阅读4分钟

vue2.6 从入口分析源码的理解

目的:源码的学习及临摹简单实现,提升自身设计能力

数据视图的双向绑定

观察着模式 目标 obj ---> observe
被观察目标实体 add:ob, remove:ob, dep.notify()

贯彻着  watcher.update()
观察着模式的实现,如何实现响应式,比如哪些 react\vue 

vue 的6大模块

1、compiler: 模板解析语法树,template、render 函数的写法最终被转换为 render
    - directives
    -codegen
    - parser 转换 template --> render  所以用template 会更好,会带有优化功能
        - html-parser
        - entity-decoder
        - filter-parser 
2、core 核心:vDom / observe/components/instance
3、platforms: web/ weex 跨平台
4、server: 服务端渲染
5、sfc: 单文件.vue的解析
6、shared: 工具、变量

从 core 的文件夹 index.js 开始

// core/index.js

import Vue from './instance/index'
// initGlobalAPI(Vue)   依赖注入Vue 完成一些属性 prototype 的挂载安装
initGlobalAPI(Vue)

进入 instance/index 文件下查看Vue

//instance/index.js
function Vue (options) {
  this._init(options)
}
// 单独抽离下面的流程,不仅仅是初始化,还生成了vue必要的参数属性 是必要的
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
  • initMixin(Vue)stateMixin(Vue) .. 等状态文件单独抽离出来,不仅仅是初始化,还生成了vue必要的参数属性。

1、initMixin 初始化

- 配置合并(props\inject\directives)
-init
    - initLifecycle: 初始化一些对象的状态 vm._watcher = null
    - initEvents : 初始化状态 vm._events 、vm._hasHookEvent
    - initRender :渲染初始化 vm._vnode = null/ vm.$slots / vm.$scopedSlots/  /vm.$createElement /defineReactive(vm, '$listeners') /defineReactive(vm, '$attrs')
    
 callHook(vm, 'beforeCreate')
 
 -init
    - initInjections :注入的数据递归响应式挂载
    - initState: initProps /initData / initMethods/ initComputed/ initWatch  
        initProps /initData 递归属性,校验属性格式及递归定义 props 及 data 为响应式对象
        initMethods检验 methods[key] 和props 里面不能有重复的,递归执行methods[key]对应的函数。
        initComputed 是每个属性 为一个new Watcher 实例 并且有缓存的watcher
        initwatch 执行每一个属性都是new watcher 实例
    - initProvide
 callHook(vm, 'created') 
 挂载$mount
 Vue.prototype._init=function(options){
     // 合并属性
     mergeOptions(options)
     // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化化渲染
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // 挂载
    vm.$mount(vm.$options.el)
 }

initMixin 总结:

- 1、initState 执行 initData 函数把 data 处理为 observe(vm.$options.data, true /* asRootData */) 作为初始化的观察对象 遍历属性为 defineReactive -- get\set --dep.notify()
- 2、在initRender渲染时 defineReactive方法 定义了 `$attr` 和 `$listeners` 为加入依赖收集
- 3、initData 和initProps 里面 处理了proxy 代理 使得 vm._data.xxx = vm.xxx   vm._props.xxx = vm.xxx 写在 data 和 props 中的属性都暴露在vm 上。
- 4、 在initState 中 处理了 
   data 的observe,
   props 的 defineReactive(props, key, value) 
   computed 每个属性都是一个new Watcher()--待缓存的watcher 实例
   initWatch  也是每个属性都是一个watcher vm.$watch(expOrFn, handler, options)

相关问题:

1.1、为何初始化的的是vue 没有用class 类对象,用的function 方法

    1、除了可读性和易读性外,后续再vue 的proototype 上进行拓展
    用函数对象更方便可读,维护及拓展

    2、用类实现Vue 会用辅助类,来丰富基础类,辅助类和其他类的隔离存放,查看不太方便
1.2、beforeCreate 之前做了什么?
合并对象 、初始化事件、初始化vm.watcher=null
beforecreate 后才有initInject initState initProvide
1.3、props 和 data是如何把属性挂在vm 上的
在initState 阶段的 initDta 和initProps 函数中 通过代理,把私有属性_data 和_props 挂载到了vm 上 
获取vm.key 就能拿到 vm._data.key || vm._props.key
proxy(vm, `_data`, key)
proxy(vm, `_props`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  // defineProperty 代理设置 get set 方法
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

1.4 为什么根vue 实例上 data 是对象,compontent 里面的data是函数?

data 如果是函数且没有返回值会报错handleError。
当 vue 组件是根组件的时候,是同 data 可以是对象,因为单页应用中只有一个根vue 实例,引用的最外层data 是同一个实例,组件中components data 是一个函数并有返回对象,如果是对象,两个组件会互相引用干扰错乱。

2、stateMinxin 做的就是挂载 一些数据到 Vue.prototype 上

Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set
Vue.prototype.$delete
Vue.prototype.$watch 有 watcher实例生成, 完成后会返回一个 watch的卸载函数 unwatchFn

2.1 Vue.$set 的本质

    就是手动设置了 针对数组用 splice 添加新对象,针对obj 新对象添加属性 然后设置了 definReactive  响应式依赖收集
    数组的方法也被重写 splicepoppushunshiftshift、sorts会改变
export function set (target: Array<any> | Object, key: any, val: any): any {

  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

3、eventMinxin Vue 挂载一些事件

  • Vue.prototype.$on 挂载一些生命周期对应的函数 hook:beforeCreate hook:created @click 等
 const hookRE = /^hook:/
 Vue.prototype.$on = function(event, fn){
    const vm = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
    }
    // 标志钩子事件 ,不是散列查找,不是重点
    if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    return vm
    }
  • Vue.prototype.$once 挂载后执行一次后删除
Vue.prototype.$once = function(event,fn){
     const vm = this
     function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
}
  • Vue.prototype.$off 卸载数据

    考虑 没有参数、没有fn 的卸载等边界情况没有写进去

Vue.prototy.$off = function(event, fn){
    const vm = this
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
}
  • Vue.prototype.$emit 递归触发函数
Vue.prototy.$off = function(event){
    const vm = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
          // 这里封装了promise 一遍执行
        // invokeWithErrorHandling(cbs[i], vm, args, vm, info)
        // 简单写就这样
         cbs[i].apply(vm, args)
      }
    }
    return vm
  }
}

4、lifecycleMixin

Vue.prototype._update  对比vnode 去更新 Vnode 的更新
Vue.prototype.$forceUpdate    vm._watcher.update()
Vue.prototype.$destroy 里面有 callHook(vm, 'beforeDestroy')、 移除自身、移除watch\派发更新树、移除所有监听、删除vm.$vode.parent 虚拟父节点为null 
4.1 Vue.prototype.$destroy 里面 beforeDestory 到destroyed 过程做了什么?
如果有父级在父组件中移除自身dom
watchers 删除
更新 vnode,派发

destroyed 后移除 所有vm的监听事件 hook:created @click 等

5、renderMixin

// 安装运行方便函数
installRenderHelpers(Vue.prototype)

Vue.prototype.$nextTick
Vue.prototype._render

注意一点,installRenderHelpers 是一个帮助函数,这个不是一开始加上去的,而是后面需要的时候在挂载到 对象中,节约性能

// 安装运行方便函数
export function installRenderHelpers (target: any) {
 target._o = markOnce
 target._n = toNumber
 target._s = toString
 target._l = renderList
 target._t = renderSlot
 target._q = looseEqual
 target._i = looseIndexOf
 target._m = renderStatic
 target._f = resolveFilter
 target._k = checkKeyCodes
 target._b = bindObjectProps
 target._v = createTextVNode
 target._e = createEmptyVNode
 target._u = resolveScopedSlots
 target._g = bindObjectListeners
 target._d = bindDynamicKeys
 target._p = prependModifier
}

5.1 nextTick

promise 存在的时候,多个 nextTick 回调函数都是在 primise.then 回调里面循环同步执行 ,可能有风险会死循环,当nextTick 里面的回调函数里面用 $set 设置内容改变,监听data 改变后又触发 nextTick 回调就会死循环。

timerFunc = () => {
   p.then(flushCallbacks)
}

new MutationObserver: MutationObserver is 在 IE11 不可靠

counter =1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
 // 监听数据
 observer.observe(textNode, {
       characterData: true
   })
 timerFunc = () => {
     // 改变数据
   counter = (counter + 1) % 2
   textNode.data = String(counter)
 }

setImmediate

  timerFunc = () => {
   setImmediate(flushCallbacks)
 }

setTimeout

 timerFunc = () => {
   setTimeout(flushCallbacks, 0)
 }

initGlobalAPI(Vue) 依赖注入Vue 完成一些属性 prototype 的挂载安装

引用挂载-

//global-api/index.js
Vue.config
// 这里是不被暴露出来的,有风险
Vue.util
Vue.set
Vue.delete
Vue.nextTick
Vue.options
Vue.observable

initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)

后续问题:

1、扩展 面试题: 选runtime only 还是 runtime+ complier 有啥区别?