Vue.js源码解析过程

Vue.js源码解析过程

在这里插入图片描述
Xmind获取地址

一. 所需知识点

1. Object.defineProperty(obj, prop, descriptor)

Object.defineProperty() 直接在对象上定义新属性并为属性绑定所需特性。

  • enumerable: 可枚举性
  • configurable: 是否允许删除该属性、添加描述符
  • writable: 是否允许为value赋值(不能和set、get同时使用)
  • value: 该属性对应的值
  • set(): 为属性赋值时做相应处理
  • get(): 获取该属性时做相应处理
/* 
* 优化:将Object.defineProperty包裹在方法内部,将value保存在局部作用中,就不用每个都手动定义变量。
*/
var reactiveFunc = function(obj, key, value) {
  // Object.defineProperty方法内部的get、set需要一个变量存储当前的value值
  // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true, // 可枚举
    get () { // 如果使用 obj.key 来访问数据, 就会调用 get 方法 ( getter, 读取器 )
      console.log(`读取${key}: ${value}`);
      return value;
    },
    set ( newVal ) { // 如果 obj.key = 'xxx', 那么就会调用 这个 set 方法, 并设置的值会最为参数传入 set
      console.log(`赋值的${key}新值为: ${newVal}`);
      value = newVal;
    }
  })
}

var reactiveObj = {name: "hang"}

reactiveFunc(reactiveObj, "age", 12);

//name属性不是通过Object.defineProperty定义的变量,所以没有做拦截处理
reactiveObj.name

//age属性为响应式,读取和赋值会触发对应的set和get
reactiveObj.age
reactiveObj.age = 15

// 输出:
//   读取age: 12
//   赋值的age新值为: 15

2.观察者模式

  • node自带 EventEmitter挂载事件并触发

    // 引入 events 模块
    var events = require('events');
    // 创建 eventEmitter 对象
    var eventEmitter = new events.EventEmitter();
    
    // 绑定事件及事件的处理程序
    eventEmitter.on('eventName', ()=> {
      console.log("---------");
    });
    
    // 触发事件
    eventEmitter.emit('eventName');
    
  • 自己实现

    // Observe.js
    class Observe {
        constructor() {
            this.container = new Map();
        }
        /* 绑定 */
        on(key, value) {
            if (this.container.has(key)) {
                let arr = this.container.get(key);
                arr.push(value);
            }
            else {
                this.container.set(key, [value]);
            }
        }
        ;
        /* 触发指定回调函数 */
        emit(key) {
            if (this.container.has(key)) {
                let arr = this.container.get(key);
                arr.forEach(element => {
                    element();
                });
            }
        }
        /* 删除 */
        delete(key, callback) {
            let err = "";
            let succ = "";
            if (this.container.has(key)) {
                this.container.delete(key);
                succ = `${key}:删除成功`;
            }
            else {
                err = `${key}:不存在绑定事件`;
            }
            callback(err, succ);
        }
        /* 删除 Promise*/
        deletePro(key) {
            return new Promise((reslove, reject) => {
                if (this.container.has(key)) {
                    this.container.delete(key);
                    reslove(`${key}:删除成功`);
                }
                else {
                    reject(`${key}:不存在绑定事件`);
                }
            });
        }
    }
    exports.Observe = Observe;
    
    // CreateObserve.js
    let CallBack = require("./Subject")
    
    class CreateObserve extends CallBack.Observe {
      constructor(name) {
        super();
        this.subName = name;
      };
      on(func) {
        super.on(this.subName, func);
      };
      delete(callback) {
        super.delete(this.subName, callback);
      };
      deletePro(callback) {
        return super.deletePro(this.subName, callback);
      };
      doSomething(func) {
        if(func instanceof Function) {
          func();
        }
        super.emit(this.subName);
      }
    }
    
    module.exports = CreateObserve
    
    // 使用
    var CreateObserve = require('./observer/CreateObserve')
    
    let sub = new ConcreteSubject("test");
    sub.on(()=> {console.log("--------");});
    sub.doSomething();
    sub.delete((err, succ) => {
      if(err) {
        console.log(err);
        return;
      }
      console.log(mess);
      console.log(succ);
    });
    sub.deletePro().then((res)=> {
      console.log(res);
    }).catch((err)=> {
      console.log(err);
    })
    

二、源码解析

1.目录结构

src
├── compiler        # 编译相关(生成AST、AST静态树),此阶段可以在构建时借助vue-loader编译(推荐),或者运行时借助带有compiler功能的vue.js编译。
├── core            # 核心代码 
	├── components		# 内置组件
	├── global-api		# 全局 API 封装
    ├── instance		# Vue 实例化
	├── observer		# 观察者
	├── util			# 工具函数
	├── vnode			# 虚拟 DOM
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # 将单个.vue 文件解析成JavaScript对象
├── shared          # 共享工具函数代码

2.简易源码解析

web/entry-runtime-with-compiler.js (以Runtime+compiler方式构建项目)为例,主要讲解核心代码,部分代码会省略。

1) 入口文件 web/entry-runtime-with-compiler.js 核心:
  • runtime/index文件引入Vue
  • 在原Vue.$mount()基础上进行拓展
    在这里插入图片描述
2) platforms/web/runtime/index核心:
  • core/index引入Vue

  • 实现Vue.prototype.$mount()方法挂载模板

  • 实现Vue.prototype._ _patch _ _方法
    在这里插入图片描述

3) core/index核心:
  • instance/index引入Vue
  • 初始化Vue全局 Api
    在这里插入图片描述
