您的位置:首页 > Web前端 > Vue.js

知识体系:Vue双向绑定实现原理

2020-07-29 19:45 85 查看

只知其然,而不知其所以然,我们还是要知道Vue,到底是这么实现双向数据绑定的,我还是依旧使用debugger的形式,一步一步的执行,看看他的底层实现
首先我先用vue-cli,就直接默认配置,初始化了一个工程,现在最新的版本是2.6,然后将main.js和App.vue进行了相应的改造
main.js //我加了两个钩子函数

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
el:"#app",
render: h => h(App),
beforeCreate(){
console.log("Vue beforeCreate")
},
mounted(){
console.log("Vue mounted")
}
})

App.vue //主要是这个v-model

<template>
<div id="apps">
<input type="text" v-model="title">
<input type="text">
<div @click="clickFn">{{ name }}</div>
</div>
</template>

<script>
export default {
data:function(){
return {
title:"我是帅田",
name:"俞菁田"
}
},
beforeCreate(){
console.log("app beforeCreate")
},
created(){
console.log("app created")
},
mounted(){
console.log("app mounted")
},
methods:{
clickFn(){
console.log(this.name)
}
}
}
</script>

接着到node_modules下的Vue模块,找到dist文件,在vue.runtime.esm.js,这个文件是运行时的Vue,也叫不完整版,是不包括编译模板的部分的,这样的好处就是减少包体积,将一部分工作挪到了编译阶段,Vue的render函数就是通过Vue-loader这个插件帮我们在编译阶段生成的
找到Vue的构造函数 并写上debugger 之后我只会说和双向数据绑定相关的内容其他的底层原理将在之后的学习中记录

function Vue (options) {
debugger
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}

我认为Vue的双向数据绑定最关键的就是
1.数据劫持,就是将数据变成响应式数据
2.依赖收集
3.给DOM添加监听函数

接下来开始debugger,当new Vue的时候就会执行 上面的this._init(options);

Vue.prototype._init = function (options){
....以上省略
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);     //这里就是对data数据进行响应式的地方
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}

if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}

这个initState(vm)就是用来添加响应式的地方,他最终执行的就是new Observer()创建观察者,循环data中的每个属性,如果这个属性也是对象,Vue还会递归这个属性中的数据,由于我们就是 {title:“我是帅田”,name:“俞菁田”}这样一个简单的对象,所以不会递归执行,最终就是循环执行defineReactive$$1函数 obj就是我们的整个对象,key就是这里的title和name,给他设置get,set方法进行劫持

function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();

var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}

var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}

而我们的依赖收集就是在get函数中进行的
这个是编译生成的render函数

var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { attrs: { id: "apps" } }, [
_c("input", {
directives: [
{
name: "model",
rawName: "v-model",
value: _vm.title,
expression: "title"
}
],
attrs: { type: "text" },
domProps: { value: _vm.title },
on: {
input: function($event) {
if ($event.target.composing) {
return
}
_vm.title = $event.target.value
}
}
}),
_c("input", { attrs: { type: "text" } }),
_c("div", { on: { click: _vm.clickFn } }, [_vm._v(_vm._s(_vm.name))])
])
}

我们绑定在Dom上的数据都是以_vm.title _vm.name的形式,当我们执行render函数的时候,就会被get劫持到,每个响应式数据都会有一个Dep容器,这个就是用来放依赖的,他会把所有用到这个变量的Watcher收集起来,等到数据发生变化的时候,在通知这些Watcher更新视图,那么Watcher是什么呢,Vue一共有三种Watcher,一种就是渲染watcher,
计算watcher,监视watcher
渲染watcher:我们每一个组件都是一个渲染watcher,包括我们的根Vue对象,每个组件都是一个新的Vue对象
计算watcher:就是我们写在组件里的computed对象
监视watcher:就是我们写在组件里的watch对象

当我们的title被劫持到就会进入这个get函数

get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},

因为这个Dep.target这个静态变量始终会指向当前Vue对象的Watcher对象,现在其实就是app.vue这个组件的渲染Watcher,接着就是先执行dep.depend();依赖收集的函数

Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};

不难看出,他会先看Dep容器中有没有这个Watcher,每一个Watcher的id是唯一的,所以不可能重复添加,如果不存在就 dep.addSub(this);将当前的渲染Watcher添加到容器中去
title属性和name属性都是一样的,都会往自己的dep容器中添加这个渲染watcher

上面的render函数中有这样一个on对象,很清楚这个就是之后在创建真实Dom的时候会给我们的input输入框添加监听事件,这个函数就是将我们的当前值赋值给this.title,告诉Vue,我的视图发生变化了,将数据也修改一下

on: {
input: function($event) {
if ($event.target.composing) {
return
}
_vm.title = $event.target.value
}
}

之后当我Vnode创建完成,Dom创建以及插入完成之后,Vue还有一个优化处理

el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
el.addEventListener('change', onCompositionEnd);

这个监听函数很清楚吧,就是当我们在输入框v-model绑定的话,如果唤起输入法,是不会触发页面重新渲染的,而我们不用v-model绑定,直接用input事件,那么就是我们输入一个,不管他有没有输入唤起输入法,都是会触发渲染的,其实换一个说法,v-model就是@input v-bind的语法糖,就是这两个的结合。到这里初始化就完成了监听事件的绑定以及依赖的收集。

接下来我们可以改变一下输入框的值,就会进入到 set

set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}

Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();  //循环依赖数组进行更新
}
};

最终就是执行dep.notify(); 通知容器中的watcher发生变化了
执行watcher.run()
最终就是执行vm._update(vm._render(), hydrating);
vm._render() 就是递归,深度遍历的生成vnode
vm._update 就是创建真实的dom,插入到页面中
和我们初始化的时候一样,也是一套组合拳
这样就形成了闭环,只要数据发生变化,都是会触发重新渲染

总结一下,在new的时候,Vue会将我们的数据变成响应式的数据,通过get函数来为每一个响应式数据收集依赖,Vue将子组件生成的render函数,根据属性给每个Dom绑定上对象的属性以及监听事件,v-model的话,就会绑定上input,change,compositionstart,compositionend四个监听事件,当我们改变值的时候,通过set函数,通知每个依赖重新生成vnode, DOM。这样就做到了数据变化引起视图的变化,视图的变化引起数据的变化

下面是我学习Vue的视频,有需要的小伙伴可以自己看看,可以跟着debugger
https://pan.baidu.com/s/1C8eDPe0WQ3WJk_MtWS1FZw 提取码:jgkq
这就是我今日的分享,有不对的可以提出来哦!!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: