Vue | 底层分析

一、下载vue

Git 仓库地址:https://github.com/vuejs/vue.git
  • Git clone https://github.com/vuejs/vue.git
  • Pnpm install(vue是用pnpm管理工具,用npm会报错,用yarn会找不到依赖包)
  • Pnpm run dev
学习思路:
先自己搜索->描述->再深入源码学习
 

二、变化侦测

0、现象

  • 在 data 里的数据可以被插入页面中,并且一旦发生改变会被更新到页面上
  • v-model 绑定的数据,在 data 里改变或者 在页面上改变后, 会被通知给对方,进行数据更新
  • Props 里面绑定的数据,当父元素中更新后,子元素的数据也会被更新
  • Watch 和 computed 里的数据,在被更新后,页面上的对应数据也会被更新
 
提出问题
这是怎么实现的呢?vue是怎么发现数据变化的呢?又是怎么对数据变化做出反应?
先进行浅层调研
 

1、准备工作

UI = render(state)
状态state是输入,页面UI输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。
 
调研vue的变化侦测
  • Data 中所有的属性,最后都出现在 vm 上
    • Vm 上所有的属性及 vue 原型上所有的属性,在 vue 模板中可以直接用
  • Object.defineProperty
    • 通过 object.defineProperty 为对象设置属性和属性值,在 getter 中取值时调用方法,setter 中赋值时调用方法就可以实现    数据更新后,怎么做才能把与该数据相关的地方都更新呢?
    Object.definedProperty 的具体实现
去源码里找答案
 

2、数据侦测

数据侦测
D:\Projects\good-projects\vue\src\core\observer\index.ts
关键在于:Observer 类
首先,在定义数据的时候,vue为所有的数据逐层绑定observer,让数据可观测,知道数据什么时候发生变化
export class Observer {
  constructor(public value: any, public shallow = false, public mock = false) {
    /* 
    * 给 value 新增一个 __ob__ 属性,值是该 value 的 observer 实例
    * 相当于给 vaue 打上标记,表示它已经被转化成响应式的了,避免重复
    */
    def(value, '__ob__', this)
    
    // 接下来处理特殊情况
    // 如果 value 是数组
    if (isArray(value)) {
      this.observeArray(value)
    } 
    // 当value是对象时,遍历所有属性,逐个将其变成 getter/setter
    else {
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }

  // 当 value 是数组时,遍历所有属性,逐个将其变成 getter/setter
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}
// 当 value 是数组时,在观察者对象上创建观察者实例
export function observe(/*...*/): Observer | void {
  // 已有观察者实例
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }
  // 在特定情况下,创建观察者实例
  if (//...) {
    return new Observer(value, shallow, ssrMockReactivity)
  }
}
/*
 * 关键代码:使一个对象转化成可观测对象
 */
export function defineReactive(/*...*/) {
  /* 
  * 核心中的核心
  */
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 处理 getter...
    },
    set: function reactiveSetter(newVal) {
      // 处理 setter...
    }
  })
}
在上面的代码中:
  1. 我们定义了observer类,它用来将一个正常的object转换成可观测的object
  2. 并且给value新增一个ob属性,值为该valueObserver实例。这个操作相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作。
  3. 然后判断数据的类型,只有object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。
  4. 最后,在defineReactive中当传入的属性值还是一个object时使用new observer(val)来递归子属性,这样我们就可以把obj中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。 也就是说,只要我们将一个object传到observer中,那么这个object就会变成可观测的、响应式的object
通过 Object.defineProperty 实现数据代理,_data 对 data 进行数据劫持
 

3、收集依赖

收集依赖
D:\Projects\good-projects\vue\src\core\observer\dep.ts
关键在于:dep 类
能够记录与当前数据存在依赖关系的所有地方,并在当前数据发生改变时,通知视图更新这些地方
提出问题
  • 什么是依赖关系?怎么收集存储依赖关系?
  • 怎么用代码表示“用到该数据的地方”?并且如何通知更新?
 
什么是依赖关系
我们把"谁用到了这个数据"称为"谁依赖了这个数据"
 
