看一下 vue 的源码,顺便做个笔记

  • 作为模块加载
    (function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
        (global.Vue = factory());
    }(this, function () { 'use strict';
        ...
    }));
  • set 方法 用于为对象添加属性
    function set(obj, key, val) {
        // 如已有该属性,覆盖 [hasOwn](#)
        if (hasOwn(obj, key)) {
            obj[key] = val;
            return;
        }
        // 如对象为 vueModel,设置 _data 中的值
        if (obj._isVue) {
            set(obj._data, key, val);
            return;
        }
        var ob = obj.__ob__;
        // 如果不是 vueModel 则简单设置值
        if (!ob) {
            obj[key] = val;
            return;
        }
        // 如果是 vueModel 须要额外处理工作
        // 将属性设置为可响应的 [convert](#)
        ob.convert(key, val);
        // 通知依赖该数据的对象 [notify](#)
        ob.dep.notify();
        // 如果有上级的 vueModel,更新键值和 watcher
        if (ob.vms) {
            var i = ob.vms.length;
            while (i--) {
                var vm = ob.vms[i];
                vm._proxy(key);
                vm._digest();
            }
        }
        return val;
    }
  • del 方法 为对象删除属性,如果有必要,触发 change 事件
    function del(obj, key) {
        // 检查是否已有该属性 [hasOwn](#)
        if (!hasOwn(obj, key)) {
            return;
        }
        delete obj[key];
        var ob = obj.__ob__;
        if (!ob) {
            if (obj._isVue) {
                delete obj._data[key];
                // 更新所有 watcher [_digest](#)
                obj._digest();
            }
            return;
        }
        // 通知依赖此属性的对象 [notify](#)
        ob.dep.notify();
        // 如果有上级的 vueModel,更新键值和 watcher
        if (ob.vms) {
            var i = ob.vms.length;
            while (i--) {
                var vm = ob.vms[i];
                vm._unproxy(key);
                vm._digest();
            }
        }
    }
  • 字符串检查 检查一个字符串是否能被转为字面量
    // 开头空格+(布尔值|正负数|字符串)结尾空格
    var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;

    function isLiteral(exp) {
        return literalValueRE.test(exp);
    }

检查是否为保留字,即是否以 $ 或 _ 开头

  • 类型转换 对特殊情况做了处理
    • _toString 转为字符串,确保空值输出为空字符串
    • toNumber 转为数字,无法转为数字时返回原值
    • toBoolean 转为布尔值,’false’ 被认为是假值
  • 字符串格式化
    • stripQuotes 去除字符串首尾引号
    • camelize 将用 - 拼接的字符串转为驼峰式字符串
    • hyphenate 将驼峰式的字符串转为用连字符连接
    • classify 将用 - 或 _ 或 / 连接的字符串转为驼峰式的类名
  • 辅助函数
    • bind 代替原生的 bind 方法
    • toArray 将类数组对象转为真正的数组
    • extend 类似于 jQuery 的 extend 不过不提供深层递归功能
    • isObject 判断是否为对象(排除 null),主要用于判断是否能被 json 编码
    • isPlainObject 判断是否为简单的对象即 toString 返回 [object Object]
    • def 为对象设置带描述符的属性
    • _debounce 防抖函数,防止函数被过于频繁地触发
    • indexOf 在数组中查询对象的索引值
    • cancellable 将一个函数包装成一个可取消的版本,主要用于回调函数
        function cancellable(fn) {
            var cb = function cb() {
                if (!cb.cancelled) {
                    return fn.apply(this, arguments);
                }
            };
            // 可以通过调用 cancel 方法来静默原函数
            cb.cancel = function () {
                cb.cancelled = true;
            };
            return cb;
        }
        
  • 辅助函数
    • looseEqual 检查两个参数是否相等,如果均为对象且 json 编码后相同也视为相等
    • hasProto 检查是否有 proto 属性
    • isBrowser 检查运行环境是否在浏览器中
    • 浏览器检查
      • isIE
      • isIE9
      • isAndroid
      • isIos
      • iosVersion
    • 浏览器兼容
      • hasMutationObserverBug 检查 iOS 版本是否高于 9.3
      • 根据浏览器设置 transition 和 animation 的前缀和事件名
  • 辅助函数
    • nextTick 设置一个异步任务,通过 nextTick(callbackfun, context) 使用
    var nextTick = (function () {
        var callbacks = [];
        var pending = false;
        var timerFunc;
        function nextTickHandler() {
            pending = false;
            var copies = callbacks.slice(0);
            callbacks = [];
            for (var i = 0; i < copies.length; i++) {
                copies[i]();
            }
        }

        /* istanbul ignore if */
        if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
            // 如果可以使用 MutationObserver 的话使用一个不断改变内容的文字节点来触发异步事件
            var counter = 1;
            var observer = new MutationObserver(nextTickHandler);
            var textNode = document.createTextNode(counter);
            observer.observe(textNode, {
                characterData: true
            });
            timerFunc = function () {
                counter = (counter + 1) % 2;
                textNode.data = counter;
            };
        } else {
            // webpack attempts to inject a shim for setImmediate
            // if it is used as a global, so we have to work around that to
            // avoid bundling unnecessary code.
            // 无法使用 MutationObserver 的时候使用 setTimeout
            var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
            timerFunc = context.setImmediate || setTimeout;
        }
        return function (cb, ctx) {
            var func = ctx ? function () {
                cb.call(ctx);
            } : cb;
            callbacks.push(func);
            if (pending) return;
            pending = true;
            timerFunc(nextTickHandler, 0);
        };
    })();
    
  • 数据类型
    • _Set 如果没有原生的集合 Set,则提供一个实现
    • Cache 缓存队列 可以通过 limit 设置队列最大长度
      • put 在队列尾部加入元素,如果队列已满,先调用一次 shift
      • shift 在队列首部移除元素
      • get 根据键名,在队列任意位置提取一个元素,并插到队列末尾
  • 命令解析
    • pushFilter 将一个 filter 字符串解析成过滤器名和参数,推入数组
    • processFilterArg 解析 filter 参数
    function processFilterArg(arg) {
        if (reservedArgRE.test(arg)) {
            // 经检查为保留字 /^in$|^-?\d+/, in 或者正负数
            return {
                value: toNumber(arg),
                dynamic: false
            };
        } else {
            // 检查是否被引号包裹
            var stripped = stripQuotes(arg);
            var dynamic = stripped === arg;
            // 如果被引号包裹则认为是字符串,否则为变量
            return {
                value: dynamic ? arg : stripped,
                dynamic: dynamic
            };
        }
    }
  • 命令解析
    • parseDirective 解析指令
    // 解析形如 "a + 1 | uppercase" 这样的字符串
    // 返回一个对象 
    // {
    //     expression: 'a + 1',
    //     filters: [{name: 'uppercase', args: null}]
    // }
    function parseDirective(s) {
        // 检查是否解析过
        var hit = cache$1.get(s);
        if (hit) {
            // 如已经解析过,直接使用缓存队列中的结果
            return hit;
        }

        // reset parser state
        str = s;
        inSingle = inDouble = false;
        curly = square = paren = 0;
        lastFilterIndex = 0;
        dir = {};

        for (i = 0, l = str.length; i < l; i++) {
            // 前一个字符
            prev = c;
            // 当前字符
            c = str.charCodeAt(i);
            if (inSingle) {
                // check single quote
                // 0x27 '  0x5C \
                if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
            } else if (inDouble) {
                // check double quote
                // 0x27 "  0x5C \
                if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
            } else if (c === 0x7C && // pipe
                // 0x7C |
                    str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
                if (dir.expression == null) {
                    // first filter, end of expression
                    lastFilterIndex = i + 1;
                    // 分离出表达式部分
                    dir.expression = str.slice(0, i).trim();
                } else {
                    // already has filter
                    // 解析处理 filter 部分
                    pushFilter();
                }
            } else {
                switch (c) {
                    case 0x22:
                        inDouble = true;break; // "
                    case 0x27:
                        inSingle = true;break; // '
                    case 0x28:
                        paren++;break; // (
                    case 0x29:
                        paren--;break; // )
                    case 0x5B:
                        square++;break; // [
                    case 0x5D:
                        square--;break; // ]
                    case 0x7B:
                        curly++;break; // {
                    case 0x7D:
                        curly--;break; // }
                }
            }
        }

        // 收尾
        if (dir.expression == null) {
            dir.expression = str.slice(0, i).trim();
        } else if (lastFilterIndex !== 0) {
            pushFilter();
        }

        cache$1.put(s, dir);
        return dir;
    }
  • 命令解析
    • 正则字符串处理
      • escapeRegex 转义字符串中的特殊字符,生成一个能用于构造正则的字符串
      • compileRegex 生成识别边界界定符的正则 (默认是 })
    • parseText 字符串解析,将字符串中的 token 提取出来 (双括号或三括号包裹的内容) 如输入 ‘a c’ 输出 [ {value: ‘a ‘}, {tag: true, value: ‘b’, html: false, oneTime: false}, {value: ‘ c’} ]
            function parseText(text) {
                if (!cache) {
                    compileRegex();
                }
                var hit = cache.get(text);
                // 如有缓存使用缓存
                if (hit) {
                    return hit;
                }
                if (!tagRE.test(text)) {
                    // 如果没有识别到边界符,直接返回
                    return null;
                }
                var tokens = [];
                var lastIndex = tagRE.lastIndex = 0;
                var match, index, html, value, first, oneTime;
                /* eslint-disable no-cond-assign */
                while (match = tagRE.exec(text)) {
                    /* eslint-enable no-cond-assign */
                    index = match.index;
                    if (index > lastIndex) {
                        // 不在双括号或三括号中的内容
                        tokens.push({
                            value: text.slice(lastIndex, index)
                        });
                    }
                    // tag token
                    // 是否有三大括号内容
                    html = htmlRE.test(match[0]);
                    // 获取括号中间的内容
                    value = html ? match[1] : match[2];
                    first = value.charCodeAt(0);
                    oneTime = first === 42; // *
                    value = oneTime ? value.slice(1) : value;
                    tokens.push({
                        tag: true,
                        value: value.trim(),
                        html: html,
                        oneTime: oneTime
                    });
                    lastIndex = index + match[0].length;
                }
                if (lastIndex < text.length) {
                    tokens.push({
                        value: text.slice(lastIndex)
                    });
                }
                cache.put(text, tokens);
                return tokens;
            }
        
  • 命令解析
    • tokensToExp 将多个 token 组合到表达式字符串中 如输入 ‘a c’ 输出 ‘“a “ + b + “ c”’
    • formatToken 处理单个 token 如果不是 tag (即双括号/三括号包裹内容),则直接返回,如果是,进行求值操作
    • inlineFilters 行内 filter 解析
    var filterRE = /[^|]\|[^|]/;
    function inlineFilters(exp, single) {
        if (!filterRE.test(exp)) {
            // 如果没有 filter,直接返回
            return single ? exp : '(' + exp + ')';
        } else {
            // 有 filter 的话对字符串进行解析
            var dir = parseDirective(exp);
            if (!dir.filters) {
                return '(' + exp + ')';
            } else {
                return 'this._applyFilters(' + dir.expression + // value
                        ',null,' + // oldValue (null for read)
                        JSON.stringify(dir.filters) + // filter descriptors
                        ',false)'; // write?
            }
        }
    }
    
  • config 全局配置 定义了 config 变量,保存配置信息,包括各种开关(debug, warning …),常量,分隔符等 设置分隔符会让命令解析的缓存失效

  • warning 根据 config 配置生成 warn 函数

  • transition 带过渡动画操作
    • appendWithTransition 添加元素
    • beforeWithTransition 在元素之前插入
    • removeWithTransition 删除元素
    • applyTransition 过渡动画实现
  • 节点操作相关
    • query 查询一个元素
    • inDoc 检查一个节点是否在文档中(nodetype 须为 1)
    • getAttr 获取一个节点的一个属性,并将属性从节点上移除
    • getBindAttr 获取一个 v-bind 属性
    • hasBindAttr 检查一个节点是否有绑定属性
    • before 在指定节点前插入节点
    • after 在指定节点后插入节点
    • remove 移除节点
    • prepend 在节点最前面插入子节点
    • replace 替换节点
    • on 在节点上绑定事件,实际上是调用 addEventListener
    • off 移除事件绑定
    • getClass 获取 css 类,包含对 IE9 的兼容
    • setClass 设置 css 类,包含对 IE9 的兼容
    • addClass 添加 css 类
    • removeClass 移除 css 类
    • extractContent 提取节点中的内容,生成 fragment 或 element
    • trimNode 去除首尾空格,注释
    • isTrimable 判断节点是否须要删除
    • isTemplate 检查节点是否为 template 标签
    • crateAnchor 创建锚点,主要用于节点的插入和删除
    • findRef 获取一个节点的 v-ref 属性,返回其驼峰形式
    • mapNodeRange 在一系列节点上 map 函数,参数为起始节点,结束节点和操作函数 这一组节点须为同一个节点的子节点并且是连续的
    • removeNodeRange 删除一系列节点,可以捕获被删除的节点,删除完成后可触发回调
    • isFragment 检查是否为 fragment,即 nodeType 为 11
    • getOuterHTML 获取 outerHtml 属性
    • isUnknownElement 检查是否为未知的标签元素
  • 组件识别
    • checkComponentAttr 检查一个元素是否为 vue 组件,如果是的话返回组件 id
    function checkComponentAttr(el, options) {
        // 获取标签名
        var tag = el.tagName.toLowerCase();
        // 检查标签是否有属性
        var hasAttrs = el.hasAttributes();
        if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
            // 如果不是原生标签也不是 vue 标签(slot|partial|component...)
            if (resolveAsset(options, 'components', tag)) {
                return { id: tag };
            } else {
                // 获取 is 属性(is 用于在 tr/option 之类的标签上,表示其为组件)
                var is = hasAttrs && getIsBinding(el, options);
                if (is) {
                    return is;
                } else if ('development' !== 'production') {
                    // 发出错误信息
                    var expectedTag = options._componentNameMap && options._componentNameMap[tag];
                    if (expectedTag) {
                        warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
                    } else if (isUnknownElement(el, tag)) {
                        warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
                    }
                }
            }
        } else if (hasAttrs) {
            return getIsBinding(el, options);
        }
    }
    
  • 组件识别
    • getIsBinding 获取 is 绑定,返回值为 {id: 组件id, dynamic: 是否为动态绑定}
  • strats 配置重写策略
    • mergeData 辅助 merge 函数,合并策略为补充,不重写。递归合并
    • strats.data data 的合并策略,对于组件的 data,检查其是否为函数,如果是函数, 返回一个函数,返回值为将父 data 调用结果 mergeData 合并到子 data 调用结果中
    • strats.el 合并策略是优先使用子值,如果没有再使用父值,不进行合并
    • strats.init strats.created strats.ready … 等钩子函数,合并策略为多个函数拼接, 得到一个函数数组
    • mergeAssets 合并 assets 属性(component/directive/elementDirective/filter/transition/partial) 合并策略是先将 assets 数组转为键值对的形式,然后用 extend 合并, 写入复数形式属性(components/directives/…)中
    • strats.watch strats.events 像钩子函数一样,也是作为数组拼接
    • strats.props strats.methods strats.computed 这些的策略是子属性覆盖父属性
    • defaultStrat 默认策略,当第二个参数存在是使用第二个参数否则使用第一个
  • 组件配置
    • guardComponents 确保组件的选项正确生效
    function guardComponents(options) {
        if (options.components) {
            // 将 components 转为 key-value 形式
            var components = options.components = guardArrayAssets(options.components);
            // 获取组件 id
            var ids = Object.keys(components);
            var def;
            if ('development' !== 'production') {
                var map = options._componentNameMap = {};
            }
            for (var i = 0, l = ids.length; i < l; i++) {
                var key = ids[i];
                if (commonTagRE.test(key) || reservedTagRE.test(key)) {
                // 检查命名是否非法
                    'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
                    continue;
                }
                // record a all lowercase <-> kebab-case mapping for
                // possible custom element case error warning
                if ('development' !== 'production') {
                    map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
                }
                def = components[key];
                if (isPlainObject(def)) {
                    // 注册子组件
                    components[key] = Vue.extend(def);
                }
            }
        }
    }
    
  • 组件配置
    • guardProps 将所有 props 转为 key-value 形式
    • guardArrayAssets 将数组形式的 assets 转为 key-value 形式,key 为 name 或者 id
    • mergeOptions 合并两个 option
    function mergeOptions(parent, child, vm) {
        // 对子选项进行预处理
        guardComponents(child);
        guardProps(child);
        if ('development' !== 'production') {
            if (child.propsData && !vm) {
                warn('propsData can only be used as an instantiation option.');
            }
        }
        var options = {};
        var key;
        // 如果有用 extends 声明扩展另一个组件
        if (child['extends']) {
            parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
        }
        // 如果使用 mixins 其他的配置项
        if (child.mixins) {
            for (var i = 0, l = child.mixins.length; i < l; i++) {
                var mixin = child.mixins[i];
                var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
                parent = mergeOptions(parent, mixinOptions, vm);
            }
        }
        // 使用合适的合并策略合并各个配置项
        for (key in parent) {
            mergeField(key);
        }
        for (key in child) {
            if (!hasOwn(parent, key)) {
                mergeField(key);
            }
        }
        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);
        }
        return options;
    }
    
  • 组件配置
  • resolveAsset 获取资源
