vue的常用属性原理总结

vue的常用属性原理总结

vue之对mvvm的理解

传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数
据。将结果返回给前端,页面重新渲染
MVVM :传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户收到操作 dom 元素,将数据绑
定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。
ViewModel 就是我们 MVVM 模式中的桥梁

在这里插入图片描述

vue之响应式的原理

1、核心点: Object.defineProperty()
2、默认 Vue 在初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属
性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通
知相关依赖进行更新操作。
在这里插入图片描述

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
   if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
   if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
   val = newVal
   childOb = !shallow && observe(newVal)
   dep.notify() /**通知相关依赖进行更新**/
 }
})

Vue之如何检测数组变化

1、使用函数劫持的方式,重写了数组的方法;
2、Vue 将 data 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组
api 时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。
在这里插入图片描述

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
]
methodsToPatch.forEach(function (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) // 进行深度监控

vue之双向绑定的原理

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者。
在这里插入图片描述

  • observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。
  • 在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name’和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

首先我们为每个vue属性用Object.defineProperty()实现数据劫持,
为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,
v-model会添加一个订阅者,{{ }}也会,v-bind也会,只要用到该属性的指令理论上都会,
接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,
在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

作者:镜轩夕照

vue之computed的实现原理

首先computed计算属性 :

1、 支持缓存,只有依赖数据发生改变,才会重新进行计算。
2、 不支持异步,当computed内有异步操作时无效,无法监听数据的变化。
3、computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值。
4、如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed。
5、如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。
6、computed变量可以用于模板的渲染。

默认 computed 也是一个 watcher 是具备缓存的,只要当依赖的属性发生变化时才会更新视图

实现原理

将定义的computed属性的每一项使用Watcher类进行实例化,不过这里是按照computed-watcher的形式,
来看下如何实例化的:

class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)
    if(options) {
      this.lazy = !!options.lazy  // 表示是computed
    }
    this.dirty = this.lazy  // dirty为标记位,表示是否对computed计算
    this.getter = expOrFn  // computed的回调函数
    this.value = undefined
  }
}

实例化已经结束。并没有和之前render-watcher以及user-watcher那般,执行get方法,这是为什么?
接着分析为何如此,补全之前初始化computed的方法:

function initComputed(vm, computed) {
  ...
  for(const key in computed) {
    const getter = computed[key]  //  computed每项对应的回调函数
    ...
    if (!(key in vm)) {
      defineComputed(vm, key, getter)
    }
    ... key不能和data里的属性重名
    ... key不能和props里的属性重名
  }
}

这里的App组件在执行extend创建子组件的构造函数时,已经将key挂载到vm的原型中了,
不过之前也是执行的defineComputed方法,所以不妨碍看它做了什么:

function defineComputed(target, key) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: noop
  })
}

这个方法的作用就是让computed成为一个响应式数据,并定义它的get属性,
也就是说当页面执行渲染访问到computed时,才会触发get然后执行createComputedGetter方法,
所以之前的点到为止再这里会续上,看下get方法是怎么定义的:

function createComputedGetter (key) { // 高阶函数
  return function () {  // 返回函数
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 原来this还可以这样用,得到key对应的computed-watcher
    if (watcher) {
      if (watcher.dirty) {  // 在实例化watcher时为true,表示需要计算
        watcher.evaluate()  // 进行计算属性的求值
      }
      if (Dep.target) {  // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
        watcher.depend()  // 收集当前watcher
      }
      return watcher.value  // 返回求到的值或之前缓存的值
    }
  }
}


class Watcher {
  ...
  evaluate () {
    this.value = this.get()  //  计算属性求值
    this.dirty = false  // 表示计算属性已经计算,不需要再计算
  }
  depend () {
    let i = this.deps.length  // deps内是计算属性内能访问到的响应式数据的dep的数组集合
    while (i--) {
      this.deps[i].depend()  // 让每个dep收集当前的render-watcher
    }
  }
}

这里的变量watcher就是之前computed对应的computed-watcher实例,
接下来会执行Watcher类专门为计算属性定义的两个方法,在执行evaluate方法进行求值的过程中又会
触发computed内可以访问到的响应式数据的get,它们会将当前的computed-watcher作为依赖收集
到自己的dep里,计算完毕之后将dirty置为false,表示已经计算过。

然后执行depend让计算属性内的响应式数据订阅当前的render-watcher,
所以computed内的响应式数据会收集computed-watcher和render-watcher两个watcher,
当computed内的状态发生变更触发set后,首先通知computed需要进行重新计算,
然后通知到视图执行渲染,再渲染中会访问到computed计算后的值,最后渲染到页面。

在这里插入图片描述

function initComputed (vm: Component, computed: Object) {
 const watchers = vm._computedWatchers = Object.create(null)
 const isSSR = isServerRendering()
 for (const key in computed) {
  const userDef = computed[key]
  const getter = typeof userDef === 'function' ? userDef : userDef.get
  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) { // 如果依赖的值没发生变化,就不会重新求值
    watcher.evaluate()
  }
   if (Dep.target) {
    watcher.depend()
  }
   return watcher.value
 }
}
}

vue之watch的实现原理

首先watch监听 :

1、不支持缓存,数据更新,直接会触发相应的操作;
2、watch支持异步操作;
3、监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
4、当一个属性发生变化时,需要执行对应的操作;一对多;
5、监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数:

  • immediate:组件加载立即触发回调函数执行;
  • deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变,注意监听数组的变动不需要这么做。
  • 注意:deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到。
  • sync:true,多次修改都能获取值,如果没有true,则多次修改只会触发一次修改