怎么进行依赖收集
我们给每个数据都建一个依赖数组(因为一个数据可能被多处使用),谁依赖了这个数据(即谁用到了这个数据)我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍,告诉他们:"你们依赖的数据变啦,你们该更新啦!"。
依赖管理器Dep
// dep 是一个依赖管理器,负责管理某个数据的依赖数据
export default class Dep {
  constructor() {
    this.id = uid++
    // subs 数据用于存放依赖
    this.subs = []
  }
  // 新增依赖
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }
  // 删除依赖,并在下一次程序冲洗时清空
  removeSub(sub: DepTarget) {
    this.subs[this.subs.indexOf(sub)] = null
    // ...
  }
  // 添加一个依赖
  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this)
      // ...
    }
  }
  // 通知所有依赖更新
  notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      // 排序
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      // ...更新
      sub.update()
    }
  }
}
 
怎么应用 dep,什么时候收集,什么时候响应
  1. 在 getter 中收集依赖
  2. 在 setter 中通知依赖更新
defineReactive里应用 dep
export function defineReactive() {
  const dep = new Dep()
  // 获取对象的自有属性描述符(非原型继承)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 跳过不可配置属性
  if (property && property.configurable === false) {
    return
  }
  // 考虑预定义的 getter/setter
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }
  // shallow:当前是否为浅层属性
  // true:则返回当前属性值
  // false:则遍历当前属性,为子属性进行观察
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // 处理 getter
      // 核心代码,收集依赖
      dep.depend()
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      // 处理 setter
      // 核心代码,通知依赖更新
      dep.notify()
    }
  })

  return dep
}
 
参数解释
window.target
在 Vue.js 的源码中,window.target 是一个全局变量,用于在依赖收集过程中标记当前正在被观察的观察者(Watcher)实例。这个机制是 Vue 响应式系统的一部分,用于确保当访问响应式数据时,能够追踪到哪些观察者依赖于这些数据。
这种使用全局变量的方式在多线程环境中可能会遇到问题,因为它假设在任何给定时刻只有一个观察者在执行。然而,在单线程的 JavaScript 环境nn中,这通常是安全的,因为 Vue 保证在任何时刻只有一个观察者实例在运行
在 Vue 3.x 中,这个功能已经被 TargetStack 替代,这是一个更安全和更健壮的实现,它使用一个栈来管理当前的观察者,而不是依赖全局变量。这种方式可以更好地处理嵌套的观察者和异步操作,确保依赖收集的准确性。
dep.target
Dep.target 的作用与 window.target 类似,但它是作为 Dep 类的一个静态属性存在的,而不是全局变量。
在 Vue.js 的响应式系统中,每个响应式数据属性都会有一个 Dep 实例与之关联。当一个观察者(Watcher)访问某个响应式数据属性时,会触发该属性的 get 访问器,进而调用 Dep 实例的 depend 方法。在这个方法中,Dep.target 被用来检查是否有当前正在执行的观察者,如果有,就将这个观察者添加到依赖列表中。
使用 Dep.target 而不是 window.target 的好处是,它可以避免全局变量可能带来的问题,并且可以更好地支持多个观察者。在 Vue 3.x 中,由于使用了 Proxy 来实现响应式系统,Dep.target 被用来在 Proxy 的 getset 陷阱(trap)函数中追踪依赖和触发更新。
 

4、通知更新

数据侦测
D:\Projects\good-projects\vue\src\core\observer\watcher.ts
关键在于:watcher 类
怎么用代码表示“用到该数据的地方”?并且如何通知更新?
其实在Vue中还实现了一个叫做Watcher的类,而Watcher类的实例就是我们上面所说的那个"谁"。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图。
 
想想 watcher 的实际使用场景
vm.$watch('a.b.c', function (newVal, oldVal) {
    // do something
})
当 data.a.b.c 发生变化时,调用函数
  • 具体实现:将当前的 watch 实例加在 data.a.b.c 数据上,一旦数据进行更新时,会通知watcher,并触发参数中的回调函数
// watcher 类是实现 depTarget 接口
export default class Watcher implements DepTarget {
  constructor(vm,expOrFn,cb/*...*/) {
    this.vm = vm;this.cb = cb;
    this.getter = parsePath(expOrFn);
    this.value = this.get();
  }
  
  // 把数据添加到对应的依赖管理中
  get() {
    pushTarget(this)
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }

  // dep.depend 中调用该方法,把 dep 加入列表中
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  // 当依赖改变时,会触发该方法
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}
// 将路径传入,例如:data{a{b{c:1}}}
// 参数 path = "a.b.c"; obj = data
export function parsePath(path: string): any {
  if (bailRE.test(path)) {
    return
  }
  // segments = [a,b,c]
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    // 逐层取值,最后返回 1
    return obj
  }
}
总结:
怎么知道观测值变了呢????????????
  • 对象,怎么写才会调用 set 呢???
  • 数组,什么时候调用 set???
getter/setter 是配置对象的属性,本质上问的是什么时候调用 object 的 getter/setter,不是 vue 中的语法
但是现在 vue 重新实现了对观测对象 getter/setter 的调用,要求用 set 方法来新增的属性才会引发调用
  • 读?.或者[]
  • 写?
    • 属性值发生变化直接赋值, vm.myObject.a = 2;vm.myArray[0] = 10
    • 数组索引或长度属性变化改变 .length,vm.myArray.length = 4
    • 新属性的添加,vm.newProperty = 'newValue'
// Vue.set 方法或者组件的 $set 方法来添加新属性,这样可以确保新属性是响应式的。
Vue.set(vm, 'newProperty', 'newValue'); // 正确的方式添加新属性
 
为什么需要这样实现数据侦测
先说目标:
为了实现 vue 的响应式编码,希望达到:
  • 数据与页面的实时响应
    • 页面上渲染的时候可以用到最新数据
    • 数据在发生变化的时候,页面上会相应地更新
 
Js 中有一个 object.defineproperty 机制,可以辅助实现一种响应式的更新
Object.defineProperty(obj, property, {
  enumerable: true,
  configurable: true,
  get: function() {
    // 当 obj[property] 被读取时,do sth
    return val
  },
  set: function(value) {
    // 当 obj[property] 被修改时,do sth
    notify("用到 value 的地方")
  }
})
 
Vue 把某数据 data 用 object.defineproperty 包装成响应式的数据,以此场景来考虑:
为了实现 “页面上渲染的时候可以用到最新数据”
  • 每次读取data,就返回 data。在 getter 里面进行操作,
为了实现“数据在发生变化的时候,页面上会相应地更新”
  • 每次更改 data,就更改页面上用到该数据的所有地方。在 setter 里操作
 
有了一个新问题:页面哪里会用到 data 呢?
为了回答这个问题
  • 每次读取 data 时,约等于这个地方用到了 data,于是把这个地方记录下来
  • 每次更改 data 时,遍历记录列表,挨个进行更新
 
又有了一个新问题:记录的到底是什么?“地方”该怎么记录?
实际上
vue 在这种情况都会放置一个 watcher 实例,
watcher 实例里面包含一个回调函数,如果 watcher 实例被触发,会调用回调函数,触发用到的地方进行更新
 
 
 

6、数组的变化侦测

为什么Object数据和Array型数据会有两种不同的变化侦测方式?
这是因为对于Object数据我们使用的是JS提供的对象原型上的方法Object.defineProperty,而这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。
 
vue对数组的一套变化侦测
懒得写
特点在于
getter可以正常触发,正常收集依赖
但是setter里,因为array不是对象,所以无法监测内部数据的变化
  • 因此重写数组方法,调用这7个数组方法,约等于数组发生改变,则进行 dep.notify 操作
注意
思考一下,我们不能直接修改 Array.prototype因为这样会污染全局的Array,我们希望 arrayMenthods只对 data中的Array 生效。
所以我们只需要把 arrayMenthods 赋值给 dataproto 上就好了。
 
 
 
参考文献
 

三、虚拟DOM

 

0、提问

什么是虚拟DOM?虚拟DOM用来做什么?存在的好处是什么?
 
虚拟DOM,就是用一个JS对象来描述一个DOM节点
<div class="a" id="b">我是内容</div>
{
  tag:'div',        // 元素标签
  attrs:{           // 属性class:'a',
    id:'b'
  },
  text:'我是内容',  // 文本内容
  children:[]       // 子元素
}
 