function resolveAsset(options, type, id, warnMissing) {
    /* istanbul ignore if */
    if (typeof id !== 'string') {
        return;
    }
    var assets = options[type];
    var camelizedId;
    // 尝试多种格式
    var res = assets[id] ||
        // camelCase ID
        assets[camelizedId = camelize(id)] ||
        // Pascal Case ID
        assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
    if ('development' !== 'production' && warnMissing && !res) {
        warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
    }
    return res;
}
  • 依赖处理
    • Dep 依赖对象
      • 构造函数,递增获得一个 id,维持一个 subs 数组
      • target 当前的 watcher
      • prototype.addSub 添加一个订阅
      • prototype.removeSub 删除一个订阅
      • prototype.depend 将自己添加到 target 中
      • prototype.notify 通知所有订阅自己的对象进行更新
    • 在原生的数组变异方法上挂载钩子
    ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
            // cache original method
            var original = arrayProto[method];
            def(arrayMethods, method, function mutator() {
                // avoid leaking arguments:
                // http://jsperf.com/closure-with-arguments
                var i = arguments.length;
                var args = new Array(i);
                while (i--) {
                    args[i] = arguments[i];
                }
                var result = original.apply(this, args);
                var ob = this.__ob__;
                var inserted;
                switch (method) {
                    case 'push':
                        inserted = args;
                        break;
                    case 'unshift':
                        inserted = args;
                        break;
                    case 'splice':
                        inserted = args.slice(2);
                        break;
                }
                // 如果新插入了值,将其加入监控
                if (inserted) ob.observeArray(inserted);
                // 通知依赖对象进行更新
                ob.dep.notify();
                return result;
            });
    });
    
  • 依赖处理
    • 添加数组原型方法
      • $set 通过索引设置数组某一项的值,其实调用的 splice
      • $remove 从数组中移除一项,其实调用的 splice
    • shouldConvert 监控配置
    // 配置开关,是否进行监听
    var shouldConvert = true;
    function withoutConversion(fn) {
        shouldConvert = false;
        fn();
        shouldConvert = true;
    }
    
  • 依赖处理
    • Observer 对象
      • 构造函数
        // 附加到每一个须要被监听的对象上,将其属性转为 getter 和 setter
        // 通过 getter 和 setter 实现依赖和更新的处理
        function Observer(value) {
            this.value = value;
            this.dep = new Dep();
            // 添加 __ob__ 属性
            def(value, '__ob__', this);
            if (isArray(value)) {
                var augment = hasProto ? protoAugment : copyAugment;
                augment(value, arrayMethods, arrayKeys);
                this.observeArray(value);
            } else {
                // 设置 getter 和 setter
                this.walk(value);
            }
        }
        
    - prototype.walk 将对象的属性转为 getter 和 setter
    - prototype.observeArray 监控数组值
    - prototype.convert 将对象的一个属性转为响应数据
    - prototype.addVm 添加拥有此数据的 vue 实例
    - prototype.removeVm 删除所有者 vue 实例