6、如果监控的key对应的是对象是无法获取老状态的,因为对象的引用关系

注意点:
1、如果监控的key对应的是对象是无法获取老状态的,因为对象的引用关系
2、watch一个属性,可以注册多个监听器
3、可以写成handler格式,监听methods里面的方法和参数传递

实现原理

watch在一开始初始化的时候,会读取一遍监听的数据的值,此时那个数据就收集到watch的watcher了
然后给watch设置的handler,watch 会放入watcher的更新函数中,当数据改变时,
通知watch的watcher进行更新,于是设置的handler就被调用。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>
  <!-- <script src="./node_modules/vue/dist/vue/index.js"></script> -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          name: 'fqniu',
          age: 26
        }
      },
      watch: {
        name: {
          // 这里可以是字符串,它会去查找methods里面的方法
          // hander: 'hander',
          hander(newValue, oldValue) {
            console.log(newValue, oldValue);
          },
          sync: true
        },
        // 1、第一种情况
        age: {
          hander(newValue, oldValue) {
            console.log(newValue, oldValue);
          },
          sync: true
        },
        // 2、第二种情况 注册多个监听器
        age: [{
          hander(newValue, oldValue) {
            console.log(newValue, oldValue);
          },
          sync: true
        }, {
          hander(newValue, oldValue) {
            console.log(newValue, oldValue);
          },
          sync: true
        }]
      },
      /*
        注意点:
        1、如果监控的key对应的是对象是无法获取老状态的,因为对象的引用关系
        2、watch一个属性,可以注册多个监听器
        3、可以写成handler格式,监听methods里面的方法和参数传递
      */
      methods: {
        hander(newValue, oldValue) {
          console.log(newValue, oldValue);
        }
      },
    })
  </script>
</body>

</html>

看源码的话就是对 watch参数的处理 然后 调用 vm.$watch

vue之Watch中的deep:true 是如何实现的

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

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 {
   if (this.deep) { // 如果需要深度监控
    traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
  }
   popTarget()
 }
  return value
}
function _traverse (val: any, seen: SimpleSet) {
 let i, keys
 const isA = Array.isArray(val)
 if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode)
{
  return
}
 if (val.__ob__) {
  const depId = val.__ob__.dep.id
  if (seen.has(depId)) {
   return
 }
  seen.add(depId)
}
 if (isA) {
  i = val.length
  while (i--) _traverse(val[i], seen)
} else {
  keys = Object.keys(val)
  i = keys.length
  while (i--) _traverse(val[keys[i]], seen)
}
}

vue之methods,computed和watch区别

方法:
页面数据每次重新渲染都会重新执行,性能消耗大,除非不希望有缓存作用;

computed:
计算属性,依赖其他属性计算值,并且对computed的值有缓存,只有当计算值变化才会返回内容;

watch:
监听到值变化才会执行回调,在回调中可以进行一些逻辑操作;

vue之nextTick的实现原理

首先Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。
在 Vue 的文档中,说明 Vue 是异步执行 DOM 更新的。关于异步的解析,可以查看阮一峰老师的这篇文章。截取关键部分如下:

具体来说,异步执行的运行机制如下。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。

如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。

所以更改了data的数据,DOM 不会立即更新,而是在下一次事件循环清空队列时更新。

为了在数据变化之后等待 Vue.js 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) ,回调在 DOM 更新完成后调用。

事件循环 = = > 更新dom = = > 事件循环 = = > 更新dom

作者:小奶冀

每次调用 Vue.nextTick(cb) 会做些什么:

  1. cb 函数经处理压入 callbacks 数组;
  2. 执行 timerFunc 函数,延迟调用 flushCallbacks 函数;
  3. 遍历执行 callbacks 数组中的所有函数;
  4. 延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout

在这里插入图片描述

let timerFunc  // 会定义一个异步方法
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // promise
 const p = Promise.resolve()
 timerFunc = () => {
  p.then(flushCallbacks)
  if (isIOS) setTimeout(noop)
}
 isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver
 isNative(MutationObserver) ||
 MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
 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' ) { // setImmediate
 timerFunc = () => {
  setImmediate(flushCallbacks)
}
} else {
 timerFunc = () => {  // setTimeout
  setTimeout(flushCallbacks, 0)
}
}
// 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()
}
}

vuex之Mutation不能使用异步函数

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

mutation-必须是同步函数

vue之set方法的原理

首先vue响应式原理,Object.defineProperty()这个方法对对象的属性方法的添加或者删除不能做到实时的监听,数组通过索引去 修改数组都是不能被检测?所以vue实现了set方法,那么实现的set方法的原理是什么呢?

vm.$set( target, propertyName/index, value )

参数:
{Object | Array} target
{string | number} propertyName/index
{any} value

用法:

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性。

注意:

对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

源码

 src/core/observer/index.js
export function set (target, key, val){
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
       ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
            'Avoid adding reactive properties to a Vue instance or its root $data ' +
            'at runtime - declare it upfront in the data option.'
        )
        return val
    }
    if (!ob) {
        target[key] = val
        return val
    }
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}

