上一篇: js错误上报下一篇: http请求

Vue知识点整理


快捷目录: 虚拟dom 数据的双向绑定 生命周期 组件

一、虚拟dom

1.虚拟dom是什么?

答:dom本质上就是一种特殊的对象,用js存储dom的数据结构就称为虚拟dom.

2.为什么使用虚拟dom?

答:举个例子,假设某个网站,有一张小渔村全村人口数据表,原来是通过遍历这张数据表,用appendChild向table中添加tr将整张表显示在网页中,当这个村有婴儿出生,或者有老人死亡,先修改数据表,然后再重新遍历这张表生成新的dom.原来小渔村只有1000人,人口增减变化不大,对dom操作不算频繁.后来,这个小渔村逐渐发展壮大,变成现在的深圳,人口将近1300万人,如果沿用原来的方法,每次有数据变化,都重新遍历表,那么,上千万次的遍历,对dom的操作,都将是杀死网站性能的杀手,所以,此时断然不能再使用原来的方法.

虚拟dom维护了一个旧的dom结构,用新的dom和旧的dom进行比较,没有变动的dom维持不变,有改动的dom比较差异,将差异应用到真实的dom上.相比之下,虚拟dom对真实dom的改动肯定比原来的全部遍历表,重新添加dom小的多.

3.虚拟dom原理

3.1虚拟dom的构造函数

想象一下,一个真实dom有哪些信息?1,标签名;2,属性;3,子节点.用js可以这样表示:

var element = {
    tagName: '',  //div,span...
    props: '',   //style, id, class...
    children: ''  //可能是一段文本,也可能是一些dom元素,还可能不存在
}

所以,构造函数可以写成:

function Element (tagName, props, children) {
    this.props = props
    this.tagName = tagName
    this.children = children
}
//为了方便使用构造函数生成实例,最好将生成实例的过程封装
function el (tagName, props, children) {
    return new Element(tagName, props, children)
}

虚拟dom最终要转化成真实dom,所以,构造函数上增加一个生成dom的方法

Element.prototype.render = function () {
    let props = this.props
    let tagName = this.tagName
    let children = this.children || []

    let el = document.createElement(tagName)
    for (let k in props) {
        el.setAttribute(k, props[k])
    }
    children.forEach(child => {
        let e = child instanceof Element ? child.render() : document.createTextNode(child)
        el.appendChild(e)
    })
    return el
}

3.2diff算法

虚拟dom的核心在于diff算法.已知旧dom的数据结构(下文称为旧树),已知新dom的数据结构(下文称为新树),对比两颗树的差异,就是diff过程.

想象一下,一个node节点的变化有哪些情况?1,这个节点是文本节点,其内部文本发生改变;2.节点是dom元素,节点名发生改变;3节点是dom元素,属性发生改变.4,元素位置发生变化.所以,概括diff的四种类型:

(1)TEXT //文本变化

(2)REPLACE //节点名发生变化,相当于整个节点被替换

(3)PROPS //属性发生变化

(4)REORDER //位置发生变化

定义一个type对象,记录所有的变化类型:

var type = {
    REPLACE: 0,
    REORDER: 1,
    PROPS: 2,
    TEXT: 3
}

遍历对比新旧两棵树,将差异记录在patches中:

function dfsWalk (newTree, oldTree, patches, index) {
    var currentPatch = []
    if (newTree === null) {
    } else if (isString(newTree) && isString(oldTree)) {
        if (newTree !== oldTree) {
            currentPatch.push({type: patch.TEXT, content: newTree})
            patches[index.index] = currentPatch
        }
    } else if (newTree.tagName === oldTree.tagName) {
        var propsPatches = diffProps(oldTree, newTree)
        currentPatch.push({ type: patch.PROPS, props: propsPatches })
        patches[index.index] = currentPatch
        diffChildren (oldTree.children, newTree.children, currentPatch, patches, index, 'key')
    } else {
        currentPatch.push({ type: patch.REPLACE, node: newTree })
        patches[index.index] = currentPatch
    }
}

3.3将差异运用到真实的dom上

旧树保留着真实dom的所有信息,所以可以对旧树进行深度优先遍历,将diff算法得到的patches运用到真实的dom上,核心代码如下:

function applyPatch (node, currentPatch) {
    var len = currentPatch.length
    var i = 0
    while (i < len) {
        var type = currentPatch[i].type
        switch (type) {
            case 0:
                var newNode = currentPatch[i].node.render()
                node.parentNode.replaceChild(newNode, node)
                break
            case 1:
                var moves = currentPatch[i].moves
                var j = 0
                while (j < moves.length) {
                    var item = moves[j]
                    if (item.type === 1) {
                        node.insertBefore(item.item.render(), node.childNodes[item.index] || null)
                            } else {
                                node.removeChild(node.childNodes[item.index])
                            }
                            j++
                        }
                        break
                    case 2:
                        setProps(node, currentPatch[i].props)
                        break
                    case 3: 
                        node.textContent = currentPatch[i].content
                        break
                    case 4: return
                }
                i++
            }
        }

总结:虚拟dom实际上就是一个将真实dom用js对象表示出来,然后对比新旧两个对象树的区别,将区别应用到页面dom的过程,其重点和难点集中在计算新旧两棵树的区别.万变不离其宗,js对比两个对象的区别,肯定是逐一遍历所有的属性,因为dom是一个典型的树结构,dom嵌套着dom,所以,利用深度遍历优先,得到两颗树的区别,然后再将所有的区别应用到页面dom上.(所以,把握好递归调用.离手写一个虚拟dom也就不远啦~)