- 辅助函数
    - protoAugment 给一个对象添加 __proto__  属性
    - copyAugment 复制函数,从一个对象复制属性到另一个对象,并设为响应式数据
    - observe 为一个值添加监控对象,如果已被监控直接返回,如果配置开关关闭返回 undefinded
    - defineReactive 为一个对象的某个属性设置 getter 和 setter 使其成为响应式的数据
        function defineReactive(obj, key, val) {
            var dep = new Dep();

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

            // cater for pre-defined getter/setters
            var getter = property && property.get;
            var setter = property && property.set;

            var childOb = observe(val);
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: function reactiveGetter() {
                    var value = getter ? getter.call(obj) : val;
                    if (Dep.target) {
                        dep.depend();
                        if (childOb) {
                            childOb.dep.depend();
                        }
                        if (isArray(value)) {
                            for (var e, i = 0, l = value.length; i < l; i++) {
                                e = value[i];
                                e && e.__ob__ && e.__ob__.dep.depend();
                            }
                        }
                    }
                    return value;
                },
                set: function reactiveSetter(newVal) {
                    var value = getter ? getter.call(obj) : val;
                    if (newVal === value) {
                        return;
                    }
                    if (setter) {
                        setter.call(obj, newVal);
                    } else {
                        val = newVal;
                    }
                    childOb = observe(newVal);
                    dep.notify();
                }
            });
        }
        
  • util 辅助对象
  • initMixin 初始化
    • 设置 Vue.prototype._init vue 实例初始化函数,每个实例都会调用,包括使用 extend 创建的实例
  • path 解析
    • getPathCharType 检查字符类型,分为几类: [, ], ., ‘, “, ident, ws, number, else
    • formatSubPath
  1. util 对象

  2. Vue 初始化方法 initMixin -. 将 options 写入实例属性,设定唯一 uid -. 绑定事件 -. fragment 存储 -. context, scope, frag 的设定,确定组件的层级关系

  3. 表达式编译状态机,提供编译过程中各种状态

  4. parse 函数,用于将字符串编译成片段数组,而且提供了缓存机制

  5. expression 对象,如果不是简单的变量,赋予生成 getter 和 setter 的能力

  6. Watcher 对象,负责触发 watcher 函数。

  7. 模板解析,将字符或节点串转成 DocumentFragment -. Fragment 对象,拥有自己的 scop,有控制自己子节点的方法,联系 vue 实例的方法

  8. 内置命令 v-for -. 参数列表 -. bind 方法 -. diff 方法,用于减少列表更新时的重绘。 -. create 方法 -. updateRef 方法 -. updateModel 方法 -. 针对 fragemnt 的创建、删除、移动、缓存等操作 未完待续