# Vue原理浅析

# Vue工作机制

Vue工作机制

初始化Vue实例时候,会先调用初始化函数init,此时会初始化生命周期,事件,props, methods,data,computed,watch等。

初始化之后调用$mount挂载组件。

其中,挂载组件之前会进行模板编译compile,模板编译分为三个阶段:

  1. parse(解析):使用正则解析template中的vue的指令(v-xxx)变量等,形成 语法树AST。 并将值进行依赖收集,与数据产生挂钩关系,将来在某个时刻数据发生 变化的时候则可以找到关联的函数执行。
    2.optimize(优化):标记一些静态节点,用作后面的性能优化,在diff的时候直接略过。
    3.generate:把第一步生成的AST转化为渲染函数render function。

有了渲染方法,下一步就是更新DOM,注意并不是直接更新,而是通过vnode,将虚拟DOM 和真实DOM产生对应关系。之后操作的数据都是虚拟DOM,虚拟DOM变化后,通过diff算法 在更新到DOM树。

之后,当数据发生变化的时候,会触发setter,然后监听器Watcher会通知进行修改, 通过对比两个DOM,进行DOM更新。

所以,我们可以了解到vue核心的内容就是,初始化的时候通过defineProperty 进行数据绑定,设置通知的机制,当编译生成的渲染函数被实际渲染的时候,触发getter 进行依赖收集,在数据变化的时候,触发setter进行数据更新。

MVVM

# 实现简单的Vue

# 实现思路

定义观察者Observer函数,以劫持监听所有属性,并收集编译阶段Compile发现的 依赖收集到依赖管理器Dep中,Dep中以数组的形式管理着所有的Watcher,Watcher中 即存在着真正执行视图更新的执行者Updater。当发现数据变化时候,Observer通知 Dep,然后调用Watcher实现视图的更新。

实现Observer,对数据进行劫持。

class Vue{
    constructor(options){
        this.$data = options.data //保存data
        this.observe(this.$data) // 执行响应式,观察者
    }

    observe(value){
        // 遍历data
        // 判断data是否存在且为对象类型
        if(!value || typeof value !== 'object'){
            return
        }
        //遍历data
        Object.keys(value).forEach(key=>{
            //为每一个key定义响应式
            this.defineReactive(value,key,value[key])
        })
    }
    //其中接受三个参数:要定义的对象,键值key和对应的值value
    defineReactive(obj,key,val){
        //递归查找嵌套的属性
        this.observe(val)

        // 为data对象定义属性
        Object.defineProperty(obj,key,{
            enumerable: true,//可枚举
            configurable: true, //可修改或删除
            get(){
                return val
            },
            set(newVal){
                if(newVal === val){
                    return
                }
                console.log('数据发生变化~')
                val = newVal
            }
        })
    }
}

接着实现依赖收集Dep,并放置监听器Watcher实现视图更新。

//依赖管理器,负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep{
    constructor(){
        this.deps = [] //deps里面存放的是Watcher实例
    }
    addDep(dep){
        this.deps.push(dep)
    }
    //通知所有watcher执行更新
    notify(){
        this.deps.forEach(dep=>{
            dep.update()
        })
    }
}

//Watcher:具体的更新执行者
class Watcher {
    constructor(){
        //将来new一个监听器时候,将当前Watcher实例附加到Dep.target上
        // 当执行数据属性get时候,可以获取到对应依赖的实例
        Dep.target = this
    }

    //更新
    update(){
        console.log('视图更新')
    }
}

这时,需要编译器Compiler完成对模板编译,得到更新函数后,才能给Watcher调用, 所以编译阶段才能创建Watcher。即编译器在做依赖收集时候创建Watcher。

