Vue学习笔记(一)

对MVVM的理解

MVVM是Model-View-ViewModel的简写。本质上是MVC的改进版。MVVM就是将其中的View的状态和行为抽象化,将试图UI和业务逻辑分开。这些事情ViewModel已经帮我们做到了,可以quchuModel的数据的同时帮忙处理View中由于需要展示内容而设计的业务逻辑。

MVVM优点

  1. 低耦合 视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  2. 可重用 可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
  3. 独立开发 开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计
  4. 可测试 界面素来是比较难于测试的,测试可以针对ViewModel来写。

Vue2 与Vue3响应式数据

vue2响应式数据

  • 对象类型值变化时,是内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性)
  • 数组类型值变化时,则是通过重写数组刚发来实现。

多层对象是通过递归来实现劫持。

Vue3响应式数据

Vue3是通过proxy来实现的.

Vue中检测数组变化

  • 数组考虑性能的原因没有用defineProperty对数组的每一项进行拦截,而是重写数组的(push,shift,pop,splice,unshift,sort,reverse)方法。
  • 数组中如果是对象类型也会进行递归劫持
  • 数组的索引和长度变化时候无法被监控
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted) // 新增的数据需要进行观测
    // notify change
    ob.dep.notify()
    return result
  })
})
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted) // 新增的数据需要进行观测
    // notify change
    ob.dep.notify()
    return result
  })
})

Vue中的依赖收集过程

  • 每个属性都拥有自己的dep属性,存放它所依赖的watcher,当属性变化后会通知自己的watcher去进行更新
  • 默认在初始化时会调用render函数,此时会触发属性依赖收集dep.depend
  • 当属性发生变化时会触发watcher更新dep.notify()

Vue中的模板编译原理

template是怎样转换成render函数的

  • 将template模板转换成ast语法树-parserHtml
  • 对静态语法做静态标记- markUp diff来做优化的静态节点
  • 重新生成代码 - codeGen

src/compiler/index.js:11

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) // 1.解析ast语法树
  if (options.optimize !== false) {          
    optimize(ast, options)                    // 2.对ast树进行标记,标记静态节点
  }
  const code = generate(ast, options)         // 3.生成代码
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) // 1.解析ast语法树
  if (options.optimize !== false) {          
    optimize(ast, options)                    // 2.对ast树进行标记,标记静态节点
  }
  const code = generate(ast, options)         // 3.生成代码
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

Vue生命周期方法

  • beforeCreate 在实例初始化之后,数据观测和event/watcher事件配置之前被调用
  • created 实例已经创建完成之后被调用,在这,实例已经完成 数据观测、属性和方法的运算,watch/event事件回调 这里没有$el
  • beforeMount 在关在开始之前被调用,相关的render函数首次被调用
  • mounted el被创建的 vm.$el 替换,并关在到实例上去之后调用该钩子
  • beforeUpdate 数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
  • updated 由于数据更改导致的虚拟DOM重新渲染和补丁,在这之后会调用改钩子。
  • beforeDestroy 实例销毁之前调用。此时,实例仍然完全可用
  • destroyed 实例销毁后调用,调用后,Vue实例的所有东西都会解除绑定,所有事件监听器会被移除,所有的子实例也会被销毁。该生命周期在服务器渲染期间不被调用。
  • keep-alive (activateddeactivaed)

Vue的生命周期钩子实现原理

  • Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 内部会对钩子进行处理 将钩子函数维护成数组的形式

src/core/instance/init.js:38 初始化合并 src/core/util/options.js:388 合并选项

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal // 儿子有
    ? parentVal 
      ? parentVal.concat(childVal) // 父亲也有,那就是合并
      : Array.isArray(childVal) // 儿子是数组
        ? childVal
        : [childVal] // 不是数组包装成数组
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal // 儿子有
    ? parentVal 
      ? parentVal.concat(childVal) // 父亲也有,那就是合并
      : Array.isArray(childVal) // 儿子是数组
        ? childVal
        : [childVal] // 不是数组包装成数组
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

Vue.mixin 的使用场景和原理

  • Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据有冲突,会采用“就近原则”以组件的数据为准
  • mixin 中有很多缺陷“命名冲突问题”、“依赖问题”、“数据来源问题”

src/core/global-api/mixin.js

  Vue.mixin = function (options) {
    this.options = mergeOptions(this.options, options);
    return this
  }

// =====
const strats = {};

lifeCycleHooks.forEach(hook => {
    strats[hook] = mergeHook
})

