Vue小记__Vue.js
发布于 3 年前 作者 banyungong 1011 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

highlight: a11y-dark

1,什么是MVC MVP MVVM

MVC

  • Model: 代表数据模型,主要任务是操作数据;
  • View:代表UI视图,主要任务是将数据模型转化成ui视图展示出来。
  • Controller: 控制器,主要任务是负责业务逻辑; View传送指令到Controller,Controller完成业务逻辑后,要求Model改变状态,Model将新的数据发送到View,用户得到反馈;

MVP

  1. 各部分之间的通信,都是双向得到;
  2. View与Model不发生联系,都通过Presenter传递;
  3. view非常薄,不部署任何业务逻辑,称为“被动视图”(Passive View),即没有任何主动性,而Presenter非常厚,所有逻辑都部署在那里;

MVVM

  • Model代表数据模型,主要任务是操作数据;
  • View代表UI视图,主要任务是将数据模型转化为UI视图展现出来;
  • ViewModel监听模型数据的改变和控制视图行为,处理用户交互,简单理解就是一个同步View和Model的对象,连接Model和View; 在mvvm架构下,View和Model之间并没有直接的联系,而是通过ViewModel进行交互,Model和ViewModel之间的交互是双向的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。ViewModel通过双向数据绑定把View层和Model层连接了起来,而View和Model之间的同步工作完全是自动的,因此开发着只需要关注业务逻辑,不需要手动操作DOM,不需要关注数据状态同步的问题,复杂的数据状态维护完全由MVVM来统一管理;

2,介绍一下VUE

VUE是一个渐进式的框架,既可以把VUE当作第三方库来使用,也可以利用vue构建复杂的单页项目;

  • 特点:入门容易,只需要学会html,css,js就可以,如果是react还要先学会es6, jsx, 函数式编程;
  • 灵活:可以当作第三方库来使用,也可以用来构架项目;
  • 高效:虚拟DOM;
  • 使用方便:指令,模版,数据双向绑定。和react相比,数据双向绑定可以自动修改,react需要手动执行setState;
  • 组件话:和react相比,vue组件话更容易被新手接收html,css, js,而react都是通过js实现;

3,组件中的data为什么不用对象

组件复用时,所有的组件实例都会共享data,如果data是对象的话,就会造成一个组件修改data以后,会影响到其他所有组件,所以需要将data改成函数,每次用到就调用一次函数获得新的对象; 当我们使用new Vue()的方式的时候,无论我们将data设置为对象还是函数都是可以的,因为new Vue()的方式是生成一个根组件,该组件不会复用,也就不存在共享data的情况了;

VueComponent.prototype.$options = {
    data: {name: 'stoney'}
}

let vc1 = new VueComponent();
vc1.$options.data = 'eric';
let vc2 = new VueComponent();
console.log(vc2.$options.data);

同一个组件被复用多次,会创建多个实例,这些实例用的都是同一个构造函数,如果data是一个对象的话,那么所有组件都共享了同一个对象。为了保证组件的数据独立性要求每个组件必须通过data函数返回一个新对象作为组件的状态;

原理:core/global-api/extend.js;

    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    function mergeOptions() {
        function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
    }
    strats.data = function (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) { //合并是会判断子类的data必须是一个函数
        if (childVal && typeof childVal !== 'function') {
          process.env.NODE_ENV !== 'production' && warn(
            'The "data" option should be a function ' +
            'that returns a per-instance value in component ' +
            'definitions.',
            vm
          )

          return parentVal
        }
        return mergeDataOrFn(parentVal, childVal)
      }

      return mergeDataOrFn(parentVal, childVal, vm)
    }

4,Vue中的v-if和v-show的区别

  • v-if如果条件不成立不会渲染当前指令所在节点的dom元素
  • v-show只是切换当前dom的显示或者隐藏 原理:

v-if经过模版编译如下:

