前言
原生js实现一版迷你vue,了解vue中的MVVM和dom-diff、计算属性、watch、observe、dep、watcher、nextTick是啥,咋实现的。。。
本篇文章注重怎么去一步步实现一版本vue的核心代码及掌握vue原理,或者说vue得源码设计,不会去过多讲解vue得应用,且 vue源码的语法使用了Flow,为了使代码更简洁,本文代码中没有使用,对于vue得具体应用可查看官网文档: cn.vuejs.org/v2/guide/ ~
你从本篇文章中可以学习到:
- vue目录结构设计
- vue核心源码实现原理
- Vue得两大核心点:MVVM和dom-diff
- vue如何实现数据劫持、监听数据的读写?
- vue中观察者模式
- 计算属性和watch区别
- vue中数据的批量更新
- nextTick实现
- proxy
本篇主要实现Vue得两大核心点:MVVM和dom-diff;
如果不知道MVVM是什么?可以查看: juejin.cn/post/684490…
如果不知道dom-diff是什么?可以查看我的文章: juejin.cn/post/684490… 都有具体的说明和实现过程。
完整项目代码可查看: github.com/lixiaoyanle…
思考问题?
-
你下载了vue,不知道怎么查看源码?vdom在哪个目录下?具体生命周期在哪?
-
vue的目录都是什么意思?
-
只想了解核心代码怎么找?
-
new Vue的时候都干了什么?
-
内部都是怎么执行的?
-
怎么就能把数据直接渲染到页面了?
-
vue全局暴露什么Api?
-
为什么说vue是数据驱动的呢?
Vue架构介绍
vue.js代码设计非常优雅,我们来下载一个vue.js,来看看vue的目录结构,都是干什么的,从而思考一下,我们在搭建项目中能不能吸取些什么经验:
Vue源码目录分工明确。Vue源码目录大致分为:
benchmarks
:测试性能,对于一些复杂情况,处理大量数据时测试Demo;dist
:各环境所需的版本包、构建不同的vue版本,比如vue.runtime.js开发仅运行版本不编译、vue.esm.js:es模块完整版本、vue.min.js开发版本等examples
:用Vue实现的一些实用Demo;如:select2下拉框demo、svg分布图demo、todoMVC、- grid列表筛选、modal等
flow
: 数据类型检测配置,强制器严格按照此什么的类型来执行packages
: 特定环境运行需要单独安装的插件,npm包,可以通过单独的npm install进行安装相应的版本,如vue-server-renderer服务器渲染、vue-template-compiler模板编译、weex-template-compiler weex多端模板编译等src
: 整个源码的核心。script
: npm脚本配置文件;npm脚本配置文件,可以结合webpack等工具进行编译、测试、构建;test
: 测试用例;types
: 新版typescript配置、主要是一些typescript类型申明文件;
src目录说明:
可以看到src为核心代码目录,我们如果想看vue的本文的代码时根据什么来实现的可以去这里查看:src中具体的文件夹目录说明如下:
- Complier:解析模板相关
- core:核心代码
- platfroms:跨平台兼容
- server:服务端ssr渲染
- sfc:转换单文件组件 .vue文件解析
- shared:全局方法常量、共享代码
核心代码原理详解:
Vue的入口路径是vue\src\platefroms\web\entry-runtime.js ,构建生成文件路径是vue\dist\vue.runtime.esm.js
主入口:src\core\index.js
(图0-1)
我们可以看到主入口主要做了三件事:
- 1、引用./instance/index中暴露的Vue构造器
- 2、调用initGlobalAPI方法,定义全局资源
- 3、暴露Vue
initGlobalAPI:
对Vue进行各种方法和属性的定义,这些api我们在写源码或者自定义一些功能时,都会遇到
src\core\global-api\index.js
- Vue.config:各种全家配置项
- Vue.util:各种工具函数
- Vue.set
- Vue.nextTick
- Vue.options 这个与new Vue(options)中的options不一样,是默认的提高资源比如可以组件、指令、过滤器;
- Vue.use 通过initUse方法定义
- Vue.mixin 通过initMixin方法定义
- Vue.extend通过initExtend方法定义
Vue核心方法
在文件目录src\core\instance\index.js中
(图0-2)
在我们写vue代码时,往往会new Vue来操作,而new Vue(options)都干什么了呢?我们通过上边的代码也可以看到,创建一个Vue的构造函数,开始检测当前的环境是不是开发环境 ,如果Vue不是通过new实例化的将警告。然后初始化this._init(options)
。
还有几个方法分别是:
1、initMixin(Vue):初始化的入口,各种初始化的工作
2、stateMixin(Vue):数据绑定的核心方法,主要功能:
- 初始化state,包括 props、methods、data、computed、watch;
- 对于prop和data属性,将其设置为vm的响应式属性,即使用Object.defineProperty来绑定vm的prop和data属性并设置其getter和setter
- 对于methods,则将每个method都挂载到vm上并将this指向vm
- 对于computed,在将其设置为vm的响应式属性之外,还需要定义watcher,用于收集依赖
- watch属性,也是将其设置为watcher实例,收集依赖
3、eventsMixin(Vue):事件的核心方法,off,$emit方法
4、lifecycleMixin(Vue):生命周期的核心方法
5、renderMixin():渲染的核心方法,用来生成人的人函数以及VNode
其他的函数有的方法如下图罗列:
(图0-3)
对于Vue这个以mixin的函数,是在原型上添加的方法,Vue.prototype. = function(){}; vue没有把所有的方法都写在函数内部,这样从代码上来说,每次实例化的时候不会生成重复的代码,也使得代码结构更清晰,利用mixin的概念,把每个模块都抽离开,这样代码在结构和扩展性都有很大提高。
ps:我们可以看到"_"为开头的方法,多半时Vue内部使用的,可以成为私有属性,属于不公开的api;以"$"为开头的方法,时文档中公开给用户使用的默认api。
准备工作
1、初始化项目
创建一个空文件lee-vue-simple, 在初始化项目:npm init -y
,如果你需要上传项目到git最好创建一个忽略文件来把忽略一些不必要的上传文件.gitignore
2、安装依赖包
在创建的空文件夹lee-vue-simple项目下,安装如果:
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
3、配置
3.1 配置package.json中scripts部分
"scripts": {
"build": "webpack --mode=development",
"dev": "webpack-dev-server --mode=development"
},
3.2 配置webpack.config.js
这也不具体讲解webpage的用法,不懂可以查看官网文档: webpack.docschina.org/guides/gett…
在根目录新建一个文件webpack.config.js,配置如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry:'./src/index.js',
output:{
filename:'main.js',
path: path.resolve(__dirname, 'dist')
},
devtool:'source-map', //便于追查源文件得错误位置
resolve:{ //更改解析模块得查找方法
modules:[path.resolve(__dirname,'source'),path.resolve('node_modules')]
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'public/index.html')
})
]
}
4、初步创建文件
按照webpage.config.js配置,创建入口文件src/index.js
创建public\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">
<title>实现vue源码</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
创建source\vue文件夹--》这个文件夹内得内容将是我们主要实现vue源码得文件
5、启动项目
我们现在启动一下项目看看能否正常跑通:
npm run dev
在工具中输入上述命令,没有报错,在打开提示启动得http://localhost:8080/
(图1)
测试一下:在src\index.js中输入
console.log('测试哈哈哈~');
(图2)
到此准备工作结束,下边开始正式来写代码吧~~~
正式
大致流程:new Vue(options) -> init -> $mount -> compile -> render -> vnode -> patch -> DOM
1、vue得基本应用
1.1、一个简单的案例
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素app
data(){
return{
msg:'hello'
}
},
computed: {
},
watch: {
}
});
console.log(vm);
<div id="app" >
{{msg}}
</div>
简单解释:
第一行:import Vue from "vue";
引入vue
,准备得时候我们webpack.config.js中通过resolve配置更改了会先去source中引入在去node_modules中查找;
第二行:我们发现Vue是可以new得说明vue是一个类或者构造函数;实例vm
new Vue(options)中传入一个对象参数(options),即传入Vue中的配置选项,里边有一些钩子函数还有el表示要渲染得元素
2、实现MVVM
思想是: Vue采用数据劫持Observe配合发布者-订阅者模式,通过Object.defineProperty来()来劫持各个属性的get和set,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
Vue的数据响应式包含三个部分:Observer
、Dep
、Watcher
。
声明:此小节实现的更新视图,不包括虚拟dom,render方法,初步采用vue1.0的更新方法;虚拟dom会在后边的小节中在实现;在重写更新视图方法;
2.1、vue入口、初始化
第一步:创建导出文件
source\vue\index.js
第二步:导出Vue
source\vue\index.js
function Vue(options){
}
export default Vue
第三步:初始化vue
源码查看vue\src\core\i nstance\index.js
function Vue(options){
this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
// vue中初始化 this.$options 表示得是vue中得参数
let vm = this;
vm.$options = options;
//MVVM原理 响应式数据 需要数据重新初始化
// 我们将不同得初始化放入到不同得文件夹中操作,方便维护
initState(vm);
}
export default Vue
(图7)
ps: initState是本节的核心代码:主要实现的功能是:
- 初始化state,包括 props、methods、data、computed、watch;
- 对于prop和data属性,将其设置为vm的响应式属性,即使用Object.defineProperty来绑定vm的prop和data属性并设置其getter和setter
- 对于methods,则将每个method都挂载到vm上并将this指向vm
- 对于computed,在将其设置为vm的响应式属性之外,还需要定义watcher,用于收集依赖
- watch属性,也是将其设置为watcher实例,收集依赖
2.2、Observe
Observe
类主要给响应式对象的属性和数组的一些方法添加get/set
用于依赖收集与派发更新
2.2.1、对象劫持
第一步:data数据初始化
源码:vue\src\core\instance\state.js
1、创建文件source\vue\observe\index
export function initState(vm){
let options = vm.$options;
if (options.data) {
initData(vm);//初始化数据
}
}
// 观察
export function observe(data){
if(typeof data !== 'object' || data == null){
return;
}
return new Observe(data);
}
// 初始化数据 将数据重新定义 核心:Object.defineProperty
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
observe(vm._data);
}
observe()返回Observe类的实例,我们可以看到如果要观察得new Vue时传入得参数options中得data钩子返回得数据中如果不是个object或者是null、undefined我们就不需要在观察了,直接返回,如果是对象我们需要重新定义即需要劫持;
initData函数中,会判断data
是不是函数, 是则取返回值不是则取自身。
2、实现响应式数据变化
source\vue\observe\observe.js
创建Observe类来劫持监听所有属性
// 定义响应式数据变化
export function defineReactive(data,key,val){
Object.defineProperty(data,key,{
get(){
return val;
},
set(newVal){
if (val === newVal) return;
val = newVal;
}
});
}
class Observe{
constructor(data) { // 此时得data就是定义得vm._data
//将用户得数据使用Object.defineProperty重新定义
this.walk(data);
}
walk(data){
for (let [key, val] of Object.entries(data)) {
defineRective(data, key, val);
}
}
}
export default Observe;
3、data中得数据值可能还是个对象,需要递归处理
import {observe} from './index';
// 定义响应式数据变化
export function defineReactive(data,key,val){
// 递归 判断val是不是对象{name:'lee',age:18}
observe(val);
Object.defineProperty(data,key,{
get(){
return val;
},
set(newVal){
if (val === newVal) return; // 如果设置的是一个对象的话,需要继续观察
observe(newVal);
val = newVal;
}
});
}
class Observe{
constructor(data) { // 此时得data就是定义得vm._data
//将用户得数据使用Object.defineProperty重新定义
this.walk(data);
}
walk(data){
for (let [key, val] of Object.entries(data)) {
defineRective(data, key, val);
}
}
}
export default Observe;
从上述代码中我们可以看到data中得数据实现响应式是使用了Object.defineProperty
核心所以是不支持ie8及以下浏览器得。
第二步:将没有导入得文件导入:import
source\vue\index中import {initState} from './observe'
source\vue\oberve\index.js中import {Observe} from './observe'
第三步:简单测试
src\index.js
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素是app
data(){
return {
msg:'hello',
obj:{name:'lee',age:18},
arr:[1,2,3]
}
}
});
console.log(vm);
运行npm run dev
打开浏览器http:localhost:8080/
(图3)
如图3我们可以看到重新定义得data在vm._data上边,有set、get方法,这样我们就可以获取和设置了。
比如重新设置msg得值:vm._data.msg = '更改值';
(图4)
2.2.2 实现代理(vm._data上得代码到vm)
从上变知道我们获取数据还是设置数据需要通过vm._data,这跟源码我们知道得vue不符合,也多了一层,我们希望直接可以用vm.msg = '更改值';
source\vue\index.js
import {Observe} from './observe'
export function initState(vm){
let options = vm.$options;
if (options.data) {
initData(vm);//初始化数据
}
if (options.computed) {
initComputed(); //初始化计算属性
}
if (options.watch) {
initWatch(); //初始化watch
}
}
// 观察
export function observe(data){
if(typeof data !== 'object' || data == null){
return;
}
return new Observe(data);
}
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newVal){
vm[source][key] = newVal;
}
})
}
// 初始化数据 将数据重新定义 核心:Object.defineProperty
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 将在vm._data上得操作代理到vm上
for(let key in data){
proxy(vm,'_data',key);
}
observe(vm._data);
}
2.2.3 数组劫持
如果想找源码可以去vue\src\core\instance\observer\array.js
我们打印console.log(vm.arr.push(12))
只有获取数据,并没有走设置数据,上述代码我们还需要增加数组的监控,且需要对数组的一些方法重写
1、什么样得数组会被监控observe
我们查看源码:vue\src\core\observer\array.js
为什么是这几个方法呢?数组的方法有很多呀?这些方法会改变原数组。这些使用了切片编程的思想,将原理的方法重写,还继续增加我们自己的一些逻辑。我们继续往下看源码:
我们需要对新增的数据继续观察,所以增加了上图的代码。因为增加的可能是对象等
2、实现
创建我们自己的array.js目录:source\vue\observe\array.js
import { observe } from "./index";
/** 拦截用户调用得push、shift、unshift、pop reverse sort splice
* 因为这些方法会改变原来得数组 这些需要重写
*/
// 先获取老得数组得方法 目前只改写这7个方法
let oldArrProtoMethods = Array.prototype;
// 拷贝一个新得对象 可以查找到老得方法
export let arrayMethods = Object.create(oldArrProtoMethods);
let methods = [
'push',
'shift',
'unshift',
'pop',
'reverse',
'sort',
'splice'
];
// 需要循环对新增的每一项数据进行观察
export function observeArray(inserted) {
for(let i=0;i<inserted.length;i++){
observe(inserted[i]);
}
}
methods.forEach(m=>{
// 函数劫持 切片编程
arrayMethods[m] = function(...arg){
const r = oldArrProtoMethods[m].apply(this,arg);
// 我们需要对新增的数据进行监控
let inserted;
switch (m) {
case 'push':
case 'unshift':
inserted = arg;
break;
case 'splice':
inserted = args.slice(2)
break;
}
if(inserted) observeArray(inserted);
return r;
}
})
增加数组监控
source\vue\observe\observe.js
import {observe} from './index';
import {observeArray, arrayMethods} from './array';
// 定义响应式数据变化
export function defineReactive(data,key,val){
// 递归 判断val是不是对象{name:'lee',age:18}
observe(val);
Object.defineProperty(data,key,{
get(){
return val;
},
set(newVal){
if (val === newVal) return;
val = newVal;
}
});
}
class Observe{
constructor(data) { // 此时得data就是定义得vm._data
//将用户得数据使用Object.defineProperty重新定义
if(Array.isArray(data)){
// 我们需要通过原型链 调用我们重写的相关的数组的方法
data.__proto__ = arrayMethods;
// 监控数组中的每一项 因为有可能数组中里边也是对象
observeArray(data);
}else{
this.walk(data);
}
}
walk(data){
for (let [key, val] of Object.entries(data)) {
defineReactive(data, key, val);
}
}
}
export default Observe
ps:需要注意:
- 无法监听数据的
length
,导致arr.length
这样的数据改变无法被监听 - 通过角标更改数据,即类似
arr[2] = 1
这样的赋值操作,也无法被监听
同时 Vue.js 提供了两个额外的“方法糖” $set
和 $remove
来弥补这方面限制带来的不便。整体上看这是个取舍有度的设计。
2.3 初步渲染到DOM
流程:
new Vue
-》initMixin(Vue)
-》_init(options)
-》$mount()
->编译,转换成render
-》mountComponent()
-》updateComponent()
-》渲染Wather
执行完所有的初始化操作后,我们需要开始挂载vue实例到dom上了。
ps:注意:
vue 是分为运行时可编译和只运行的版本,所以如果需要编译,在Vue原型上添加了$mount方法。Vue不同构建版本可以看Vue对不同构建版本的解释。
2.3.1、$mount挂载
可查看源码的目录:
vue\src\platforms\web\entry-runtime-with-compiler.js(运行时+编译器)
vue\src\platforms\web\runtime/index.js(运行时版本)
(图8)
$mount
方法就是整个渲染过程的起始点。具体定义是在vue\src\entries\web-runtime-with-compiler.js
中
如图8,在渲染过程中,我们可以通过自定义render函数、template、el三种方式渲染页面。
案例:
el的写法:
var vm = new Vue({
el: '#app',
data: {
message: 'Hello!'
}
})
template的写法:
var vm = new Vue({
data: {
message: 'Hello!'
},
template:'<div>{{message}}</div>'
})
render函数的写法:
import App from './App.vue';
import router from './router';
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
这三种方式都是要得到Render函数.
代码:
ps:先初步实现渲染el的方式,考虑从简到易,先不去判断render方法,因为之后的虚拟dom阶段会在重写;这边直接用渲染new watcher函数里边,第二个参数传入updateComponent更新函数(其中方法的vm._update()暂时用的vue1.0方法),会直接先执行。
source\vue\index.js
import {initState} from './observe'
import Watcher from './observe/watcher';
import { complier } from './compiler';
function Vue(options){
this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
// vue中初始化 $options 表示得是vue中得参数
// this指向Vue的实例,所以这里是将Vue的实例缓存给vm变量
let vm = this;
vm.$options = options;
//MVVM原理 响应式数据 需要数据重新初始化
// 我们将不同得初始化放入到不同得文件夹中操作,方便维护
// 初始化state,包括props\methods\data\compunted\watch
// props\data属性将其设置为vm的响应式属性即需要Object.defineProperty
// 绑定vm的props和打他属性并设置其getter和setter
initState(vm);
// 将实例挂载到dom上
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
//获取dom
function query(el){
if(typeof el === 'string'){
return document.querySelector(el);
}
return el;
}
const mount = Vue.prototype.$mount;
// 渲染页面将组件进行挂载
Vue.prototype.$mount = function(el){
let vm = this;
// 获取dom
el = vm.$el = el && query(el);
// 渲染时通过 watcher来渲染
/**
* 渲染watcher
* vue2.0 组件级别更新 new Vue产生一个组件
*
*/
// 更新组件、渲染逻辑
let updateComponent = ()=>{
// 更新组件
vm._update();
}
// 渲染watcher
new Watcher(vm,updateComponent);
}
// 目前用vue1.0 没有使用组件
// 用户传入的数据 去更新视图
Vue.prototype._update = function(){
let vm = this;
let el = vm.$el;
// -------------现在vue1.0之后会用vdom重写----
// 要循环这个元素 将里边的内容 换成我们的数据
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
node.appendChild(firstChild); //appendChild 具有移动的功能
}
//编译 一些指令{{}} v-html v-model
complier(node,vm);
el.appendChild(node);
// 需要匹配{{}}的方式来进行替换
// 我需要让每个数据 它更改了 重新渲染
}
export default Vue
2.3.2、初步渲染watcher
ps:2.4会在继续讲解watcher
import { isFun } from "../utils";
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
/**
*Creates an instance of Watcher.
* @param {*} vm 当前组件的实例 new VUe
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} ops 一些其他参数
* @memberof Watcher
*/
constructor(vm,exprOrFn,cb=()=>{},ops={}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(isFun(exprOrFn)){
//getter 就是new Watcher传入的第二个函数
this.getter = exprOrFn;
}
this.cb = cb;
this.ops = ops;
this.uid = uid++;
// 默认创建一个watcher 会调用自身的get方法
this.get();
}
get(){
// 让这个当前传入的函数执行 默认就会执行
this.getter();
}
}
// 渲染使用 计算属性 也用vm.watch
export default Watcher;
ps:
发现vue中会有大量的这种设置一个全局变量let uid = 0;在类的构造器中在this.uid = uid++;这种写法,表示每次new Watcher都会有一个每一标识;
watcher方法在渲染时、 计算属性、 vm.watch 都会被调用
2.3.3、complier
第一步:
source\vue\complier\complier.js
import {isElement, isText} from '../utils'
import { util} from './diretive'
export function complier(node,vm){
let childNodes = node.childNodes;
[...childNodes].forEach(c=>{
if(isElement(c)){ // 元素节点
complier(c,vm);
}else if(isText(c)){ //文本节点
return util.compilerText(c,vm);
}
})
}
第二步:指令 {{}}编译
source\vue\complier\diretive\diretives.js
// 编译指令 比如{{}} v-mode v-text v-html
// 匹配 {{}}
const defaultReg = /\{\{((?:.|\r?\n)+?)\}\}/g;
export const util = {
getValue(vm, expr) {
let keys = expr.split('.');
return keys.reduce((m, cur) => {
m = m[cur];
return m;
}, vm)
},
// 编译文本 {{msg}} {{obj.name}}
compilerText(node, vm) {
if(!node.expr){
// 给节点增加一个属性 为了后续更新操作
node.expr = node.textContent;
}
node.textContent = node.expr.replace(defaultReg, function (...args) {
return util.getValue(vm, args[1]);
})
}
}
第三步:导出
source\vue\complier\diretive\index.js 表示diretive文件夹的出口
export * from './diretives'
source\vue\complier\index.js 表示complier文件夹的出口
export * from './compiler';
2.3.4、测试:
在public\index.html中加入
<div id="app">
{{msg}}
</div>
打开页面发现如下图,表示成功渲染
(图9)
ps:Vue.prototype._update方法:先从vue1.0版本实现,后边讲解虚拟dom时,重新实现渲染代码。
2.4、dep
实现数据收集(当前响应式对象的依赖关系),记录哪些watcher依赖自己的变化,使用了发布订阅设计模式;
源码所在目录:vue\src\core\instance\dep.js
1、对象收集
实现步骤:
- 1、 全局定义一个uid,在Dep类中
this.uid = uid++;
,表示每次创建一个Dep实例时都有不同的uid,为了方便去重; - 2、在Dep类中创建数组subs=[],用来存放Watcher的实例;
- 3、创建addSub方法,添加Watcher的实例;
- 4、创建notify方法,通知所有的Wather执行更新。遍历subs数组,调用每个Wather的更新(update)方法
- 5、创建depend方法,调用存在Dep.target上的addDep方法,而Dep.target时一个watcher实例,所以addDep方法是Watcher类的方法,作用是
- 6、创建pushTarget方法,是为了将watcher观察者实例设置给Dep.target,用以依赖收集,同时将该实例存入stack栈中;
- 7、创建popTarget方法,作用:将观察者实例从stack栈中取出并设置给Dep.target
代码
source\vue\observe\dep.js
import {remove} from '../utils'
let uid = 0;
// 收集数据 收集的时一个个watcher
class Dep{
constructor() {
// Dep实例的uid,为了方便去重
this.uid = uid++;
// 存储收集器中需要通知的Watcher
this.subs = [];
}
// 添加订阅 搜集watcher Watcher 的实例,将来用来通知更新
addSub(watcher){
this.subs.push(watcher);
}
removeSub(sub) {
remove(this.subs, sub)
}
// 通知watcher执行update方法更新
notify(){
this.subs.forEach(watcher=>watcher.update());
}
depend(){
// 防止直接调用depend方法
if(Dep.target){
// Dep.target 是一个渲染的watcher
// 希望与watcher互相记忆
Dep.target.addDep(this);
}
}
}
let stack = [];//watcher 栈
// 用来保存当前的watcher
/**
* 将watcher观察者实例设置给Dep.target, 用以依赖收集。
* 同时将该实例存入stack栈中
* * @param {*} watcher
*/
export function pushTarget(watcher){
Dep.target = watcher;
stack.push(watcher);
}
/**
将观察者实例从stack栈中取出并设置给Dep.target
*/
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length-1];
}
export default Dep;
2、数组的依赖收集
步骤:
1、在Observe类的中创建一个Dep实例,这个实例dep专门用来收集数组的依赖关系;
2、我们对数据data每一项增加一个__ob__
属性,返回值为当前Observe的实例
3、在defineReactive方法中,我们需要先观察每一个data的属性值,这个观察返回的就是一个Observe的实例,childOb;
4、我们在defineProperty的get中增加数组的收集方法,如果childOb存在,就增加收集方法,因为dep绑在了Observe的实例上;注意要对数组中的每一个属性收集,因为有可能是arr = [[1],2,3],这种二维数组
5、在调用数组方法的时候,通知watcher更新方法
6、在观察者中增加判断,如果data.__ob__
存在就直接返回data.__ob__
这里边已经保存了Observe实例,
代码:
source\vue\observe\observe.js
import {observe} from './index';
import {observeArray, arrayMethods,dependArray} from './array';
import Dep from './dep';
// 定义响应式数据变化
export function defineReactive(data,key,val){
// 递归 判断val是不是对象{name:'lee',age:18} [1,2]
let childOb = observe(val);
// 收集依赖 收集的是watcher
let dep = new Dep();
Object.defineProperty(data,key,{
get(){ // 只要对这个属性 精选取值操作,就会将当前的watcher存入
// 如果有值 就是渲染的watcher
if(Dep.target){
/** 我们希望存入的watcher 不能重复,如果重复会造成更新多次
*dep.depend() 让中可以存watcher watcher中存dep
* dep 实现多对多的关系
*/
dep.depend();
// childOb 针对数组的收集
if(childOb){
// 收集数组的依赖
childOb.dep.depend();
// [[1,2],3,4] 需要对数组中的每一项收集,比如二维数组
dependArray(val);
}
}
return val;
},
set(newVal){
if (val === newVal) return;
// 如果设置的是一个对象的话,需要继续观察
observe(newVal);
val = newVal;
// 通知订阅器找到对应的观察者,通知观察者更新视图
dep.notify();
}
});
}
export class Observe{
constructor(data) { // 此时得data就是定义得vm._data
// this.dep 专门来收集数组的
this.dep = new Dep();
// 给data添加一个属性__ob__,每个数组和对象都有__obj__属性,返回的是当前Observe实例
Object.defineProperty(data,'__ob__',{
get:()=>this
});
//将用户得数据使用Object.defineProperty重新定义
if(Array.isArray(data)){
// 我们需要通过原型链 调用我们重写的相关的数组的方法
data.__proto__ = arrayMethods;
// 监控数组中的每一项 因为有可能数组中里边也是对象
observeArray(data);
}else{
this.walk(data);
}
}
walk(data){
for (let [key, val] of Object.entries(data)) {
// 对每一个属性 重新用defineProperty定义
defineReactive(data, key, val);
}
}
}
export default Observe;
source\vue\observe\array.js
import { observe } from "./index";
import { isArray } from "../utils";
// 先获取老得数组得方法 目前只改写这7个方法
let oldArrProtoMethods = Array.prototype;
// 拷贝一个新得对象 可以查找到老得方法
export let arrayMethods = Object.create(oldArrProtoMethods);
let methods = [
'push',
'shift',
'unshift',
'pop',
'reverse',
'sort',
'splice'
];
// 需要循环对新增的每一项数据进行观察
export function observeArray(inserted) {
for(let i=0;i<inserted.length;i++){
observe(inserted[i]);
}
}
// 递归收集数组中的每一项数据
export function dependArray(val){
for(let i=0;i<val.length;i++){
let cur = val[i];
cur.__ob__ && cur.__ob__.dep.depend();
if(isArray(cur)){
dependArray(cur);
}
}
}
methods.forEach(m=>{
// 函数劫持 切片编程
arrayMethods[m] = function(...arg){
const r = oldArrProtoMethods[m].apply(this,arg);
// 我们需要对新增的数据进行监控
let inserted;
switch (m) {
case 'push':
case 'unshift':
inserted = arg;
break;
case 'splice':
inserted = args.slice(2)
break;
}
if(inserted) observeArray(inserted);
if(this.__ob__) this.__ob__.dep.notify();
return r;
}
})
source\vue\observe\index.js中observe方法
export function observe(data){
if(typeof data !== 'object' || data == null){
return;
}
// 已经被监控过了
if(data.__ob__){
return data.__ob__;
}
return new Observe(data);
}
思考问题:(优秀代码分析)
为什么会有pushTarget
方法?
pushTarget:主要是为了watcher观察者实例设置给Dep.target, 用以依赖收集。同时将该实例存入stack栈中;而Dep.target在每次我们使用observe观察每个属性时,需要收集watcher,这时我们就用Dep.target来判断如果存在就表示这个属性所对的watcher已经被收集过了,不要在重复收集,也就避免了我们在更新的时候多次调用;
2.5、watcher
watcher:监听变化,是一个render函数的观察者,可用于更新视图;实例分为渲染watcher、计算属性 watcher、vm.$watch()调用 watcher ;
源码所在目录:vue\src\core\instance\watcher.js
什么时候会实例化Watcher呢?
- 1、渲染watcher,Vue生成了render函数,更新视图时
- 2、计算属性computed时
- 3、使用vm.$watch()方法时
- 4、watch选项
渲染watcher步骤:
- 1、全局声明uid,初始为0,每次实例一个Watcher时,uid++;是唯一标识
- 2、创建Watcher类,参数目前为:
- 第一个参数:当前实例;第二个参数:传入的需要更新的表达式或者函数;第三个参数:用户传入的回调函数,比如vm.$watch('msg',cb)中的cb;第四个参数:一写其他的参数;
- 3、判断第二个参数是函数时,getter函数执行;
- 4、创建deps=[],收集dep,
- 5、depsUid是一个set集合用于收集不能重复的dep的uid;
- 6、创建update方法,调用get方法;调用时机是:在Observe观察者观察到数据更新了或者说是重新设置值了调用Dep的notify来通知Watcher的update方法;
- 7、创建get方法;作用是执行调用Watcher的第二个参数是函数的方法,(即更新组件的_update方法);这个函数执行前我们需要将这个需要更新的Watcher搜集到Dep的栈stack中;函数执行;函数执行后,在将这个watcher从这个Dep的stack栈中去除;
- 8、创建addDep,调用时机:在Observe观察数据读取数据时,我们将这个watcher添加收集到Dep中,为了不重复收集我们需要将watcher和dep相互绑定;
渲染watcher代码:
source\vue\observe\watcher.js
import { isFun } from "../utils";
import {pushTarget,popTarget} from './dep'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
/**
*Creates an instance of Watcher.
* @param {*} vm 当前组件的实例 new VUe
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} ops 一些其他参数
* @memberof Watcher
*/
constructor(vm,exprOrFn,cb=()=>{},ops={}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(isFun(exprOrFn)){
//getter 就是new Watcher传入的第二个函数
this.getter = exprOrFn;
}
this.cb = cb;
this.ops = ops;
this.uid = uid++;
this.deps = [];
this.depsUid = new Set();
// 默认创建一个watcher 会调用自身的get方法
this.get();
}
get(){
//
/**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
* Dep.target = watcher;
*/
pushTarget(this);
// 让这个当前传入的函数执行 默认就会执行
this.getter();
popTarget();
}
update(){
this.get();
}
addDep(dep){ //同一个watcher 不应该重复记录dep
let uid = dep.uid; //msg的dep
if(!this.depsUid.has(uid)){
this.depsUid.add(uid);
// 让watcher记住当前的dep
this.deps.push(dep);
dep.addSub(this);
}
}
}
// 渲染使用 计算属性 也用vm.watch
export default Watcher;
2.6、为每个数据属性添加收集依赖
步骤:
- 1、创建Dep实例;
- 2、在每次读取数据时,收集依赖关系
- 3、在每次设置数据时,调用dep.notify通知watcher的update执行
代码:
source\vue\observe\observe.js
import {observe} from './index';
import {observeArray, arrayMethods} from './array';
import Dep from './dep';
// 定义响应式数据变化
export function defineReactive(data,key,val){
// 递归 判断val是不是对象{name:'lee',age:18}
observe(val);
// 收集依赖 收集的是watcher
let dep = new Dep();
Object.defineProperty(data,key,{
get(){ // 只要对这个属性 精选取值操作,就会将当前的watcher存入
// 如果有值 就是渲染的watcher
if(Dep.target){
/** 我们希望存入的watcher 不能重复,如果重复会造成更新多次
*dep.depend() 让中可以存watcher watcher中存dep
* dep 实现多对多的关系
*/
dep.depend();
}
return val;
},
set(newVal){
if (val === newVal) return;
// 如果设置的是一个对象的话,需要继续观察
observe(newVal);
val = newVal;
// 通知订阅器找到对应的观察者,通知观察者更新视图
dep.notify();
}
});
}
export class Observe{
constructor(data) { // 此时得data就是定义得vm._data
//将用户得数据使用Object.defineProperty重新定义
if(Array.isArray(data)){
// 我们需要通过原型链 调用我们重写的相关的数组的方法
data.__proto__ = arrayMethods;
// 监控数组中的每一项 因为有可能数组中里边也是对象
observeArray(data);
}else{
this.walk(data);
}
}
walk(data){
for (let [key, val] of Object.entries(data)) {
// 对每一个属性 重新用defineProperty定义
defineReactive(data, key, val);
}
}
}
export default Observe;
代码思考:
为什么我们在defineReactive调用Object.defineProperty中的get时,收集watcher用了dep.depend()
没有使用dep.addSubs(Dep.target)
?优化了什么问题?
我们在页面中会有多次调用同一个数据的情况比如
<div id="app">{{msg}}{{msg}}{{msg}}</div>
上述的html中我们调用了三次的数据msg,那么会调用Object.defineProperty中的get三次,如果使用dep.addSubs(Dep.target)
将会增加重复的watcher,Dep.target就是一个watcher的实例;dep.depend()
方法与watcher互相记忆,它会调用watcher的addDep方法,这个方法中有收集dep的唯一标识uid,如果uid已经存在就不会在添加,所以不会重复收集watcher,在notify通知watcher的update更新时就会重复多次执行相同的watcher方法;
3、异步批量更新
vue的特点就是批量更新 防止重复渲染
问题:
如果我们使用异步的方法,多次设置同一个值,根据目前已经写好的代码执行,我们的set方法会被多次执行那会多次通知notify订阅者watcher去调用update方法;update方法调用watcher中的get方法,会导致页面多次刷新;
测试:
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素是app
data(){
return {
msg:'hello',
obj:{name:'lee',age:18},
arr:[1,2,3]
}
},
computed: {
},
watch: {
}
});
setTimeout(() => {
vm.msg = '第一次更新msg';
vm.msg = '第二次更新msg';
vm.msg = '第三次更新msg';
vm.msg = '第四次更新msg';
vm.msg = '第五次更新msg';
}, 2000);
解决问题的步骤:
1、创建has={}存放watcher每个实例的uid,queue存放每个watcher实例
2、创建queueWatcher方法,我们在通知更新watcher的update方法时,先判断当前的watcher的uid是不是在缓存uid中有、没有在缓存,在调用一个异步方法,异步方法的作用是这时的这个watcher是最后一次设置的值;
3、nextTick方法,实现异步方法,我们这里想尽快的执行异步方法,根据事件环原理我们异步方法也有先后顺序,先微任务、在宏任务,在考虑性能问题;
4、flushQueue方法:在执行当前watcher的get方法,我们这里新建了一个方法用run方法调用get方法,
代码:
source\vue\observe\watcher.js
import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
constructor(vm,exprOrFn,cb=()=>{},ops={}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(isFun(exprOrFn)){
//getter 就是new Watcher传入的第二个函数
this.getter = exprOrFn;
}
this.cb = cb;
this.ops = ops;
this.uid = uid++;
this.deps = [];
this.depsUid = new Set();
// 默认创建一个watcher 会调用自身的get方法
this.get();
}
get(){
//
/**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
* Dep.target = watcher;
*/
pushTarget(this);
// 让这个当前传入的函数执行 默认就会执行
this.getter();
popTarget();
}
update(){
queueWatcher(this);
}
run(){
this.get();
}
addDep(dep){ //同一个watcher 不应该重复记录dep
let uid = dep.uid; //msg的dep
if(!this.depsUid.has(uid)){
this.depsUid.add(uid);
// 让watcher记住当前的dep
this.deps.push(dep);
dep.addSub(this);
}
}
}
let has = {};
let queue = [];
//
function flushQueue(){
// 等待当前这一轮全部更新后 在去让watcher一次执行
queue.forEach(watcher => watcher.run());
has = {};
queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
let uid = watcher.uid;
if(isUndef(has[uid])){
has[uid] = uid;
// 相同的watcher只会存一个
queue.push(watcher);
// 延迟清空队列
nextTick(flushQueue);
}
}
// 渲染使用 计算属性 也用vm.watch
export default Watcher;
source\vue\utils\nextTick.js
let callbacks = [];
function flushCallbacks(){
callbacks.forEach(cb=>cb());
}
/**
*异步刷新这个callbacks,获取一个异步的方法
异步是分支执行顺序的
会先执行微任务(promise\mutationObserver\)
在执行宏任务(setImmediate\setTimeout)
* @export
* @param {*} cb
*/
export function nextTick(cb){
callbacks.push(cb);
let timerFnCb = ()=>{
flushCallbacks();
}
if(Promise) return Promise.resolve().then(timerFnCb);
if(MutationObserver){
let observe = new MutationObserver(timerFnCb);
let textNode = document.createTextNode(1);
observe.observe(textNode,{characterData:true});
textNode.textContent(2);
return;
}
if(setImmediate){
return setImmediate(timerFnCb);
}
setTimeout(timerFnCb, 0);
}
我们再次时候用测试代码查看只执行了一次更新;
事件环:不了解的话可以查看文档: juejin.cn/post/684490…
MutationObserver: developer.mozilla.org/zh-CN/docs/…
4、watch实现
之前我们在小节2.5中实现watcher说过有三种方式会实例化Watcher,而这说的用户自己调用watch方法就是一种。
应用案例
src\index.js
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素是app
data(){
return {
msg:'hello',
obj:{name:'lee',age:18},
arr:[[1],2,3]
}
},
watch: {
msg(newVal,oldVal){
console.log(newVal,oldVal);
}
}
});
vm.msg = "msg改变喽"
步骤
1、初始化watch
2、给watch中每一个属性的方法去调用Vue实例上的$watch()方法;
3、在Vue原型上实现$watch方法,原理:创建一个watcher,注意传入参数{user:true}
标识是用户自己写的一个watcher,跟其他的实例化的watcher区分
4、在watcher方法中实现watch属性的方法handler,重点区分新老值,
代码:
source\vue\observe\index.js
function createWatcher(vm,key,handler){
return vm.$watch(key,handler);
}
function initWatch(vm) {
// 用户传入的watch属性
let watch = vm.$options.watch;
for (let [key, val] of Object.entries(watch)) {
createWatcher(vm, key, val);
}
}
source\vue\index.js增加下面方法$watch
Vue.prototype.$watch = function(expr,handler){
let vm = this;
// 原理 创建一个watcher {user:true}标识是用户自己写的一个watcher
new Watcher(vm,expr,handler,{user:true});
}
source\vue\observe\watcher.js
import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
import {util} from '../compiler/diretive'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
constructor(vm,exprOrFn,cb=()=>{},ops={}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(isFun(exprOrFn)){
//getter 就是new Watcher传入的第二个函数
this.getter = exprOrFn;
}else{
this.getter = function(){
return util.getValue(vm,exprOrFn);
}
}
// 标识是用户自己写的watcher
if (ops.user) {
this.user = true;
}
this.cb = cb;
this.ops = ops;
this.uid = uid++;
this.deps = [];
this.depsUid = new Set();
// 默认创建一个watcher 会调用自身的get方法
// 创建watcher时,先将表达式对应的值取出来
this.value = this.get();
}
get(){
//
/**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
* Dep.target = watcher;
*/
pushTarget(this);
// 让这个当前传入的函数执行 默认就会执行
let value = this.getter();
popTarget();
return value;
}
update(){
queueWatcher(this);
}
run(){
let value = this.get();
// this.value是老值,value是新值
if(this.value !== value){
this.cb(value,this.value);
}
}
addDep(dep){ //同一个watcher 不应该重复记录dep
let uid = dep.uid; //msg的dep
if(!this.depsUid.has(uid)){
this.depsUid.add(uid);
// 让watcher记住当前的dep
this.deps.push(dep);
dep.addSub(this);
}
}
}
let has = {};
let queue = [];
//
function flushQueue(){
// 等待当前这一轮全部更新后 在去让watcher一次执行
queue.forEach(watcher => watcher.run());
has = {};
queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
let uid = watcher.uid;
if(isUndef(has[uid])){
has[uid] = uid;
// 相同的watcher只会存一个
queue.push(watcher);
// 延迟清空队列
nextTick(flushQueue);
}
}
// 渲染使用 计算属性 也用vm.watch
export default Watcher;
5、计算属性
案例:
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素是app
data(){
return {
msg:'hello',
obj:{name:'lee',age:18},
arr:[[1],2,3],
firstName:'lee',
lastName:'Hello'
}
}
computed: {
fullName(){
return this.firstName+this.lastName;
}
}
});
vm.firstName = '周'
<div id="app">
{{fullName}}
</div>
步骤、流程:
1、初始化计算属性initComputed
2、核心是为每个computed属性实例化一个watcher,需注意在实例watcher的过程中,需要传入参数{lazy:true}
的配置项,表示此watcher实例是计算属性;并将这些实例化的watcher缓存到Vue的实例上vm;
3、在watcher类中,判断如果是lazy为true,表示是计算属性,暂时不执行函数(不调用get方法),即不收集依赖关系,定义dirty为当前传的lazy值,dirty为true时,我们就不收集依赖;
4、当用户在页面读取这个计算属性时,我们才调用这个计算属性的getter函数,所以我们之前初始化计算属性时,还需要给计算属性每个属性在读取时的创建一个函数:createComputedGetter
,其返回值也是一个函数;
5、当前的这个页面读取的计算属性的watcher的dirty如果为true,表示我们需要调用watcher的get方法;
注意:此时watcher上定义的deps数组中有两个watcher,一个渲染watcher、一个计算属性的watcher,Dep.target指向计算属性watcher,执行getter方法即当前计算属性的方法,移除计算属性watcher,Dep.target指向渲染watcher,dirty为false,表示计算属性已经读取;
6、但是往往计算属性的值返回的数据也是需要读取的属性,例如:
fullName(){return this.firstName+this.lastName}
;
我们还要读取this.fristName
和this.lastName
这两个;
调用这两个数据的获取属性,此时Dep.target存在,则会继续收集依赖,将这两个数据的watcher依赖放入到Watcher类的deps数组中,并添加到dep类的数组中subs,因为在data中设置了这两个数据的值,所以会调用这两个的数据的set方法通知这两个属性的收集notify方法执行就调用这两个属性的watcher的update方法执行,
7、计算属性改变了,重新设置了值,如下:
fullName(){return this.firstName+this.lastName}
vm.firstName="改变了值";
因为是设置了fristName的值,我们调用set方法,调用dep的notify方法,调用watcher的update方法,此时deps中有两个watcher,一个计算属性的watcher,value可以看到是上次的值leeHello
,一个是修改的fristName的watcher,计算属性的watcher让dirty改为true,fristName的watcher调用update,Dep.target指向渲染watcher,
此时我们的计算属性的定义的get方法上createComputedGetter
函数,还需要增加判断,如果Dep.target还存在说明watcher的deps数组中还有watcher我们需要执行(此时是firstName的渲染Watcher),所以我们需要在watcher中创建depend方法来判断deps数组中每一个dep都去调用即调用dep的depend方法,当然调用后就需要在这个deps数组去掉避免多次调用收集;
8、nextTick执行
代码:
source\vue\observe\index.js
import {Observe} from './observe'
import Watcher from './watcher';
import Dep from './dep'
export function initState(vm){
let options = vm.$options;
if (options.data) initData(vm);//初始化数据
if (options.watch) initWatch(vm); //初始化watch
if (options.computed) initComputed(vm,options.computed); //初始化计算属性
}
//.....省略
function createComputedGetter(vm,key) {
let watcher = vm._watchersComputed[key];
return function(){ //用户取值会执行此方法
if(watcher){
// 如果页面取值,dirty是true,就会去调用watcher的get方法
// dirty是false 不需要重新执行计算属性的方法
if(watcher.dirty){
// 计算
watcher.evaluate();
}
if(Dep.target){
watcher.depend();
}
return watcher.value;
}
}
}
// 计算属性 默认不执行 等用户取值时在执行,会缓存取值结果
function initComputed(vm,computed){
// 将计算属性的配置 放到vm上
let watchers = vm._watchersComputed = Object.create(null); //创建储存watcher的对象
// computed 是{fullName:()=>this.firstName+this.lastName}
for (let key in computed) {
const userDef = computed[key];
//如果computed的属性值为函数,取这个函数为watcher的第二个参数 即getter
// 如果不是function,则为这个属性定义一个
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// lazy:true 表示刚调用的watcher先不执行此方法 实例化的过程中不去完成收集依赖
watchers[key] = new Watcher(vm, getter, () => {}, {
lazy: true
});
if(!(key in vm)){
Object.defineProperty(vm, key, {
get: createComputedGetter(vm, key)
})
}
}
}
source\vue\observe\watcher.js
import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
import {util} from '../compiler/diretive'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
constructor(vm,exprOrFn,cb=()=>{},ops={}) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if(isFun(exprOrFn)){
//getter 就是new Watcher传入的第二个函数
this.getter = exprOrFn;
}else{
this.getter = function(){
return util.getValue(vm,exprOrFn);
}
}
// 标识是用户自己写的watcher
if (ops.user) {
this.user = true;
}
this.lazy = ops.lazy;//如果lazy为true 说明是计算属性
this.dirty = ops.lazy;
this.cb = cb;
this.ops = ops;
this.uid = uid++;
this.deps = [];
this.depsUid = new Set();
this.immediate = ops.immediate;
// 默认创建一个watcher 会调用自身的get方法
// 创建watcher时,先将表达式对应的值取出来
// 如果当前我们是计算属性的话 不会立刻执行 在用户调用时才调用
this.value = this.lazy ? undefined : this.get();
if(this.immediate){
this.cb(this.value);
}
}
get(){
pushTarget(this);
// 让这个当前传入的函数执行 默认就会执行
let value = this.getter.call(this.vm);
popTarget();
return value;
}
evaluate(){
this.value = this.get();
this.dirty = false;
}
depend(){
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
update(){
// 计算属性
if(this.lazy){
this.dirty = true;
}else{
queueWatcher(this);
}
}
run(){
let value = this.get();
// this.value是老值,value是新值
if(this.value !== value){
this.cb(value,this.value);
}
}
addDep(dep){ //同一个watcher 不应该重复记录dep
let uid = dep.uid; //msg的dep
if(!this.depsUid.has(uid)){
this.depsUid.add(uid);
// 让watcher记住当前的dep
this.deps.push(dep);
dep.addSub(this);
}
}
}
let has = {};
let queue = [];
//
function flushQueue(){
// 等待当前这一轮全部更新后 在去让watcher一次执行
queue.forEach(watcher => watcher.run());
has = {};
queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
let uid = watcher.uid;
if(isUndef(has[uid])){
has[uid] = uid;
// 相同的watcher只会存一个
queue.push(watcher);
// 延迟清空队列
nextTick(flushQueue);
}
}
// 渲染使用 计算属性 也用vm.watch
export default Watcher;
6、虚拟dom和dom-diff
关于虚拟dom和dom-diff算法的概念和详细实现流程可查看文章: juejin.cn/post/684490… 有vue和react版本都有,如果只想看vue的原理实现可以查看小节2.1和4.4;
在本文章的小节2,我们讲过当时用的渲染是用的vue1.0操作的dom,虽然用了优化都放入到文档碎片,编译指令,最后在放入dom中,但是更改时,还是频繁的操作dom,造成重绘性能会变差;现在的vue中使用的是虚拟dom,就是一个描述dom树的一个js对象;在渲染到dom上,更新时,我们将两个虚拟dom,diff比对,一样的我们可以复用,patch在渲染;
6.1、一个简单案例
import Vue from "vue";
let vm = new Vue({
el:'#app', //表示要渲染得元素是app
render(h){
return h('div',{id:"div",style:{'color':'red'}},'hello');
}
});
6.2、初始化将虚拟dom渲染
步骤:
1、生成虚拟dom,即render函数中的h函数;
创建h函数的参数有('标签','属性',子节点);
返回虚拟dom类;
2、创建虚拟dom类,VNode,:
- tag:表示当前的标签名
- props:表示当前标签上的属性;
- key:表示唯一的,用于dom-diff比对
- children:表示子节点
- elm:表示当前的元素
- _type: VNODE_TYPE, 表示是虚拟dom节点
- text :文本值,与children不可同时有值
3、创建一个render函数,初次时候只是渲染这个虚拟dom到页面上
代码:
1、创建虚拟dom的类
source\vue\vdom\vnode.js
// 通过 symbol 保证唯一性,用于检测是不是 vnode
const VNODE_TYPE = Symbol('virtual-node')
/**
* @param {String} tag 'div' 标签名 可以是元素的选择器 可参考jq
* @param {Object} props {'style': {background:'red'}} 对应的Vnode绑定的数据
属性集 包括attribute、 eventlistener、 props, style、hook等等
* @param {Array} children [ h(), 'text'] 子元素集
* @param {String} text当前的text 文本 itemA
* @param {Element} elm 对应的真是的dom 元素的引用
* @param { String} key 唯一 而且需要一一对应 用于不同vnode之前的比对
* 主要是用在需要循环渲染的dom元素在进行diff运算时的优化算法
* @return {Object} vnode
*/
function vnode(tag, props = {}, children, key, text, elm) {
return {
_type: VNODE_TYPE,
tag,
props,
children,
key,
text,
elm
}
}
export default vnode;
2、创建h函数
source\vue\vdom\h.js
/**
* h函数的主要工作就是把传入的参数封装为vnode
*/
import vnode from './vnode';
import {
hasValidKey,
isPrimitive, isArray
} from '../utils'
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* RESERVED_PROPS 要过滤的属性的字典对象
* 在react源码中hasValidRef和hasValidKey方法用来校验config中是否存在ref和key属性,
* 有的话就分别赋值给key和ref变量。
* 然后将config.__tagf和config.__source分别赋值给tagf和source变量, 如果不存在则为null。
* 在本代码中先忽略掉ref、 __tagf、 __source这几个值
*/
const RESERVED_PROPS = {
key: true,
__tagf: true,
__source: true
}
// 将原来的props通过for in循环重新添加到props对象中,
// 且过滤掉RESERVED_PROPS里边属性值为true的值
function getProps(props) {
let props = {};
const keys = Object.keys(props);
if (keys.length == 0) {
return props;
}
for (let propName in props) {
if (hasOwnProperty.call(props, propName) && !RESERVED_PROPS[propName]) {
props[propName] = props[propName]
}
}
return props;
}
function h(tag, config, ...children) {
let props = {},c,text,key;
c = children || [];
// 获取key
key = hasValidKey(props) ? props.key : undefined;
props = getProps(config);
// 因为children也可能是一个深层的套了好几层h函数所以需要处理扁平化
return vnode(tag, props, c.map(child => {
return isPrimitive(child) ? vnode(undefined, undefined, undefined, undefined, child) : child
}), key, text, undefined);
}
export default h;
3、渲染虚拟dom到页面
source\vue\vdom\patch.js
// 不考虑hook 除了初次渲染还需要dom-diff操作
import htmlApi from '../utils/domUtils';
import {isDef
} from './utils';
import attr from '../utils/updataAttrUtils'
export function render(vnode,container){
let el = createElement(vnode);
htmlApi.appendChild(container,el);
}
/**
* 从vdom生成真是dom *
* @param {Object} vnode 虚拟dom
* @returns 真是的dom
*/
function createElement(vnode) {
let {tag,children,text,elm} = vnode;
// 如果没有选择器,则说ing这是一个文本节点
if (isDef(tag)) {
elm = vnode.elm = htmlApi.createElement(tag);
attr.initCreateAttr(vnode);
if (isArray(children)) {
children.forEach(c => {
htmlApi.appendChild(elm, createElement(c))
});
} else if (isPrimitive(text)) {
// 子元素是文本节点直接插入当前到vnode节点
htmlApi.appendChild(elm, htmlApi.createTextNode(text));
}
}else{
elm = vnode.elm = htmlApi.createTextNode(text);
}
return vnode.elm;
}
4、处理属性更新
source\vue\utils\updataAttrUtils.js
/** 处理属性
*/
import {
isArray
} from './utils'
/**
*更新style属性
*
* @param {Object} vnode 新的虚拟dom节点对象
* @param {Object} oldStyle
* @returns
*/
function undateStyle(vnode, oldStyle = {}) {
let doElement = vnode.elm;
let newStyle = vnode.props.style || {};
// 删除style
for(let oldAttr in oldStyle){
if (!newStyle.hasOwnPrpperty[oldAttr]) {
doElement.style[oldAttr] = '';
}
}
for(let newAttr in newStyle){
doElement.style[newAttr] = newStyle[newAttr];
}
}
function filterKeys(obj) {
return Object.keys(obj).filter(k => {
return k !== 'style' && k !== 'id' && k !== 'class'
})
}
/**
*更新props属性
* 支持 vnode 使用 props 来操作其它属性。
* @param {Object} vnode 新的虚拟dom节点对象
* @param {Object} oldProps
* @returns
*/
function undateProps(vnode, oldProps = {}) {
let doElement = vnode.elm;
let props = vnode.props.props || {};
filterKeys(oldProps).forEach(key => {
if (!props.hasOwnPrpperty[key]) {
delete doElement[key];
}
})
filterKeys(props).forEach(key => {
let old = oldProps[key];
let cur = props[key];
if (old !== cur && (key !== 'value' || doElement[key] !== cur)) {
doElement[key] = cur;
}
})
}
/**
*更新className属性 html 中的class
* 支持 vnode 使用 props 来操作其它属性。
* @param {Object} vnode 新的虚拟dom节点对象
* @param {*} oldName
* @returns
*/
function updateClassName(vnode, oldName) {
let doElement = vnode.elm;
const newName = vnode.props.className || vnode.props.class;
if (!oldName && !newName) return
if (oldName === newName) return
if (typeof newName === 'string' && newName) {
doElement.className = newName.toString()
} else if (isArray(newName)) {
let oldList = [...doElement.classList];
oldList.forEach(c => {
if (!newName.indexOf(c)) {
doElement.classList.remove(c);
}
})
newName.forEach(v => {
doElement.classList.add(v)
})
} else {
// 所有不合法的值或者空值,都把 className 设为 ''
doElement.className = ''
}
}
function initCreateAttr(vnode) {
updateClassName(vnode);
undateProps(vnode);
undateStyle(vnode);
}
function updateAttrs(oldVnode, vnode) {
updateClassName(vnode, oldVnode.props.className || oldVnode.props.class);
undateProps(vnode, oldVnode.props.props);
undateStyle(vnode, oldVnode.props.style);
}
export const styleApis = {
undateStyle,
undateProps,
updateClassName,
initCreateAttr,
updateAttrs
};
export default styleApis;
5、导出
source\vue\vdom\index.js
import h from './h';
import {render} from './patch';
// 统一对外暴露的虚拟dom实现的出口
export {
h,
render
}
6、测试
import {h,render} from "../source/vue/vdom";
let container = document.getElementById('app');
let vnode = h('div', null, 'hello',
h('span',{style:{color:'red'}},'world'));
render(vnode, container);
6.3、diff、patch
步骤:
1、比较两个虚拟节点标签,不一样直接整个替换;
2、比较属性,更新属性
3、比较两个虚拟节点的文本节点
4、比较儿子节点,使用双指针比较:头头、尾尾、头尾、尾头,在就是比较key
代码:
1、在vnode.js中增加下面方法:
/**
* 检查两个 vnode 是不是同一个: key 相同且 type 相同
*
* @param {Object} oldVnode
* @param {Object} newVnode
* @returns {Boolean} 是则 true,否则 false
*/
export function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
/**
* 校验是不是 vnode, 主要检查 __type。
* @param {Object} vnode 要检查的对象
* @return {Boolean} 是则 true,否则 false
*/
export function isVnode(vnode) {
return vnode && vnode._type === VNODE_TYPE
}
2、在patch.js中增加方法:
// 不考虑hook 除了初次渲染还需要dom-diff操作
import htmlApi from '../utils/domUtils';
import {isDef,isUndef, isArray} from '../utils';
import attr from '../utils/updataAttrUtils'
import { isSameVnode} from './vnode'
export function render(vnode,container){
let el = createElement(vnode);
htmlApi.appendChild(container,el);
return el;
}
/**
* 从vdom生成真是dom *
* @param {Object} vnode 虚拟dom
* @returns 真是的dom
*/
function createElement(vnode) {
let {tag,children,text,elm} = vnode;
// 如果没有选择器,则说ing这是一个文本节点
if (isDef(tag)) {
elm = vnode.elm = htmlApi.createElement(tag);
attr.initCreateAttr(vnode);
if (isArray(children)) {
children.forEach(c => {
htmlApi.appendChild(elm, createElement(c))
});
} else if (isPrimitive(text)) {
// 子元素是文本节点直接插入当前到vnode节点
htmlApi.appendChild(elm, htmlApi.createTextNode(text));
}
}else{
elm = vnode.elm = htmlApi.createTextNode(text);
}
return vnode.elm;
}
export function patch(oldVnode,vnode){
let elm, parent;
// 如果 oldVnode 和 vnode 是同一个 vnode(相同的 key 和相同的选择器),
// 那么更新 oldVnode。
if (isSameVnode(oldVnode, vnode)) {
return patchVnode(oldVnode, vnode)
} else {
// 新旧vnode不同,那么直接替换掉 oldVnode 对应的 DOM
elm = oldVnode.elm;
parent = htmlApi.parentNode(elm);
createElement(vnode);
if (parent !== null) {
// 如果老节点对应的dom父节点有并且有同级节点,
// 那就在其同级节点之后插入 vnode 的对应 DOM。
htmlApi.insertBefore(parent, vnode.elm, htmlApi.nextSibling(elm));
// 在把 vnode 的对应 DOM 插入到 oldVnode 的父节点内后,移除 oldVnode 的对应 DOM,完成替换。
removeVnodes(parent, [oldVnode], 0, 0);
}
}
return elm;
}
/**
*
* 1、更新 本身对应的dom
* 2、根据children的变化去确定是否递归 patch children里的每个vnode
*
* @param {Object} oldVnode 老 vnode
* @param {Object} vnode 新 vnode
* @param {*} insertedVnodeQueue 记录插入的 vnode
*
*/
function patchVnode(oldVnode, vnode) {
// let elm = vnode.elm = oldVnode.elm,因为vnode没有被渲染,这时的vnode.elm是undefined,
// 新把老的给它
let elm = vnode.elm = oldVnode.elm,
oldCh = oldVnode.children,
newCh = vnode.children;
// 如果两个vnode完全相同,直接返回 不需要更新操作
if (oldVnode === vnode) return;
if (isDef(vnode.props)) {
// 属性的比较更新
attr.updateAttrs(oldVnode, vnode);
}
// 新节点不是文本节点
if (isUndef(vnode.text)) {
if (oldCh && newCh && oldCh.length > 0 && newCh.length > 0) {
// 新旧节点均存在 children,且不一样时,对 children 进行 diff
updateChildren(elm, oldCh, newCh);
} else if (newCh && newCh.length > 0) {
//如果vnode有子节点,oldvnode没子节点
//oldvnode是text节点,则将elm的text清除,因为children和text不同同时有值
if (!oldVnode.text) htmlApi.setTextContent(elm, '');
//并添加vnode的children
addVnodes(elm, null, newCh, 0, newCh.length - 1);
} else if (oldCh && oldCh.length > 0) {
// 新节点不存在 children 旧节点存在 children 移除旧节点的 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
} //如果oldvnode的text和vnode的text不同,则更新为vnode的text
else if (oldVnode.text !== vnode.text) {
htmlApi.setTextContent(elm, vnode.text);
}
return elm;
}
/**
*从parent dom删除vnode 数组对应的dom
*
* @param {Element} parentElm 父元素
* @param {Array} vnodes vnode数组
* @param {Number} startIdx 要删除的对应的vnodes的开始索引
* @param {Number} endIdx 要删除的对应的vnodes的结束索引
*/
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
let ch = vnodes[startIdx];
if (ch) {
htmlApi.removeChild(parentElm, ch.elm);
}
}
}
/**
* 比较新旧 children 并更新
*
* @param {Element} parentDOMElement 父 dom,children 对应的 dom 将要挂载的
* @param {Array} oldChildren 旧 children,vnode 数组
* @param {Array} newChildren 新 children,vnode 数组
/**
*
*
* @param {*} parentDOMElement
* @param {*} oldChildren
* @param {*} newChildren
*/
function updateChildren(parentDOMElement, oldChildren, newChildren) {
// 两组数据 首尾双指针比较
let oldStartIdx = 0,
oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1,
oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0,
newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1,
newEndVnode = newChildren[newEndIdx];
// `oldKeyToIdx` 来映射新节点的`key`在oldChildren中的索引
let oldKeyToIdx;
// 记录当前节点key的索引通过在 oldKeyToIdx对象属性查找
let oldIdxByKeyMap;
let elmToMove;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 先排除 vnode为空 4 个 vnode 非空,
// 左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标
if (oldStartVnode == null) {
oldStartVnode = oldChildren[++oldStartIdx];
} else if (oldEndVnode == null) {
oldEndVnode = oldChildren[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newChildren[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newChildren[--newEndIdx];
}
/** oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
* 1、 oldStartVnode - > newStartVnode
* 2、 oldEndVnode - > newEndVnode
* 3、 newEndVnode - > oldStartVnode
* 4、 newStartVnode - > oldEndVnode
*
* 对上述四种情况执行对应的patch
*/
// 1、新的开始节点跟老的开始节点相比较 是不是一样的vnode
// oldStartVnode - > newStartVnode 比如在尾部新增、删除节点
//
else if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
}
// 2、oldEndVnode - > newEndVnode 比如在头部新增、删除节点
else if (isSameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIdx];
newEndVnode = newChildren[--newEndIdx];
}
// 3、newEndVnode - > oldStartVnode 将头部节点移动到尾部
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patch(oldStartVnode, newEndVnode);
// 把旧的开始节点插入到末尾
htmlApi.insertBefore(parentDOMElement, oldStartVnode.elm, htmlApi.nextSibling(oldEndVnode.elm));
oldStartVnode = oldChildren[++oldStartIdx];
newEndVnode = newChildren[--newEndIdx];
}
// 4、oldEndVnode -> newStartVnode 将尾部移动到头部
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patch(oldEndVnode, newStartVnode);
// 将老的oldEndVnode移动到oldStartVnode的前边,
htmlApi.insertBefore(parentDOMElement, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldChildren[--oldEndIdx];
newStartVnode = newChildren[++newStartIdx];
}
/** 5、4种情况都不相等
* // 1. 从 oldChildren 数组建立 key --> index 的 map。
// 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
// 以它的 key 从上面的 map 里拿到 index;
// 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
// 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
// 创建对应的 dom 并插入。
*/
else {
/** 如果 oldKeyToIdx 不存在,
* 1、创建 old children 中 vnode 的 key 到 index 的
* 映射, 方便我们之后通过 key 去拿下标
* */
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createOldKeyToIdx(oldChildren, oldStartIdx, oldEndIdx);
}
// 2、尝试通过 newStartVnode 的 key 去拿下标
oldIdxByKeyMap = oldKeyToIdx[newStartVnode.key];
// 4、 下标索引不存在,说明newStartVnode 是全新的 vnode
if (oldIdxByKeyMap == null) {
// 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
htmlApi.insertBefore(parentDOMElement, createElement(newStartVnode), oldStartVnode.elm);
newStartVnode = newChildren[++newStartIdx];
}
// 3、下标存在 说明oldChildren中有相同key的vnode
else {
elmToMove = oldChildren[oldIdxByKeyMap];
// key相同还要比较tag,tag不同,需要创建 新dom
if (elmToMove.tag !== newStartVnode.tag) {
htmlApi.insertBefore(parentDOMElement, createElement(newStartVnode), oldStartVnode.elm);
}
// tag相同,key也相同,说明是一样的vnode,需要打补丁patch
else {
patch(elmToMove, newStartVnode);
oldChildren[oldIdxByKeyMap] = undefined;
htmlApi.insertBefore(parentDOMElement, elmToMove.elm, oldStartVnode.elm);
}
newStartVnode = newChildren[++newStartIdx];
}
}
}
// 说明循环比较完后,新节点还有数据,这时候需要将这些虚拟节点的创建真是dom
// 新增引用到老的虚拟dom的`elm`上,且新增位置是老节点的oldStartVnode即末尾;
if (newStartIdx <= newEndIdx) {
let before = newChildren[newEndIdx + 1] == null ? null : newChildren[newEndIdx + 1].elm;
addVnodes(parentDOMElement, before, newChildren, newStartIdx, newEndIdx);
}
if (oldStartIdx <= oldEndIdx) {
// newChildren 已经全部处理完成,而 oldChildren 还有旧的节点,需要将多余的节点移除
removeVnodes(parentDOMElement, oldChildren, oldStartIdx, oldEndIdx);
}
}
/**
* 为 vnode 数组 begin~ end 下标范围内的 vnode
* 创建它的 key 和 下标 的映射。
*
* @param {Array} children
* @param {Number} startIdx
* @param {Number} endIdx
* @returns {Object} key在children中所映射的index索引对象
* children = [{key:'A'},{key:'B'},{key:'C'},{key:'D'},{key:'E'}];
* startIdx = 1; endIdx = 3;
* 函数返回{'B':1,'C':2,'D':3}
*/
function createOldKeyToIdx(children, startIdx, endIdx) {
const map = {};
let key;
for (let i = startIdx; i <= endIdx; ++i) {
let ch = children[i];
if (ch != null) {
key = ch.key;
if (!isUndef(key)) map[key] = i;
}
}
return map;
}
/**
*添加 vnodes 数组对应的 dom 到parentElm dom
*
* @param {Element} parentElm 父dom
* @param {Element} before 添加到指定的 before dom 被参照的节点(即要插在该节点之前)
* 当传入null时, 新插入的元素将会插入到父元素的子元素列表末尾。
* @param {Array} vnodes 需要添加的vnode数组
* @param {Number} startIdx vnodes 开始索引
* @param {Number} endIdx vnodes 结束索引
*/
function addVnodes(parentElm, before, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
htmlApi.insertBefore(parentElm, createElement(ch), before);
}
}
}
在source\vdom\index.js导出
import h from './h';
import {render,patch} from './patch';
// 统一对外暴露的虚拟dom实现的出口
export {
h,
render,
patch
}
7、vue中融合虚拟dom
在本篇文章中小节2,中更新视图时使用了_update方法,我们将重写;
步骤:
1、应用使用了render
2、在$mount中updateComponent函数中调用Vue实例上的_update方法,参数vm._render方法;
代码:
source\vue\index.js
import {initState} from './observe'
import Watcher from './observe/watcher';
import { complier } from './compiler';
import {h,render,patch} from './vdom';
function Vue(options){
this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
// vue中初始化 this.$options 表示得是vue中得参数
// this指向Vue的实例,所以这里是将Vue的实例缓存给vm变量
let vm = this;
vm.$options = options;
//MVVM原理 响应式数据 需要数据重新初始化
// 我们将不同得初始化放入到不同得文件夹中操作,方便维护
// 初始化state,包括props\methods\data\compunted\watch
// props\data属性将其设置为vm的响应式属性即需要Object.defineProperty
// 绑定vm的props和打他属性并设置其getter和setter
initState(vm);
// 将实例挂载到dom上
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
function query(el){
if(typeof el === 'string'){
return document.querySelector(el);
}
return el;
}
const mount = Vue.prototype.$mount;
// 渲染页面将组件进行挂载
Vue.prototype.$mount = function(el){
let vm = this;
// 获取dom
el = vm.$el = el && query(el);
// 渲染时通过 watcher来渲染
//
/**
* 渲染watcher
* vue2.0 组件级别更新 new Vue产生一个组件
*
*/
// 更新组件、渲染逻辑
let updateComponent = ()=>{
// 更新组件
vm._update(vm._render());
}
// 渲染watcher
new Watcher(vm,updateComponent);
}
//
Vue.prototype._render = function(){
let vm = this;
let render = vm.$options.render; //获取用户编写的render方法
// render函数中的this,是当前Vue的实例
// render 返回的是虚拟节点
let vnode = render.call(vm,h);
return vnode;
}
// 用户传入的数据 去更新视图
Vue.prototype._update = function(vnode){
console.log("更新操作");
let vm = this;
let el = vm.$el;
let preVnode = vm.preVnode;
// 第一次渲染 preVnode不存在
if(!preVnode){
vm.preVnode = vnode;
render(vnode,el);
}else{
vm.$el = patch(preVnode,vnode);
}
}
Vue.prototype.$watch = function(expr,handler,opts){
let vm = this;
// 原理 创建一个watcher {user:true}标识是用户自己写的一个watcher
new Watcher(vm,expr,handler,{user:true,...opts});
}
export default Vue
总结
回过头来看,这里的渲染逻辑并不是特别复杂,核心关键的几步流程还是非常清晰的:
- new Vue,执行初始化,对数据进行劫持、收集依赖关系,实现响应式数据;
- 挂载
$mount
方法,通过自定义Render方法、template、el等生成Render函数,本文中没有实现template,因为需要考虑到ast,后续再分析; - 通过Watcher监听数据的变化;
- 当数据发生变化时,Render函数执行生成VNode对象
- 通过patch方法,对比新旧VNode对象,使用了diff算法,添加、修改、删除真正的DOM元素
vue源码中的知识点
1、 rollup: 模块打包工具,官方文档:www.rollupjs.com/
2、Flow :官方文档:flow.org/en/
VUE的源码采用rollup和 flow至于为什么不采用typescript,尤大在知乎说过主要考虑工程上成本和收益的考量。(3.0+版本确定改用typesript) 。
3、发布订阅模式
4、Object.defineProperty
5、es6
6、原型链、闭包
7、函数柯里化
8、事件环
9、切片编程
10、vnode
11、dom-diff算法
12、AST