// =====
function mergeOptions(parent, child) {
    const options = {};//合并之后的结果
    for (let key in parent) {
        mergeField(key);
    }
    for (let key in child) {
        if (parent.hasOwnProperty(key)) {
            continue;
        }
        mergeField(key)
    }
    function mergeField(key) {
        let parentValue = parent[key];
        let childValue = child[key];
        // 策略模式
        if (strats[key]) {
            options[key] = strats[key](parentValue, childValue);
        } else {
            if (isObject(parentValue) && isObject(childValue)) {
                options[key] = { ...parentValue, ...childValue }
            } else {
                // 父亲中有 儿子中没有 就用父亲的
                options[key] = child[key] || parent[key]
            }
        }

    }
    return options;
}
  Vue.mixin = function (options) {
    this.options = mergeOptions(this.options, options);
    return this
  }

// =====
const strats = {};

lifeCycleHooks.forEach(hook => {
    strats[hook] = mergeHook
})

// =====
function mergeOptions(parent, child) {
    const options = {};//合并之后的结果
    for (let key in parent) {
        mergeField(key);
    }
    for (let key in child) {
        if (parent.hasOwnProperty(key)) {
            continue;
        }
        mergeField(key)
    }
    function mergeField(key) {
        let parentValue = parent[key];
        let childValue = child[key];
        // 策略模式
        if (strats[key]) {
            options[key] = strats[key](parentValue, childValue);
        } else {
            if (isObject(parentValue) && isObject(childValue)) {
                options[key] = { ...parentValue, ...childValue }
            } else {
                // 父亲中有 儿子中没有 就用父亲的
                options[key] = child[key] || parent[key]
            }
        }

    }
    return options;
}

Vue组件data是函数的原因

  • 每次使用组件时都会组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。
function Vue() {}
function Sub() { // 会将data存起来
    this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
    Sub.options = options;
    return Sub;
}
let Child = Vue.extend({
    data: { name: 'html' }
});
// 两个组件就是两个实例, 希望数据互不干扰

let child1 = new Child();
let child2 = new Child();

console.log(child1.data.name);
child1.data.name = 'js';
console.log(child2.data.name);
function Vue() {}
function Sub() { // 会将data存起来
    this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
    Sub.options = options;
    return Sub;
}
let Child = Vue.extend({
    data: { name: 'html' }
});
// 两个组件就是两个实例, 希望数据互不干扰

let child1 = new Child();
let child2 = new Child();

console.log(child1.data.name);
child1.data.name = 'js';
console.log(child2.data.name);

nextTick的使用场景和原理

  • nextTick中的回调是在下载DOM更新循环结束之后执行的延迟回调
  • 可以用于获取更新后的DOM
  • Vue中数据更新时异步的,使用nextTick方法可以保证用户定义的逻辑在更新之后执行。

let waiting = false;
const callback = [];
function flushCallback() {
    waiting = false
    const copyCallback = callback.slice(0);
    callback.length = 0;
    copyCallback.forEach(cb => cb())
}
let timerFn = null;
if (Promise) {
    timerFn = () => Promise.resolve().then(flushCallback);
} else if (MutationObserver) {
    let textNode = document.createTextNode(1);
    let observe = new MutationObserver(flushCallback);
    observe.observe(textNode, {
        characterData: true
    })
    timerFn = () => {
        textNode.textContent = 12;
    }
} else if (setImmediate) {
    timerFn = () => {
        setImmediate(flushCallback);
    }
} else {
    timerFn = () => {
        setTimeout(flushCallback, 0);
    }
}

export function nextTick(cb, ctx) {
    callback.push(cb);
    if (!waiting) {
        timerFn();
        waiting = true;
    }
}

let waiting = false;
const callback = [];
function flushCallback() {
    waiting = false
    const copyCallback = callback.slice(0);
    callback.length = 0;
    copyCallback.forEach(cb => cb())
}
let timerFn = null;
if (Promise) {
    timerFn = () => Promise.resolve().then(flushCallback);
} else if (MutationObserver) {
    let textNode = document.createTextNode(1);
    let observe = new MutationObserver(flushCallback);
    observe.observe(textNode, {
        characterData: true
    })
    timerFn = () => {
        textNode.textContent = 12;
    }
} else if (setImmediate) {
    timerFn = () => {
        setImmediate(flushCallback);
    }
} else {
    timerFn = () => {
        setTimeout(flushCallback, 0);
    }
}

export function nextTick(cb, ctx) {
    callback.push(cb);
    if (!waiting) {
        timerFn();
        waiting = true;
    }
}