const VueTemplateCompiler = require('vue-template-compiler')
let r1 = VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`)
console.log(r1)

render:
   'with(this){return (true)?_c(\'div\',_l((3),function(i){return _c(\'span\',[_v("hello")])}),0):_e()}',

v-show经过模版编译如下:

let r2 = VueTemplateCompiler.compile(`<div v-show="true"></div>`)
console.log(r2)

/**
 * with(this){
 *  return _c(\'div\',
 *  {directives:[{
 *    name:"show",
 *    rawName:"v-show",
 *    value:(true),
 *    expression:"true"
 *  }]
 * })
 * }
 */
 
 //v-show操作的是样式,定义在platforms/web/runtime/directives/show.js
 export default {
  bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    } else {
      el.style.display = value ? originalDisplay : 'none'
    }
  },

5, 为什么v-for和v-if不能连用

const r3 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`)
console.log(r3)
/**
 * with(this){
 *  return _l((3),function(i){
 *    return (false)?_c(\'div\',[_v("hello")]):_e()
 *  })
 * }
 * errors:
   [ 'Cannot use v-for on stateful component root element because it renders multiple elements.' ],
 */

v-for比v-if优先级高一些,如果连用的话,v-if会给每个元素都添加一下,会造成性能浪费;

6, ajax请求放在哪个生命周期中

  • 在created的时候,视图中的dom并没有渲染出来,所以此时如果直接去操作dom节点,无法找到相关的元素;
  • 在mounted中,由于此时dom已经渲染出来了,所以可以直接操作dom节点; 一般情况下都放到mounted中,保证逻辑的统一性,因为生命周期是同步执行的,ajax是异步执行的;服务端渲染不支持mounted方法,所以在服务端渲染的情况下统一放到created中;

7, 何时需要使用beforeDestroy

  • 当前页面中使用了$on方法,需要在组件销毁前解绑
  • 清除自己定义的定时器
  • 解除事件的绑定scroll mousemove…

8, Vue中v-html会导致哪些问题

  • 可能会导致xss攻击
  • v-html会替换掉标签内部的子元素 原理
let r4 = VueTemplateCompiler.compile(`<div v-html=" '<span>hrllo</span>' "></div>`)
console.log(r4)

/**
 * with(this){return _c(\'div\',{domProps:{"innerHTML":_s( \'<span>hrllo</span>\' )}})}
 */

9, Vue父子组件生命周期调用顺序

加载渲染过程

父beforeCreated -> 父created -> 父beforeMount -> 子beforeCreated -> 子created -> 子beforeMount -> 子mounted -> 父moubted

子组件更新过程

父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

父组件更新过程

父beforeUpdate -> 父updated

销毁过程

父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

总结

  • 组件的调用顺序都是先付后子,渲染完成的顺序肯定是先子后父;
  • 组件的销毁操作是先父后子,销毁完成是先子后父;

10, Vue组件相关

什么是组件化,有什么好处

任何一个页面我们都可以抽象成一堆组件构成的组件树。大到一个页面,小到一个按钮都可以是一个组件,一个页面就是由很多组件嵌套拼接组成,这就是组件化;

组件化的好处

  • 复用性强
  • 分工开发
  • 代码好管理
  • 耦合度低

vue如何创建组件

全局组件

Vue.component(组件的名称'', 组件详情的对象);

局部组件

new Vue({
    ...
    components: {
        组件的名称: 组件的详情对象
    }
})

单文件组件

.vue文件

<template>
<script>
<style>

Vue组件如何通信

  • 父子间通信 父->子通过props, 子->父, $on, $emit;
  • 获取父子组件实例的方式, $parent, $children;
  • 在父组件中提供子组件进行消费Provider, inject;
  • Ref获取实例的方式调用组件的属性或者方法;
  • Event Bus实现跨组件通信;
  • 插槽
  • Vuex状态管理实现通信;
  • attrs: 包含了父作用域中不作为prop被识别(且获取)的attribute绑定(class和style除外), 当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和 style除外),并且可以通过v-bind="$attrs"传入内部组件-在创建高级别组件时非常有用;
  • provider inject; provide和inject主要在开发高阶插件/组件库时使用,并不推荐用于普通应用程序代码中;父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject方法,那么就可以注入provider中的数据;而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用;
  • v-model: 父组件通过v-model传递给子组件时,会自动传递一个value的prop的属性,在子组件中通过this.$emit9’input’, val)自动修改v-model绑定的值;

11, Vue中相同的逻辑如何抽离

Vue.mixin用法,给组件每个生命周期,函数都混入一些公共逻辑;

12, 请说一下响应式数据的原理

  • 核心点:Object.defineProperty
  • 默认Vue在初始化 数据时,会给data的属性使用Object.defineProperty重新定义所有属性,当页面取到对应属性时,会进行依赖收集(收集但前组件的watcher),如果属性发生变化会通知相关依赖进行更新操作;