虚拟DOM的作用
直接操作真实DOM是非常消耗性能的,可以用JS的计算性能来换取操作DOM所消耗的性能。
考虑在更新视图的时候尽可能地少操作DOM节点
最直观的思路就是我们不要盲目的去更新视图,而是通过对比数据变化前后的状态,计算出视图中哪些地方需要更新,只更新需要更新的地方
 

1、虚拟DOM的定义

vnode.ts
关键在于:VNode 类
export default class VNode {
    constructor(
        tag?: string,
        data?: VNodeData,
        children?: Array<VNode> | null,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) {
        key
patch.js
整个patch无非就是干三件事,对比新旧节点:
  • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode
 
1、创建节点:
// 创建节点
  function createElm(/**/) {
    // 如果存在,则克隆节点
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    // 标签非空,说明是元素节点
    if (isDef(tag)) {
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)    // 创建元素节点
      setScope(vnode)
      createChildren(vnode, children, insertedVnodeQueue)    // 创建元素节点的子节点
      insert(parentElm, vnode.elm, refElm)    // 插入节点
    }
    // 注释非空,说明是注释节点 
    else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)    // 创建注释节点
      insert(parentElm, vnode.elm, refElm)    // 插入到DOM中
    } 
    // 否则就认为是文本节点
    else {
      vnode.elm = nodeOps.createTextNode(vnode.text)    // 创建文本节点
      insert(parentElm, vnode.elm, refElm)    // 插入到DOM中
    }
  }
 
2、删除节点:
// 删除节点
function removeNode(el) {
    const parent = nodeOps.parentNode(el)
    // element may have already been removed due to v-html / v-text
    // 利用该节点的父节点进行删除操作
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)
    }
}
3、更新节点:
  1. 静态节点
  2. 文本节点
  3. 元素节点
    1. 有子节点
    2. 无子节点
