我们都知道Vue是响应式的,Vue就是基于数据劫持+发布者-订阅者模式实现的数据响应式,数据变化驱动视图更新,而通过操作视图也能改变数据。
数据劫持是通过Object.defineProperty把Vue中的data转化成gettersetter实现,ES6中也有Proxy对数据进行拦截处理。
首先来着一个图,大概总结一个流程。

代码开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options

if(this.$el){
//数据劫持-发布者+订阅者
new Observer(this.$data)
//编译模板
new Compile(this.$el,this)
//数据代理 可通过 this.person 直接访问数据
this.proxyData(this.$data)
}
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
  • 数据响应式:
    对data的所有数据进行劫持,转化为gettersetter
  • 模板编译
    模板解析,处理指令和事件绑定,比如,v-text="msg",v-on:click="handler"
  • 数据双向绑定与数据代理
    v-model 实现;数据代理实现

    数据响应式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    class Observer{
    constructor(data){
    this.observe(data)
    }
    observe(data){
    if(data && typeof data === 'object'){
    Object.keys(data).forEach(key=>{
    this.defineReactive(data,key,data[key])
    })
    }
    }
    defineReactive(obj,key,value){
    //递归遍历
    this.observe(value)
    //创建依赖
    const dep = new Dep()
    Object.defineProperty(obj,key,{
    configurable:true,
    enumerbale:true,
    get(){
    //模板编译的时候会取数据时会创建Watcher 然后收集进dep中
    Dep.target && dep.addSub(Dep.target)
    return value
    },
    set:(newVal)=>{
    //重新监听新值,解决对属性赋值时监听不到的问题 如 this.$data.person = {a:1}
    this.observe(newVal)
    if(newVal !== value){
    value = newVal
    }
    //数据修改时,通知订阅者,更新视图
    dep.notify()
    }
    })
    }
    }
    class Dep{
    constructor() {
    this.subs = []
    }
    addSub(watcher){
    this.subs.push(watcher)
    }
    notify(){
    this.subs.forEach(w=>w.update())
    }
    }
    class Watcher{
    constructor(vm,expr,cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.oldVal = this.getOldVal()
    }
    getOldVal(){
    //依赖
    Dep.target = this
    const oldVal = compileUtil.getVal(this.expr,this.vm)
    Dep.target = null
    return oldVal
    }
    update(){
    const newVal = compileUtil.getVal(this.expr,this.vm)
    if(this.oldVal !== newVal){
    //数据变化,回调更新视图
    this.cb(newVal)
    }
    }
    }
  • 数据响应式实现了啥?
  1. 对数据进行劫持
  2. 对依赖收集
  3. 数据变化,回调更新

    模板编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    class Compile{
    constructor(el,vm){
    //判断是否为元素节点,否则获取
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    //获取文档碎片对象,减少页面的回流和重绘
    const fragment = this.node2Fragment(this.el)
    //编译模板
    this.compile(fragment)
    //追加到#app上
    this.el.appendChild(fragment)
    }
    isElementNode(node){
    return node.nodeType === 1
    }
    node2Fragment(el){
    const f = document.createDocumentFragment()
    let firstChild
    while(firstChild = el.firstChild){
    f.appendChild(firstChild)
    }
    return f
    }
    compile(fragment){
    const childNodes = fragment.childNodes
    ;[...childNodes].forEach(child=>{
    if(this.isElementNode(child)){
    //元素节点
    this.compileElement(child)
    }else{
    //文本节点
    this.compileText(child)
    }
    //递归遍历子节点
    if(child.childNodes && child.childNodes.length){
    this.compile(child)
    }
    })
    }
    compileElement(node){

    const attrs = node.attributes
    ;[ ...attrs].forEach(attr=>{
    //取出名和其值,v-html = 'htmlStr' , v-on:click='handler'
    const {name,value} = attr
    //是否为指令
    if(this.isDirective(name)){
    const [ ,directive] = name.split('-') //分离出指令名,text,html,on,model
    const [dirName,eventName] = directive.split(':') //分离出事件名[html,undefined] [on,click]
    // console.log(dirName,eventName)
    //value 为 expr 也就是 htmlStr/person.name/person.age
    compileUtil[dirName](node,value,this.vm,eventName)
    //删除标签上的指令属性
    node.removeAttribute('v-'+directive)
    }else if(this.isShortOn(name)){
    let [,eventName] = name.split('@')
    compileUtil['on'](node,value,this.vm,eventName)
    node.removeAttribute('@'+eventName)
    }else if(this.isShortBind(name)){
    let [,eventName] = name.split(':')
    compileUtil['bind'](node,value,this.vm,eventName)
    node.removeAttribute(':'+eventName)
    }
    })
    }
    compileText(node){
    const content = node.textContent
    //检测是否有 {{msg}}此类指令
    if(/\{\{(.+?)\}\}/.test(content)){
    compileUtil['text'](node,content,this.vm)
    }
    }
    isDirective(name){
    //是否以 v-开头
    return name.startsWith('v-')
    }
    isShortOn(name){
    //是否以 @开头
    return name.startsWith('@')
    }
    isShortBind(name){
    //是否以 :开头
    return name.startsWith(':')
    }
    }

    //编译工具对象
    const compileUtil = {
    text(node,expr,vm){
    let value
    if(expr.indexOf('{{') !== -1){
    value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
    new Watcher(vm,args[1],(newVal)=>{
    this.updater.textUpdate(node,this.getContentVal(expr,vm))
    })
    return this.getVal(args[1],vm)
    })
    }else{
    value = this.getVal(expr,vm)
    }
    this.updater.textUpdate(node,value)
    },
    html(node,expr,vm){
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal)=>{
    this.updater.htmlUpdate(node,newVal)
    })
    this.updater.htmlUpdate(node,value)
    },
    model(node,expr,vm){
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal)=>{
    this.updater.modelUpdate(node,newVal)
    })
    node.addEventListener('input',(e)=>{
    this.setVal(vm,expr,e.target.value)
    })
    this.updater.modelUpdate(node,value)
    },
    on(node,expr,vm,eventName){
    let fn = vm.$options.methods && vm.$options.methods[expr];
    //绑定事件
    node.addEventListener(eventName,fn.bind(vm),false)
    },
    bind(node,expr,vm,propertyName){
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal)=>{
    this.updater.bindUpdate(node,propertyName,newVal)
    })
    this.updater.bindUpdate(node,propertyName,value)

    },
    updater:{
    textUpdate(node,value){
    node.textContent = value
    },
    htmlUpdate(node,value){
    node.innerHTML = value
    },
    modelUpdate(node,value){
    node.value = value
    },
    bindUpdate(node,propertyName,value){
    node.setAttribute(propertyName,value)
    }

    },
    getVal(expr,vm){
    return expr.split('.').reduce((data,currentVal)=>data[currentVal],vm.$data)
    },
    getContentVal(expr,vm){
    return value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
    return this.getVal(args[1],vm)
    })
    },
    setVal(vm,expr,inputVal){
    return expr.split('.').reduce((data,currentVal)=>{
    data[currentVal] = inputVal
    },vm.$data)
    }
    }
  • 模板编译实现了啥?
  1. 对模板中的指令、语法进行解析赋值,绑定事件
  2. 对每个使用了data中的数据添加一个Watcher(观察者)
  3. 在模板编译赋值的过程中会取值,就会触发data中的getter,这时也就会把Watcher push进dep(收集依赖的容器)中。
  4. 一个旦有数据变化,就会触发data中的setter,从而会触发dep.notify通知所有watcher更新,从而驱动视图更新。
  5. 双向数据绑定,无非就是对表单添加事件inputonchange

    完整代码

    完整代码