一:浅谈Vue2.0的双向数据绑定与模板渲染原理

1. 基本原理(大概)

MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
vue2.0 采用 ES5ObjectdefineProperty() 方法来进行数据劫持,元素节点来进行绑定判断

  1. 先通过js来获取页面上的vue实例(#app),然后通过 node.childNodes 属性来获取他的子元素(包含文本节点和属性节点)列表,循环遍历子元素列表,通过 node.nodeType 属性来判断他的节点类型(1为元素节点,2为文本节点),判断完后需再次判断该节点是否还有子元素,如有需递归调用(为了防止元素多层嵌套)
  2. 当节点类型为文本节点时可以用 node.nodeValue 属性来获取文本内容,然后再用正则来进行查找是否包含{{}},即检查是否含有插值表达式,如果有,取出data里面的数据进行替换
  3. 当节点类型为元素节点时,则需要通过 node.attribute 属性来获取该元素的所有属性节点,然后遍历,通过attr.nodeName来获取属性节点名(每一个属性节点都是一个对象),再来判断该节点名是否是 v-text, v-mode,
    v-on, v-html等vue指令,如果是,则分别进行处理
  4. 数据劫持:先获取vue的data对象,然后通过 Object.keys(data) 来获取属性名。再通过Object.defineProperty(data, key, descriptor) 进行数据劫持(参数一:目标对象,参数二:对象的属性名,参数三:对劫持属性的一些列操作,是个对象{})
  5. 当解析每一个文本节点或属性节点时,会new一个定义好的Watcher类(俗名观察者),他里面有一个方法,会获取data里面的值来触发 defineProperty 里面的 get 方法,在第一次遍历data里面的属性时,会在每次遍历时new 一个 Dep 类(俗名发布者),Dep类里面有个方法,里面有一个数组,会存放所有使用到当前属性的 Watcher 对象,
  6. 当设修改属性值时,会触发 defineProperty 里面的 set 方法,通过判断属性值来是否遍历Dep里面的数组进行数据改变,即页面的数据修改

上代码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge,chrome=1">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <p>----{{msg1}}----</p>
    <div>
      <p>{{index}}</p>
    </div>
    <p>----{{md.value}}----</p>
    <p>---点击了: {{index}} 次</p>
    <p v-html="html" title="123"></p>
    <p v-text="obj.value"></p>
    <input type="text" v-model="md.value">
    <p>obj.value----{{obj.value}}</p>
    <button v-on:click="onClick">测试</button>
  </div>

  <script src="./src/watcher.js"></script>
  <script src="./src/observer.js"></script>
  <script src="./src/compile.js"></script>  
  <script src="./src/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg1: '测试1',
        msg: 'v-text',
        html: '<h2>123</h2>',
        md: {
          value: 'v-model测试',
        },
        index: 0,
        obj:{
          value: '对象数据测试'
        }
      },
      methods:{
        onClick(){
          this.index++
        }
      }
    })
  </script>
</body>

</html>

vue.js

class Vue {
  constructor (options = {}){
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods

    // 绑定属性劫持
    new ObServer(this.$data)

    // 数据代理---把data和methods里面的熟悉都添加到Vue实例上
    this.porxy(this.$data,this.$methods)

    // 编译模板
    if (this.$el) {
      new Compile(this.$el, this)
    }
  }

  porxy(data){
    // 因为最开始就把data里面的所有数据(包含深层数据)都进行了监听,所以只需要监听绑定后的最外层数据就行,不需要再次递归
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get(){
          return data[key]
        },
  
        set(newValue) {
          console.log(newValue)
          if(data[key] === newValue) return
          data[key] = newValue
        }
      })
    })
  }
}

obServer.js

用于进行数据劫持

class ObServer {
    constructor(data) {
        this.data = data

        // 编辑数据,添加setter,getter
        this.walk(data)
    }