444.png

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() //收集依赖
        if (childOb) {
          childOb.dep.depend()
          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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify() // 通知相关依赖进行更新
    }
  })

13, Vue中是如何检测数组变化的

  • 使用函数劫持的方法,重写了数组的方法
  • Vue将data中的数组,进行了原型链重写,指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新,如果数组中包含了引用类型,会对数组中的引用类型再次进行监控;

5555.png

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
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
  })
})

this.observeArray(value) //进行深度监控

14, 为何Vue采用异步渲染

vue是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图;

666.png

 update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this) //当数据变化时,将watcher放到一个队列中批量更新
    }
  }
  
  export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue) // 调用nextTick方法,批量的进行更新操作
    }
  }
}

15, nextTick实现原理

nextTick方法主要是使用了宏任务和微任务定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列,所以这个nextTick方法就是异步方法;

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}


let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

16, method,computed,watch的区别

  • method:页面数据每次重新渲染都会重新执行,性能消耗大,除非不希望有缓存的时候用;
  • computed:计算属性,依赖其他属性计算值,并且computed的值有缓存,只有当计算值变化才会返回内容;
  • watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
  • 总结:除非不希望缓存,一般都不会用方法,一般来说需要依赖别的属性来动态获得值的时候可以使用computed,对于监听到值的变化需要做异步操作或者开销较大的操作时用watch

17, vue生命周期相关

什么是生命周期

vue的生命周期就是vue从初始化到销毁的过程;

什么是钩子函数

在生命周期的过程中,我们有很多特殊的时间段,我希望在这些特殊的时间段对vue做一些事情,所以出现了钩子函数,钩子函数就是作者在设计vue的时候,在vue从初始化到销毁这段时间内的特殊时间段给我们一些定义函数的权利。如果定义了,就会执行,不定义就不执行;

vue有哪些生命周期

  • beforeCreate创建前
  • created 创建后
  • beforeMount 挂载前
  • mounted 挂载后
  • beforeUpdate 数据更新前
  • updated 数据更新后
  • beforeDestroy 销毁前
  • destroyed 销毁后

这些有什么区别意义

  • beforeCreate 创建前,刚执行new的操作,其他什么都没做,即在实例初始化之后,数据观测(data observer)之前被调用;
  • created 创建后,属性和方法挂载到了实例上面;在这一步,实例已完成以下配置:数据观测(data observer),属性和方法的运算,watch/event事件回调,这里没有$el;
  • beforeMount 挂载前,找到了 el或者mount对应的节点范围,但是数据还没有替换;相关的render函数首次被调用;
  • mounted 挂载后,vue范围内的变量都会被替换成data里面对应的数据值;
  • beforeUpdate 数据更新前时调用,发生在虚拟DOM重新渲染和打补丁之前;
  • updated 数据更新后,由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy 实例销毁之前调用,在这一步,实例仍然完全可用。
  • destroyed 销毁后。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

我们在项目中什么时候用到

  • created:实例已经创建完毕,因为它是最早触发的原因可以进行一些数据资源的请求。
  • mounted:实例已经挂载完成,可以进行一些DOM操作;
  • beforeUpdate:在这个钩子中进一步地更改状态,这不会触发附加的冲渲染过程。
  • updated:可以执行依赖于DOM的操作,然而在大多数情况下,你应该避免在此期间更改状态,因为这可能导致无限循环。该钩子在服务器端渲染期间不被调用;
  • destroyed:可以执行一些优化操作,清空定时器,解除绑定事件;

18, keep-alive组件的作用

主要用在组件切换的时候保存组件的状态,防止多次渲染,可以用keep-alive组件曝光需要保存的组件; 对于keep-alive组件来说,他拥有两个独有的生命周期钩子函数,activated和deactived。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactived钩子函数;

  • include - 字符串或正则表达式。只有在名称匹配时的组件会被缓存。
  • exclude - 字符串或者正则表达式,任何名称匹配的组件都不会被缓存。
  • max = 数字,最多可以缓存多少组件实例;

19, Vue loader