二、数据的双向绑定

场景一:如下,data返回的对象里有一个属性text,设置其初始值为一段文字,与页面div绑定,当改变text的内容,页面显示的内容也随之变化;

<template>
    <div>
        <div>{{name}}</div>
    </div>
</template>
<script>
    export default {
        data () {
            return {
                text: '我的名字是miki'
            }
        }
    }
</script>

场景二:如下,将和text绑定的页面元素改为input,,当改变text,输入框显示的文本发生改变,当在输入框中重新输入一段文本,在控制台打出text,发现text变成输入框输入的新的文本.

<template>
    <div>
        <input v-model="text" />
    </div>
</template>
<script>
    export default {
        data () {
            return {
                text: '我的名字是miki'
            }
        }
    }
</script>

以上两个场景,都体现了一种现象,页面元素和某个变量绑定,当变量发生变化,页面的显示情况也随之变化,这是数据单向绑定;如果页面元素是input,textarea等可输入元素,可输入元素的输入值发生改变时,变量也随之改变,像这种绑定的元素和变量能相互影响,就是双向绑定.双向绑定基于单向绑定,只要input,textarea等可输入元素发生change事件的时候,改变所绑定的变量的值,就实现了从页面到变量的改变.所以,本节主要分析数据单向绑定的实现原理.

vue实现数据单向绑定原理:

(1)基于Object.defineProperty的数据劫持;

(2)收集相关依赖函数;

(3)数据改动触发依赖函数.

1.数据劫持

利用Object.defineProperty,给对象的每个属性设置getset,达到监听数据变动的目的.

//假设data返回的对象为obj
Object.keys(obj).forEach(k => {
    let val = obj[k]
    Object.defineProperty(obj, k, {
        get () {
            return val
        }
        set (v) {
            val = v
        }
    })
})
2.依赖收集类

利用发布-订阅模式,实现一个依赖收集的类,它的实例对象能收集依赖函数,并且能触发依赖函数.

//Dep类
class Dep {
    constructor () {
        this.sub = []
    }
    depend (fn) {
        if (!this.sub.includes(fn)) {
            this.sub.push(fn)
        }
    }
    notify () {
        this.sub.forEach(s => s())
    }
}

#####3.数据单向绑定实现

let obj = {price: 3, num: 5}
let target
class Dep {
    //....此处与上面的Dep类代码一样,故省略
}
Object.keys(obj).forEach(k => {
    let val = obj[k]
    let dep = new Dep
    Object.defineProperty(obj, k, {
        get () {
            dep.depend(target)
            return val
        }
        set (v) {
            val = v
            dep.notify()
        }
    })
})
function watcher (fn) {
    target = fn
    fn()
    target = null
}
watcher( _ => {
    obj.total = obj.price * obj.num
})

三、生命周期

1.定义

vue是一个组件系统,任何复杂的应用都可以看做是由各个小的组件组成,所以,组件是vue的最小单位.生命周期是指一个组件从生成到销毁的过程,生命周期的各个阶段,vue定义了与之匹配的钩子,如果注册了相应的钩子函数,组件发展到对应的阶段就会触发执行钩子函数.

2.钩子函数

四、组件

1.什么是vue组件,如何设计一个组件?

答:符合vue组件定义规范的,有一套vue的完整生命周期的组件,就是vue的组件.设计一个组件,首先要考虑,组件的输入,输出是什么,有一套怎样的生命周期,生命周期的每个阶段做了哪些相应的事情.

2.如何理解vue是一个组件系统?

答:new一个vue类,生成一个vue实例,这个实例添加相应的配置后,能被当做一个组件引用,同时,这个实例也能引用其他实例.一个以.vue结尾的页面,可以是一个vue实例,也可以是多个vue实例组合而成,所以说,vue是一个组件系统.

3.vue父子组件之间的通信

3.1父组件向子组件传递数据示例:

//----父组件----
<div>
    <child :name=send_name><child>   //传递动态数据给子组件
    <child name="miki"><child>   //传递静态数据给子组件
</div>

//----子组件----
<div>{{name}}</div>
<script>
    props: {
        name: ''    //规定父组件传递数据的属性名
    }
</script>

父组件直接在子组件的标签上添加要传递的数据,传递的数据是动态的,就以: + 属性名 = 父组件变量的形式,传递的数据是静态的,就以属性名 = 属性值的形式;在子组件内部,props属性的值是一个对象,这个对象用于规定父组件向子组件传递数据时的属性名.

3.2子组件向父组件传递数据

//----父组件----
<div>
    <child @send_name="getDataFromChild"><child>
</div>
<script>
    methods: {
        getDataFromChild(p)  {
            //...  参数p就是子组件向父组件传递的数据
        }
    }
</script> 

//----子组件----
<div @click='send'>
    //html省略....
</div>
<script>
    methods: {
        send () {
            this.$emit('send_name', 'kiki')
        }
    }
</script>

子组件向父组件传递数据,在子组件内部,用vue的实例方法$emit,这个方法的第一个参数是发射出去的函数名从,第二个参数开始,都是子组件向父组件传递的数据,子组件也可以将要传递的多个参数放在一个对象里传递给福组件.父组件在引用子组件的时候,在子组件的标签里以@ + 发射的函数名 = 父组件定义的函数的形式,接收子组件传递的数据,,父组件可以声明与之相对应的形参,或者访问arguments获得子组件的数据.

上一篇: js错误上报下一篇: http请求