关于vue.js:内部分享篇Vue2源码浅析

10次阅读

共计 7011 个字符,预计需要花费 18 分钟才能阅读完成。

1、内容及模式

  • 1. 介绍目录构造、找到外围入口
  • 2. 介绍全局 api
  • 3. 以发问互动形式浏览外围源码

2、目录构造

  • .circleci 继续集成
  • benchmarks 性能评测
  • dist 输入目录
  • examples 案例
  • flow flow 申明文件
  • packages vue 中的包
  • scripts 工程化
  • src 源码目录
  • test 测试相干
  • types ts 申明文件

3、外围源码目录


├─compiler       # 编译的相干逻辑
│  ├─codegen
│  ├─directives
│  └─parser
├─core           # vue 外围代码
│  ├─components  # vue 中的内置组件 keep-alive
│  ├─global-api  # vue 中的全局 api
│  ├─instance    # vue 中的外围逻辑
│  ├─observer    # vue 中的响应式原理
│  ├─util        
│  └─vdom        # vue 中的虚构 dom 模块
├─platforms      # 平台代码
│  ├─web         # web 逻辑 - vue
│  │  ├─compiler
│  │  ├─runtime
│  │  ├─server
│  │  └─util
│  └─weex        # weex 逻辑 - app
│      ├─compiler
│      ├─runtime
│      └─util
├─server         # 服务端渲染模块
├─sfc            # 用于编译.vue 文件
└─shared         # 共享的办法和常量 

4、打包流程及入口剖析

1.package.json

"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",

2.scripts/build.js

// 1. 获取不同的打包的配置 
let builds = require('./config').getAllBuilds()

// 2. 进行打包
build(builds)

3.scripts/config.js

exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

// 找到打包入口
src/platforms/web/entry-runtime.js
src/platforms/web/entry-runtime-with-compiler.js

4. 外围代码、全局 API initGlobalAPI()

  • 1.import Vue from ‘./runtime/index’
  • 2.import Vue from ‘core/index’
  • 3.import Vue from ‘./instance/index’
//global-api/index.js
 Vue.set 
 Vue.delete 
 Vue.nextTick 
 initUse(Vue) Vue.use
 initMixin(Vue) Vue.mixin
 initExtend(Vue) Vue.extend

5.Vue.set / Vue.delete

export function set (target: Array<any> | Object, key: any, val: any): any {//1. 数组 应用 splice 触发视图更新 vue.set(arr,0,99)
  if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = M .max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  //2. 对象 是对象自身的属性,间接增加即可
  if (key in target && !(key in Object.prototype)) {target[key] = val
    return val
  }
 
  const ob = (target: any).__ob__
   //3. 不是响应式的 不须要将其定义成响应式属性
  if (!ob) {target[key] = val
    return val
  }
  // 4. 将属性定义成响应式的
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

export function del (target: Array<any> | Object, key: any) {
  // 1. 数组 仍旧调用 splice 办法
  if (Array.isArray(target) && isValidArrayIndex(key)) {target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  // 2. 自身没有这个属性
  if (!hasOwn(target, key)) {return}
   // 3. 删除这个属性
  delete target[key]
  if (!ob) {return}
  // 4. 告诉更新
  ob.dep.notify()}

对象只会拦挡曾经存在的属性 更改数组索引也不会引发视图更新

6.Vue.nextTick

const callbacks = []; // 寄存 nextTick 回调
let pending = false; 
function flushCallbacks () { // 清空队列
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {copies[i]()}
}
let timerFunc
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {if (cb) {
      try {cb.call(ctx) // 1. 将回调函数存入到 callbacks 中
      } catch (e) {handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {_resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc();     // 2. 异步刷新队列}
  // 3. 反对 promise 写法
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {_resolve = resolve})
  }
}

将回调函数存入到一个队列中,最初异步的清空这个队列

timerFunc

// 1. 默认先应用 Promise 因为 mutationObserver 有 bug 可能不工作
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()
  timerFunc = () => {p.then(flushCallbacks)
    // 解决队列不刷新问题
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
// 2. 应用 MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||
  
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let 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)
  }
  isUsingMicroTask = true
// 3. 应用 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)
  }
// 4. 应用 setTimeout
} else {timerFunc = () => {setTimeout(flushCallbacks, 0)
  }
}

EventLoop 微工作和宏工作,先微工作优先级 降级解决 promise => MutationObserver => setImmediate => setTimeout

5. 初始化过程

initMixin(Vue)  //_init
stateMixin(Vue) //$set $delete $watch
eventsMixin(Vue) // $on $emit $once $off 
lifecycleMixin(Vue) //_update
renderMixin(Vue) //_render

// 初始化 init.js
initLifecycle(vm)  // 初始化组件间的父子关系 
initEvents(vm)     // 更新组件的事件
initRender(vm)     // 初始化_c 办法创立虚构节点
initInjections(vm) // resolve injections before data/props // 初始化 inject
initState(vm)       // 初始化状态
initProvide(vm) // resolve provide after data/props / 初始化 provide