如果我们用脚手架开发vue,或者利用webpack配置开发vue项目的时候,肯定会解除组件化开发; 组件的定义有三种形式,全局组件,局部组件,文件组件;所谓的文件组件就是.vue的组件形式,一个文件一个组件;但是会出现一个问题就是.vue的文件无法被浏览器解析;vue loader就是把.vue文件解析成浏览器能看懂的html, css, js文件。当然webpack中还有很多loader作用都是一样的;

当style标签有scoped属性时,它的css只作用于当前组件中的元素;

<style>
    /*全局样式*/
</style>

<style scoped>
    /*本地样式*/
</style>

两者是可以混用的,使用 scoped后,父组件的样式不会渗透到子组件中;

20, 为什么要使用异步组件

如果组件功能多,打包出的结果会变大,我们可以采用异步的方式来加载组件,主要依赖import()这个语法,可以实现文件的分割加载。

components: {
    AddCustomerSchedulr: (resolve) => import ("../component/AddCustomerSchedulr")
}

21, 什么是作用域插槽

插槽

  • 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来,当初始化组件时,通过插槽将儿子进行分类{a:[vnode], b[vnode]};
  • 渲染组件时会拿出对应的slot属性的节点进行替换操作。(插槽的作用域为父组件)
let ele = VueTemplateCompiler.compile(`
  <my-component>
    <div slot="header">node</div>
    <div>react</div>
    <div slot="footer">vue</div>
  </my-component>
`)

console.log(ele)
/**
 * with(this){
 *  return _c(\'my-component\',
 *    [_c(\'div\',{
 *      attrs:{
 *        "slot":"header"
 *      },
 *      slot:"header"
 *    },[_v("node")]
 * ),_v(" "),_c(\'div\',[_v("react")]),_v(" "),_c(\'div\',{
 *    attrs:{
 *      "slot":"footer"
 *    },
 *    slot:"footer"
 *    },[_v("vue")])])
 * }
 */
let ele = VueTemplateCompiler.compile(`
  <div>
    <slot name="header"></slot>
    <slot name="footer"></slot>/slot>
  </div>
`)
console.log(ele)

/**
 * with(this){return _c(\'div\',[_t("header"),_v(" "),_t("footer"),_v("/slot>\\n  ")],2)}
 */

作用域插槽

  • 作用域插槽在解析的时候,不会作为组件的孩子节点,当子组件渲染时,会调用此函数进行渲染(插槽的作用域为子组件)
let ele = VueTemplateCompiler.compile(`
  <app>
    <div slot-scope="msg" slot="footer">{{msg.a}}</div>
  </app>
`)
console.log(ele)

/**
 * with(this){
 *  return _c(\'app\',{
 *    scopedSlots:_u([{
 *      key:"footer",
 *      fn:function(msg){
 *        return _c(\'div\',{},[_v(_s(msg.a))])
 *      }
*      }])
    })
  }
 */
let ele = VueTemplateCompiler.compile(`
  <div>
    <slot name="footer" a="1" b="2"></slot>
  </div>
`)

console.log(ele)
/**
 * with(this){
 *  return _c(\'div\',[_t("footer",null,{
 *    "a":"1",
 *    "b":"2"
 *  })],2)
 * }
 */

22, diff算法的时间复杂度

两棵树完全的diff算法是一个时间复杂度为O(n^3),Vue进行了优化, 将O(n^3)负责度的问题转换为O(n)复杂度的问题(只比较同级,不考虑跨级问题),在前端中,很少会跨层级移动dom元素,所以Virtual DOM只会对同一个层级的元素进行对比。

23, 简述vue中diff算法的原理

  1. 先同级比较,再比较子节点
  2. 先判断一方有儿子一方没有儿子的情况
  3. 比较都有儿子的情况
  4. 递归比较子节点(双指针)

24, Vue中常见的性能优化

1,编码优化

  1. 不要将所有的数据都放在 data中,data中的数据都会增加getter和 setter,会收集对应的watcher;
  2. vue在v-for时给每项元素绑定事件需要用到事件代理;
  3. spa页面采用keep-alive缓存组件;
  4. 拆分组件(提高复用性,增加代码的可维护性,减少不必要的渲染)
  5. v-if:当值为false时,内部指令不会执行,具有阻断功能,很多情况下使用v-if代替v-show;
  6. key:保证唯一性(默认vue会采用就地复用策略);
  7. object.freeze冻结数据;
  8. 合理使用路由懒加载,异步组件;
  9. 尽量采用 runtime运行时版本;
  10. 数据持久化问题(防抖 ,截流)

