Vue(v2.6.11)万行源码生啃,就硬刚!

本文详细介绍了Vue.js v2.6.11的源码阅读过程,包括工具函数、数据监听、VNode、Observer、事件机制、模板编译等核心部分。文章通过分模块的方式逐行解析源码,深入理解Vue的内部工作机制,如Dep、VNode的创建和更新、数据响应式原理、事件处理和模板解析等,旨在帮助读者掌握Vue的底层实现。
摘要由CSDN通过智能技术生成

前言

源码阅读可能会迟到,但是一定不会缺席!

众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。

<html>
<head></head>
<body>
    <div id="app">
        {
  { message }}
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'See Vue again!'
        },
    })
</script>
</html>

源码cdn地址:https://cdn.jsdelivr.net/npm/vue/dist/vue.js,当下版本:v2.6.11。

本瓜选择生啃的原因是,可以更自主地选择代码段分轻重来阅读,一方面测试自己的掌握程度,一方面追求更直观的源码阅读。

当然你也可以选择在 https://github.com/vuejs/vue/tree/dev/src 分模块的阅读,也可以看各路大神的归类整理。

其实由于本次任务量并不算小,为了能坚持下来,本瓜将源码尽量按 500 行作为一个模块来形成一个 md 文件记录(分解版本共 24 篇感兴趣可移步),结合注释、自己的理解、以及附上对应查询链接来逐行细读源码,此篇为合并版本

目的:自我梳理,分享交流。

最佳阅读方式推荐:先点赞👍再阅读📖,靴靴靴靴😁

正文

第 1 行至第 10 行

// init

(
    function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());
    }(
        this,
        function () {
            'use strict';
            //...核心代码...
        }
    )
);
// 变形
if (typeof exports === 'object' && typeof module !== 'undefined') { // 检查 CommonJS
    module.exports = factory()
} else {
    if (typeof define === 'function' && define.amd) { // AMD 异步模块定义 检查JavaScript依赖管理库 require.js 的存在 [link](https://stackoverflow.com/questions/30953589/what-is-typeof-define-function-defineamd-used-for)
        define(factory)
    } else {
        (global = global || self, global.Vue = factory());
    }
}
// 等价于
window.Vue=factory() 
// factory 是个匿名函数,该匿名函数并没自执行 设计参数 window,并传入window对象。不污染全局变量,也不会被别的代码污染

第 11 行至第 111 行

// 工具代码

var emptyObject = Object.freeze({});// 冻结的对象无法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)

// 接下来是一些封装用来判断基本类型、引用类型、类型转换的方法

  • isUndef//判断未定义

  • isDef// 判断已定义

  • isTrue// 判断为 true

  • isFalse// 判断为 false

  • isPrimitive// 判断为原始类型

  • isObject// 判断为 obj

  • toRawType // 切割引用类型得到后面的基本类型,例如:[object RegExp] 得到的就是 RegExp

  • isPlainObject// 判断纯粹的对象:“纯粹的对象”,就是通过 { }、new Object()、Object.create(null) 创建的对象

  • isRegExp// 判断原生引用类型

  • isValidArrayIndex// 检查val是否是一个有效的数组索引,其实就是验证是否是一个非无穷大的正整数

  • isPromise// 判断是否是 Promise

  • toString// 类型转成 String

  • toNumber// 类型转成 Number

第 113 行至第 354 行

  • makeMap// makeMap 方法将字符串切割,放到map中,用于校验其中的某个字符串是否存在(区分大小写)于map中
    e.g.
var isBuiltInTag = makeMap('slot,component', true);// 是否为内置标签
isBuiltInTag('slot'); //true
isBuiltInTag('slot1'); //undefined
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');// 是否为保留属性
  • remove// 数组移除元素方法

  • hasOwn// 判断对象是否含有某个属性

  • cached// ※高级函数 cached函数,输入参数为函数,返回值为函数。同时使用了闭包,其会将该传入的函数的运行结果缓存,创建一个cache对象用于缓存运行fn的运行结果。link