// 1. 如果有 el 就开始挂载 
if (vm.$options.el) {vm.$mount(vm.$options.el)
}

// 2. 组件的挂载
Vue.prototype.$mount = function (el,hydrating){el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating);
}

// 3. 创立渲染 watcher 进行渲染
export function mountComponent (vm,el,hydrating) {
  vm.$el = el
  let updateComponent
  updateComponent = () => {vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, { // 创立渲染 Watcher
    before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  return vm
}

6、数据劫持 + 发问?

1.this 如何拜访 data 和 methods?

// 初始化 data 数据
function initData(vm) {
  let data = vm.$options.data;
  //   实例的_data 属性就是传入的 data
  // vue 组件 data 举荐应用函数 避免数据在组件之间共享
  data = vm._data = typeof data === "function" ? data.call(vm) : data || {};

  // 把 data 数据代理到 vm 也就是 Vue 实例下面 咱们能够应用 this.a 来拜访 this._data.a
  for (let key in data) {proxy(vm, `_data`, key);
  }
}
// 数据代理
function proxy(object, sourceKey, key) {
  Object.defineProperty(object, key, {get() {return object[sourceKey][key];
    },
    set(newValue) {object[sourceKey][key] = newValue;
    },
  });
}

通过 this 间接拜访到 data 外面的数据的起因是:data 里的属性最终会存储到 new Vue 的实例(vm)上的 _data 对象中,拜访 this.xxx,是拜访 Object.defineProperty 代理后的 this._data.xxx

// initMethods
function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}


function nativeBind (fn, ctx) {return fn.bind(ctx)
}
var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind

通过 this 间接拜访到 methods 外面的函数的起因是:因为 methods 里的办法通过 bind 指定了 this 为 new Vue 的实例 (vm)

2. 数组的响应式?

  • 1. 因为对数组下标的拦挡太节约性能 对 Observer 构造函数传入的数据参数减少了数组的判断

    // src/obserber/index.js
    import {arrayMethods} from "./array";
    class Observer {constructor(value) {if (Array.isArray(value)) {
        // 这里对数组做了额定判断
        // 通过重写数组原型办法来对数组的七种办法进行拦挡
        value.__proto__ = arrayMethods;
        // 如果数组外面还蕴含数组 须要递归判断
        this.observeArray(value);
      } else {this.walk(value);
      }
    }
    observeArray(items) {for (let i = 0; i < items.length; i++) {observe(items[i]);
      }
    }
    }
  • 2. 每个响应式数据减少了一个不可枚举的__ob__属性 并且指向了 Observer 实例

    • 1. 能够依据这个属性来避免曾经被响应式察看的数据重复被观测
    • 2. 响应式数据能够应用__ob__来获取 Observer 实例的相干办法
   // src/obserber/index.js
class Observer {
  // 观测值 给每个 value 增加__ob__属性
  constructor(value) {
    Object.defineProperty(value, "__ob__", {
      //  值指代的就是 Observer 的实例
      value: this,
      //  不可枚举
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
}
  • 3. 对数组原型重写

    // src/obserber/array.js
    // 先保留数组原型
    const arrayProto = Array.prototype;
    // 而后将 arrayMethods 继承自数组原型
    // 这里是面向切片编程思维(AOP)-- 不毁坏封装的前提下,动静的扩大性能
    export const arrayMethods = Object.create(arrayProto);
    // 为什么重写七个办法?会扭转原数组
    // 其它办法如何?join concat map filter forEach    reduce every somesome flat slice
    // value.__proto__  => arrayMethods => arrayMethods.__proto__ => Array.prototype
    
    let methodsToPatch = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "reverse",
    "sort",
    ];
    methodsToPatch.forEach((method) => {arrayMethods[method] = function (...args) {
      //   这里保留原型办法的执行后果
      const result = arrayProto[method].apply(this, args);
      // 这句话是要害
      // this 就是被检测的数据,比方数据是 {msg:[1,2,3]},msg.push(4) this 就是 msg ob 就是 msg.__ob__(上段代码减少,该数据曾经被响应式察看)所以上面就能够应用 ob.observeArray
      const ob = this.__ob__;
    
      // 这里的标记就是代表数组有新增操作
      let inserted;
      switch (method) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
        default:
          break;
      }
      // 如果有新增的元素 inserted 是一个数组 调用 Observer 实例的 observeArray 对数组每一项进行观测
      if (inserted) ob.observeArray(inserted);
      // 之后咱们还能够在这里检测到数组扭转了之后从而触发视图更新的操作 -- 后续源码会揭晓
      return result;
    };
    });
    

    7、简易版 Vue

    https://github.com/minxiang51…

8、总结

源码并非遥不可及,从源码中学习思维,千里之行始于足下!

正文完
 0