史上最全的vue.js源码解析(一)
虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下。vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到。所以我近期会对vue.js的源码进行解读,分享值得去学习的代码片段,这篇文章将会持续更新。
一、哪些值得我们学习的地方:
1.代码严谨,做了很多值和类型判断的工具类.例如判断已定义、未定义、判断原始类型、判断对象等等。
2.源码中多处使用到call,bind,apply,所以学习call,apply,bind很重要,请阅读我的另一篇文章可进行学习:https://blog.csdn.net/qq_43237014/article/details/118601597
3. Object.freeze冻结对象,可以提高性能。
4. 高阶函数:(很多大厂面试可能会问到)
①cached函数 创建纯函数的缓存版本。
高阶函数cached函数,输入参数为函数,返回值为函数。利用了闭包变量不会被回收的特点,
可以缓存变量,下次再调用的时候可以从缓存中读取,如果存在缓存就使用缓存,如果不存在就重新计
算下
②looseEqual
高级函数 对对象的浅相等进行判断 ES6有一个方法来判断两个对象是否相等 Object.is()
这个方法判断的是a和b是不是同一个指针的对象
判断a、b两个集合是否相等,如果a包含于b,且b包含于a,则 A = B 判断两个对象相等 (判断两个对象键名与键值对应相同 而非指引用地址相同)
③polyfillBind
高级函数,简单绑定polyfill,用于不支持它的环境,例如PhantomJS 1.x;
Polyfill是一个js库,主要抚平不同浏览器之间对js实现的差异
④passive
passived主要用于优化浏览器页面滚动的性能,Chrome提出的一个新的浏览器特性:Web开发者通过一个新的属性passive来告诉浏览器,
当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,
以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,
代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动
(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性
当前仅支持mousewheel/touch相关事件
2.Passive Event Listeners特性是为了提高页面的滑动流畅度而设计的,页面滑动流畅度的提升, 直接影响到用户对这个页面最直观的感受。
3.passive的简单实现 function handler(event) {
console.log(event.type); } document.addEventListener(“mousewheel”, handler, {passive:true});
二、vue核心
①数据监听最重要之一的 Dep
dep是一个可观察的,可以有多个订阅它的指令 ,是订阅者Watcher对应的数据依赖 , Dep 相当于把
Observe 监听到的信号做一个收集 ,然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。
在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性,都会创建一个dep对象负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知,调用watcher对象中的update方法去更新视图简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知
②视图更新最重要的 VNode
VNode是基于面向对象进行设计的 将template模板描述成 VNode,然后一系列操作之后通过VNode形成真实DOM进行挂载
更新的时候对比旧的VNode和新的VNode,只更新有变化的那一部分,提高视图更新速度
④双向绑定Observer:遍历对象的所有属性将其进行双向绑定
Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。
如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。
监听数组:从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。如果当前浏览器支持__proto__这个属性的话就可以直接覆盖该属性则使数组对象具有了重写后的数组方法。如果没有该属性的浏览器,则必须通过遍历def所有需要重写的数组方法
⑤派发更新:defineReactive 函数(响应式的核心函数)
defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符, 然后对子对象递归调用 observe
方法,这样就保证了无论 obj 的结构多复杂, 它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,
也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加
getter 和 setter。 核心就是利用 Object.defineProperty 给数据添加了 getter 和
setter,目的就是 为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter
做的事情是派发更新
三、1~1200行的代码解读:
//AMD规范和commonJS规范,都是为了模块化
//AMD规范则是非同步加载模块,允许指定回调函数。
//CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
//是个匿名函数,该匿名函数并没自执行 设计参数 window,并传入window对象。不污染全局变量,也不会被别的代码污染
(function (global, factory) {//检查 CommonJS,CommonJS模块作用域,每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见,
//所有代码都运行在模块作用域,不会污染全局作用域,
//模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
//要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() ://AMD异步模块定义 检查JavaScript依赖管理库 require.js 的存在typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());
}(this, function () {'use strict';/* *///冻结对象,一个被冻结的对象再也不能被修改。可以使用Object.freeze提升性能var emptyObject = Object.freeze({});//判断未定义function isUndef (v) {return v === undefined || v === null}
//判断已定义function isDef (v) {return v !== undefined && v !== null}function isTrue (v) {return v === true}function isFalse (v) {return v === false}/*** 判断为原始类型*/function isPrimitive (value) {return (typeof value === 'string' ||typeof value === 'number' ||// $flow-disable-linetypeof value === 'symbol' ||typeof value === 'boolean')}// 判断为对象function isObject (obj) {return obj !== null && typeof obj === 'object'}/*** 获取值的原始类型字符串,例如,[对象]*/var _toString = Object.prototype.toString;
// 切割引用类型得到后面的基本类型,例如:[object RegExp] 得到的就是 RegExpfunction toRawType (value) {return _toString.call(value).slice(8, -1)}//判断纯粹的对象:"纯粹的对象",就是通过 {}、new Object()、Object.create(null) 创建的对象function isPlainObject (obj) {return _toString.call(obj) === '[object Object]'}// 判断原生引用类型function isRegExp (v) {return _toString.call(v) === '[object RegExp]'}/*** 检查val是否是有效的数组索引,验证是否是一个非无穷大的正整数。*/function isValidArrayIndex (val) {var n = parseFloat(String(val));return n >= 0 && Math.floor(n) === n && isFinite(val)}// 判断是否是Promisefunction isPromise (val) {return (isDef(val) &&typeof val.then === 'function' &&typeof val.catch === 'function')}/*** 将值转换为字符串。*/function toString (val) {return val == null? '': Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)? JSON.stringify(val, null, 2): String(val) }/**将输入值转换为数字以便持久化。如果转换失败,则返回原始字符串。*/function toNumber (val) {var n = parseFloat(val);return isNaN(n) ? val : n}/*** makeMap 方法将字符串切割,放到map中,用于校验其中的某个字符串是否存在(区分大小写)于map中*/function makeMap (str,expectsLowerCase) {var map = Object.create(null);var list = str.split(',');for (var i = 0; i < list.length; i++) {map[list[i]] = true;}return expectsLowerCase? function (val) { return map[val.toLowerCase()]; }: function (val) { return map[val]; }}/*** 检查标记是否为内置标记。*/var isBuiltInTag = makeMap('slot,component', true);/*** 检查属性是否为保留属性。*/var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');/*** 从数组中删除项*/function remove (arr, item) {if (arr.length) {var index = arr.indexOf(item);if (index > -1) {return arr.splice(index, 1)}}}/*** 检查对象是否具有该属性。* hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。*/var hasOwnProperty = Object.prototype.hasOwnProperty;function hasOwn (obj, key) {return hasOwnProperty.call(obj, key)}/*** 创建纯函数的缓存版本。* 高阶函数cached函数,输入参数为函数,返回值为函数。利用了闭包变量不会被回收的特点,* 可以缓存变量,下次再调用的时候可以从缓存中读取,如果存在缓存就使用缓存,如果不存在就重新计算下*/function cached (fn) {var cache = Object.create(null);return (function cachedFn (str) {var hit = cache[str];return hit || (cache[str] = fn(str))})}/*** 驼峰化一个连字符连接的字符串*/var camelizeRE = /-(\w)/g;var camelize = cached(function (str) {return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })});/*** 将字符串首字母大写。*/var capitalize = cached(function (str) {return str.charAt(0).toUpperCase() + str.slice(1)});/*** 用字符号连接一个驼峰的字符串*/var hyphenateRE = /\B([A-Z])/g;var hyphenate = cached(function (str) {return str.replace(hyphenateRE, '-$1').toLowerCase()});/*** 高级函数,简单绑定polyfill,用于不支持它的环境,例如PhantomJS 1.x;* Polyfill是一个js库,主要抚平不同浏览器之间对js实现的差异*//* istanbul ignore next */function polyfillBind (fn, ctx) {function boundFn (a) {var l = arguments.length;return l? l > 1? fn.apply(ctx, arguments): fn.call(ctx, a): fn.call(ctx)}boundFn._length = fn.length;return boundFn}function nativeBind (fn, ctx) {return fn.bind(ctx)}Function.prototype.bind()
// bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,
// 而其余参数将作为新函数的参数,供调用时使用。var bind = Function.prototype.bind? nativeBind: polyfillBind;/*** 将类似数组的对象转换为实数组*/function toArray (list, start) {start = start || 0;var i = list.length - start;var ret = new Array(i);while (i--) {ret[i] = list[i + start];}return ret}/*** 将多个属性插入目标的对象*/function extend (to, _from) {for (var key in _from) {to[key] = _from[key];}return to}/*** 将对象数组合并为单个对象。*/function toObject (arr) {var res = {};for (var i = 0; i < arr.length; i++) {if (arr[i]) {extend(res, arr[i]);}}return res}/* eslint-disable no-unused-vars *//*** Perform no operation.* Stubbing args to make Flow happy without leaving useless transpiled code* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).*/function noop (a, b, c) {}/*** 总是返回false。*/var no = function (a, b, c) { return false; };/* eslint-enable no-unused-vars *//*** 返回相同的值*/var identity = function (_) { return _; };/*** 从编译器模块生成包含静态键的字符串。*/function genStaticKeys (modules) {return modules.reduce(function (keys, m) {return keys.concat(m.staticKeys || [])}, []).join(',')}/**高级函数 对对象的浅相等进行判断* ES6有一个方法来判断两个对象是否相等 Object.is() 这个方法判断的是a和b是不是同一个指针的对象* 判断a、b两个集合是否相等,如果a包含于b,且b包含于a,则 A = B*判断两个对象相等 (判断两个对象键名与键值对应相同 而非指引用地址相同)*/function looseEqual (a, b) {//判断是否恒相等if (a === b) { return true }//判断是否为对象var isObjectA = isObject(a);var isObjectB = isObject(b);if (isObjectA && isObjectB) {try {// 当a,b都是数组时var isArrayA = Array.isArray(a);var isArrayB = Array.isArray(b);if (isArrayA && isArrayB) {//递归判断两个数组中的每一项return a.length === b.length && a.every(function (e, i) {return looseEqual(e, b[i])})// 否则判断a,b是否为Date类型,} else if (a instanceof Date && b instanceof Date) {//使a和b恒相等return a.getTime() === b.getTime()//当a,b是对象时,首先判断length长度是否相同,长度相同再判断每个属性对应的属于值是否相同} else if (!isArrayA && !isArrayB) {var keysA = Object.keys(a);var keysB = Object.keys(b);return keysA.length === keysB.length && keysA.every(function (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}}/*** 返回索引,如果没找到返回-1,否则执行looseEqual()*/function looseIndexOf (arr, val) {for (var i = 0; i < arr.length; i++) {if (looseEqual(arr[i], val)) { return i }}return -1}/*** 确保函数只调用一次。*/function once (fn) {var called = false;return function () {if (!called) {called = true;fn.apply(this, arguments);}}}// cached
// polyfillBind
// looseEqual
//闭包,类型判断,函数之间的相互调用调用//定义变量var SSR_ATTR = 'data-server-rendered';// 服务端渲染// 全局函数 component、directive、filtervar ASSET_TYPES = ['component','directive','filter'];// 生命周期var LIFECYCLE_HOOKS = ['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured','serverPrefetch'];/* */// 全局配置var config = ({/*** 选项合并策略(用于core/util/options)*/// $flow-disable-lineoptionMergeStrategies: Object.create(null),/*** 是否抑制警告*/silent: false,/*** 启动时显示生产模式提示消息?*/productionTip: "development" !== 'production',/*** 是否启用devtools*/devtools: "development" !== 'production',/*** 是否记录性能*/performance: false,/*** 观察程序错误的错误处理程序*/errorHandler: null,/*** 观察者警告的警告处理程序*/warnHandler: null,/*** 忽略某些自定义元素*/ignoredElements: [],/*** v-on的自定义用户密钥别名*/// $flow-disable-linekeyCodes: Object.create(null),/*** 检查标记是否已保留,以便无法将其注册为组件。这取决于平台,可能会被覆盖。*/isReservedTag: no,/**检查属性是否已保留,以使其不能用作组件属性。这取决于平台,可能会被覆盖。*/isReservedAttr: no,/*** 检查标记是否为未知元素。依赖于平台。*/isUnknownElement: no,/*** 获取元素的命名空间*/getTagNamespace: noop,/*** 解析特定平台的真实标记名。*/parsePlatformTagName: identity,/*** 检查属性是否必须使用属性绑定,例如valuePlatform dependent。*/mustUseProp: no,/*** 异步执行更新。打算由Vue Test Utils使用,如果设置为false,这将显著降低性能。*/async: true,/*** 因遗留原因暴露*/_lifecycleHooks: LIFECYCLE_HOOKS});/* *//**用于解析html标记、组件名称和属性路径的unicode字母。使用https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname */var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;/*** 检查字符串是否以$或_开头*/function isReserved (str) {var c = (str + '').charCodeAt(0);return c === 0x24 || c === 0x5F}/*** 定义属性。* 在一个对象上定义一个属性的构造函数,其中 !!enumerable 强制转换 boolean*/function def (obj, key, val, enumerable) {Object.defineProperty(obj, key, {value: val,enumerable: !!enumerable,writable: true,configurable: true});}/*** 解析简单路径。*/var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]"));function parsePath (path) {if (bailRE.test(path)) {return}var segments = path.split('.');return function (obj) {for (var i = 0; i < segments.length; i++) {if (!obj) { return }obj = obj[segments[i]];}return obj}}/* */var hasProto = '__proto__' in {};// 判断浏览器环境var inBrowser = typeof window !== 'undefined';// 运行环境是微信var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();//浏览器 UA 判断var UA = inBrowser && window.navigator.userAgent.toLowerCase();// IE的内核是tridentvar isIE = UA && /msie|trident/.test(UA);var isIE9 = UA && UA.indexOf('msie 9.0') > 0;var isEdge = UA && UA.indexOf('edge/') > 0;// 判断 android iosvar isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');// 判断chromevar isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge;var isPhantomJS = UA && /phantomjs/.test(UA);var isFF = UA && UA.match(/firefox\/(\d+)/);// Firefox has a "watch" function on Object.prototype...var nativeWatch = ({}).watch;/*https://blog.csdn.net/dj0379/article/details/528833151.passive是什么?passived主要用于优化浏览器页面滚动的性能,Chrome提出的一个新的浏览器特性:Web开发者通过一个新的属性passive来告诉浏览器,当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件2.Passive Event Listeners特性是为了提高页面的滑动流畅度而设计的,页面滑动流畅度的提升,直接影响到用户对这个页面最直观的感受。3.passive的简单实现function handler(event) {console.log(event.type);}document.addEventListener("mousewheel", handler, {passive:true});
*/var supportsPassive = false;if (inBrowser) {try {var opts = {};Object.defineProperty(opts, 'passive', ({get: function get () {/* istanbul ignore next */supportsPassive = true;}})); // https://github.com/facebook/flow/issues/285window.addEventListener('test-passive', null, opts);} catch (e) {}}// 视图服务器渲染器可以设置视图环境var _isServer;var isServerRendering = function () {if (_isServer === undefined) {if (!inBrowser && !inWeex && typeof global !== 'undefined') {// 检测是否存在vue服务器呈现程序并避免网页包填充进程_isServer = global['process'] && global['process'].env.VUE_ENV === 'server';} else {_isServer = false;}}return _isServer};var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;// 判断一个函数是否为 JavaScript 内置方法的方法function isNative (Ctor) {return typeof Ctor === 'function' && /native code/.test(Ctor.toString())}// 判断是否含有 Symbol 类型var hasSymbol =typeof Symbol !== 'undefined' && isNative(Symbol) &&typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);var _Set;//判断Set是否为内置对象if (typeof Set !== 'undefined' && isNative(Set)) {// use native Set when available._Set = Set;} else {// a non-standard Set polyfill that only works with primitive keys._Set = /*@__PURE__*/(function () {function Set () {this.set = Object.create(null);}// Set.prototype.has()方法接受一个参数,返回一个布尔值,表明该参数是否存在于Set对象中Set.prototype.has = function has (key) {return this.set[key] === true};// Set.prototype.add()方法用于向Set对象末尾添加一个指定的值,并返回Set对象本身。如果值已存在,则添加不会成功Set.prototype.add = function add (key) {this.set[key] = true;};// 用来清空一个Set对象的所有元素,没有返回值Set.prototype.clear = function clear () {this.set = Object.create(null);};return Set;}());}/* 日志信息模块 */var warn = noop;var tip = noop;//generateComponentTrace这个function,这个方法初始化的值也是noop,什么都没有做,目的是解决流量检查问题var generateComponentTrace = (noop);var formatComponentName = (noop);{var hasConsole = typeof console !== 'undefined';// 这个正则就是把连接符转换成的驼峰写法, 并且第一个字符大写^|[-_]的意思是字符串的开头, 或者 -_ 后面的一个字符var classifyRE = /(?:^|[-_])(\w)/g;var classify = function (str) { return str.replace(classifyRE, function (c) { return c.toUpperCase(); }).replace(/[-_]/g, ''); };//控制台打印错误提示warn = function (msg, vm) {var trace = vm ? generateComponentTrace(vm) : '';//如果配置的console.silent, 就不会打印错误日志if (config.warnHandler) {config.warnHandler.call(null, msg, vm, trace);} else if (hasConsole && (!config.silent)) {console.error(("[Vue warn]: " + msg + trace));}};//控制台打警告提示tip = function (msg, vm) {if (hasConsole && (!config.silent)) {console.warn("[Vue tip]: " + msg + (vm ? generateComponentTrace(vm) : ''));}};//格式化组件名称formatComponentName = function (vm, includeFile) {//如果是根组件, 它会有一个属性.$root 指向它自己if (vm.$root === vm) {return '<Root>'}// 先判断option有没自定义name,如果没有就用vm.name, 这个name应该是vue自己配置的一个随机数var options = typeof vm === 'function' && vm.cid != null? vm.options: vm._isVue? vm.$options || vm.constructor.options: vm;var name = options.name || options._componentTag;var file = options.__file;if (!name && file) {//如果没有name,但存在此文件,则取文件名var match = file.match(/([^/\\]+)\.vue$/);name = match && match[1];}return (//返回驼峰组件名称(name ? ("<" + (classify(name)) + ">") : "<Anonymous>") +//提示文件路径出错(file && includeFile !== false ? (" at " + file) : ''))};//返回一个str循环n次的字符串,例如str="a",n=5,则返回"aaaaa"var repeat = function (str, n) {var res = '';while (n) {if (n % 2 === 1) { res += str; }if (n > 1) { str += str; }n >>= 1;}return res};//生成组件跟踪路径(组件数规则)generateComponentTrace = function (vm) {if (vm._isVue && vm.$parent) {var tree = [];var currentRecursiveSequence = 0;while (vm) {if (tree.length > 0) {var last = tree[tree.length - 1];if (last.constructor === vm.constructor) {currentRecursiveSequence++;vm = vm.$parent;continue} else if (currentRecursiveSequence > 0) {tree[tree.length - 1] = [last, currentRecursiveSequence];currentRecursiveSequence = 0;}}tree.push(vm);vm = vm.$parent;}return '\n\nfound in\n\n' + tree.map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm)? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)"): formatComponentName(vm))); }).join('\n')} else {return ("\n\n(found in " + (formatComponentName(vm)) + ")")}};}var uid = 0;// Vue核心:数据监听最重要之一的 Dep// dep是一个可观察的,可以有多个订阅它的指令// Dep是订阅者Watcher对应的数据依赖// Dep 相当于把 Observe 监听到的信号做一个收集// 然后通过dep.notify()再通知到对应 Watcher ,从而进行视图更新。// 要实现数据的响应机制 即数据变化 视图变化
// 在vue的响应机制中 我们要使用观察模式来监听数据的变化
// 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性
// 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知
// 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知 var Dep = function Dep () {this.id = uid++;this.subs = [];};//向Dep实例中的subs数组中添加监视器对象Dep.prototype.addSub = function addSub (sub) {this.subs.push(sub);};
//删除Dep实例中subs数组中指定的监视器对象Dep.prototype.removeSub = function removeSub (sub) {remove(this.subs, sub);};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
//如果target存在就会继续监听Dep.prototype.depend = function depend () {if (Dep.target) {Dep.target.addDep(this);}};//通知监听者// 变量Dep的subs数组中的监视器并调用其的update方法Dep.prototype.notify = function notify () {var subs = this.subs.slice();if (!config.async) {// 如果未运行异步,则不会在调度程序中对sub进行排序subs.sort(function (a, b) { return a.id - b.id; });}//通知所有绑定 Watcher。调用watcher的update()for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}};// 正在评估的当前目标观察程序。Dep.target = null;var targetStack = [];// 如果Dep实例的target的值为真向targetStack数组尾部添加此监视器,设置当前Dep实例的target为传入的监视器。function pushTarget (target) {targetStack.push(target);Dep.target = target;}// 从targetStack数组中的头部取出一个监视器对象赋值给Dep实例的target属性。function popTarget () {targetStack.pop();Dep.target = targetStack[targetStack.length - 1];}// VNode是基于面向对象进行设计的// 视图更新最重要的VNode// 将template模板描述成 VNode,然后一系列操作之后通过VNode形成真实DOM进行挂载// 更新的时候对比旧的VNode和新的VNode,只更新有变化的那一部分,提高视图更新速度var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {this.tag = tag; //标签属性this.data = data;//渲染成真实DOM后,节点上到class attr style 事件等this.children = children;//子节点,也上vnodethis.text = text; // 文本this.elm = elm;//对应着真实的dom节点this.ns = undefined;//当前节点的namespace(命名空间)this.context = context;//编译的作用域this.fnContext = undefined;// 函数化组件上下文this.fnOptions = undefined;// 函数化组件配置项this.fnScopeId = undefined;// 函数化组件ScopeIdthis.key = data && data.key;//只有绑定数据下存在,在diff的过程中可以提高性能this.componentOptions = componentOptions;// 通过vue组件生成的vnode对象,若是普通dom生成的vnode,则此值为空this.componentInstance = undefined;//当前组件实例this.parent = undefined;//vnode、组件的占位节点this.raw = false; //是否为原生HTML或只是普通文本this.isStatic = false; //静态节点标识 || keep-alivethis.isRootInsert = true; // 是否作为根节点插入this.isComment = false;// 是否为注释节点this.isCloned = false; //是否为克隆节点this.isOnce = false; //是否为v-once节点this.asyncFactory = asyncFactory;// 异步工厂方法this.asyncMeta = undefined; //异步Metathis.isAsyncPlaceholder = false;//是否为异步占位};var prototypeAccessors = { child: { configurable: true } };prototypeAccessors.child.get = function () {return this.componentInstance};// 通过 Object.defineProperties 为 VNode 的原型绑定了对象 prototypeAccessors ,// prototypeAccessors 设置 child 是可修改的状态。Object.defineProperties( VNode.prototype, prototypeAccessors );/*创建一个空VNode节点*/var createEmptyVNode = function (text) {if ( text === void 0 ) text = '';var node = new VNode();node.text = text;node.isComment = true;return node};/*创建一个文本节点*/function createTextVNode (val) {return new VNode(undefined, undefined, undefined, String(val))}// cloneVNode 克隆VNode节点function cloneVNode (vnode) {var cloned = new VNode(vnode.tag,vnode.data,// 克隆子数组以避免在克隆时突变原始数组的子数组vnode.children && vnode.children.slice(),vnode.text,vnode.elm,vnode.context,vnode.componentOptions,vnode.asyncFactory);cloned.ns = vnode.ns;cloned.isStatic = vnode.isStatic;cloned.key = vnode.key;cloned.isComment = vnode.isComment;cloned.fnContext = vnode.fnContext;cloned.fnOptions = vnode.fnOptions;cloned.fnScopeId = vnode.fnScopeId;cloned.asyncMeta = vnode.asyncMeta;cloned.isCloned = true;return cloned}/*取得原生数组的原型*/var arrayProto = Array.prototype;/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/var arrayMethods = Object.create(arrayProto);/*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];// 数组⽐较特别,它的操作⽅法不会触发setter,需要特别处理。修改数组7个变更⽅法使其可以发送更新通知methodsToPatch.forEach(function (method) {/*将数组的原生方法缓存起来*/var original = arrayProto[method];def(arrayMethods, method, function mutator () {var args = [], len = arguments.length;while ( len-- ) args[ len ] = arguments[ len ];/*调用原生的数组方法*/var result = original.apply(this, args);/*数组新插入的元素需要重新进行observe才能响应式*/var ob = this.__ob__;var inserted;switch (method) {case 'push':case 'unshift':inserted = args;breakcase 'splice':inserted = args.slice(2);break}// 方法用于异步监视数组发生的变化,类似于针对对象的 Object.observe() 。if (inserted) { ob.observeArray(inserted); }/*dep通知所有注册的观察者进行响应式处理*/ob.dep.notify();return result});});// 一般也是用来获取一个JSON对象中所有属性,// getOwnPropertyNames 获取对象自身的可枚举和不可枚举属性,不包括属性名为Symbol值的属性 var arrayKeys = Object.getOwnPropertyNames(arrayMethods);/*** In some cases we may want to disable observation inside a component's* update computation.*/var shouldObserve = true;// 这个方法是vue内部对逻辑的一个优化,如果当前组件是根组件,那么根组件是不应该有props的。// toggleObserving就是禁止掉根组件 props的依赖收集function toggleObserving (value) {shouldObserve = value;}// 附加到每个被观察对象的观察者类。一旦附加,观察者将目标对象的属性键转换为getter/setter,用于收集依赖项和分派更新。// Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。
// 如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。export class Observer {var Observer = function Observer (value) {this.value = value;//这个就是传入的要被监听的对象this.dep = new Dep();// 保存新的Dep实例this.vmCount = 0;// 设置vmCount的值为0// 为被监听对象定义了一个 __ob__ 属性,这个属性的值就是当前 Observer 实例对象// 其中 def 函数其实就是 Object.defineProperty 函数的简单封装//之所以这里使用 def 函数定义 __ob__ 属性是因为这样可以定义不可枚举的属性,// 这样后面遍历数据对象的时候就能够防止遍历到 __ob__ 属性def(value, '__ob__', this);/*如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。*/if (Array.isArray(value)) {if (hasProto) {/*直接覆盖原型的方法来修改目标对象*/protoAugment(value, arrayMethods);} else {/*定义(覆盖)目标对象或数组的某一个方法*/copyAugment(value, arrayMethods, arrayKeys);}/*如果是数组则需要遍历数组的每一个成员进行observe*/this.observeArray(value);} else {// 如果是对象,遍历执行响应式的操作this.walk(value);}};// 遍历每个属性并将它们转换为getter/setter。此方法只应在值类型为Object时调用。Observer.prototype.walk = function walk (obj) {var keys = Object.keys(obj);for (var i = 0; i < keys.length; i++) {defineReactive$$1(obj, keys[i]);}};// 观察数组项的列表,如果是数组,遍历每一项,对每一项进行一次observeObserver.prototype.observeArray = function observeArray (items) {for (var i = 0, l = items.length; i < l; i++) {observe(items[i]);}};// helpers// 直接指定原型function protoAugment (target, src) {/* eslint-disable no-proto */target.__proto__ = src;/* eslint-enable no-proto */}//遍历keys,定义(覆盖)目标对象或数组的某一个方法function copyAugment (target, src, keys) {for (var i = 0, l = keys.length; i < l; i++) {var key = keys[i];def(target, key, src[key]);}}// 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,
// 如果已有Observer实例则返回现有的Observer实例。
// Vue的响应式数据都会有一个__ob__的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。function observe (value, asRootData) {/*判断是否是一个对象或者是否是一个虚拟节点*/if (!isObject(value) || value instanceof VNode) {return}var ob;/*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__;} else if (/*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/shouldObserve &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value);}/*如果是根数据则计数,后面Observer中的observe的asRootData非true*/if (asRootData && ob) {ob.vmCount++;}return ob}
// defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,
// 然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,
// 它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,
// 也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。
// 核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是
// 为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新function defineReactive$$1 (obj,key,val,customSetter,shallow) {// 这个 dep 常量所引用的 Dep 实例对象// 每一个数据字段都通过闭包引用着属于自己的 dep 常量// 每次调用 defineReactive 定义访问器属性时,该属性的 setter/getter 都闭包引用了一个属于自己的“筐var dep = new Dep();// 不可配置的直接返回// 获取该字段可能已有的属性描述对象var property = Object.getOwnPropertyDescriptor(obj, key);// 判断该字段是否是可配置的// 一个不可配置的属性是不能使用也没必要使用 Object.defineProperty 改变其属性定义的。if (property && property.configurable === false) {return}
// 保存了来自 property 对象的 get 和 set // 避免原有的 set 和 get 方法被覆盖var getter = property && property.get;var setter = property && property.set;if ((!getter || setter) && arguments.length === 2) {val = obj[key];}// 递归响应式处理 给每一层属性附加一个Obeserver实例// shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childO// 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象// walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是 und// 默认就是深度观测var childOb = !shallow && observe(val);// 数据拦截// 通过Object.defineProperty对obj的key进行数据拦截Object.defineProperty(obj, key, {enumerable: true,configurable: true,// 进行依赖收集get: function reactiveGetter () {var value = getter ? getter.call(obj) : val;// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象if (Dep.target) {// 加入到dep去管理watcher dep.depend();if (childOb) {childOb.dep.depend();if (Array.isArray(value)) {// 循环添加watcherdependArray(value);}}}return value},set: function reactiveSetter (newVal) {// 获取value值 触发依赖收集var 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 (customSetter) {customSetter();}// 对于没有setter的访问器属性 返回if (getter && !setter) { return }if (setter) {// 设置新值setter.call(obj, newVal);} else {// 如果没有setter ,直接给新值val = newVal;}// 递归,对新来的值 对新值进行observe 返回ob实例childOb = !shallow && observe(newVal);// 当set时触发通知dep.notify();}});}// 给对象设置一个属性,添加新属性和添加触发更改通知(dep.notify),如果这个属性不是早已存在function set (target, key, val) {// 判断数据 是否是undefined或者null// 判断数据类型是否是string,number,symbol,booleanif (isUndef(target) || isPrimitive(target)) {// target必须是对象或者数组,否则发出警告warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));}// 如果是数组 并且检查key是否是有效的数组索引if (Array.isArray(target) && isValidArrayIndex(key)) {// 设置数组长度target.length = Math.max(target.length, key);// 像数组尾部添加一个新数据,相当于pushtarget.splice(key, 1, val);return val}// 如果key在target上 并且不是通过原型链查找的,就直接赋值if (key in target && !(key in Object.prototype)) {target[key] = val;return val}// 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中var ob = (target).__ob__;// 如果是vue 或者 检测vue被实例化的次数 vmCountif (target._isVue || (ob && ob.vmCount)) {warn('Avoid adding reactive properties to a Vue instance or its root $data ' +'at runtime - declare it upfront in the data option.');return val}// 如果ob不存在,证明没有添加观察者,不是相应,直接赋值返回if (!ob) {target[key] = val;return val}// 通过defineReactive将ob.value加入的观察者defineReactive$$1(ob.value, key, val);// 触发通知更新,通知订阅者obj.value更新数据ob.dep.notify();return val}
史上最全的vue.js源码解析(一)相关推荐
- 史上最全的vue.js源码解析(四)
虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下.vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到.所以我近期会对vue.js的源码进行解读,分享值得去学习 ...
- 【Vue.js源码解析 一】-- 响应式原理
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 课程目标 Vue.js 的静态成员和实例成员初始化过程 首次渲染的过程 数据响应式原理 – 最核心的特性之一 准备工作 ...
- js define函数_不夸张,这真的是前端圈宝藏书!360前端工程师Vue.js源码解析
优秀源代码背后的思想是永恒的.普适的. 这些年来,前端行业一直在飞速发展.行业的进步,导致对从业人员的要求不断攀升.放眼未来,虽然仅仅会用某些框架还可以找到工作,但仅仅满足于会用,一定无法走得更远.随 ...
- 【Vue.js源码解析 三】-- 模板编译和组件化
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 模板编译 模板编译的主要目的是将模板 (template) 转换为渲染函数 (render) <div> ...
- 【Vue.js源码解析 二】-- 虚拟 DOM
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 虚拟 DOM 基本介绍 什么是虚拟 DOM 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象 ...
- 史上最全基于vue的图片裁剪vue-cropper使用
史上最全基于vue的图片裁剪vue-cropper使用 基于vue的图片裁剪vue-cropper 新的需求 vue-cropper官网 代码拷贝 最后 基于vue的图片裁剪vue-cropper 最 ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(github.com/answershuto-)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些产出也会对同样想要学习Vue.j ...
- Vue.js 框架源码与进阶 - Vue.js 源码剖析 - 响应式原理
文章目录 一.准备工作 1.1 Vue 源码的获取 1.2 源目录结构 1.3 了解 Flow 1.4 调试设置 1.5 Vue 的不同构建版本 1.6 寻找入口文件 1.7 从入口开始 二.Vue ...
- Vue.js 源码目录设计
Vue.js 源码目录设计 Vue.js 的源码都在 src 目录下,其目录结构如下. src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # ...
- vue源码解析(3)—— Vue.js 源码构建
Vue.js 源码构建 Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下. 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.json ...
最新文章
- 神经网络相关名词解释
- Android进阶知识:绘制流程(上)
- 指针(*)、取地址()、解引用(*)与引用()
- 阿里云+HCT双证认证,架构师年薪达不到25.6万全额退款
- linux 编写java代码
- CSND博客几年没有登录了,终于找回密码来报个到!
- 拳王虚拟项目公社:微店闲鱼怎样自动化卖虚拟商品,虚拟资源自动化收钱项目
- 安卓手机多开助手v1.2 BY im大朋友
- 您的组织策略阻止我们为您完成此操作,有关详细信息,请联系技术支持
- 特价机票退票费高达80% 律师称航班延误应补偿-特价机票-退票费-霸王条款
- 2012文件服务器 读写日志,管理用户访问日志记录记录
- 探索C++虚函数在g++中的实现
- 神经元的结构模型图片,神经元模型图片解析
- 12.1.2、Doris__基本使用、doris的基本命令、建表概念、语句、建表语法、建表方式(引擎存储规则)、导入数据的方式、支持的数据类型、rollup索引
- 如何使用Grafana轻松实现OVL数据可视化
- 2020国内十大优秀炒外汇平台最新排名
- linux内存不足导致tomcat宕机
- (P13)元组:戴上了枷锁的列表
- OFC2020论文笔记 M4F.1280 Gbs IMDD PS-PAM-8 Transmission Over 10 km SSMF at O-band For Optical Interconne
- SVN服务端以及客户端的安装包(含汉化包)