    walk(data) {
        // 如果data为空或者不是一个对象
        if (!data || typeof data != 'object') {
            return
        }

        Object.keys(data).forEach(key => {
            // 进行数据劫持
            this.defineReactive(data, key, data[key])

            // 递归调用---防止data[key]值为对象
            this.walk(data[key])
        })

    }

    defineReactive(data, key, value) {
        let that = this
        let dep = new Dep()
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                // Dep.target 存在时,即设置(修改)数据时才添加
                Dep.target && dep.addSub(Dep.target)
                return value
            },

            set(newValue) {
                if (value === newValue) return
                // 设置值
                value = newValue

                // 设置完后调用walk进行绑定劫持,为了防止设置的是一个对象而没有监听到
                that.walk(newValue)

                dep.notify()
            }
        })
    }
}

compile.js

用于进行模板编译

class Compile {
    constructor(el, vm) {
        this.el = typeof el === "string" ? document.querySelector(el) : el
        this.vm = vm
        // 把所有的子节点放入fragment中
        let fragment = this.node2fragment(this.el)

        // 编辑fragment
        this.compile(fragment)

        // 把内存节点添加到页面上
        this.el.appendChild(fragment)
    }

    /**
     * 核心方法
     * node2fragment ------------------ 创建文档碎片,并把文档的子节点添加到文档碎片中
     * compile ------------------------ 编译内存碎片
     * compileElementNode ------------- 解析元素节点
     * compileTextNode ---------------- 解析文本节点
     */
    node2fragment(node) {
        // 创建文档碎片
        let fragment = document.createDocumentFragment()

        // 获取所有的子节点,包含文本节点
        let childNodes = node.childNodes

        // 遍历往fragment中添加子节点
        this.toArray(childNodes).forEach(node => {
            fragment.appendChild(node)
        })

        return fragment
    }