// 更新节点
  function patchVnode(/**/) {
    // 如果 oldVnode 和 vnode 完全一样
    if (oldVnode === vnode) {
      return
    }
    const elm = (vnode.elm = oldVnode.elm)
    // oldVnode 和 vnode 是否都是静态节点
    if (
      isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      return
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    // vnode 没有 text 属性
    if (isUndef(vnode.text)) {
      // vnode 的子节点和 oldvnode 的子节点是否都存在
      if (isDef(oldCh) && isDef(ch)) {
        // 若都存在,判断子节点是否相同,不同则更新子节点
        if (oldCh !== ch)
          updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } 
      // 如果只有 vnode 子节点存在
      // 不管 oldVnode 这有什么,都清空,把 vnode 的内容拿过来
      else if (isDef(ch)) {
        /**
         * 判断oldVnode是否有文本?
         * 若没有,则把vnode的子节点添加到真实DOM中
         * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
         */
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } 
      // 如果只有 oldVnode 子节点存在
      else if (isDef(oldCh)) {
        // 清空 DOM 中的子节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } 
      // 如果 vnode 和 oldVnode 都没有子节点,但是oldVnode 中有文本
      else if (isDef(oldVnode.text)) {
        // 清空 oldVnode 文本
        nodeOps.setTextContent(elm, '')
      }
    }
    //  如果有 oldVnode 有 text 属性
    else if (oldVnode.text !== vnode.text) {
      // 用 vnode 的 text 替换真实 DOM 文本
      nodeOps.setTextContent(elm, vnode.text)
    }
  }
 
4、更新子节点:
当新的VNode与旧的oldVNode都是元素节点并且都包含子节点时,对newChildrenoldChildren上的每个子节点进行逐个的对比:
  • 增:newChildren有,oldChildren没有,对oldChildren创建子节点
  • 删:newChildren没有,oldChildren有,对oldChildren删除子节点
  • 移动:两者都有,但所处的位置不同,需要根据newChildren调整oldChildren位置
  • 更新:两者都有,所处的位置相同,需要更新oldChildren里的该节点,使之与newChildren里的节点相同
 
常规的更新方式:
合适的位置是所有未处理节点之前,而并非所有已处理节点之后。
// 更新子节点 
  function updateChildren(/**/) {
      // 如果不属于以上四种情况,则进行常规对比更新 pathch
      else {
          // 如果在 oldChildren 里找不到当前循环的 newChildren 里的子节点
        if (isUndef(idxInOld)) {
          // New element
          // 新增节点,并插入到对应的位置上
          createElm(/**/)
        } 
        // 如果在 oldChildren 里找到了当前循环的 newChildren 里的子节点
        else {
          vnodeToMove = oldCh[idxInOld]
          // 如果两个节点相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 用 patchVnode 方法更新节点
            patchVnode(/**/)
            oldCh[idxInOld] = undefined
            // 如果 canMove 为 true,则表示需要移动节点
            canMove && nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(/**/)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
 
👆若相同,则移动节点,把第一个子节点移动到数据中所有未处理节点之前
👆若相同,则移动节点,把第一个子节点移动到数组中所有未处理节点之后
// 更新子节点 
function updateChildren(/**/) {
    let oldStartIdx = 0 // oldChildren 开始索引
    let oldEndIdx = oldCh.length - 1  // oldChildren 结束索引
    let oldStartVnode = oldCh[0]  // oldChildren 中所有未处理节点中的第一个
    let oldEndVnode = oldCh[oldEndIdx]  // oldChildren 中所有未处理节点中的最后一个

    let newStartIdx = 0 // newChildren 开始索引
    let newEndIdx = newCh.length - 1  // newChildren 结束索引
    let newStartVnode = newCh[0]  // newChildren 中所有未处理节点中的第一个
    let newEndVnode = newCh[newEndIdx]  // newChildren 中所有未处理节点中的最后一个
    
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果 oldStartVnode 不存在,跳过,进入下一个
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } 
      // 如果 oldEndVnode 不存在,跳过,进入上一个
      else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } 
      // 如果新旧节点 起始索引下 值相同,把两个节点通过 patchVnode 进行更新
      // 不需要移动
      else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(/**/)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
      // 如果新旧节点 结束索引下 值相同,把两个节点通过 patchVnode 进行更新
      // 不需要移动
      else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(/**/)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } 
      // 如果 新后 和 旧前 节点相同,先把两个节点用 patch 进行更新
      // 然后将 旧前 节点移动到 oldChildren 中所有未处理节点后
      else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(/**/)
        canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } 
      // 如果 新前 和 旧后 节点相同,先把两个节点用 patch 进行更新
      // 然后把 旧后 节点移动到 oldChildren 中所有未处理节点前
      else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(/**/)
        canMove &&
          nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
      // 如果不属于以上四种情况,则进行常规对比更新 pathch
      else {
      
      }
      // 如果 oldChildren 先遍历完了
      // 将 newChildren 里所有节点新增一份,插入 DOM 中
      if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
        addVnodes(/**/)
      } 
      // 如果 newChildren 先遍历完了
      // 将 oldChildren 里所有节点删除
      else if (newStartIdx > newEndIdx) {
        removeVnodes(oldCh, oldStartIdx, oldEndIdx)
      }
}
 
 

四、模板编译

0、提问

 
  • DOM存在的必要条件是得先有VNode,那么VNode又是从哪儿来的呢?
把用户写的模板进行编译,就会产生VNode
  • template怎么识别、编译、渲染?
答案在源码中
 

1、模板编译总结

 
模板内容的可能性:
  • 文本,例如“难凉热血”
  • HTML注释,例如<!-- 我是注释 -->
  • 条件注释,例如<!-- [if !IE]> -->我是注释<!--< ![endif] -->
  • DOCTYPE,例如<!DOCTYPE html>
  • 开始标签,例如<div>
  • 结束标签,例如</div>
 
暂时无法在飞书文档外展示此内容
 

五、生命周期

 
 
 
 
 
 
 
 
 
 
 
 

六、keep-alive

 
 
 
 
 
 
 

七、其他

 
一些值得借鉴的代码写法:
 
把项目共享方法,抽取出来放在一个单独的文件夹里
并且注意:一个函数只做一件事
 
较长的表达式用这种方式格式化
 
!!用于将数据转换成对应的 boolean 类型
 
参考文献:
 
posted @ 2024-04-06 16:34  不是勇士  阅读(50)  评论(0)    收藏  举报