首先,判断在非生产环境,传入的target如果是undefined、null或是原始类型,则直接跑出错误。
其次,如果判断target如果是个数组,并且key是索引的话,那么就取当前数组长度与key这两者的最大值作为数组的新长度,然后使用数组的splice方法将传入的索引key对应的val值添加进数组。Array类型数据的变化侦测方式时说过,数组的splice方法已经被我们创建的拦截器重写了,也就是说,当使用splice方法向数组内添加元素时,该元素会自动被变成响应式的。
接下来,如果传入的target不是数组,那就当作是对象来处理。首先判断传入的key是否已经存在于target中,如果存在,表明这次操作不是新增属性,而是对已有的属性进行简单的修改值,那么就只修改属性值即可,接下来获取到traget的__ob__属性,之前说过,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是Vue 实例,或者是 Vue实例的根数据对象,则抛出警告并退出程序,接着判断如果ob属性为false,那么表明target不是一个响应式对象,那么我们只需简单给它添加上新的属性,不用将新属性转化成响应式,最后,如果target是对象,并且是响应式,那么就调用defineReactive方法将新属性值添加到target上,defineReactive方会将新属性添加完之后并将其转化成响应式,最后通知依赖更新。

在这里插入图片描述

vue之渲染过程

首先核心关键的几步流程如下:

  1. new Vue,执行初始化;
  2. 挂载$mount方法,通过自定义Render方法、template、el等生成Render函数;
  3. 通过Watcher监听数据的变化;
  4. 当数据发生变化时,Render函数执行生成VNode对象;
  5. 通过patch方法,对比新旧VNode对象,通过DOM Diff算法,添加、修改、删除真正的DOM元素

至此,整个new Vue的渲染过程完毕。

vue之虚拟dom

1、虚拟dom是什么?
vue2.x版本才有虚拟dom,本质是js对象

2、虚拟dom在vue里面做了什么?
vue的渲染过程(html,css,js)

1、将真实dom转化成虚拟dom(js对象)
2、更新的时候作对比

在这里插入图片描述

3、虚拟dom是如何提升vue的渲染效率的?

vue的两点核心
1、组件化;
2、数据驱动;

虚拟dom是如何提升vue的渲染效率的?
1、局部更新(节点数据)
2、将直接操作dom的地方拿到两个js对象之中去作比较

vue之diff中的patch()

1、初始化 patch(container, vnode)
2、更新 update(vnode, newVnode)

vue之为何采用异步渲染?

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染.所以为了性能考虑。 Vue
会在本轮数据更新后,再去异步更新视图!
在这里插入图片描述

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 // 会对相同的watcher进行过滤
 if (has[id] == null) {
  has[id] = true
  if (!flushing) {
   queue.push(watcher)
 } else {
   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方法 批量的进行更新
 }
}
}

vue之组件的生命周期

要掌握每个生命周期什么时候被调用

  1. beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。
  2. created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data
    observer),属性和方法的运算, watch/event 事件回调。这里没有$el
  3. beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  4. mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  5. beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  6. updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  7. beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  8. destroyed Vue 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件
    监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

要掌握每个生命周期内部可以做什么事

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

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

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

服务端渲染不支持mounted方法,所以在服务端渲染的情况下统一放到created中

何时需要使用beforeDestroy

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

1.什么是vue生命周期?有什么作用?

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做 生命周期钩子 的函数,这给了用户在不同阶段添加自己的代码的机会。(ps:生命周期钩子就是生命周期函数)例如如果要通过某些插件操作DOM节点,如想在页面渲染完后弹出广告窗, 那最早可在mounted 中进行;

2.vue生命周期的8个阶段?

beforeCreate:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法;

created:data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作;

beforeMount:执行到这个钩子的时候,在内存中已经编译好模板了,但是还没有挂载到页面中,此时,页面还是旧的;

mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。 如果我们想要通过插件操作页面上的DOM节点,最早可以在这个阶段中进行;

beforeUpdate: 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的, 页面还没有和最新的数据保持同步;

updated:页面显示的数据和data中的数据已经保持同步了,都是最新的;

beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁;

destroyed: 这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。

3.第一次加载页面会触发哪些钩子?

beforeCreate, created, beforeMount, mounted

vue之模板编译原理

将 template 转化成 render 函数

function baseCompile (
 template: string,
 options: CompilerOptions
) {
 const ast = parse(template.trim(), options) // 1.将模板转化成ast语法树
 if (options.optimize !== false) {      // 2.优化树
  optimize(ast, options)
}
 const code = generate(ast, options)     // 3.生成树
 return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}
})
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是
标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
let root;
let currentParent;
let stack = []
function createASTElement(tagName,attrs){
  return {
    tag:tagName,
    type:1,
    children:[],
    attrs,
    parent:null
 }
}
function start(tagName,attrs){
  let element = createASTElement(tagName,attrs);
  if(!root){
    root = element;
 }
  currentParent = element;
  stack.push(element);
}
function chars(text){
  currentParent.children.push({
    type:3,
    text
 })
}
function end(tagName){
  const element = stack[stack.length-1];
  stack.length --;
  currentParent = stack[stack.length-1];
  if(currentParent){
    element.parent = currentParent;
    currentParent.children.push(element)
 }
}
function parseHTML(html){
  while(html){
    let textEnd = html.indexOf('<');
    if(textEnd == 0){
      const startTagMatch = parseStartTag();
      if(startTagMatch){
        start(startTagMatch.tagName,startTagMatch.attrs);
        continue;
     }
      const endTagMatch = html.match(endTag);
      if(endTagMatch){
        advance(endTagMatch[0].length);
        end(endTagMatch[1])
     }
   }
    let text;
    if(textEnd >=0 ){
      text = html.substring(0,textEnd)
   }
    if(text){
      advance(text.length);
      chars(text);
   }
 }
  function advance(n) {
    html = html.substring(n);
 }
  function parseStartTag(){
    const start = html.match(startTagOpen);
    if(start){
      const match = {
        tagName:start[1],
        attrs:[]
     }
      advance(start[0].length);
      let attr,end
      while(!(end = html.match(startTagClose)) &&
(attr=html.match(attribute))){
        advance(attr[0].length);
        match.attrs.push({name:attr[1],value:attr[3]})
     }
      if(end){
        advance(end[0].length);
        return match
     }
   }
 }
}
// 生成语法树
parseHTML(`<div id="container"><p>hello<span>zf</span></p></div>`);
function gen(node){
  if(node.type == 1){
    return generate(node);
 }else{
    return `_v(${JSON.stringify(node.text)})`
 }
}