function cached(fn) {
    var cache = Object.create(null);// 创建一个空对象
    return (function cachedFn(str) {// 获取缓存对象str属性的值,如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
        var hit = cache[str];
        return hit || (cache[str] = fn(str))
    })
}
  • camelize// 驼峰化一个连字符连接的字符串

  • capitalize// 对一个字符串首字母大写

  • hyphenateRE// 用字符号连接一个驼峰的字符串

  • polyfillBind// ※高级函数 参考link

  • Function.prototype.bind() // link1link2

  • toArray// 将像数组的转为真数组

  • extend// 将多个属性插入目标的对象

  • toObject// 将对象数组合并为单个对象。

e.g.

console.log(toObject(["bilibli"]))
//{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ƒ}
  • no// 任何情况都返回false

  • identity // 返回自身

  • genStaticKeys// 从编译器模块生成包含静态键的字符串。TODO:demo

  • looseEqual//※高级函数 对对象的浅相等进行判断

//有赞、头条面试题

function looseEqual(a, b) {
    if (a === b) return true
    const isObjectA = isObject(a)
    const isObjectB = isObject(b)
    if(isObjectA && isObjectB) {
        try {
            const isArrayA = Array.isArray(a)
            const isArrayB = Array.isArray(b)
            if(isArrayA && isArrayB) {
                return a.length === b.length && a.every((e, i) => {
                    return looseEqual(e, b[i])
                })
            }else if(!isArrayA && !isArrayB) {
                const keysA = Object.keys(a)
                const keysB = Object.keys(b)
                return keysA.length === keysB.length && keys.every(key => {
                    return looseEqual(a[key], b[key])
                })
            }else {
                return false
            }
        } catch(e) {
            return false
        }
    }else if(!isObjectA && !isObjectB) {
        return String(a) === String(b)
    }else {
        return false
    }
}
  • looseIndexOf// 返回索引,如果没找到返回-1,否则执行looseEqual()
  • once// 确保函数只被调用一次,用到闭包

阶段小结

  • cached
  • polyfillBind
  • looseEqual

这三个函数要重点细品!主要的点是:闭包、类型判断,函数之间的互相调用。也即是这部分工具函数的精华!

第 356 行 至 第 612 行

// 定义常量和配置

  • SSR_ATTR// 服务端渲染
  • ASSET_TYPES// 全局函数 component、directive、filter
  • LIFECYCLE_HOOKS// 生命周期,无需多言
  • config // 全局配置 link
  • unicodeRegExp//用于解析html标记、组件名称和属性pat的unicode字母
  • isReserved// 检查变量的开头是 $ 或 _
  • def// 在一个对象上定义一个属性的构造函数,其中 !!enumerable 强制转换 boolean
  • parsePath// 解析一个简单路径 TODO:
  • userAgent// 浏览器识别
  • inBrowser
  • _isServer//检测 vue的服务器渲染是否存在, 而且避免webpack去填充process
  • isNative //这里判断 函数是否是系统函数, 比如 Function Object ExpReg window document 等等, 这些函数应该使用c/c++实现的。这样可以区分 Symbol是系统函数, 还是用户自定义了一个Symbol
  • hasSymbol//这里使用了ES6的Reflect方法, 使用这个对象的目的是, 为了保证访问的是系统的原型方法, ownKeys 保证key的输出顺序, 先数组 后字符串
  • _Set// 设置一个Set

link

第 616 行至第 706 行

//设置warn,tip等全局变量 TODO:

  • warn
  • tip
  • generateComponentTrace// 生成组件跟踪路径(组件数规则)
  • formatComponentName// 格式化组件名

第 710 行至第 763 行

Vue核心:数据监听最重要之一的 Dep

// Dep是订阅者Watcher对应的数据依赖
var Dep = function Dep () {
  //每个Dep都有唯一的ID
  this.id = uid++;
  //subs用于存放依赖
  this.subs = [];
};

//向subs数组添加依赖
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  //通知所有绑定 Watcher。调用watcher的update()
  for (var i = 0, l = subs.length; i &lt; l; i++) {
    subs[i].update();
  }
};

强烈推荐阅读:link

Dep 相当于把 Observe 监听到的信号做一个收集(collect dependencies),然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。

第 767 行至第 900 行

Vue核心:视图更新最重要的 VNode( Virtual DOM)

  • VNode
  • createEmptyVNode
  • createTextVNode
  • cloneVNode

把你的 template 模板 描述成 VNode,然后一系列操作之后通过 VNode 形成真实DOM进行挂载

更新的时候对比旧的VNode和新的VNode,只更新有变化的那一部分,提高视图更新速度。

