Vue2源码——响应式原理及依赖

79 阅读4分钟

1.initData 初始化state

对data中的对象和数组通过Observer递归劫持(Object.defineProperty),数组劫持的是push,unshift,splice等方法,添加进对象时劫持并将对象通过Observer劫持

<div id="app">hello {{ msg }}</div>
<script>
  let vm = new Vue({
    el:"#app",
    data() {
      return {
        msg: "world"
      }
    }
  })
</script>

2.获取dom

将模板进行编译变成ast语法树,变成render(),生成虚拟节点(vnode),变成真实dom,放到页面上

1.ast语法树:

image.png 解析方式: 获取到html(el.outerHtml): <div id="app">hello {{ msg }}</div> 通过正则从头开始匹配 image.png while(html)循环,将匹配到的转换为ast后就删除,直到字符串为空

<div id="app">hello {{ msg }}</div> => 
id="app">hello {{ msg }}</div> => 
hello {{ msg }}</div> =>......

最终变成以下ast语法树格式

{
    tag: "div",
    attrs: [{name:'id',value:'app'}],
    type: 1,
    parent: null,
    children: [{type: 3,text: "hello {{ msg }}"}]
}

2.render():

code = `_c('div',{"id": "app"}, _v("hello"+_s(msg)))`

此时的render为字符串, 要变成函数

let render = new Function(`with(this){
  return ${code}
}`)


// with:  目的是在实例中调用render函数的时候,能够获取到在vue实例中挂载的_c, _v, _s
let obj = {a:1,b:2}
with(obj) {
  console.log(a,b)  //1,2
}

在vue实例中挂载_c(处理标签)、_v(处理文本)、_s(处理变量)

3.虚拟dom:

{
  tag: 'div'
  data: {
    id: 'app'
  },
  key: undefine,
  children: [
    {
      tag: undefine,
      data: undefine,
      text: 'hello world',
      key: undefine,
      children: undefine
    }
  ],
}

4.真实dom

遍历虚拟dom 通过creatElement创建元素,appendChild添加子元素,得到真实的DOM(html结构)

<div id="app">
  hello world
</div>

获取到父元素body,并插入到body中,删除旧的dom

3.生命周期

将全局的生命周期和各组件的生命周期按先后顺序通过 策略模式混入到一起,挂载到$options上 image.png image.png 合并后:

{
    $el: "app",
    $options: {
      created: [a,b,c]  //a b为全局混入的created,c为组件内的created  三个都是函数
      data: [],
      watch: []
    }
}

调用生命周期(设计模式,订阅发布): 遍历created: [a,b,c] image.png init初始化时调用create image.png 挂载dom时调用mount image.png

4.Watcher 自动更新视图

image.png 不能每次都手动调用vm._updata

image.png 第四个参数判断是初次渲染还是更新

1.Watcher

import {pushTarget,popTarget} from "./Dep"
let id = 0
class Watcher {
  constructor(vm,updataComponent,cb,options){
    this.vm = vm
    this.exprOrfn = updataComponent
    this.cb = cb
    this.options = options
    this.id = id++ //每一次new Watcher 都会生成一个id
    if(typeof updataComponent === 'function'){
    	this.getter = updataComponent //用来更新视图
  	}
  }
  //更新视图
  this.get()
  //初次渲染
  get() {
    pushTarget(this)//给dep 添加 watcher
    this.getter() //渲染
    popTarget()//给dep 取消 watcher
  }
  //更新
  update() {
    this.getter()
  }
}
export default Watcher

收集依赖 vue dep watcher data:{name,msg} dep: dep和data中的属性是一一对应 watcher: 比如dep.name(data.name), 在视图上用到了几处,就有几个watcher,用来在数据变化时更新视图 dep与watcher: 一对多(下面会有多对多) dep.name=[watcher1,watcher2]

2.Dep