function genChildren(el){
  const children = el.children;
  if(el.children){
    return `[${children.map(c=>gen(c)).join(',')}]`
 }else{
    return false;
 }
}
function genProps(attrs){
  let str = '';
  for(let i = 0; i < attrs.length;i++){
    let attr = attrs[i];
    str+= `${attr.name}:${attr.value},`;
 }
  return `{attrs:{${str.slice(0,-1)}}}`
}
function generate(el){
  let children = genChildren(el);
  let code = `_c('${el.tag}'${
    el.attrs.length? `,${genProps(el.attrs)}`:''
  }${
    children? `,${children}`:''
  })`;
  return code;
}
// 根据语法树生成新的代码
let code = generate(root);
let render = `with(this){return ${code}}`;
// 包装成函数
let renderFn = new Function(render);
console.log(renderFn.toString());

vue之v-if和v-show的区别

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

const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in
3">hello</span></div>`);
/**
with(this) {
 return (true) ? _c('div', _l((3), function (i) {
   return _c('span', [_v("hello")])
 }), 0) : _e()
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
let r2 = VueTemplateCompiler.compile(`<div v-show="true"></div>`);
/**
with(this) {
 return _c('div', {
   directives: [{
     name: "show",
     rawName: "v-show",
     value: (true),
     expression: "true"
   }]
 })
}
*/
// v-show 操作的是样式 定义在platforms/web/runtime/directives/show.js
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'
 }
}

vue之为什么V-for和v-if不能连用

const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in
3">hello</div>`);
/**
with(this) {
 return _l((3), function (i) {
   return (false) ? _c('div', [_v("hello")]) : _e()
 })
}
*/
console.log(r1.render);

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

vue之用vnode来描述一个DOM结构

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

function $createElement(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,props,key,children);
}
export function vnode(tag,data,key,children,text){
  return {
    tag, // 表示的是当前的标签名
    data, // 表示的是当前标签上的属性
    key, // 唯一表示用户可能传递
    children,
    text
 }
}

vue之diff算法的时间复杂度

两个树的完全的 diff 算法是一个时间复杂度为 O(n3) , Vue 进行了优化·O(n3) 复杂度的问题转换成
O(n) 复杂度的问题(只比较同级不考虑跨级问题) 在前端当中, 你很少会跨越层级地移动Dom元素。 所
以 Virtual Dom只会对同一个层级的元素进行对比。

vue之diff算法原理

  1. 先同级比较,在比较子节点
  2. 先判断一方有儿子一方没儿子的情况
  3. 比较都有儿子的情况
  4. 递归比较子节点

在这里插入图片描述

// core/vdom/patch.js

const oldCh = oldVnode.children // 老的儿子
const ch = vnode.children  // 新的儿子
if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    // 比较孩子
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue,
removeOnly)
 } else if (isDef(ch)) { // 新的儿子有 老的没有
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
 } else if (isDef(oldCh)) { // 如果老的有新的没有 就删除
    removeVnodes(oldCh, 0, oldCh.length - 1)
 } else if (isDef(oldVnode.text)) {  // 老的有文本 新的没文本
    nodeOps.setTextContent(elm, '') // 将老的清空
 }
} else if (oldVnode.text !== vnode.text) { // 文本不相同替换
  nodeOps.setTextContent(elm, vnode.text)
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue,
removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly
  if (process.env.NODE_ENV !== 'production') {
   checkDuplicateKeys(newCh)
 }
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
   if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh,
newEndIdx)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
  } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh,
newEndIdx)
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
  } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm,
oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
  } else {
    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh,
oldStartIdx, oldEndIdx)
    idxInOld = isDef(newStartVnode.key)
     ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    if (isUndef(idxInOld)) { // New element
     createElm(newStartVnode, insertedVnodeQueue, parentElm,
oldStartVnode.elm, false, newCh, newStartIdx)
   } else {
     vnodeToMove = oldCh[idxInOld]
     if (sameVnode(vnodeToMove, newStartVnode)) {
      patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
      oldCh[idxInOld] = undefined
      canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm,
oldStartVnode.elm)
    } else {
      // same key but different element. treat as new element
      createElm(newStartVnode, insertedVnodeQueue, parentElm,
oldStartVnode.elm, false, newCh, newStartIdx)
    }
   }
    newStartVnode = newCh[++newStartIdx]
  }
 }
  if (oldStartIdx > oldEndIdx) {
   refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
   addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx,
insertedVnodeQueue)
 } else if (newStartIdx > newEndIdx) {
   removeVnodes(oldCh, oldStartIdx, oldEndIdx)
 }
}

vue之v-for中为什么要用key

在这里插入图片描述

vue之组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用
$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法。

在这里插入图片描述

vue之data是函数形式

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

// core/global-api/extend.js line:33

