0
点赞
收藏
分享

微信扫一扫

vue2源码解析(学习笔记)

金刚豆 2022-04-18 阅读 87

以下文章转载或借鉴vue源码社区,仅供个人学习使用,如有文章涉及侵权及其他问题,请及时联系作者修改或删除,感谢!!!
vue源码中文社区:https://vue-js.com/learn-vue/

一、object变化侦测篇

1.通过Object.defineProperty()改造属性

let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
  enumerable: true,
  configurable: true,
  get(){
    console.log('price属性被读取了')
    return val
  },
  set(newVal){
    console.log('price属性被修改了')
    val = newVal
  }
})

源码位置:src/core/observer/index.js

/**
 1. Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 
  constructor (value: any) {
    this.value = value
    // 实例化一个依赖管理器
    this.dep = new Dep()
    this.vmCount = 0
    // 给value新增一个__ob__属性,值为该value的Observer实例
    // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
    def(value, '__ob__', this)
    // 当value为数组时
    if (Array.isArray(value)) {
    // hasProto = '__proto__' in {} 判断hasProto是否可用,有        些浏览器不支持该属性      
    //if (hasProto) {
    // const arrayProto = Array.prototype
    // export const arrayMethods = Object.create(arrayProto)
    // function protoAugment (target, src: Object) {
    //    target.__proto__ = src
    // }
    // 覆盖__proto__,拦截器挂载
        protoAugment(value, arrayMethods)
      } else {
      // 获取arrayMethods的所有属性
      // const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
     // function copyAugment (target: Object, src: Object, keys: Array<string>) {
     //   for (let i = 0, l = keys.length; i < l; i++) {
     //     const key = keys[i]
     //     间接挂载
     //     def(target, key, src[key])
     //   }
     // }
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 把数组中的每一项转换为可观测属性
      this.observeArray(value)
    } else {
    // 把对象的每一项转换为可观测属性
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
/**
 2. 使一个对象转化成可观测对象
 3. @param { Object } obj 对象
 4. @param { String } key 对象的key
 5. @param { Any } val 对象的某个key的值
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
// 生成依赖管理器
  const dep = new Dep()
  // 定义获取指定对象的自身属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  const getter = property && property.get
  const setter = property && property.set
  // 如果只传了obj和key,那么val = obj[key]
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  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()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 依赖更新
      dep.notify()
    }
  })
}

小结:定义observer类,它用来将一个正常的object转换成可观测的object。并且给value新增一个__ob__属性,值为该value的Observer实例。这个操作相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作。
只有object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。 最后,在defineReactive中当传入的属性值还是一个object时使用new observer(val)来递归子属性,这样我们就可以把obj中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。 也就是说,只要我们将一个object传到observer中,那么这个object就会变成可观测的、响应式的object。

2.依赖收集(依赖管理器)

在getter中收集依赖,在setter中通知依赖更新。

// 源码位置:src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor() {
    this.id = uid++;
    this.subs = [];
  }
  addSub(sub: Watcher) {
    this.subs.push(sub);
  }
  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }
  // 收集依赖
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  // 通知所有依赖更新
  notify() {
    const subs = this.subs.slice();
    if (process.env.NODE_ENV !== "production" && !config.async) {
      subs.sort((a, b) => a.id - b.id);
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

3.Watcher 类

作用:在之后数据变化时,不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图,可以理解成watch类就代表这个依赖。

// watch类大概实现
export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

watch类解析:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher实例就代表这个依赖,当数据变化时,我们就通知Watcher实例,由Watcher实例再去通知真正的依赖。
当实例化Watcher类时,会先执行其构造函数;
1. 在构造函数中调用了this.get()实例方法;
2. 在get()方法中,首先通过window.target = this把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
3. 而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。

总结:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。

object侦测总结:
1.Data通过observer转换成了getter/setter的形式来追踪变化。
当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
2.当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。
3.Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

二、Array变化侦测

1.Array型数据的set get

get

array不存在对象的Object.defineProperty方法,get操作是通过如下形式实现的:

data(){
  return {
    arr:[1,2,3]
  }
}

每次获取数据的时候都是通过this.arr获取的,每当通过this.属性获取时,就会触发的array的get属性,收集依赖等。

set

let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
  console.log('arr被修改了')
  this.push(val)
}
arr.newPush(4)

通过拟定array的push方法,达到可以监听改变的目的。
数组的setget方法如上生成。

array的方法拦截器

// 源码位置:/src/core/observer/array.js
const arrayProto = Array.prototype
// 创建一个新的数组,原型为Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 把methodsToPatch数组内的方法指向mutator方法
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)
    // 通知更新
    ob.dep.notify()
    return result
  })
})
// def 转化为defineProperty
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

解析: 当调用methodsToPatch数组中的方法时,实际都会指向mutator方法,在mutator方法中执行通知更新操作,而mutator函数内部执行了original函数,这个original函数就是Array.prototype上对应的原生方法。

拦截器挂载

Observer类如下,挂载数组拦截器

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
      // target.__proto__ = src 直接挂载
        protoAugment(value, arrayMethods)
      } else {
      // 把数组的方法全部挂到value上
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

解析: 首先判断当前浏览器是否支持__proto__ ,如果支持直接把__proto__ 重定向为arrayMethods对象,如果不支持,则是需要把拦截器中重写的7个方法循环加入到value上。

2.array依赖收集

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
// 返回一个属性在此对象上的操作符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
// observe 判断val是否转换为可观测的属性,如果不是就转换,是就返回__ob__
  let childOb = !shallow && observe(val)
  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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      //  在setter中通知依赖更新
      dep.notify()
    }
  })
}

三、虚拟dom

1.虚拟DOM简介

所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,像如下示例:

<div class="a" id="b">我是内容</div>

{
  tag:'div',        // 元素标签
  attrs:{           // 属性
    class:'a',
    id:'b'
  },
  text:'我是内容',  // 文本内容
  children:[]       // 子元素
}

为什么要有虚拟DOM?
Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的。
通过JS的计算性能来换取操作DOM所消耗的性能

2.vnode类

// 源码位置:src/core/vdom/vnode.js

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 当前节点的标签名
    this.data = data // 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
    this.children = children // 当前节点的子节点,是一个数组
    this.text = text // 当前节点的子节点,是一个数组
    this.elm = elm // 当前虚拟节点对应的真实dom节点
    this.ns = undefined // 当前节点的名字空间
    this.context = context // 当前组件节点对应的Vue实例
    this.fnContext = undefined // 函数式组件对应的Vue实例
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key // 节点的key属性,被当作节点的标志,用以优化
    this.componentOptions = componentOptions // 组件的option选项
    this.componentInstance = undefined // 当前节点对应的组件的实例
    this.parent = undefined // 当前节点的父节点
    this.raw = false // 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
    this.isStatic = false // 静态节点标志
    this.isRootInsert = true // 是否作为跟节点插入
    this.isComment = false // 是否为注释节点
    this.isCloned = false // 是否为克隆节点
    this.isOnce = false // 是否有v-once指令
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
  get child (): Component | void {
    return this.componentInstance
  }
}

3.VNode的类型

共有六种节点类型,区别:只是在实例化传入的参数不同
注释节点
文本节点
元素节点
组件节点
函数式组件节点
克隆节点

源码位置:src/core/vdom/vnode.js

  • 注释节点
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
 const node = new VNode()
 // 具体的注释信息
 node.text = text
 // isComment 用来标识一个节点是否是注释节点
 node.isComment = true
 return node
}
  • 文本节点
// 创建文本节点
export function createTextVNode (val: string | number) {
  // text属性,用来表示具体的文本信息
 return new VNode(undefined, undefined, undefined, String(val))
}
  • 元素节点
  • 组件节点
    组件节点除了有元素节点具有的属性之外,它还有两个特有的属性
    componentOptions :组件的option选项,如组件的props等
    componentInstance :当前组件节点对应的Vue实例
  • 函数式组件节点
    函数式组件节点相较于组件节点,它又有两个特有的属性:
    fnContext:函数式组件对应的Vue实例
    fnOptions: 组件的option选项
  • 克隆节点
// 创建克隆节点
// 把一个已经存在的节点复制一份出来,它主要是为了做模板编译优化时使用
export function cloneVNode (vnode: VNode): VNode {
 const cloned = new VNode(
   vnode.tag,
   vnode.data,
   vnode.children,
   vnode.text,
   vnode.elm,
   vnode.context,
   vnode.componentOptions,
   vnode.asyncFactory
 )
 cloned.ns = vnode.ns
 cloned.isStatic = vnode.isStatic
 cloned.key = vnode.key
 cloned.isComment = vnode.isComment
 cloned.fnContext = vnode.fnContext
 cloned.fnOptions = vnode.fnOptions
 cloned.fnScopeId = vnode.fnScopeId
 cloned.asyncMeta = vnode.asyncMeta
 cloned.isCloned = true
 return cloned
}

vnode的作用

在视图渲染之前,把写好的template模板编译成vnode并缓存下来,等数据发生变化时,对比上一次的vnode,,找出差异,然后有差异的VNode对应的真实DOM节点就是需要重新渲染的节点,最后根据有差异的VNode创建出真实的DOM节点再插入到视图中,最终完成一次视图更新

4.DOM-Diff

4.1.patch过程

patch简介:在vue中,把DOM-Diff过程叫做patch过程,patch译为补丁的意思,即对旧的vnode进行修补。
绑定旧的vnode之后,更新之后只会通过对比更新旧的vnode,不会直接用新的Vnode直接替换旧的Vnode
patch需要做的事情

  • 创建节点:新的Vnode中有而旧的oldVNode中没有,就在旧的中创建
  • 删除节点:新的Vnode中没有而旧的oldVNode中用,在旧的中删除
  • 更新节点:如果新的旧的都有。则以新的vnode中的为准,更新旧的oldVNode

4.2 创建节点

Vnode类可以创建6种类型的节点,只有元素节点、注释节点、文本节点可以被插入到DOM中

// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      	vnode.elm = nodeOps.createElement(tag, vnode)   // 创建元素节点
        createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
        insert(parentElm, vnode.elm, refElm)       // 插入到DOM中
    } 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中
    }
  }
  • 判断是否为元素节点只需判断该VNode节点是否有tag标签即可。如果有tag属性即认为是元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,那就递归遍历创建所有子节点,将所有子节点创建好之后insert插入到当前元素节点里面,最后把当前元素节点插入到DOM中。
  • 判断是否为注释节点,只需判断VNode的isComment属性是否为true即可,若为true则为注释节点,则调用createComment方法创建注释节点,再插入到DOM中。
  • 如果既不是元素节点,也不是注释节点,那就认为是文本节点,则调用createTextNode方法创建文本节点,再插入到DOM中。
    在这里插入图片描述

4.3 删除节点

如果某些节点再新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。源码如下:

function removeNode (el) {
    const parent = nodeOps.parentNode(el)  // 获取父节点
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)  // 调用父节点的removeChild方法
    }
  }
举报

相关推荐

0 条评论