class Dep {
  constructor() {
    this.subs = []
  }
  //收集依赖
  depend() {
    this.subs.push(Dep.target)
  }
  //更新
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

// *******************此时的Dep.target是一个全局变量**********************
//Vue源码中,Dep.target 是在 Dep 类中定义的一个静态属性  
Dep.target = null
//添加watcher
export function pushTarget(watcher) {
  Dep.target = watcher
}
export function popTarget() {
  Dep.target = null
}
export default Dep

3.收集依赖

劫持对象数据时收集依赖 image.png

4.更新视图

数据发生改变时,发布通知,更新视图

	//更新
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
	//更新
  update() {
    this.getter()
  }

image.png

5.双向依赖收集

dep和watcher多对多 (computed):

let id = 0
class Dep {
  constructor() {
    this.id = id++
    this.subs = []
  }
  //收集依赖
  depend() {
    //希望watcher可以存放dep ****************************************
    //双向记忆
    // this.subs.push(Dep.target)
    Dep.target.addDep(this)
  }
  //***********************
  addSub(watcher) {
    this.subs.push(watcher)
  }
  //更新
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}
// *******************此时的Dep.target是一个全局变量**********************
//Vue源码中,Dep.target 是在 Dep 类中定义的一个静态属性  
Dep.target = null
//添加watcher
export function pushTarget(watcher) {
  Dep.target = watcher
}
export function popTarget() {
  Dep.target = null
}
export default Dep
import {pushTarget,popTarget} from "./Dep"
let id = 0
class Watcher {
  constructor(vm,updataComponent,cb,options){
    this.vm = vm
    this.exprOrfn = updataComponent
    this.cb = cb
    this.options = options
    this.id = id++ //每一次new Watcher 都会生成一个id
    this.deps = [] //watcher存放dep
    this.depsId = new Set() //存放id set可以去重
    if(typeof updataComponent === 'function'){
    	this.getter = updataComponent //用来更新视图
  	}
  }
  //更新视图
  this.get()
  addDep() {
    //1.去重
    let id = dep.id
    if(this.depsId.has(id)) {
      this.deps.push(dep)
      this.depsId.add(id)
      deps.addSub(this)
    }
  }
  //初次渲染
  get() {
    pushTarget(this)//给dep 添加 watcher
    this.getter() //渲染
    popTarget()//给dep 取消 watcher
  }
  //更新
  update() {
    this.getter()
  }
}
export default Watcher

双向依赖收集的过程如下:

  1. 首先,在执行模板编译时,会遍历模板中的每个指令、表达式或计算属性,为每个表达式创建一个 Watcher 实例。
  2. 当创建 Watcher 实例时,会将当前的 Watcher 对象赋值给 Dep.target,然后执行表达式的求值操作。在求值过程中,如果访问了响应式数据(如访问了 data 对象的属性),就会触发属性的 getter 方法。
  3. 在属性的 getter 方法中,会判断当前是否存在 Dep.target(即是否正在进行依赖收集),如果存在,则将当前的 Dep 实例添加到 Watcher 实例的依赖列表中,并同时将 Watcher 实例添加到 Dep 实例的观察者列表中。这样就建立了 DepWatcher 之间的双向依赖关系。
  4. 接着,如果在表达式的求值过程中,访问了其他的响应式数据,同样会触发相应的 getter 方法,进而进行依赖收集,建立其他属性与当前 Watcher 之间的依赖关系。
  5. 当任意一个响应式数据发生变化时,对应的 setter 方法会通知相应的 Dep 实例,然后 Dep 实例会遍历其中的所有观察者,并调用观察者的 update 方法进行更新。

通过这种双向依赖收集的机制,Vue 能够准确地追踪数据的依赖关系,并在数据变化时自动触发相关的依赖更新,保证了视图与数据的同步。 需要注意的是,Vue 3 对于依赖收集的方式进行了一些改进,使用了 Proxy 对象和 ES6 的原生 Proxy API 来代替了 Vue 2 中的双向依赖收集机制,以提高性能和效率。

6.数组收集

思路: 1.给所有对象类型增加一个dep (数组[]的类型是object)