2,vue加载性能优化

  • 第三方模块按需导入(babel-plugin-component);
  • 滚动到可视区域动态加载;
  • 图片懒加载;

3,用户体验优化

  • app-skeleton骨架屏;
  • app-shell app壳

4,seo优化

  • 预渲染插件prerender-spa-plugin
  • 服务端渲染ssr

5,打包优化

  • 使用cdn的方式加载第三方模块;
  • 多线程打包;

24, action和mutation区别

  • mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测)
  • action是异步操作,可以获取数据后调用mutation提交最终数据

25, vue中computed的特点

默认computed也是一个watcher,但是其具备缓存,只有当依赖的属性发生变化时才会更新数据;

computed.png

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

function createComputedGetter (key) {
  return function computedGetter () { //取值的时候会调用此方法
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {  //做了一个dirty实现了缓存的机制
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) { //计算属性,依赖的数据发生变化
      this.dirty = true
    } else if (this.sync) { //同步watcher
      this.run()
    } else {
      queueWatcher(this) //将watcher放入队列
    }
  }

26, Watche中的deep: true是如何实现的

当用户指定了watch中的deep属性为true时,如果当前监控的值是数组类型,会对对象中的每一项进行求职,此时会将当前watcher存入到对应属性依赖中,这样数组中对象发生变化时,也会通知数据更新;

 /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this) //将当前依赖放到Dep.target上
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) { // 如果需要深度监控
        traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

27, 用vnode来描述一个DOM结构

虚拟节点就是用一个对象来描述真实的dom元素

function _c(tag, data, ...children) {
  let key = data.key
  delete data.key
  children = children.map(child => {
    if (typeof child === 'object') {
      return child
    } else {
      return vnode(undefined, undefined, undefined, undefined, child)
    }
  })
  return vnode(tag, data, key, children)
}

function vnode(tag, data, key, children, text) {
  return {
    tag, // 表示的是当前的标签名
    data, // 表示的是当前标签上的属性
    key, // 唯一表示用户可能传递
    children,
    text
  }
}

let r = _c('div', { id: 'container' }, _c('p', {}, 'hello'), 'zf')
console.log(r)

/**
 * { tag: 'div',
  data: { id: 'container' },
  key: undefined,
  children:
   [ { tag: 'p',
       data: {},
       key: undefined,
       children: [Array],
       text: undefined },
     { tag: undefined,
       data: undefined,
       key: undefined,
       children: undefined,
       text: 'zf' } ],
  text: undefined }

 */

28, vue3.0有哪些改进

  • 采用了TS来编写
  • 支持composition API
  • 响应式数据原理改成proxy
  • vdom的对比算法更新,只更新vdom的绑定了动态数据的部分

29, vue模版渲染的原理

<span>{{msg}}</span> => <span>你好</span> 正则表达式,匹配到{{xxx}}格式,读取xxx数据再替换; data: { msg: '你好' } 将template转化成render函数

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) //1, 将模版转化成est树
  if (options.optimize !== false) {
    optimize(ast, options) // 2,优化树
  }
  const code = generate(ast, options) // 生成树
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

30, vue-router传参

最简单的就是url传值,URL传值又分两种,params和query参数传值,

  • params传值是指动态路由传值;
{path: '/user/:id'} // 定义一个路由参数
<router-link to="/user/123"></router-link> // 传值
 this.$route.params.id // 取值
  • query传值,指通过?后面的拼接参数传值
<router-link to="/user?id=123"></router-link> // 传值
this.$route.query.id // 取值

31, 路由的原理,如何实现

  • 路由定义:通过点击不同的按钮展示不同的页面或者组件的功能;
  • 原理:根据不同的路径地址,展示不同的页面,组件;
  • 实现:
  1. hash #a #b
  2. history /c /d
  • 如何监听这两者发生变化
  1. hash: hashchange
  2. history: popstate

32, vue中事件绑定的原理

vue的事件绑定分为两种,一种是原生的事件绑定,还有一种是组件的事件绑定;

  1. 原生的dom事件的绑定采用的是addEventListener实现;
  2. 组件的绑定采用的是$on方法 原理: 事件的编译