    compile(node) {
        let childNodes = node.childNodes
        this.toArray(childNodes).forEach(node => {

            let nodeType = this.isNodeType(node)

            // 元素节点
            if (nodeType == 1) {
                this.compileElementNode(node)
            }

            // 文本节点
            if (nodeType == 3) {
                this.compileTextNode(node)
            }

            // 还有子节点时递归调用
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }

    compileElementNode(node) {
        // 获取该节点的所有属性---伪数组
        let attributes = node.attributes
        this.toArray(attributes).forEach(attr => {
            // attr为一个属性节点
          console.dir(attr)
            let attrName = attr.nodeName

            if (this.isDirective(attrName)) {
                // 去除 v-
                let type = attrName.slice(2)
                // 获取指令值
                let expr = attr.nodeValue

                /*
                // text节点
                if(type === "text") {
                  node.innerText = this.vm.$data[expr]
                }
        
                // html节点
                if(type === "html") {
                  node.innerHTML = this.vm.$data[expr]
                }
        
                // model节点
                if(type === "model") {
                  node.value = this.vm.$data[expr]
                }
        
                // v-on
                if(this.isEventDirective(type)) {
                  // 获取v-on后面的事件源
                  let eventType = type.split(":")[1]
                  // 为节点绑定事件并改变事件体中的this指向
                  node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm)) 
                }
                 */

                // 代码优化
                if (this.isEventDirective(type)) {
                    CompileUtil['eventHandler'](type, node, this.vm, expr)
                } else {
                    CompileUtil[type] && CompileUtil[type](node, this.vm, expr)
                }
            }
        })
    }

    compileTextNode(node) {
        CompileUtil.mustache(node, this.vm)
    }


    /**
     * 工具方法
     * toArray -------------------------- 将类数组变成数组
     * isNodeType ----------------------- 判断节点类型 1-元素节点 2-属性节点 3-文本节点
     * isDirective ---------------------- 是否为vue的指令
     */
    toArray(likeArray) {
        return Array.from(likeArray)
    }

    isNodeType(node) {
        return node.nodeType
    }

    isDirective(attrName) {
        return attrName.startsWith('v-')
    }

    isEventDirective(type) {
        return type.split(':')[0] === "on"
    }
}

/**
 * 解析文本节点的工具对象
 * mustache ------------------------- 解析插值表达式
 * text ----------------------------- 解析 v-text
 * html ----------------------------- 解析 v-html
 * model ---------------------------- 解析 v-model
 * eventHandler --------------------- 解析 v-on
 * getVmValue ----------------------- 数据处理,用获得到的属性值去data里面寻找数据
 * setVmValue ----------------------- 设置data值,用于双向数据绑定
 */
let CompileUtil = {
    mustache(node, vm) {
        let attrText = node.nodeValue
        let reg = /\{\{(.+)\}\}/g
        if (reg.test(attrText)) {
          let expr = RegExp.$1
            // node.textContent = attrText.replace(reg, vm.$data[expr])
            node.textContent = attrText.replace(reg, this.getVmValue(vm, expr))
            new Watcher(vm, expr, (newValue) => {
                node.textContent = attrText.replace(reg, newValue)
            })
        }
    },
    text(node, vm, expr) {
        node.innerText = this.getVmValue(vm, expr)
        new Watcher(vm, expr, (newValue) => {
            node.innerText = newValue
        })
    },
    html(node, vm, expr) {
        node.innerHTML = this.getVmValue(vm, expr)
        new Watcher(vm, expr, (newValue) => {
            node.innerHTML = newValue
        })
    },
    model(node, vm, expr) {
        let that = this
        node.value = this.getVmValue(vm, expr)
        node.addEventListener('input', function () {
          that.setVmValue(vm, expr, this.value)
        })
        new Watcher(vm, expr, (newValue) => {
            node.value = newValue
        })
    },
    eventHandler(type, node, vm, expr) {
        let eventType = type.split(":")[1]
        let fn = vm.$methods && vm.$methods[expr]
        if (eventType && fn) node.addEventListener(eventType, fn.bind(vm))
    },
    // 数据data中的值
    getVmValue(vm, expr) {
      let data = vm.$data
        expr.split('.').forEach(key => {
            data = data[key]
        })
        return data
    },
    // 设置data中的值
    setVmValue(vm, expr, value) {
        let data = vm.$data
        let arr = expr.split('.')
        let length = arr.length
        // 遍历找到对应的键并赋值
        arr.forEach((item, index) => {
            if (index === length - 1) {
                data[item] = value
            } else {
                data = data[item]
            }
        })
    }
}

watcher.js

用于进行数据监听-即观察者

// 用于数据更新
class Watcher{
  constructor(vm, expr, cb) {
    /**
     * vm: vue实例
     * expr: 属性名
     * cb: 调用更新的回调函数
     */
    this.vm = vm
    this.expr = expr
    this.cb = cb

    // 保存每一个订阅者
    Dep.target = this
    console.log(this)
    // this.getVmValue 调用时触发数据监听,添加订阅者
    this.oldValue = this.getVmValue(this.vm, this.expr)
    // 订阅者添加后置空
    Dep.target = null

  }

  // 判断是否更新
  updata(){
    let oldValue = this.oldValue
    let newValue = this.getVmValue(this.vm, this.expr)
    if(oldValue != newValue){
      this.cb(newValue)
    }
  }

  // 数据处理
  getVmValue(vm, expr){
    let data = vm.$data
    expr.split('.').forEach( key => {
      data = data[key]
    } )
    return data
  }
}


// 发布者
class Dep{
  constructor(){
    this.subs = []
  }

  // 多个订阅者放在一起
  // 订阅者是指页面的节点,即:多个节点订阅data里面的一个变量
  addSub(watcher){
    this.subs.push(watcher)
  }

  // 发布,所有的订阅者一起更新
  notify(){
    this.subs.forEach( item => {
      item.updata()
    } )
  }
}

运行过程

/* 友情提示:最好结合上面的代码看比较好
	首先,index.html文件运行到 new Vue() 时会触发 Vue 类,传递了一个对象,对象里面的属性
分别是页面上的Vue容器对象:el,数据:data,方法:methods

    然后通过 new ObServer(this.$data)来把数据传递过去进行数据劫持 
    
    此时进入 ObServer类里面
    先调用 walk 方法进行一个基础的兼容性处理,然后通过 Object.keys(data).forEach 循环调用
this.defineReactive方法进行数据劫持

	每次循环时都会为一个属性创建 Dep类的对象,defineProperty里面的configurable属性大致意思是是否
可以修改值,enumerable是是否乐意被for...in遍历到,即:是否可以枚举,get方法里面的 Dep.target &&
dep.addSub(Dep.target) 先不说明,后面会说明,set方法里面 value = newValue 就是把设置的值替换掉
以前的值,that.walk(newValue) 是为了防止新设置的值是一个对象,所以保险起见需要再次遍历进行绑定劫持
dep.notify() 也先不说明

    ObServer类执行完后又返回到Vue类里面,继续下面代码,调用 this.porxy()方法
    
    porxy方法的作用就是把 this.$data.msg 简写变成 this.msg,methods同理,所以再次调用
Object.defineProperty方法进行数据劫持,与ObServer中的不同的是,一个是监听的属性绑定在this.$data
上,一个直接绑定到this上,即直接挂载到vue实例上,所以当this.msg调用时,先触发porxy里面监听的get(),
由于return data[key],即从data里面获取了数据,所以会再次触发 ObServer类 里面的数据劫持,然后进行
数据处理一系列操作,set方法同理

	porxy里面执行完了后又返回到vue类里面,然后执行 new Complie 指令,进入到Complie类里面
	
	Complie 类主要用于模板编译
	首先,先获取Vue容器对象(el),然后调用node2fragment()方法把el里面的所有子节点添加到文档碎片里
面去,这样可以全部编译完成后一次性添加到页面上去,性能优化

	fragment创建好后就可以进行模板解析,先通过node.childNodes属性获取他的子节点,然后进行遍历判断
节点类型是否是元素节点还是文本节点,如果是元素节点时先通过 node.attributes 属性获取它所有的属性节点
,然后遍历判断属性名书否是vue的指令,是的话进入到对应的方法里面进行处理

	例:一个v-text指令时,此时进入到text方法里面,getVmValue()方法是用于获取data里面的值,然后给
对应的节点赋值,然后执行 
	new Watcher(vm, expr, (newValue) => {  
		// 参数一:vue实例,参数二:属性名,参数三:更新时的回调函数
		node.innerText = newValue
    })
    此时进入到 Watcher 类里面
    
    Dep.target = this 的作用是每次调用解析指令的方法时,都会保存这个节点,方便更新的时候直接找到
该节点(因为有个回调函数,回调函数里面有个节点)

	然后执行下一行代码,因为调用了this.getVmValue()方法用于获取data里面的值,所以会触发ObServer
类里面监听该属性的get()方法,此时 Dep.target 的第二个用处在于是知道此时是设置(修改)数据,而不是第
一次的数据监听,然后调用dep对象(最开始进行属性监听的时候为每个属性常见的对象)的 addSub(Dep.target)
方法,用于保存所有使用了这个属性的Watcher对象,当这个属性值修改时会统一遍历这个数组,调用这个对象一开
始保存的回调函数来更新页面上的数据

	v-model 与 v-text 的区别在于解析v-model1时候为当前节点绑定了input事件,事件触发后会设置当前
data里面的值,然后会触发ObServer里面的set方法,然后会触发dep对象的notify()方法,用于通知所有订阅
了该属性的节点进行数据更新,然后编辑这个属性上存放Watcher对象的数组-subs,来调用每个对象的回调函数
进行页面的数据更新

	事件绑定原理:当解析到v-on时调用对应的处理函数,获取v-on后面的函数名,并在methods对象里面找到对
应的函数,然后再为该节点绑定对应的事件即可

	插值表达式的解析:因为本人正则不熟,所以插值表达式里面的正则只能处理一对{{}},匹配到{{}}里面值后
就在data里面遍历查找对应的值,然后进行替换

*/
Logo

前往低代码交流专区

更多推荐