4) instance/index核心
  • new Vue() 前,加载Vue相关文件阶段,向Vue.prototype中混入方法
    • initMixin():初始化 Vue.prototype._init,Vue初始化最开始执行的函数,后边会详细介绍。

    • stateMixin():注册属性响应式相关方法:

      1. Vue.prototype.$set:为对象中新增响应式属性
      2. Vue.prototype.$delete:通过普通方法delete obj.name,页面上使用到obj.name的地方不会清空,还是使用删除前的值;通过this.$delete(this.obj, “name”)会删除该属性(当读取obj.name时为undefined),同时让页面对应更新。
      3. Vue.prototype.$watch:模板中watcher属性就是使用该方法进行注册的,创建观察Watcher,方法内部读取响应式属性时,属性对应的Dep会绑定当前的观察Watcher,方法内部属性变化时,也会触发当前Watcher重新执行。
    • eventsMixin():注册事件相关方法(对vm._events的操作):

      1. Vue.prototype.$on:注册(存储)事件到_events。
      2. Vue.prototype.$off:取消_events中指定事件。
      3. Vue.prototype.$once:对$on和$off包装使方法执行一次。
      4. Vue.prototype.$emit:触发指定key注册的全部方法。
    • lifecycleMixin():注册与生命周期相关方法:

      1. Vue.prototype._update:通过比对新旧VNode,最小程度的更改dom结构,达到更新页面的效果。
      2. Vue.prototype.$forceUpdate:触发当前vm绑定的模板Watcher的getter方法(重新渲染页面、执行某个方法等),可以利用该方法,在对象上通过普通方式添加新属性(非响应式数据)后执行该方法,也可达到响应式的效果,不过一般不使用。
      3. Vue.prototype.$destroy:执行beforeDestroy回调,在父组件中移除自己,取消所有依赖追踪和事件监听器,执行destroy回调。
    • renderMixin():注册渲染相关方法:

      1. Vue.prototype.$nextTick:等待当前调用栈执行完,进行事件回调时执行(微任务、宏任务自行了解),依次判断浏览器支持情况:Promise -> MutationObserver -> setImmediate -> setTimeout,前边的如果浏览器支持就直接使用,所以可能放到微任务也可能是宏任务。当人,也可以称为下一个滴答后执行回调方法,
      2. Vue.prototype._render:内部使用编译好的render函数(借助vm._c或vm.$createElement + data等属性)生成VNode
  • new Vue()方法时执行 _init 方法进行初始化
    在这里插入图片描述
5)instance/init核心
  • 合并options配置:当 new Vue(options) 参数options中包含有 mixins 属性时,要将其进行合并。
  • initLifecycle():初始化生命周期,即Vue属性并附初值,包含 $parent、$children、$refs 等属性。
  • initEvents():初始化事件中心。 将父组件v-on绑定在子组件上的方法借助 Vue.prototype.$on 等方法存储到vm._events = {eventName: String, eventFunc: Function}
  • initRender():初始化渲染,vm._c用于被vue-loader和vue-template-compilre解析模板使用生成好render函数。Vue.$createElement用于用户手写的render使用
  • callHook(vm, 'beforeCreate')首先将当前模板(vue)对应的Watcher放入全局唯一的Dep.target属性,执行 beforeCreate 生命周期函数,最后将当前Dep.target赋值成上一个Watcher.(执行vue规定的几个回调函数的统一流程)
  • initInjections(): 暂时不讨论
  • initState():初始化 data、props、computed、watcher等等(后面详细讲解)
  • initProvide():暂时不讨论
  • callHook(vm, 'created'):执行 created 生命周期函数
  • vm.$mount(vm.$options.el):生成vnode -> 真实dom -> 并替换指定dom(#id)
    在这里插入图片描述
6)core/observer/index核心:数据响应式处理

​ 递归为每个对象 / 数组中的属性进行响应式处理, defineReactive 中为每个属性创建对应 Dep 对象,通过 Object.defineProperty重写属性:

  • get 获取属性值时进行依赖收集(Dep与全局Watcher进行关联)
  • set 修改属性值时派发更新(遍历当前deps,将watcher添加到队列中,在下一个滴答 nextTick 执行 vm._update(vm._render() 更新页面)
7)$mount挂载方法
  1. 首先,它对 el 做了限制,Vue 不能挂载在 bodyhtml 这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。(运行时编译较为消耗性能 )
  2. 调用 mountComponent() 函数,核心:
    • callHook(vm, 'beforeMount')
    • new Watcher(vm, () => {vm._update(vm._render(), hydrating)}):初始化Watcher时将回调函数保存到对象中,数据更新(没有同步标识)时会在下一个tick nextTick 执行回调函数更新页面。想要操作更新后的dom,就必须在 nextTick 后执行。
      • 将当前watcher赋值给全局唯一Watcher Dep.target
      • 执行vm._update(vm._render(), hydrating)
        • vm._render:该render函数由 createCompiler 生成,其中使用到函数科里化,将AST作为局部变量存储。最终都会调用 createElement 方法通过AST结合data生成VNode。中间会访问 vm 上的数据,这时候就会触发数据对象的getter,进行依赖收集 dep.depend()(将当前数据对象对应的Dep对象与当前全局的唯一Watcher关联).
        • vm._update:进行 vm.__patch__ 新旧vnode比较,进行页面更新.
      • 将全局Watcher赋值上一个watcher
    • callHook(vm, 'mounted')

参考文章

Vue.js 技术揭秘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值