function VueComponent(){}
VueComponent.prototype.$options = {
 data:{name:'zf'}
}
let vc1 = new VueComponent();
vc1.$options.data = 'jw';
let vc2 = new VueComponent();
console.log(vc2.$options.data);
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)
}

vue之事件绑定的原理

Vue 的事件绑定分为两种一种是原生的事件绑定,还有一种是组件的事件绑定;
1.原生 dom 事件的绑定,采用的是 addEventListener 实现
2.组件绑定事件采用的是 $on 方法

// 事件的编译:

let compiler = require('vue-template-compiler');
let r1 = compiler.compile('<div @click="fn()"></div>');
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></my-
component>');
console.log(r1); // {on:{click}}
console.log(r2); // {nativeOnOn:{click},on:{click}}

在这里插入图片描述

1.原生dom 的绑定

Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks
会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法

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

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
) {
 target.addEventListener( // 给当前的dom添加事件
  name,
  handler,
  supportsPassive
   ? { capture, passive }
  : capture
)
}

2.组件中绑定事件

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

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之v-model中的实现原理及如何自定义v-model

组件的v-model 就是value+input的语法糖

理解:
组件的 v-model 是 value+input方法 的语法糖
可以自己重新定义 v-model 的含义

原理:
会将组件的 v-model 默认转化成 value + input

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


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)
}
<el-checkbox :value="" @input=""></el-checkbox>
<el-checkbox v-model="check"></el-checkbox>
Vue.component('el-checkbox',{
  template:`<input type="checkbox" :checked="check"
@change="$emit('change',$event.target.checked)">`,
  model:{
    prop:'check', // 更改默认的value的名字
    event:'change' // 更改默认的方法名
 },
  props: {
    check: Boolean
 },
})
core/vdom/create-component.js line:155
原生的  v-model ,会根据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el-
checkbox>');
// 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
}
}
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
/**
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
     }
   }
 })
}
*/
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
   }
  }
 }
}

编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js

运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js

vue之v-html会导致哪些问题?

  • 可能会导致 xss 攻击
  • v-html 会替换掉标签内部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)
// with(this){return _c('div',{domProps:
{"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);
// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
   if (vnode.children) vnode.children.length = 0
   if (cur === oldProps[key]) continue
   // #6601 work around Chrome version <= 55 bug where single textNode
   // replaced by innerHTML/textContent retains its parentNode property
   if (elm.childNodes.length === 1) {
    elm.removeChild(elm.childNodes[0])
  }
}

vue之父子组件生命周期调用顺序

加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程
父beforeUpdate->父updated

销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

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

在这里插入图片描述

function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
   if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
   return
 }
  let isInitialPatch = false
  const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组
  // somthing ...
  createElm(
    vnode,
    insertedVnodeQueue,
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
 )
  // somthing...
  // 最终会依次调用收集的insert hook
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm
}
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
    // createChildren会递归创建儿子组件
    createChildren(vnode, children, insertedVnodeQueue)
    // something...
}
// 将组件的vnode插入到数组中
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
   cbs.create[i](emptyNode, vnode)
 }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
   if (isDef(i.create)) i.create(emptyNode, vnode)
   if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
 }
}
// insert方法中会依次调用mounted方法
insert (vnode: MountedComponentVNode) {
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
   componentInstance._isMounted = true
   callHook(componentInstance, 'mounted')
 }
}
function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i]); // 调用insert方法
    }
  }
}
Vue.prototype.$destroy = function () {
  callHook(vm, 'beforeDestroy') //
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null) // 先销毁儿子
  // fire destroyed hook
  callHook(vm, 'destroyed')
}

Vue之组件如何通信? 单向数据流

  • 父子间通信 父->子通过 props 、子-> 父 $ on、$emit (发布订阅)
  • 获取父子组件实例的方式 $ parent、$children
  • 在父组件中提供数据子组件进行消费 Provide、inject 插件
  • Ref 获取实例的方式调用组件的属性或者方法
  • Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
  • Vuex 状态管理实现通信 $ attrs,$ listeners

vue之相同逻辑如何抽离?

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

Vue.mixin = function (mixin: Object) {
  this.options = mergeOptions(this.options, mixin); // 将当前定义的属性合并到每个
组件中
  return this
}
export function mergeOptions (
 parent: Object,
 child: Object,
 vm?: Component
): Object {
 if (!child._base) {
  if (child.extends) { // 递归合并extends
   parent = mergeOptions(parent, child.extends, vm)
 }
  if (child.mixins) { // 递归合并mixin
   for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
 }
}
 const options = {} // 属性及生命周期的合并
 let key
 for (key in parent) {
  mergeField(key)
}
 for (key in child) {
  if (!hasOwn(parent, key)) {
   mergeField(key)
 }
}
 function mergeField (key) {
  const strat = strats[key] || defaultStrat
  // 调用不同属性合并策略进行合并
  options[key] = strat(parent[key], child[key], vm, key)
}
 return options
}

vue之为什么要使用异步组件?

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