computedwatch区别

  • computedwatch都是Watcher来实现的
  • computed属性是具备缓存,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
  • watch则是监控值的变化,当值发生变化时调用对应的回调函数
  Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
    options.user = true; // 标记为用户watcher
    const watcher = new Watcher(this, exprOrFn, cb, options);
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
  }

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.user = !!options.user;
        if (typeof exprOrFn === 'function') {
            this.getter = exprOrFn;
        } else {
            this.getter = function () {
                let path = exprOrFn.split('.');
                let obj = vm;
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj;
            }
        }
        this.vm = vm;
        this.dirty = options.lazy;
        this.lazy = !!options.lazy;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set();
        this.options = options;
        // 默认应该 让 exprOrFn 执行
        this.id = id++;
        // 是否是lazy -> computed
        this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
    }

    get() { // 稍后用户更新时 可以重新调用getter方法
        // defineProperty.get 每个属性都可以收集自己的watcher
        // 结论: 一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
        pushTarget(this);
        const value = this.getter.call(this.vm); // 会调用render方法 会进行取值
        popTarget(); // Dep.target = null; 如果有值  说明在模板中有使用
        return value;
    }

    run() {
        let value = this.get();
        let oldValue = this.value;
        this.value = value;
        if (this.user) {
            this.cb.call(this.vm, value, oldValue)
        }
    }

    addDep(dep) {
        let id = dep.id;
        if (!this.depsId.has(id)) {
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

    update() {
        if (this.lazy) { // 如果是计算的属性,把dirty设置为脏的。 在下次取值时从新计算
            this.dirty = true;
        } else {
            // 每次跟新时 多次调用update 先将watcher缓存下来 ,等下一起更新
            queueWatcher(this);
            // this.get()
        }
    }

    evaluate() {
        this.dirty = false; // 表示已经取过值了
        this.value = this.get();
    }

    depend() {
        let i = this.deps.length;
        while (i--) {
            this.deps[i].depend();
        }
    }
}
  Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
    options.user = true; // 标记为用户watcher
    const watcher = new Watcher(this, exprOrFn, cb, options);
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
  }

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.user = !!options.user;
        if (typeof exprOrFn === 'function') {
            this.getter = exprOrFn;
        } else {
            this.getter = function () {
                let path = exprOrFn.split('.');
                let obj = vm;
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj;
            }
        }
        this.vm = vm;
        this.dirty = options.lazy;
        this.lazy = !!options.lazy;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set();
        this.options = options;
        // 默认应该 让 exprOrFn 执行
        this.id = id++;
        // 是否是lazy -> computed
        this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
    }

    get() { // 稍后用户更新时 可以重新调用getter方法
        // defineProperty.get 每个属性都可以收集自己的watcher
        // 结论: 一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
        pushTarget(this);
        const value = this.getter.call(this.vm); // 会调用render方法 会进行取值
        popTarget(); // Dep.target = null; 如果有值  说明在模板中有使用
        return value;
    }

    run() {
        let value = this.get();
        let oldValue = this.value;
        this.value = value;
        if (this.user) {
            this.cb.call(this.vm, value, oldValue)
        }
    }

    addDep(dep) {
        let id = dep.id;
        if (!this.depsId.has(id)) {
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

    update() {
        if (this.lazy) { // 如果是计算的属性,把dirty设置为脏的。 在下次取值时从新计算
            this.dirty = true;
        } else {
            // 每次跟新时 多次调用update 先将watcher缓存下来 ,等下一起更新
            queueWatcher(this);
            // this.get()
        }
    }

    evaluate() {
        this.dirty = false; // 表示已经取过值了
        this.value = this.get();
    }

    depend() {
        let i = this.deps.length;
        while (i--) {
            this.deps[i].depend();
        }
    }
}

Vue.set方法实现

  • 我们给对象和数组本身都增加了dep属性
  • 当给对象新增不存在的属性则触发对象依赖watcher去更新
  • 当修改数组索引时 调用组数本身的splice方法去更新数组

export function set(target, key, val) {
    // target 为数组
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val)
        return val;
    }
    // target 为对象, key在targer或者target.prototype上
    // 必须同时不能在Object.prototype上
    if (key in targer && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }

    // 以上都不成立,那么久开始给target创建一个全新的属性
    // 获取Observe实例
    const ob = target.__ob__;
    if (!ob) {
        target[key] = val;
        return val;
    }

    defineReactive(ob.val, key, val);
    ob.dep.notify();
    return val;
}

export function set(target, key, val) {
    // target 为数组
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val)
        return val;
    }
    // target 为对象, key在targer或者target.prototype上
    // 必须同时不能在Object.prototype上
    if (key in targer && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }

    // 以上都不成立,那么久开始给target创建一个全新的属性
    // 获取Observe实例
    const ob = target.__ob__;
    if (!ob) {
        target[key] = val;
        return val;
    }

    defineReactive(ob.val, key, val);
    ob.dep.notify();
    return val;
}