// 扫描模板中所有依赖,{{}},dom属性,v-xxx指令,@方法等创建函数和Watcher
class Compile{
    //el 宿主元素或选择器
    //vm 当前Vue实例
    constructor(el,vm){
        this.$vm = vm
        this.$el = document.querySelector(el)//设置为默认选择器

        if(this.$el){
            // 将dom转换成代码块Fragment,提高性能,效率
            this.$fragment = this.node2Fragment(this.$el)
            //执行编译
            this.compile(this.$fragment)
            //将生成的结果追加至宿主元素
            this.$el.appendChild(this.$fragment)
        }
    }
    node2Fragment(el){
        // 创建新的Fragment
        const fragment = document.createDocumentFragment()
        let child;
        // 将原生节点拷贝至fragment
        while(child = el.firstChild){ //获取el任意子节点,有可能是注释,文本节点,元素等
            // appendChild是移动操作
            fragment.appendChild(child)
        }
        return fragment
    }
    // 编译指定片段
    compile(el){
        let childNodes = el.childNodes
        // 将类数组转为数组并遍历节点
        Array.from(childNodes).forEach(node => {
            //判断node类型,做相应处理
            if(this.isElementNode(node)){
                //元素节点,要识别v-xx或者@xx
                this.compileElement(node)
            } else if(this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent)){
                // 文本节点,只关系{{xx}}形式
                this.compileText(node,RegExp.$1); //RegExp.$1,匹配内容
            }
            // 递归,遍历可能存在的子节点
            if(node.childNodes && node.childNodes.length){
                this.compile(node)
            }
        })
    }

    //编译元素节点
    compileElement(node){
        //<div v-test='test' @click='handleClick'></div>
        const attrs = node.attributes
        Array.from(attrs).forEach(attr => {
            //规定指令 v-test='test' @click='handleClick'
            const attrName = attr.name //属性名
            const exp = attr.value  //属性值
            if(this.isDirective(attrName)){ //指令
                const dir = attrName.substr(2)  //v-text -> text
                this[dir] && this[dir](node,this.$vm,exp)
            } else if (this.isEventDirective(attrName)){ //事件
                const dir = attrName.substr(1) //click
                this.eventHandler(node,this.$vm,exp,dir)
            }
        })
    }
    //编译文本节点
    compileText(node,exp){
        this.text(node,this.$vm,exp)
    }
    isElementNode(node){
        return node.nodeType == 1 //元素节点
    }
    isTextNode(node){
        return node.nodeType == 3 //文本节点
    }

    isDirective(attr){
        return attr.indexOf('v-') == 0
    }
    isEventDirective(dir){
        return dir.indexOf('@') == 0
    }
    //文本更新
    text(node,vm,exp){
        this.update(node,vm,exp,'text')
    }
    //处理html
    html(node,vm,exp){
        this.update(node,vm,exp,'html')
    }
    //双向绑定
    model(node,vm,exp){
        this.update(node,vm,exp,'model')

        const val = vm.exp
        //双向绑定还要处理视图对模型的更新
        node.addEventListener('input',e=>{
            vm[exp] = e.target.value
        })
    }

    //更新函数Updater
    update(node,vm,exp,dir){
        let updateFn = this[dir+'Updater']
        updateFn && updateFn(node,vm[exp]) //立刻执行更新,get Compile -> Updater
        new Watcher(vm,exp,function (value) { //当监听到变化时再执行更新    Watcher -> Updater
            updateFn && updateFn(node,value)
        })
    }
    //执行DOM操作
    textUpdater(node,value){
        node.textContent = value
    }
    htmlUpdater(node,value){
        node.innerHTML = value
    }
    modelUpdater(node,value){
        node.value = value
    }

    eventHandler(node,vm,exp,dir){
        let fn = vm.$options.methods && vm.$options.methods[exp]
        if(dir && fn){
            node.addEventListener(dir,fn.bind(vm)) // fn.bind,绑定当前实例vm
        }
    }
}

最后,完善Watcher并实例化compile。

//vue.js
class Vue{
    constructor(options){
        this.$options = options //Compile调用
        
        this.$compile = new Compile(options.el,this)
    }
    ...
}

//Watcher:具体的更新执行者
class Watcher {
    constructor(vm,key,cb){
        this.vm = vm
        this.key = key
        this.cb = cb
        //将来new一个监听器时候,将当前Watcher实例附加到Dep.target上
        // 当执行数据属性get时候,可以获取到对应依赖的实例
        Dep.target = this
        this.vm[this.key] // 触发data的get
        Dep.target = null //避免重复添加defineReactive中defineProperty重复set
    }

    //更新
    update(){
        console.log('视图更新')
        this.cb.call(this.vm,this.vm[this.key])
    }
}

# 迷你版vue链接

代码地址:https://github.com/PCAaron/blogCode/tree/master/vue/easy-vue
MVVM效果图

# 概念了解

# 虚拟DOM

虚拟DOM就是用JavaScript对象来描述DOM结构。数据修改的时候,先修改虚拟DOM中的 数据,然后数据做diff算法,最后再汇总所有的diff差异,找出这个最小差别并且在真实 DOM上只更新这个最小的变化内容,极大程度地降低了对DOM的操作带来的性能开销。

//dom
<div id='app' style='color:red' @click='handleClick'>
    <a>click me</a>
</div>

//virtual dom
{
  tag:'div',        // 元素标签
  attrs:{           // 属性
    id:'app'
    style:{color:red},
    onClick: handleClick
  },
  children:[
      {
          tag:'a',
          text:'click me'
      }
  ]       // 子元素
}

# diff算法

Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。

Diff算法有三大策略,其执行顺序也是顺序执行:

  1. Tree Diff
    Tree Diff 是对树每一层进行遍历,找出不同。
  2. Component Diff
    Component Diff 是数据层面的差异比较,如果都是同一类型的组件,按照原策略继续比较Virtual DOM树即可; 如果出现不是同一类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。
  3. Element Diff
    Element Diff真实DOM渲染,结构差异的比较。

# Object.defineProperty(obj, prop, descriptor)

Object对象方法,defineProperty方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性, 并返回这个对象。

其中接受三个参数分别表示:

  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的属性描述符。

其中,属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有 值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter 函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

描述符可同时具有的键值:

configurable enumerable value writable get set
数据描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes
<div id="app">
        <p>
            hello
            <span id="name"></span>
        </p>
</div>
<script>
    var obj = {}
    Object.defineProperty(obj,'name',{
        get(){
            return document.getElementById('name').innerHTML
        },
        set(inner){
            document.getElementById('name').innerHTML = inner
        }
    })
    console.log(obj.name)   // ''
    obj.name='set'
    console.log(obj.name)   // 'set'
</script>

# 推荐阅读

vuejs原理浅析:https://blog.csdn.net/saucxs/article/details/87550808

Diff算法:https://www.jianshu.com/p/cdb4ad82df20