components:{ 
  AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") //
  require([])
}
export function (
 Ctor: Class<Component> | Function | Object | void,
 data: ?VNodeData,
 context: Component,
 children: ?Array<VNode>,
 tag?: string
): VNode | Array<VNode> | void {
 // async component
 let asyncFactory
 if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回
undefiend
  // 第二次渲染时Ctor不为undefined
  if (Ctor === undefined) {
   return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
    asyncFactory,
    data,
    context,
    children,
    tag
  )
 }
}
}
function resolveAsyncComponent (
 factory: Function,
 baseCtor: Class<Component>
): Class<Component> | void {
 if (isDef(factory.resolved)) { // 3.在次渲染时可以拿到获取的最新组件
  return factory.resolved
}
 const resolve = once((res: Object | Class<Component>) => {
   factory.resolved = ensureCtor(res, baseCtor)
   if (!sync) {
    forceRender(true) //2. 强制更新视图重新渲染
  } else {
    owners.length = 0
  }
})
 const reject = once(reason => {
   if (isDef(factory.errorComp)) {
     factory.error = true
     forceRender(true)
  }
})
 const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用
resolve方法后
 sync = false
 return factory.resolved
}

vue之什么是作用域插槽?

<app><div slot="a">xxxx</div><div slot="b">xxxx</div></app>
slot name="a"
slot name="b"

创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿
子进行分类 {a:[vnode],b[vnode]}
渲染组件时会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件)

作用域插槽:
作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此
函数进行渲染。(插槽的作用域为子组件)

在这里插入图片描述

const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
  <my-component>
    <div slot="header">node</div>
    <div>react</div>
    <div slot="footer">vue</div>
  </my-component>
`)
/**
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")])])
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
  <div>
    <slot name="header"></slot>
    <slot name="footer"></slot>
    <slot></slot>
  </div>
`);
/**
with(this) {
 return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "),
_t("default")], 2)
}
**/
// _t定义在 core/instance/render-helpers/index.js


//  作用域插槽:
let ele = VueTemplateCompiler.compile(`
  <app>
    <div slot-scope="msg" slot="footer">{{msg.a}}</div>
  </app>
`);
/**
with(this) {
 return _c('app', {
   scopedSlots: _u([{ // 作用域插槽的内容会被渲染成一个函数
     key: "footer",
     fn: function (msg) {
       return _c('div', {}, [_v(_s(msg.a))])
     }
   }])
 })
}
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(`
  <div>
    <slot name="footer" a="1" b="2"></slot>
  </div>
`);
/**
with(this) {
 return _c('div', [_t("footer", null, {
   "a": "1",
   "b": "2"
 })], 2)
}
**/

vue之keep-alive的原理

keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性
include / exclude ,2个生命周期 activated , deactivated LRU算法

// core/components/keep-alive.js


export default {
 name: 'keep-alive',
 abstract: true, // 抽象组件
 props: {
  include: patternTypes,
  exclude: patternTypes,
  max: [String, Number]
},
 created () {
  this.cache = Object.create(null) // 创建缓存列表
  this.keys = [] // 创建缓存组件的key列表
},
 destroyed () { // keep-alive销毁时 会清空所有的缓存和key
  for (const key in this.cache) { // 循环销毁
   pruneCacheEntry(this.cache, key, this.keys)
 }
},
 mounted () { // 会监控include 和 include属性 进行组件的缓存处理
  this.$watch('include', val => {
   pruneCache(this, name => matches(val, name))
 })
  this.$watch('exclude', val => {
   pruneCache(this, name => !matches(val, name))
 })
},
 render () {
  const slot = this.$slots.default // 会默认拿插槽
  const vnode: VNode = getFirstComponentChild(slot) // 只缓存第一个组件
  const componentOptions: ?VNodeComponentOptions = vnode &&
vnode.componentOptions
  if (componentOptions) {
   // check pattern
   const name: ?string = getComponentName(componentOptions) // 取出组件的名字
   const { include, exclude } = this
   if ( // 判断是否缓存
    // not included
   (include && (!name || !matches(include, name))) ||
    // excluded
   (exclude && name && matches(exclude, name))
  ) {
    return vnode
  }
   const { cache, keys } = this
   const key: ?string = vnode.key == null
    // same constructor may get registered as different local components
    // so cid alone is not enough (#3269)
    ? componentOptions.Ctor.cid + (componentOptions.tag ?
`::${componentOptions.tag}` : '')
   : vnode.key // 如果组件没key 就自己通过 组件的标签和key和cid 拼接一个key
   if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance // 直接拿到组件实// make current key freshest
    remove(keys, key) // 删除当前的 [b,c,d,e,a]  // LRU 最近最久未使用法
    keys.push(key) // 并将key放到后面[b,a]
  } else {
    cache[key] = vnode // 缓存vnode
    keys.push(key) // 将key 存入
    // prune oldest entry
    if (this.max && keys.length > parseInt(this.max)) { // 缓存的太多超过了max
就需要删除掉
     pruneCacheEntry(cache, keys[0], keys, this._vnode) // 要删除第0个 但是现
在渲染的就是第0}
  }
   vnode.data.keepAlive = true // 并且标准keep-alive下的组件是一个缓存组件
 }
  return vnode || (slot && slot[0]) // 返回当前的虚拟节点
}
}

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 )
滚动到可视区域动态加载 ( https://tangbc.github.io/vue-virtual-scroll-list )
图片懒加载 (https://github.com/hilongjw/vue-lazyload.git)

3.用户体验:
app-skeleton 骨架屏
app-shell app壳
pwa serviceworker

4.SEO优化:
预渲染插件 prerender-spa-plugin
服务端渲染 ssr

5.打包优化:
使用 cdn 的方式加载第三方模块
多线程打包 happypack
splitChunks 抽离公共文件
sourceMap 生成

6.缓存,压缩
客户端缓存、服务端缓存
服务端 gzip 压缩

vue之Vue3.0的改进?

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

vue之Vue-Router中导航守卫有哪些?

完整的导航解析流程 (runQueue)

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter 。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter 。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

vue之vuex的action 和 mutation区别

mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报错

action 异步操作,可以获取数据后调佣 mutation 提交最终数据

vue之vuex工作原理

在这里插入图片描述
vuex仅仅是作为vue的一个插件而存在,不像Redux,MobX等库可以应用于所有框架,vuex只能使用在vue上,很大的程度是因为其高度依赖于vue的computed依赖检测系统以及其插件系统,

通过官方文档,每一个vue插件都需要有一个公开的install方法,vuex也不例外。其代码比较简单,调用了一下applyMixin方法,该方法主要作用就是在所有组件的beforeCreate生命周期注入了设置this.$store这样一个对象;

// src/store.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}


// src/mixins.js
// 对应applyMixin方法
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

在业务中使用vuex需要类似以下的写法

const store = new Vuex.Store({
    state,
    mutations,
    actions,
    modules
});

那么 Vuex.Store到底是什么样的东西呢?先看看他的构造函数


// src/store.js
constructor (options = {}) {
  const {
    plugins = [],
    strict = false
  } = options

  // store internal state
  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  this._watcherVM = new Vue()

  const store = this
  const { dispatch, commit } = this
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
}
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
}

  // strict mode
  this.strict = strict

  const state = this._modules.root.state

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)

  // 重点方法 ,重置VM
  resetStoreVM(this, state)

  // apply plugins
  plugins.forEach(plugin => plugin(this))

}

