2

Vue更新视图的思想

Vue的响应式的核心是defineProperty,通过defineProperty来设置响应式的变量,当变量的值改变时就触发对应的setter方法,从而调用视图更新的方法,更新视图。那么Vue中究竟如何渲染视图的呢?

Vue的思想:

  • 实例化vue时初始化数据,将data中的变量设置成响应式;
  • 渲染模版初始化页面时,进行依赖收集,也就是会将有读取data中的变量的dom对象存在Watcher对象中;
  • 修改数据,触发setter,触发对应的watch对象,调用渲染函数,重新渲染被收集的dom,更新视图。

模拟vue实现视图更新

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

<head>
    <title></title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>

    <div id="app">
        <div>
            {{message}}
        </div>
    </div>
</body>

<script>
    document.addEventListener('DOMContentLoaded', function () {
        let app = {
            el: '#app',
            data: {
                message: '页面加载于 ' + new Date().toLocaleString()
            }
        }
        let vm = new miniVue(app)
        setTimeout(() => {
            app.data.message = '加载完成!!'
        }, 2000);
    })

    class miniVue {
        constructor(opt) {
            this.opt = opt
            this.observe(opt.data)
            let root = document.querySelector(opt.el)
            this.compile(root)
            console.log(this)
        }
        // 为响应式对象 data 里的每一个 key 绑定一个观察者对象
        observe(data) {
            Object.keys(data).forEach(key => {
                let obv = new Observer()
                data["_" + key] = data[key]
                // 通过 getter setter 暴露 for 循环中作用域下的 obv,闭包产生
                Object.defineProperty(data, key, {
                    get() {
                        Observer.target && obv.addSubNode(Observer.target);
                        return data['_' + key]
                    },
                    set(newVal) {
                        obv.update(newVal)
                        data['_' + key] = newVal
                    }
                })
            })
        }
        // 初始化页面,遍历 DOM,收集每一个key变化时,随之调整的位置,以观察者方法存放起来    
        compile(node) {
            [].forEach.call(node.childNodes, child => {
                if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
                    let key = RegExp.$1.trim()
                    child.innerHTML = child.innerHTML.replace(new RegExp('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm'), this.opt
                        .data[key])
                    Observer.target = child
                    this.opt.data[key]
                    Observer.target = null
                } else if (child.firstElementChild)
                    this.compile(child)
            })
        }
    }
    // 常规观察者类
    class Observer {
        constructor() {
            this.subNode = []
        }
        addSubNode(node) {
            this.subNode.push(node)
        }
        update(newVal) {
            this.subNode.forEach(node => {
                node.innerHTML = newVal
            })
        }
    }
</script>

</html>

注意以上demo:

巧妙的使用了闭包

在obsever方法中的Object.keys(data).forEach(key => {})中使用到了闭包,data中的每个变量对应的都有一个new Observer()观察者对象,这个对象一直被setter方法引用着。(可以试着把遍历data属性的方法改成for循环遍历,这时候就没有了闭包,new Observer()不会被保存在setter方法中,数据改变时触发setter方法读取不到new Observer()对象,最终无法更新视图)

闭包的判断依据:函数内部的函数一直对该函数的作用域保持着引用,这个引用就是闭包。闭包的好处就是当函数执行完,作用域不会被释放,还可以读到其内部的数据。

观察者模式解耦代码技巧

编译渲染的时候,在Observe类上定义了属性target,作为全部变量用来标记依赖,同时在渲染方法中又读取了一次变量,用来触发obsever方法中的变量对应的getter方法来存放依赖。

image.png

image.png


贝er
58 声望6 粉丝

不仅仅是程序员