e.g.

<div class="parent" style="height:0" href="2222">
    111111
</div>

//转成Vnode
{    

    tag: 'div',    

    data: {        

        attrs:{href:"2222"}

        staticClass: "parent",        

        staticStyle: {            

            height: "0"

        }
    },    

    children: [{        

        tag: undefined,        

        text: "111111"

    }]
}

强烈推荐阅读:link

  • methodsToPatch

将数组的基本操作方法拓展,实现响应式,视图更新。

因为:对于对象的修改是可以直接触发响应式的,但是对数组直接赋值,是无法触发的,但是用到这里经过改造的方法。我们可以明显的看到 ob.dep.notify() 这一核心。

阶段小结

这一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重点突破!!!

第 904 行至第 1073 行

Vue核心:数据监听最重要之一的 Observer

  • 核心的核心!Observer(发布者) => Dep(订阅器) => Watcher(订阅者)

类比一个生活场景:报社将各种时下热点的新闻收集,然后制成各类报刊,发送到每家门口的邮箱里,订阅报刊人们看到了新闻,对新闻作出评论。

在这个场景里,报社发布者,新闻数据,邮箱订阅器,订阅报刊的人订阅者,对新闻评论==视图更新

  • Observer//Observer的调用过程:initState()–>observe(data)–>new Observer()
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};
  • ※※ defineReactive 函数,定义一个响应式对象,给对象动态添加 getter 和 setter ,用于依赖收集和派发更新。
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()// 1. 为属性创建一个发布者

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get // 依赖收集
  const setter = property && property.set // 派发更新
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)// 2. 获取属性值的__ob__属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()// 3. 添加 Dep
        if (childOb) {
          childOb.dep.depend()//4. 也为属性值添加同样的 Dep 
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

第 4 步非常重要。为对象的属性添加 dep.depend(),达到监听对象(引用的值)属性的目的

重点备注

Vue对数组的处理跟对象还是有挺大的不同,length是数组的一个很重要的属性,无论数组增加元素或者删除元素(通过splice,push等方法操作)length的值必定会更新,为什么不直接操作监听length呢?而需要拦截splice,push等方法进行数组的状态更新?

原因是:在数组length属性上用defineProperty拦截的时候,会报错。

Uncaught TypeError: Cannot redefine property: length

再用Object.getOwnPropertyDescriptor(arr, ‘length’)查看一下://(Object.getOwnPropertyDescriptor用于返回defineProperty.descriptor)

{
configurable: false
enumerable: false
value: 0
writable: true
}
configurable为false,且MDN上也说重定义数组的length属性在不同浏览器上表现也是不一致的,所以还是老老实实拦截splice,push等方法,或者使用ES6的Proxy。

第 1075 行至第 1153 行

  • set //在对象上设置一个属性。如果是新的属性就会触发更改通知(旧属性也会触发更新通知,因为第一个添加的时候已经监听了,之后自动触发,不再手动触发)
  • del //删除一个属性,如果必要触发通知
  • dependArray // 收集数组的依赖
    link

第 1157 行至第 1568 行

// 配置选项合并策略

ar strats = config.optionMergeStrategies;
  • mergeData
  • strats.data
  • mergeDataOrFn
  • mergeHook
  • mergeAssets
  • strats.watch
  • strats.computed
  • defaultStrat
  • checkComponents
  • validateComponentName
  • normalizeProps
  • normalizeInject
  • normalizeDirectives
  • assertObjectType
  • mergeOptions

这一部分代码写的就是父子组件配置项的合并策略,包括:默认的合并策略、钩子函数的合并策略、filters/props、data合并策略,且包括标准的组件名、props写法有一个统一化规范要求。

一图以蔽之

强烈推荐阅读:link

阶段小结

这一部分最重要的就是 Observer(观察者) ,这也是 Vue 核心中的核心!其次是 mergeOptions(组件配置项的合并策略),但是通常在用的过程中,就已经了解到了大部分的策略规则。

第 1570 行至第 1754 行

  • resolveAsset// resolveAsset 全局注册组件用到

e.g.

我们的调用 resolveAsset(context. o p t i o n s , ′ c o m p o n e n t s ′ , t a g ) , 即 拿 v m . options, 'components', tag),即拿 vm. options,components,tag)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值