除了一堆初始化外,注意到了这样一行代码
resetStoreVM(this, state) 他就是整个vuex的关键

// src/store.js
function resetStoreVM (store, state, hot) {
  // 省略无关代码
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
}

去除了一些无关代码后发现,其本质就是将传入的state作为一个隐藏的vue组件的data,也就是说,我们的commit操作,本质上其实是修改这个组件的data值,结合上文的computed,修改被defineReactive代理的对象值后,会将其收集到的依赖的watcher中的dirty设置为true,等到下一次访问该watcher中的值后重新获取最新值。

这样就能解释了为什么vuex中的state的对象属性必须提前定义好,如果该state中途增加一个属性,因为该属性没有被defineReactive,所以其依赖系统没有检测到,自然不能更新。

由上所说,可以得知

store._vm.$data.$$state === store.state // true

可以在任何含有vuex框架的工程验证这一点。

vue之eventbus原理

EventBus是消息传递的一种方式,基于一个消息中心,订阅和发布消息的模式,称为发布订阅者模式。

  • on(‘name’, fn)订阅消息,name:订阅的消息名称, fn: 订阅的消息
  • emit(‘name’, args)发布消息, name:发布的消息名称 , args:发布的消息

在这里插入图片描述
注:只不过在vue中已经替我们实现好了 $ emit,$ on这些方法,所以直接用的时候去 new Vue()就可以

使用:

import Vue from 'vue'
const eventBus = new Vue()
export default eventBus

// 2.需要监听通信的组件
import eventBus from "@/utils/Event";

export default {
  created() {
    eventBus.$on("set_message", (obj) => {
      console.log("收到的名字:"+obj.name+"收到的值:"+obj.value)
    });
  },
};

// 3.发起通讯的组件
import eventBus from "@/utils/Event";
export default {
  created() {
    eventBus.$emit("set_message", {
      name: "名字",
      value: "值",
    });
  },
};

如果想移除事件的监听,可以像下面这样操作:

import eventBusfrom "@/utils/Event";

eventBus.$off("set_message", {})

你也可以使用 eventBus.$off("set_message") 来移除应用内所有对此某个事件的监听。或者直接调用 eventBus.$off() 来移除所有事件频道,不需要添加任何参数 。

上面就是 EventBus 的使用方法,是不是很简单。上面的示例中也看到了,每次使用 EventBus 时都需要在各组件中引入 Event.js 。

vue3之响应式原理

vue2原理:
在这里插入图片描述

vue之指令原理

自定义指令是vue中使用频率仅次于组件,其包含 bind、inserted、update、componentUpdated、unbind 五个生命周期钩子。

指令工作原理

初始化:

初始化全局API时,在platforms/web下,调用 createPatchFunction 生成VNode转换为真实DOM的patch方法,初始化中比较重要一步是定义了与DOM节点相对应的hooks方法,在DOM的创建(create)、激活(avtivate)、更新(update)、移除(remove)、销毁(destroy)过程中,分别会轮询调用对应的hooks方法,这些hooks中一部分是指令声明周期的入口。

// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    // modules对应vue中模块,具体有class, style, domListener, domProps, attrs, directive, ref, transition
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 最终将hooks转换为{hookEvent: [cb1, cb2 ...], ...}形式
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  // ....
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

模板编译:

模板编译就是解析指令参数,具体解构后的 ASTElement 如下所示:

{
  tag: 'input',
  parent: ASTElement,
  directives: [
    {
      arg: null, // 参数
      end: 56, // 指令结束字符位置
      isDynamicArg: false, // 动态参数,v-xxx[dynamicParams]='xxx'形式调用
      modifiers: undefined, // 指令修饰符
      name: "model",
      rawName: "v-model", // 指令名称
      start: 36, // 指令开始字符位置
      value: "inputValue" // 模板
    },
    {
      arg: null,
      end: 67,
      isDynamicArg: false,
      modifiers: undefined,
      name: "focus",
      rawName: "v-focus",
      start: 57,
      value: ""
    }
  ],
  // ...
}

生成渲染方法:

vue推荐采用指令的方式去操作DOM,由于自定义指令可能会修改DOM或者属性,所以避免指令对模板解析的影响,在生成渲染方法时,首先处理的是指令,如v-model,本质是一个语法糖,在拼接渲染函数时,会给元素加上value属性与input事件(以input为例,这个也可以用户自定义)。

with (this) {
    return _c('div', {
        attrs: {
            "id": "app"
        }
    }, [_c('input', {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (inputValue),
            expression: "inputValue"
        }, {
            name: "focus",
            rawName: "v-focus"
        }],
        attrs: {
            "type": "text"
        },
        domProps: {
            "value": (inputValue) // 处理v-model指令时添加的属性
        },
        on: {
            "input": function($event) { // 处理v-model指令时添加的自定义事件
                if ($event.target.composing)
                    return;
                inputValue = $event.target.value
            }
        }
    })])
}

生成VNode

vue的指令设计是方便我们操作DOM,在生成VNode时,指令并没有做额外处理。

生成真实DOM
在vue初始化过程中,需要记住两点:

状态的初始化是 父 -> 子,如beforeCreate、created、beforeMount,调用顺序是 父 -> 子
真实DOM挂载顺序是 子 -> 父,如mounted,这是因为在生成真实DOM过程中,如果遇到组件,会走组件创建的过程,真实DOM的生成是从子到父一级级拼接。

在patch过程中,每此调用createElm生成真实DOM时,都会检测当前VNode是否存在data属性,存在,则会调用invokeCreateHooks,走初创建的钩子函数,核心代码如下:

// src/core/vdom/patch.js
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // ...
    // createComponent有返回值,是创建组件的方法,没有返回值,则继续走下面的方法
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    // ....
    if (isDef(data)) {
        // 真实节点创建之后,更新节点属性,包括指令
        // 指令首次会调用bind方法,然后会初始化指令后续hooks方法
        invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // 从底向上,依次插入
    insert(parentElm, vnode.elm, refElm)
    // ...
  }

以上是指令钩子方法的第一个入口,是时候揭露directive.js神秘的面纱了,核心代码如下:

// src/core/vdom/modules/directives.js

// 默认抛出的都是updateDirectives方法
export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    // 销毁时,vnode === emptyNode
    updateDirectives(vnode, emptyNode)
  }
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

function _update (oldVnode, vnode) {
  const isCreate = oldVnode === emptyNode
  const isDestroy = vnode === emptyNode
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
  // 插入后的回调
  const dirsWithInsert = [
  // 更新完成后回调
  const dirsWithPostpatch = []

  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
    // 新元素指令,会执行一次inserted钩子方法
    if (!oldDir) {
      // new directive, bind
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
    } else {
      // existing directive, update
      // 已经存在元素,会执行一次componentUpdated钩子方法
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    // 真实DOM插入到页面中,会调用此回调方法
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    // VNode合并insert hooks
    if (isCreate) {
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }

  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // no longer present, unbind
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

对于首次创建,执行过程如下:

oldVnode === emptyNode,isCreate为true,调用当前元素中所有bind钩子方法。
检测指令中是否存在inserted钩子,如果存在,则将insert钩子合并到VNode.data.hooks属性中。
DOM挂载结束后,会执行invokeInsertHook,所有已挂载节点,如果VNode.data.hooks中存在insert钩子。则会调用,此时会触发指令绑定的inserted方法。

一般首次创建只会走bind和inserted方法,而update和componentUpdated则与bind和inserted对应。在组件依赖状态发生改变时,会用VNode diff算法,对节点进行打补丁式更新,其调用流程:

响应式数据发生改变,调用dep.notify,通知数据更新。
调用patchVNode,对新旧VNode进行差异化更新,并全量更新当前VNode属性(包括指令,就会进入updateDirectives方法)。

如果指令存在update钩子方法,调用update钩子方法,并初始化componentUpdated回调,将postpatch hooks挂载到VNode.data.hooks中。

当前节点及子节点更新完毕后,会触发postpatch hooks,即指令的componentUpdated方法
核心代码如下:

// src/core/vdom/patch.js
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // ...
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 全量更新节点的属性
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // ...
    if (isDef(data)) {
    // 调用postpatch钩子
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

unbind方法是在节点销毁时,调用invokeDestroyHook,这里不做过多描述。

注意事项:

使用自定义指令时,和普通模板数据绑定,v-model还是存在一定的差别,如虽然我传递参数(v-xxx=‘param’)是一个引用类型,数据变化时,并不能触发指令的bind或者inserted,这是因为在指令的声明周期内,bind和inserted只是在初始化时调用一次,后面只会走update和componentUpdated。

指令的声明周期执行顺序为bind -> inserted -> update -> componentUpdated,如果指令需要依赖于子组件的内容时,推荐在componentUpdated中写相应业务逻辑

vue中,很多方法都是循环调用,如hooks方法,事件回调等,一般调用都用try catch包裹,这样做的目的是为了防止一个处理方法报错,导致整个程序崩溃,这一点在开发过程中可以借鉴使用。

1.双向绑定和 vuex 是否冲突?
2. Vue 中内置组件transition、transition-group 的源码实现原理?
3.说说patch函数里做了啥?
4.知道 vue 生命周期内部怎么实现的么 ?
5. ssr 项目如果并发很大服务器性能怎么优化?
6.说下项目中怎么实现权限校验?
7.讲 vue-lazyloader 的原理,手写伪代码?
8. Vue.set 的原理?
9. vue compile 过程详细说一下,指令、插值表达式等 vue 语法如何生效的?

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值