let r1 = VueTemplateCompiler.compile('<div @click="fn()"></div>')
let r2 = VueTemplateCompiler.compile('<my-component @click.native="fn" @click="fn1"></my-component>')
console.log(r1.render)
console.log(r2.render)

with(this){return _c('div',{on:{"click":function($event){return fn()}}})}
with(this){return _c('my-component',{on:{"click":fn1},nativeOn:{"click":function($event){return fn($event)}}})}

1,原生dom的绑定

  • vue在创建真实 dom时会调用createElm,默认会调用invokeCreateHooks;
  • 会遍历当前平台下相对的属性处理代码,其中就有updateDOMListeners方法,内部会传入add方法;
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  target = vnode.elm
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}

function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  // async edge case #6566: inner click event triggers patch, event handler
  // attached to outer element during patch, and triggered again. This
  // happens because browsers fire microtask ticks between event propagation.
  // the solution is simple: we save the timestamp when a handler is attached,
  // and the handler would only fire if the event passed to it was fired
  // AFTER it was attached.
  if (useMicrotaskFix) {
    const attachedTimestamp = currentFlushTimestamp
    const original = handler
    handler = original._wrapper = function (e) {
      if (
        // no bubbling, should always fire.
        // this is just a safety net in case event.timeStamp is unreliable in
        // certain weird environments...
        e.target === e.currentTarget ||
        // event is fired after handler attachment
        e.timeStamp >= attachedTimestamp ||
        // #9462 bail for iOS 9 bug: event.timeStamp is 0 after history.pushState
        e.timeStamp === 0 ||
        // #9448 bail if event is fired in another document in a multi-page
        // electron/nw.js app, since event.timeStamp will be using a different
        // starting reference
        e.target.ownerDocument !== document
      ) {
        return original.apply(this, arguments)
      }
    }
  }
  target.addEventListener( // 给当前的dom添加事件
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

vue中绑定事件是直接绑定给真实dom元素的

2,组件中的绑定事件

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

function add (event, fn) {
  target.$on(event, fn)
}

组件绑定事件是通过vue自定义的$on=方法来实现的

33, v-model中的实现原理及如何自定义v-model

v-model可以看成是value+input方法的语法糖;

<el-checkbox :value="" @input=""></el-checkbox>
<el-checkbox v-model="check"></el-checkbox>

可以自己重新定义v-model的含义

Vue.component('el-checkbox', {
    template: `<input type="checkbox" :checked="check" @change="$emit('change', $event.target.checked)">`,
    model: {
        prop: 'check',
        event: 'change'
    },
    props: {
        check: Boolean
    }
})
        

会将组件的v-model默认转化为value+input

let r1 = VueTemplateCompiler.compile(`<el-checkbox v-model="check"></el-checkbox>`)
console.log(r1)

/**
 * with(this){
 *  return _c(\'el-checkbox\',{
 *    model:{
 *      value:(check),
 *      callback:function ($$v) {
 *        check=$$v
 *      },
 *      expression:"check"
 *    }
 *  })
 * }
 */

源码:

function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    on[event] = callback
  }
}

原生的v-model,会根据标签的不同生成不同的事件和属性

let r1 = VueTemplateCompiler.compile('<input v-model="value" />')
console.log(r1)

/**
 * with(this){
 *  return _c(\'input\',{
 *    directives:[{
  *    name:"model",
  *    rawName:"v-model",
  *    value:(value),
  *    expression:"value"
 *    }],
 *    domProps:{
 *      "value":(value)
 *    },
 *    on:{
 *      "input":function($event){
 *        if($event.target.composing)return;
 *        value=$event.target.value
 *      }
 *    }
 *  })
 * }
 */

编译时:不同的标签解析出的内容不一样, platform/web/compiler/directive/mode.js

if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  }

运行时:会对元素处理一些关于输入法的问题:

inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', () => {
          directive.componentUpdated(el, binding, vnode)
        })
      } else {
        setSelected(el, binding, vnode.context)
      }
      el._vOptions = [].map.call(el.options, getValue)
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart)
        el.addEventListener('compositionend', onCompositionEnd)
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener('change', onCompositionEnd)
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true
        }
      }
    }
  },
```<p style="line-height: 20px; color: #ccc">
        版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
        作者: Stoney_S
        原文链接:<a href='https://juejin.im/post/6957953951740198943'>https://juejin.im/post/6957953951740198943</a>
      </p>
回到顶部